diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java index ed084ce5a..494c561cb 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java @@ -81,6 +81,7 @@ public final class Accounts { public static final YggdrasilAccountFactory FACTORY_MOJANG = YggdrasilAccountFactory.MOJANG; public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER, Accounts::getOrCreateAuthlibInjectorServer); public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(OAUTH_CALLBACK)); + public static final BoundAuthlibInjectorAccountFactory FACTORY_LITTLE_SKIN = getAccountFactoryByAuthlibInjectorServer(new AuthlibInjectorServer("https://littleskin.cn/api/yggdrasil/")); public static final List> FACTORIES = immutableListOf(FACTORY_OFFLINE, FACTORY_MOJANG, FACTORY_MICROSOFT, FACTORY_AUTHLIB_INJECTOR); // ==== login type / account factory mapping ==== @@ -96,14 +97,24 @@ public final class Accounts { } public static String getLoginType(AccountFactory factory) { - return Optional.ofNullable(factory2type.get(factory)) - .orElseThrow(() -> new IllegalArgumentException("Unrecognized account factory")); + String type = factory2type.get(factory); + if (type != null) return type; + + if (factory instanceof BoundAuthlibInjectorAccountFactory) { + return factory2type.get(FACTORY_AUTHLIB_INJECTOR); + } + + throw new IllegalArgumentException("Unrecognized account factory"); } public static AccountFactory getAccountFactory(String loginType) { return Optional.ofNullable(type2factory.get(loginType)) .orElseThrow(() -> new IllegalArgumentException("Unrecognized login type")); } + + public static BoundAuthlibInjectorAccountFactory getAccountFactoryByAuthlibInjectorServer(AuthlibInjectorServer server) { + return new BoundAuthlibInjectorAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER, server); + } // ==== public static AccountFactory getAccountFactory(Account account) { @@ -119,10 +130,10 @@ public final class Accounts { throw new IllegalArgumentException("Failed to determine account type: " + account); } - private static ObservableList accounts = observableArrayList(account -> new Observable[] { account }); - private static ReadOnlyListWrapper accountsWrapper = new ReadOnlyListWrapper<>(Accounts.class, "accounts", accounts); + private static final ObservableList accounts = observableArrayList(account -> new Observable[] { account }); + private static final ReadOnlyListWrapper accountsWrapper = new ReadOnlyListWrapper<>(Accounts.class, "accounts", accounts); - private static ObjectProperty selectedAccount = new SimpleObjectProperty(Accounts.class, "selectedAccount") { + private static final ObjectProperty selectedAccount = new SimpleObjectProperty(Accounts.class, "selectedAccount") { { accounts.addListener(onInvalidating(this::invalidated)); } @@ -231,6 +242,14 @@ public final class Accounts { triggerAuthlibInjectorUpdateCheck(); } + Schedulers.io().execute(() -> { + try { + FACTORY_LITTLE_SKIN.getServer().fetchMetadataResponse(); + } catch (IOException e) { + LOG.log(Level.WARNING, "Failed to fetch authlib-injector server metdata: " + FACTORY_LITTLE_SKIN.getServer(), e); + } + }); + for (AuthlibInjectorServer server : config().getAuthlibInjectorServers()) { if (selected instanceof AuthlibInjectorAccount && ((AuthlibInjectorAccount) selected).getServer() == server) continue; @@ -313,7 +332,7 @@ public final class Accounts { // ==== // ==== Login type name i18n === - private static Map, String> unlocalizedLoginTypeNames = mapOf( + private static final Map, String> unlocalizedLoginTypeNames = mapOf( pair(Accounts.FACTORY_OFFLINE, "account.methods.offline"), pair(Accounts.FACTORY_MOJANG, "account.methods.yggdrasil"), pair(Accounts.FACTORY_AUTHLIB_INJECTOR, "account.methods.authlib_injector"), diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListPage.java index 8c97b3749..adda97468 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListPage.java @@ -122,6 +122,14 @@ public class AccountListPage extends DecoratorAnimatedPage implements DecoratorP microsoftItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_MICROSOFT))); boxMethods.getChildren().add(microsoftItem); + AdvancedListItem littleSkinItem = new AdvancedListItem(); + littleSkinItem.getStyleClass().add("navigation-drawer-item"); + littleSkinItem.setActionButtonVisible(false); + littleSkinItem.setTitle(i18n("account.methods.little_skin")); + littleSkinItem.setLeftGraphic(wrap(SVG::server)); + littleSkinItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_LITTLE_SKIN))); + boxMethods.getChildren().add(littleSkinItem); + VBox boxAuthServers = new VBox(); authServerItems = MappedObservableList.create(skinnable.authServersProperty(), server -> { AdvancedListItem item = new AdvancedListItem(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/CreateAccountPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/CreateAccountPane.java index d909c071c..908c73a77 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/CreateAccountPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/CreateAccountPane.java @@ -40,6 +40,7 @@ import org.jackhuang.hmcl.auth.CharacterSelector; import org.jackhuang.hmcl.auth.NoSelectedCharacterException; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccountFactory; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer; +import org.jackhuang.hmcl.auth.authlibinjector.BoundAuthlibInjectorAccountFactory; import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccountFactory; import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory; import org.jackhuang.hmcl.auth.yggdrasil.GameProfile; @@ -194,9 +195,8 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware { setPrefWidth(560); } - public CreateAccountPane(AuthlibInjectorServer authserver) { - this(Accounts.FACTORY_AUTHLIB_INJECTOR); - ((AccountDetailsInputPane) detailsPane).selectAuthServer(authserver); + public CreateAccountPane(AuthlibInjectorServer authServer) { + this(Accounts.getAccountFactoryByAuthlibInjectorServer(authServer)); } private void onAccept() { @@ -338,7 +338,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware { private static class AccountDetailsInputPane extends GridPane { // ==== authlib-injector hyperlinks ==== - private static final String[] ALLOWED_LINKS = { "register" }; + private static final String[] ALLOWED_LINKS = { "homepage", "register" }; private static List createHyperlinks(AuthlibInjectorServer server) { if (server == null) { @@ -360,12 +360,12 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware { } // ===== - private AccountFactory factory; - private @Nullable JFXComboBox cboServers; + private final AccountFactory factory; + private @Nullable AuthlibInjectorServer server; private @Nullable JFXTextField txtUsername; private @Nullable JFXPasswordField txtPassword; private @Nullable JFXTextField txtUUID; - private BooleanBinding valid; + private final BooleanBinding valid; public AccountDetailsInputPane(AccountFactory factory, Runnable onAction) { this.factory = factory; @@ -383,45 +383,33 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware { int rowIndex = 0; - if (factory instanceof AuthlibInjectorAccountFactory) { + if (factory instanceof BoundAuthlibInjectorAccountFactory) { + this.server = ((BoundAuthlibInjectorAccountFactory) factory).getServer(); + Label lblServers = new Label(i18n("account.injector.server")); setHalignment(lblServers, HPos.LEFT); add(lblServers, 0, rowIndex); - cboServers = new JFXComboBox<>(); - cboServers.setCellFactory(jfxListCellFactory(server -> new TwoLineListItem(server.getName(), server.getUrl()))); - cboServers.setConverter(stringConverter(AuthlibInjectorServer::getName)); - bindContent(cboServers.getItems(), config().getAuthlibInjectorServers()); - cboServers.getItems().addListener(onInvalidating( - () -> Platform.runLater( // the selection will not be updated as expected if we call it immediately - cboServers.getSelectionModel()::selectFirst))); - cboServers.getSelectionModel().selectFirst(); - cboServers.setPromptText(i18n("account.injector.empty")); - BooleanBinding noServers = createBooleanBinding(cboServers.getItems()::isEmpty, cboServers.getItems()); - classPropertyFor(cboServers, "jfx-combo-box-warning").bind(noServers); - classPropertyFor(cboServers, "jfx-combo-box").bind(noServers.not()); - HBox.setHgrow(cboServers, Priority.ALWAYS); - HBox.setMargin(cboServers, new Insets(0, 10, 0, 0)); - cboServers.setMaxWidth(Double.MAX_VALUE); + Label lblServerName = new Label(this.server.getName()); + lblServerName.setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(lblServerName, Priority.ALWAYS); HBox linksContainer = new HBox(); linksContainer.setAlignment(Pos.CENTER); - onChangeAndOperate(cboServers.valueProperty(), server -> linksContainer.getChildren().setAll(createHyperlinks(server))); + linksContainer.getChildren().setAll(createHyperlinks(this.server)); linksContainer.setMinWidth(USE_PREF_SIZE); - JFXButton btnAddServer = new JFXButton(); - btnAddServer.setGraphic(SVG.plus(Theme.blackFillBinding(), 20, 20)); - btnAddServer.getStyleClass().add("toggle-icon4"); - btnAddServer.setOnAction(e -> { - Controllers.dialog(new AddAuthlibInjectorServerPane()); - }); - - HBox boxServers = new HBox(cboServers, linksContainer, btnAddServer); + HBox boxServers = new HBox(lblServerName, linksContainer); + boxServers.setAlignment(Pos.CENTER_LEFT); add(boxServers, 1, rowIndex); rowIndex++; } + if (factory instanceof AuthlibInjectorAccountFactory) { + throw new IllegalArgumentException("Use BoundAuthlibInjectorAccountFactory instead"); + } + if (factory.getLoginType().requiresUsername) { Label lblUsername = new Label(i18n("account.username")); setHalignment(lblUsername, HPos.LEFT); @@ -523,8 +511,6 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware { valid = new BooleanBinding() { { - if (cboServers != null) - bind(cboServers.valueProperty()); if (txtUsername != null) bind(txtUsername.textProperty()); if (txtPassword != null) @@ -535,8 +521,6 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware { @Override protected boolean computeValue() { - if (cboServers != null && cboServers.getValue() == null) - return false; if (txtUsername != null && !txtUsername.validate()) return false; if (txtPassword != null && !txtPassword.validate()) @@ -551,11 +535,8 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware { private boolean requiresEmailAsUsername() { if (factory instanceof YggdrasilAccountFactory) { return true; - } else if ((factory instanceof AuthlibInjectorAccountFactory) && cboServers != null) { - AuthlibInjectorServer server = cboServers.getValue(); - if (server != null && !server.isNonEmailLogin()) { - return true; - } + } else if ((factory instanceof AuthlibInjectorAccountFactory) && this.server != null) { + return !server.isNonEmailLogin(); } return false; } @@ -572,7 +553,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware { } public @Nullable AuthlibInjectorServer getAuthServer() { - return cboServers == null ? null : cboServers.getValue(); + return this.server; } public @Nullable String getUsername() { @@ -587,10 +568,6 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware { return valid; } - public void selectAuthServer(AuthlibInjectorServer authserver) { - cboServers.getSelectionModel().select(authserver); - } - public void focus() { if (txtUsername != null) { txtUsername.requestFocus(); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index da13edfe7..892b56256 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -78,6 +78,7 @@ account.hmcl.hint=You need to click on "Login" and complete the process in the o account.injector.add=Add an Authentication Server account.injector.empty=None (You can click on the plus button on the right to add one) account.injector.http=Warning\: This server uses the unsafe HTTP protocol, anyone between your connection will be able to see your credentials in cleartext. +account.injector.link.homepage=Homepage account.injector.link.register=Register account.injector.server=Authentication Server account.injector.server_url=Server URL @@ -90,6 +91,7 @@ account.register=Register account.manage=Account List account.methods=Login Type account.methods.authlib_injector=authlib-injector +account.methods.little_skin=Little Skin account.methods.microsoft=Microsoft Account account.methods.microsoft.birth=How to update your account birthday account.methods.microsoft.close_page=Microsoft account authorization is now completed. \n\ diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index 14bd14146..030497e74 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -90,6 +90,7 @@ account.register=Registrarse account.manage=Lista de cuentas account.methods=Tipo de inicio de sesión account.methods.authlib_injector=authlib-injector +account.methods.little_skin=Little Skin account.methods.microsoft=Cuenta Microsoft account.methods.microsoft.birth=Cómo actualizar la fecha de nacimiento de tu cuenta account.methods.microsoft.close_page=La autorización de la cuenta de Microsoft ha finalizado. \n\ diff --git a/HMCL/src/main/resources/assets/lang/I18N_ja.properties b/HMCL/src/main/resources/assets/lang/I18N_ja.properties index 1655270a3..e9a217039 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ja.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ja.properties @@ -88,6 +88,7 @@ account.register=登録 account.manage=アカウントリスト account.methods=ログインタイプ account.methods.authlib_injector=authlib-インジェクター +account.methods.little_skin=Little Skin account.methods.microsoft=Microsoft Account account.methods.microsoft.birth=誕生日の設定を編集する方法... account.methods.microsoft.deauthorize=アカウントのバインドを解除 diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index 356b43818..8c8ec42ae 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -88,6 +88,7 @@ account.register=Регистрация account.manage=Список аккаунтов account.methods=Метод авторизации account.methods.authlib_injector=authlib-injector +account.methods.little_skin=Little Skin account.methods.microsoft=Аккаунт Microsoft account.methods.microsoft.birth=Как изменить настройки дня рождения... account.methods.microsoft.deauthorize=Отменить авторизацию аккаунта diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 27f2414e6..74891f62c 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -76,6 +76,7 @@ account.hmcl.hint=你需要點擊“登入”按鈕,並在打開的網頁中 account.injector.add=新增認證伺服器 account.injector.empty=無 (按一下右側 + 加入) account.injector.http=警告: 此伺服器使用不安全的 HTTP 協定,您的密碼在登入時會被明文傳輸。 +account.injector.link.homepage=首頁 account.injector.link.register=註冊 account.injector.server=認證伺服器 account.injector.server_url=伺服器位址 @@ -88,6 +89,7 @@ account.register=註冊 account.manage=帳戶列表 account.methods=登入方式 account.methods.authlib_injector=authlib-injector 登入 +account.methods.little_skin=Little Skin account.methods.microsoft=微軟帳戶 account.methods.microsoft.birth=如何修改帳戶出生日期 account.methods.microsoft.deauthorize=解除帳戶授權 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 85eca4e7c..ca6fb2d5c 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -76,6 +76,7 @@ account.hmcl.hint=你需要点击“登录”按钮,并在打开的网页中 account.injector.add=添加认证服务器 account.injector.empty=无(点击右侧加号添加) account.injector.http=警告:此服务器使用不安全的 HTTP 协议,您的密码在登录时会被明文传输! +account.injector.link.homepage=主页 account.injector.link.register=注册 account.injector.server=认证服务器 account.injector.server_url=服务器地址 @@ -88,6 +89,7 @@ account.register=注册 account.manage=帐户列表 account.methods=登录方式 account.methods.authlib_injector=外置登录 (authlib-injector) +account.methods.little_skin=Little Skin account.methods.microsoft=微软帐户 account.methods.microsoft.birth=如何修改帐户出生日期 account.methods.microsoft.close_page=已完成微软帐户授权,接下来启动器还需要完成剩余登录步骤。你已经可以关闭本页面了。 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccountFactory.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccountFactory.java index 04eb219ea..dbf6d038d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccountFactory.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccountFactory.java @@ -33,8 +33,8 @@ import java.util.function.Function; import static org.jackhuang.hmcl.util.Lang.tryCast; public class AuthlibInjectorAccountFactory extends AccountFactory { - private AuthlibInjectorArtifactProvider downloader; - private Function serverLookup; + private final AuthlibInjectorArtifactProvider downloader; + private final Function serverLookup; /** * @param serverLookup a function that looks up {@link AuthlibInjectorServer} by url @@ -64,14 +64,17 @@ public class AuthlibInjectorAccountFactory extends AccountFactory storage) { Objects.requireNonNull(storage); + String apiRoot = tryCast(storage.get("serverBaseURL"), String.class) + .orElseThrow(() -> new IllegalArgumentException("storage does not have API root.")); + AuthlibInjectorServer server = serverLookup.apply(apiRoot); + return fromStorage(storage, downloader, server); + } + + static AuthlibInjectorAccount fromStorage(Map storage, AuthlibInjectorArtifactProvider downloader, AuthlibInjectorServer server) { YggdrasilSession session = YggdrasilSession.fromStorage(storage); String username = tryCast(storage.get("username"), String.class) .orElseThrow(() -> new IllegalArgumentException("storage does not have username")); - String apiRoot = tryCast(storage.get("serverBaseURL"), String.class) - .orElseThrow(() -> new IllegalArgumentException("storage does not have API root.")); - - AuthlibInjectorServer server = serverLookup.apply(apiRoot); tryCast(storage.get("profileProperties"), Map.class).ifPresent( it -> { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/BoundAuthlibInjectorAccountFactory.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/BoundAuthlibInjectorAccountFactory.java new file mode 100644 index 000000000..bfbffe6db --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/BoundAuthlibInjectorAccountFactory.java @@ -0,0 +1,61 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2020 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.auth.authlibinjector; + +import org.jackhuang.hmcl.auth.AccountFactory; +import org.jackhuang.hmcl.auth.AuthenticationException; +import org.jackhuang.hmcl.auth.CharacterSelector; + +import java.util.Map; +import java.util.Objects; + +public class BoundAuthlibInjectorAccountFactory extends AccountFactory { + private final AuthlibInjectorArtifactProvider downloader; + private final AuthlibInjectorServer server; + + /** + * @param server Authlib-Injector Server + */ + public BoundAuthlibInjectorAccountFactory(AuthlibInjectorArtifactProvider downloader, AuthlibInjectorServer server) { + this.downloader = downloader; + this.server = server; + } + + @Override + public AccountLoginType getLoginType() { + return AccountLoginType.USERNAME_PASSWORD; + } + + public AuthlibInjectorServer getServer() { + return server; + } + + @Override + public AuthlibInjectorAccount create(CharacterSelector selector, String username, String password, ProgressCallback progressCallback, Object additionalData) throws AuthenticationException { + Objects.requireNonNull(selector); + Objects.requireNonNull(username); + Objects.requireNonNull(password); + + return new AuthlibInjectorAccount(server, downloader, username, password, selector); + } + + @Override + public AuthlibInjectorAccount fromStorage(Map storage) { + return AuthlibInjectorAccountFactory.fromStorage(storage, downloader, server); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilSession.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilSession.java index 97a09a02b..6f9854760 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilSession.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilSession.java @@ -78,6 +78,8 @@ public class YggdrasilSession { } public static YggdrasilSession fromStorage(Map storage) { + Objects.requireNonNull(storage); + UUID uuid = tryCast(storage.get("uuid"), String.class).map(UUIDTypeAdapter::fromString).orElseThrow(() -> new IllegalArgumentException("uuid is missing")); String name = tryCast(storage.get("displayName"), String.class).orElseThrow(() -> new IllegalArgumentException("displayName is missing")); String clientToken = tryCast(storage.get("clientToken"), String.class).orElseThrow(() -> new IllegalArgumentException("clientToken is missing"));