feat(account): OAuth account relogin.

This commit is contained in:
huanghongxun 2021-10-12 22:00:41 +08:00
parent 948d64237e
commit 23c2c0689c
13 changed files with 209 additions and 43 deletions

View File

@ -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 {

View File

@ -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<AuthInfo> 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<AuthInfo> 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();
}

View File

@ -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<AuthInfo> 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<AuthInfo> success, Runnable failed) {
public ClassicAccountLoginDialog(ClassicAccount oldAccount, Consumer<AuthInfo> success, Runnable failed) {
this.oldAccount = oldAccount;
this.success = success;
this.failed = failed;

View File

@ -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<AuthInfo> success;
private final Runnable failed;
private final BooleanProperty logging = new SimpleBooleanProperty();
public OAuthAccountLoginDialog(OAuthAccount account, Consumer<AuthInfo> 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();
}
}

View File

@ -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

View File

@ -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=帳戶列表

View File

@ -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=帐户列表

View File

@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> 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.

View File

@ -0,0 +1,29 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
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;
}

View File

@ -0,0 +1,56 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
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;
}
}
}

View File

@ -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);

View File

@ -134,11 +134,6 @@ public class OfflineAccount extends Account {
}
}
@Override
public AuthInfo logInWithPassword(String password) throws AuthenticationException {
return logIn();
}
@Override
public Optional<AuthInfo> playOffline() throws AuthenticationException {
return Optional.of(logIn());

View File

@ -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;