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 e8638e767..816200412 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java @@ -24,13 +24,7 @@ import javafx.beans.property.ReadOnlyListWrapper; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.ObservableList; import org.jackhuang.hmcl.Metadata; -import org.jackhuang.hmcl.auth.Account; -import org.jackhuang.hmcl.auth.AccountFactory; -import org.jackhuang.hmcl.auth.AuthenticationException; -import org.jackhuang.hmcl.auth.CharacterDeletedException; -import org.jackhuang.hmcl.auth.NoCharacterException; -import org.jackhuang.hmcl.auth.ServerDisconnectException; -import org.jackhuang.hmcl.auth.ServerResponseMalformedException; +import org.jackhuang.hmcl.auth.*; import org.jackhuang.hmcl.auth.authlibinjector.*; import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount; import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccountFactory; @@ -382,6 +376,8 @@ public final class Accounts { return i18n("account.methods.microsoft.error.add_family_probably"); } else if (exception instanceof MicrosoftAuthenticationServer.MicrosoftAuthenticationNotSupportedException) { return i18n("account.methods.microsoft.snapshot"); + } else if (exception instanceof OAuthAccount.WrongAccountException) { + return i18n("account.failed.wrong_account"); } else if (exception.getClass() == AuthenticationException.class) { return exception.getLocalizedMessage(); } else { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/DialogController.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/DialogController.java index 7ba3c5b54..9fce1daf9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/DialogController.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/DialogController.java @@ -17,12 +17,9 @@ */ package org.jackhuang.hmcl.ui; -import org.jackhuang.hmcl.auth.Account; -import org.jackhuang.hmcl.auth.AuthInfo; -import org.jackhuang.hmcl.auth.AuthenticationException; -import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount; -import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; -import org.jackhuang.hmcl.ui.account.AccountLoginWithPasswordDialog; +import org.jackhuang.hmcl.auth.*; +import org.jackhuang.hmcl.ui.account.ClassicAccountLoginDialog; +import org.jackhuang.hmcl.ui.account.OAuthAccountLoginDialog; import java.util.Optional; import java.util.concurrent.CancellationException; @@ -36,11 +33,23 @@ public final class DialogController { } public static AuthInfo logIn(Account account) throws CancellationException, AuthenticationException, InterruptedException { - if (account instanceof YggdrasilAccount) { + if (account instanceof ClassicAccount) { CountDownLatch latch = new CountDownLatch(1); AtomicReference res = new AtomicReference<>(null); runInFX(() -> { - AccountLoginWithPasswordDialog pane = new AccountLoginWithPasswordDialog(account, it -> { + ClassicAccountLoginDialog pane = new ClassicAccountLoginDialog((ClassicAccount) account, it -> { + res.set(it); + latch.countDown(); + }, latch::countDown); + Controllers.dialog(pane); + }); + latch.await(); + return Optional.ofNullable(res.get()).orElseThrow(CancellationException::new); + } else if (account instanceof OAuthAccount) { + CountDownLatch latch = new CountDownLatch(1); + AtomicReference res = new AtomicReference<>(null); + runInFX(() -> { + OAuthAccountLoginDialog pane = new OAuthAccountLoginDialog((OAuthAccount) account, it -> { res.set(it); latch.countDown(); }, latch::countDown); @@ -48,8 +57,6 @@ public final class DialogController { }); latch.await(); return Optional.ofNullable(res.get()).orElseThrow(CancellationException::new); - } else if (account instanceof MicrosoftAccount) { - } return account.logIn(); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountLoginWithPasswordDialog.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/ClassicAccountLoginDialog.java similarity index 95% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountLoginWithPasswordDialog.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/account/ClassicAccountLoginDialog.java index 1783eb536..5fa155c3b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountLoginWithPasswordDialog.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/ClassicAccountLoginDialog.java @@ -27,8 +27,8 @@ import javafx.scene.control.Label; import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; -import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.AuthInfo; +import org.jackhuang.hmcl.auth.ClassicAccount; import org.jackhuang.hmcl.auth.NoSelectedCharacterException; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; @@ -48,8 +48,8 @@ import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; import static org.jackhuang.hmcl.util.Logging.LOG; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -public class AccountLoginWithPasswordDialog extends StackPane { - private final Account oldAccount; +public class ClassicAccountLoginDialog extends StackPane { + private final ClassicAccount oldAccount; private final Consumer success; private final Runnable failed; @@ -57,7 +57,7 @@ public class AccountLoginWithPasswordDialog extends StackPane { private final Label lblCreationWarning = new Label(); private final JFXProgressBar progressBar; - public AccountLoginWithPasswordDialog(Account oldAccount, Consumer success, Runnable failed) { + public ClassicAccountLoginDialog(ClassicAccount oldAccount, Consumer success, Runnable failed) { this.oldAccount = oldAccount; this.success = success; this.failed = failed; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OAuthAccountLoginDialog.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OAuthAccountLoginDialog.java new file mode 100644 index 000000000..f7798666b --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OAuthAccountLoginDialog.java @@ -0,0 +1,86 @@ +package org.jackhuang.hmcl.ui.account; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.scene.control.Label; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import org.jackhuang.hmcl.auth.AuthInfo; +import org.jackhuang.hmcl.auth.OAuthAccount; +import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService; +import org.jackhuang.hmcl.game.MicrosoftAuthenticationServer; +import org.jackhuang.hmcl.setting.Accounts; +import org.jackhuang.hmcl.task.Schedulers; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.construct.*; +import org.jackhuang.hmcl.util.javafx.BindingMapping; + +import java.util.function.Consumer; +import java.util.logging.Level; + +import static org.jackhuang.hmcl.util.Logging.LOG; +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + +public class OAuthAccountLoginDialog extends DialogPane { + private final OAuthAccount account; + private final Consumer success; + private final Runnable failed; + private final BooleanProperty logging = new SimpleBooleanProperty(); + + public OAuthAccountLoginDialog(OAuthAccount account, Consumer success, Runnable failed) { + this.account = account; + this.success = success; + this.failed = failed; + + setTitle(i18n("account.login.refresh")); + + VBox vbox = new VBox(8); + Label usernameLabel = new Label(account.getUsername()); + + HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO); + hintPane.textProperty().bind(BindingMapping.of(logging).map(logging -> + logging + ? i18n("account.methods.microsoft.manual") + : i18n("account.methods.microsoft.hint"))); + hintPane.setOnMouseClicked(e -> { + if (logging.get() && MicrosoftAuthenticationServer.lastlyOpenedURL != null) { + FXUtils.copyText(MicrosoftAuthenticationServer.lastlyOpenedURL); + } + }); + + HBox box = new HBox(8); + JFXHyperlink birthLink = new JFXHyperlink(i18n("account.methods.microsoft.birth")); + birthLink.setOnAction(e -> FXUtils.openLink("https://support.microsoft.com/zh-cn/account-billing/如何更改-microsoft-帐户上的出生日期-837badbc-999e-54d2-2617-d19206b9540a")); + JFXHyperlink profileLink = new JFXHyperlink(i18n("account.methods.microsoft.profile")); + profileLink.setOnAction(e -> FXUtils.openLink("https://account.live.com/editprof.aspx")); + JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.yggdrasil.purchase")); + purchaseLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.PURCHASE_URL)); + box.getChildren().setAll(profileLink, birthLink, purchaseLink); + GridPane.setColumnSpan(box, 2); + + vbox.getChildren().setAll(usernameLabel, hintPane, box); + setBody(vbox); + } + + @Override + protected void onAccept() { + setLoading(); + + Task.supplyAsync(account::logInWhenCredentialsExpired) + .whenComplete(Schedulers.javafx(), authInfo -> { + success.accept(authInfo); + onSuccess(); + }, e -> { + LOG.log(Level.INFO, "Failed to login when credentials expired: " + account, e); + onFailure(Accounts.localizeErrorMessage(e)); + }).start(); + } + + @Override + protected void onCancel() { + failed.run(); + super.onCancel(); + } +} diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 4b62a54e9..3e17f9fd2 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -70,6 +70,7 @@ account.failed.invalid_token=Please log out and re-enter your password to login. account.failed.migration=Your account needs to be migrated to a Microsoft account. If already migrated, you should login your migrated Microsoft account instead. account.failed.no_character=No character in this account. account.failed.server_response_malformed=Invalid server response. The authentication server may have an error. +account.failed.wrong_account=Logged in with mismatched account. account.injector.add=Add an authentication server account.injector.empty=Empty (Click the plus button on the right to add) account.injector.http=Warning: This server uses HTTP so your password will be transmitted in clear text. @@ -79,6 +80,7 @@ account.injector.server_url=Server URL account.injector.server_name=Server Name account.login=Login account.login.hint=We will not save your password. +account.login.refresh=Re-log in account.logout=Logout account.register=Register account.manage=Account List diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index c5aa9b71f..11c3089b7 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -70,6 +70,7 @@ account.failed.invalid_token=請嘗試登出並重新輸入密碼登入 account.failed.migration=你的帳號需要被遷移至微軟帳號。如果你已經遷移,你需要使用微軟登錄方式登錄遷移後的微軟帳號。 account.failed.no_character=該帳戶沒有角色 account.failed.server_response_malformed=無法解析認證伺服器回應,可能是伺服器故障 +account.failed.wrong_account=登錄了錯誤的帳號 account.injector.add=新增認證伺服器 account.injector.empty=無 (按一下右側 + 加入) account.injector.http=警告: 此伺服器使用不安全的 HTTP 協定,您的密碼在登入時會被明文傳輸。 @@ -79,6 +80,7 @@ account.injector.server_url=伺服器位址 account.injector.server_name=伺服器名稱 account.login=登入 account.login.hint=我們不會保存你的密碼 +account.login.refresh=重新登錄 account.logout=登出 account.register=註冊 account.manage=帳戶列表 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 23f2a5e4c..81560785d 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -70,6 +70,7 @@ account.failed.invalid_token=请尝试登出并重新输入密码登录 account.failed.migration=你的帐号需要被迁移至微软帐号。如果你已经迁移,你需要使用微软登录方式登录迁移后的微软帐号。 account.failed.no_character=该帐号没有角色 account.failed.server_response_malformed=无法解析认证服务器响应,可能是服务器故障 +account.failed.wrong_account=登录了错误的帐号 account.injector.add=添加认证服务器 account.injector.empty=无(点击右侧加号添加) account.injector.http=警告:此服务器使用不安全的 HTTP 协议,您的密码在登录时会被明文传输。 @@ -79,6 +80,7 @@ account.injector.server_url=服务器地址 account.injector.server_name=服务器名称 account.login=登录 account.login.hint=我们不会保存你的密码 +account.login.refresh=重新登录 account.logout=登出 account.register=注册 account.manage=帐户列表 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/Account.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/Account.java index e94a649f3..26af74f32 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/Account.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/Account.java @@ -1,6 +1,6 @@ /* * Hello Minecraft! Launcher - * Copyright (C) 2020 huangyuhui and contributors + * Copyright (C) 2021 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 @@ -60,11 +60,6 @@ public abstract class Account implements Observable { */ public abstract AuthInfo logIn() throws AuthenticationException; - /** - * Login with specified password. - */ - public abstract AuthInfo logInWithPassword(String password) throws AuthenticationException; - /** * Play offline. * @return the specific offline player's info. diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/ClassicAccount.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/ClassicAccount.java new file mode 100644 index 000000000..b8e05ece3 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/ClassicAccount.java @@ -0,0 +1,29 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2021 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; + +public abstract class ClassicAccount extends Account { + + /** + * Login with specified password. + * + * When credentials expired, the auth server will ask you to login with password to refresh + * credentials. + */ + public abstract AuthInfo logInWithPassword(String password) throws AuthenticationException; +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/OAuthAccount.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/OAuthAccount.java new file mode 100644 index 000000000..f52ee4931 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/OAuthAccount.java @@ -0,0 +1,56 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2021 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; + +import java.util.UUID; + +public abstract class OAuthAccount extends Account { + + /** + * Fully login. + * + * OAuth server may ask us to do fully login because too frequent action to log in, IP changed, + * or some other vulnerabilities detected. + * + * Difference between logIn & logInWhenCredentialsExpired. + * logIn only update access token by refresh token, and will not ask user to login by opening the authorization + * page in web browser. + * logInWhenCredentialsExpired will open the authorization page in web browser, asking user to select an account + * (and enter password or PIN if necessary). + */ + public abstract AuthInfo logInWhenCredentialsExpired() throws AuthenticationException; + + public static class WrongAccountException extends AuthenticationException { + private final UUID expected; + private final UUID actual; + + public WrongAccountException(UUID expected, UUID actual) { + super("Expected account " + expected + ", but found " + actual); + this.expected = expected; + this.actual = actual; + } + + public UUID getExpected() { + return expected; + } + + public UUID getActual() { + return actual; + } + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftAccount.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftAccount.java index c79c403ca..0e114225b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftAccount.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftAccount.java @@ -33,7 +33,7 @@ import java.util.logging.Level; import static java.util.Objects.requireNonNull; import static org.jackhuang.hmcl.util.Logging.LOG; -public class MicrosoftAccount extends Account { +public class MicrosoftAccount extends OAuthAccount { protected final MicrosoftService service; protected UUID characterUUID; @@ -99,8 +99,11 @@ public class MicrosoftAccount extends Account { } @Override - public AuthInfo logInWithPassword(String password) throws AuthenticationException { + public AuthInfo logInWhenCredentialsExpired() throws AuthenticationException { MicrosoftSession acquiredSession = service.authenticate(); + if (!Objects.equals(characterUUID, acquiredSession.getProfile().getId())) { + throw new WrongAccountException(characterUUID, acquiredSession.getProfile().getId()); + } if (acquiredSession.getProfile() == null) { session = service.refresh(acquiredSession); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccount.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccount.java index 45af43baa..a4dd99eb6 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccount.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccount.java @@ -134,11 +134,6 @@ public class OfflineAccount extends Account { } } - @Override - public AuthInfo logInWithPassword(String password) throws AuthenticationException { - return logIn(); - } - @Override public Optional playOffline() throws AuthenticationException { return Optional.of(logIn()); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.java index 108d17adb..1b3c97664 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.java @@ -18,14 +18,7 @@ package org.jackhuang.hmcl.auth.yggdrasil; import javafx.beans.binding.ObjectBinding; -import org.jackhuang.hmcl.auth.Account; -import org.jackhuang.hmcl.auth.AuthInfo; -import org.jackhuang.hmcl.auth.AuthenticationException; -import org.jackhuang.hmcl.auth.CharacterDeletedException; -import org.jackhuang.hmcl.auth.CharacterSelector; -import org.jackhuang.hmcl.auth.CredentialExpiredException; -import org.jackhuang.hmcl.auth.NoCharacterException; -import org.jackhuang.hmcl.auth.ServerResponseMalformedException; +import org.jackhuang.hmcl.auth.*; import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter; import org.jackhuang.hmcl.util.javafx.BindingMapping; @@ -36,7 +29,7 @@ import java.util.logging.Level; import static java.util.Objects.requireNonNull; import static org.jackhuang.hmcl.util.Logging.LOG; -public class YggdrasilAccount extends Account { +public class YggdrasilAccount extends ClassicAccount { protected final YggdrasilService service; protected final UUID characterUUID;