feat: 使用授权代码流登录微软帐户 (#5330)

This commit is contained in:
CiiLu
2026-02-10 00:41:12 +08:00
committed by Glavo
parent 33dc39e59e
commit b7fbebae43
15 changed files with 533 additions and 243 deletions

View File

@@ -22,7 +22,7 @@ import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.OAuth;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
@@ -37,8 +37,8 @@ import java.util.concurrent.ExecutionException;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
private final int port;
@@ -104,8 +104,11 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
String html;
try {
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"))
.replace("%style%", Themes.getTheme().toColorScheme().toStyleSheet().replace("-monet", "--monet"))
.replace("%lang%", Locale.getDefault().toLanguageTag())
.replace("%close-page%", i18n("account.methods.microsoft.close_page"));
.replace("%success%", i18n("message.success"))
.replace("%ok%", i18n("button.ok"))
.replace("%close_page%", i18n("account.methods.microsoft.close_page"));
} catch (IOException e) {
LOG.error("Failed to load html", e);
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_HTML, "");
@@ -123,7 +126,8 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
public static class Factory implements OAuth.Callback {
public final EventManager<GrantDeviceCodeEvent> onGrantDeviceCode = new EventManager<>();
public final EventManager<OpenBrowserEvent> onOpenBrowser = new EventManager<>();
public final EventManager<OpenBrowserEvent> onOpenBrowserAuthorizationCode = new EventManager<>();
public final EventManager<OpenBrowserEvent> onOpenBrowserDevice = new EventManager<>();
@Override
public OAuth.Session startServer() throws IOException, AuthenticationException {
@@ -150,17 +154,13 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
}
@Override
public void openBrowser(String url) throws IOException {
public void openBrowser(OAuth.GrantFlow grantFlow, String url) throws IOException {
lastlyOpenedURL = url;
try {
Thread.sleep(1500);
} catch (InterruptedException ignored) {
switch (grantFlow) {
case AUTHORIZATION_CODE -> onOpenBrowserAuthorizationCode.fireEvent(new OpenBrowserEvent(this, url));
case DEVICE -> onOpenBrowserDevice.fireEvent(new OpenBrowserEvent(this, url));
}
FXUtils.openLink(url);
onOpenBrowser.fireEvent(new OpenBrowserEvent(this, url));
}
@Override

View File

@@ -19,7 +19,7 @@ package org.jackhuang.hmcl.ui;
import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.ui.account.ClassicAccountLoginDialog;
import org.jackhuang.hmcl.ui.account.OAuthAccountLoginDialog;
import org.jackhuang.hmcl.ui.account.MicrosoftAccountLoginPane;
import java.util.Optional;
import java.util.concurrent.CancellationException;
@@ -49,10 +49,10 @@ public final class DialogController {
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<AuthInfo> res = new AtomicReference<>(null);
runInFX(() -> {
OAuthAccountLoginDialog pane = new OAuthAccountLoginDialog((OAuthAccount) account, it -> {
MicrosoftAccountLoginPane pane = new MicrosoftAccountLoginPane(account, it -> {
res.set(it);
latch.countDown();
}, latch::countDown);
}, latch::countDown, false);
Controllers.dialog(pane);
});
latch.await();

View File

@@ -130,7 +130,7 @@ public final class AccountListPage extends DecoratorAnimatedPage implements Deco
microsoftItem.getStyleClass().add("navigation-drawer-item");
microsoftItem.setTitle(i18n("account.methods.microsoft"));
microsoftItem.setLeftIcon(SVG.MICROSOFT);
microsoftItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_MICROSOFT)));
microsoftItem.setOnAction(e -> Controllers.dialog(new MicrosoftAccountLoginPane()));
AdvancedListItem offlineItem = new AdvancedListItem();
offlineItem.getStyleClass().add("navigation-drawer-item");

View File

@@ -23,9 +23,7 @@ import javafx.application.Platform;
import javafx.beans.NamedArg;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -45,7 +43,6 @@ import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccountFactory;
import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.game.OAuthServer;
import org.jackhuang.hmcl.game.TexturesLoader;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.task.Schedulers;
@@ -54,7 +51,6 @@ import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.upgrade.IntegrityChecker;
import org.jackhuang.hmcl.util.StringUtils;
@@ -85,13 +81,11 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
private final JFXButton btnAccept;
private final SpinnerPane spinner;
private final Node body;
private final HBox actions;
private Node detailsPane; // AccountDetailsInputPane for Offline / Mojang / authlib-injector, Label for Microsoft
private final Pane detailsContainer;
private final BooleanProperty logging = new SimpleBooleanProperty();
private final ObjectProperty<OAuthServer.GrantDeviceCodeEvent> deviceCode = new SimpleObjectProperty<>();
private final WeakListenerHolder holder = new WeakListenerHolder();
private TaskExecutor loginTask;
@@ -146,10 +140,10 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
btnCancel.setOnAction(e -> onCancel());
onEscPressed(this, btnCancel::fire);
HBox hbox = new HBox(spinner, btnCancel);
hbox.setAlignment(Pos.CENTER_RIGHT);
actions = new HBox(spinner, btnCancel);
actions.setAlignment(Pos.CENTER_RIGHT);
setActions(lblErrorMessage, hbox);
setActions(lblErrorMessage, actions);
}
if (showMethodSwitcher) {
@@ -226,7 +220,6 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
Runnable doCreate = () -> {
logging.set(true);
deviceCode.set(null);
loginTask = Task.supplyAsync(() -> factory.create(new DialogCharacterSelector(), username, password, null, additionalData))
.whenComplete(Schedulers.javafx(), account -> {
@@ -281,88 +274,22 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
detailsContainer.getChildren().remove(detailsPane);
lblErrorMessage.setText("");
lblErrorMessage.setVisible(true);
actions.setVisible(true);
actions.setVisible(true);
}
if (factory == Accounts.FACTORY_MICROSOFT) {
VBox vbox = new VBox(8);
detailsPane = vbox;
if (Accounts.OAUTH_CALLBACK.getClientId().isEmpty()) {
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.WARNING);
hintPane.setSegment(i18n("account.methods.microsoft.snapshot"));
vbox.getChildren().add(hintPane);
return;
}
if (!IntegrityChecker.isOfficial()) {
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.WARNING);
hintPane.setSegment(i18n("unofficial.hint"));
vbox.getChildren().add(hintPane);
}
VBox codeBox = new VBox(8);
Label hint = new Label(i18n("account.methods.microsoft.code"));
Label code = new Label();
code.setMouseTransparent(true);
code.setStyle("-fx-font-size: 24");
codeBox.getChildren().addAll(hint, code);
codeBox.setAlignment(Pos.CENTER);
vbox.getChildren().add(codeBox);
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
HintPane errHintPane = new HintPane(MessageDialogPane.MessageType.ERROR);
errHintPane.setVisible(false);
errHintPane.setManaged(false);
codeBox.setVisible(false);
codeBox.setManaged(false);
FXUtils.onChangeAndOperate(deviceCode, deviceCode -> {
if (deviceCode != null) {
FXUtils.copyText(deviceCode.getUserCode());
code.setText(deviceCode.getUserCode());
hintPane.setSegment(i18n("account.methods.microsoft.manual", deviceCode.getVerificationUri()));
codeBox.setVisible(true);
codeBox.setManaged(true);
} else {
hintPane.setSegment(i18n("account.methods.microsoft.hint"));
codeBox.setVisible(false);
codeBox.setManaged(false);
}
hintPane.setText(i18n("account.methods.microsoft.hint"));
vbox.getChildren().addAll(new MicrosoftAccountLoginPane(true));
btnAccept.setOnAction(e -> {
fireEvent(new DialogCloseEvent());
Controllers.dialog(new MicrosoftAccountLoginPane());
});
lblErrorMessage.setVisible(false);
lblErrorMessage.textProperty().addListener((obs, oldVal, newVal) -> {
boolean hasError = !newVal.isEmpty();
errHintPane.setSegment(newVal);
errHintPane.setVisible(hasError);
errHintPane.setManaged(hasError);
hintPane.setVisible(!hasError);
hintPane.setManaged(!hasError);
codeBox.setVisible(!hasError && deviceCode.get() != null);
codeBox.setManaged(!hasError && deviceCode.get() != null);
});
FXUtils.onClicked(codeBox, () -> {
if (deviceCode.get() != null) FXUtils.copyText(deviceCode.get().getUserCode());
});
holder.add(Accounts.OAUTH_CALLBACK.onGrantDeviceCode.registerWeak(value ->
runInFX(() -> deviceCode.set(value))
));
HBox linkBox = new HBox();
JFXHyperlink profileLink = new JFXHyperlink(i18n("account.methods.microsoft.profile"));
profileLink.setExternalLink("https://account.live.com/editprof.aspx");
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.microsoft.purchase"));
purchaseLink.setExternalLink(YggdrasilService.PURCHASE_URL);
JFXHyperlink deauthorizeLink = new JFXHyperlink(i18n("account.methods.microsoft.deauthorize"));
deauthorizeLink.setExternalLink("https://account.live.com/consent/Edit?client_id=000000004C794E0A");
JFXHyperlink forgotpasswordLink = new JFXHyperlink(i18n("account.methods.forgot_password"));
forgotpasswordLink.setExternalLink("https://account.live.com/ResetPassword.aspx");
linkBox.getChildren().setAll(profileLink, purchaseLink, deauthorizeLink, forgotpasswordLink);
vbox.getChildren().addAll(hintPane, errHintPane, linkBox);
actions.setManaged(false);
actions.setVisible(false);
btnAccept.setDisable(false);
} else {
detailsPane = new AccountDetailsInputPane(factory, btnAccept::fire);

File diff suppressed because one or more lines are too long

View File

@@ -1,109 +0,0 @@
package org.jackhuang.hmcl.ui.account;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
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.OAuthServer;
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.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.DialogPane;
import org.jackhuang.hmcl.ui.construct.HintPane;
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.util.logging.Logger.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 ObjectProperty<OAuthServer.GrantDeviceCodeEvent> deviceCode = new SimpleObjectProperty<>();
private final WeakListenerHolder holder = new WeakListenerHolder();
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);
FXUtils.onChangeAndOperate(deviceCode, deviceCode -> {
if (deviceCode != null) {
FXUtils.copyText(deviceCode.getUserCode());
hintPane.setSegment(
"<b>" + i18n("account.login.refresh.microsoft.hint") + "</b>\n"
+ i18n("account.methods.microsoft.manual", deviceCode.getUserCode(), deviceCode.getVerificationUri())
);
} else {
hintPane.setSegment(
"<b>" + i18n("account.login.refresh.microsoft.hint") + "</b>\n"
+ i18n("account.methods.microsoft.hint")
);
}
});
FXUtils.onClicked(hintPane, () -> {
if (deviceCode.get() != null) {
FXUtils.copyText(deviceCode.get().getUserCode());
}
});
HBox box = new HBox(8);
JFXHyperlink birthLink = new JFXHyperlink(i18n("account.methods.microsoft.birth"));
birthLink.setExternalLink("https://support.microsoft.com/account-billing/how-to-change-a-birth-date-on-a-microsoft-account-837badbc-999e-54d2-2617-d19206b9540a");
JFXHyperlink profileLink = new JFXHyperlink(i18n("account.methods.microsoft.profile"));
profileLink.setExternalLink("https://account.live.com/editprof.aspx");
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.microsoft.purchase"));
purchaseLink.setExternalLink(YggdrasilService.PURCHASE_URL);
box.getChildren().setAll(profileLink, birthLink, purchaseLink);
GridPane.setColumnSpan(box, 2);
vbox.getChildren().setAll(usernameLabel, hintPane, box);
setBody(vbox);
holder.add(Accounts.OAUTH_CALLBACK.onGrantDeviceCode.registerWeak(this::onGrantDeviceCode));
}
private void onGrantDeviceCode(OAuthServer.GrantDeviceCodeEvent event) {
FXUtils.runInFX(() -> {
deviceCode.set(event);
});
}
@Override
protected void onAccept() {
setLoading();
Task.supplyAsync(account::logInWhenCredentialsExpired)
.whenComplete(Schedulers.javafx(), (authInfo, exception) -> {
if (exception == null) {
success.accept(authInfo);
onSuccess();
} else {
LOG.info("Failed to login when credentials expired: " + account, exception);
onFailure(Accounts.localizeErrorMessage(exception));
}
}).start();
}
@Override
protected void onCancel() {
failed.run();
super.onCancel();
}
}

View File

@@ -572,6 +572,36 @@
-fx-text-fill: -monet-on-surface;
}
/*******************************************************************************
* *
* Microsoft Login Dialog *
* *
******************************************************************************/
.method-title {
-fx-text-fill: -monet-on-surface;
-fx-font-weight: bold;
}
.method-desc {
-fx-text-fill: -monet-outline;
-fx-font-size: 0.9em;
-fx-line-spacing: 2;
-fx-wrap-text: true;
-fx-text-alignment: center;
}
.code-box {
-fx-background-color: -monet-surface-variant;
-fx-background-radius: 6;
-fx-alignment: center;
}
.code-box .code-label {
-fx-font-size: 22px;
-fx-font-weight: bold;
-fx-text-fill: -monet-primary;
}
/*******************************************************************************
* *

View File

@@ -118,11 +118,14 @@ account.methods.microsoft.error.wrong_verify_method=Failed to log in. Please try
account.methods.microsoft.logging_in=Logging in...
account.methods.microsoft.makegameidsettings=Create Profile / Edit Profile Name
account.methods.microsoft.hint=Click the "Log in" button to start adding your Microsoft account.
account.methods.microsoft.manual=Please enter the code shown above on the pop-up webpage to complete the login.\n\
\n\
If the website fails to load, please open %s manually in your browser.\n\
\n\
<b>If your internet connection is bad, it may cause web pages to load slowly or fail to load altogether. You may try again later or switch to a different internet connection.</b>
account.methods.microsoft.methods.or=Or
account.methods.microsoft.methods.device=Log in on another device
account.methods.microsoft.methods.device.hint=Scan the QR code on another device, or visit \n %s \n to complete log in
account.methods.microsoft.methods.device.copy=Copy code
account.methods.microsoft.methods.browser=Log in via browser (Recommended)
account.methods.microsoft.methods.browser.copy_open=Copy link and visit in browser
account.methods.microsoft.methods.browser.hint=Click the following button to copy and open the browser to log in
account.methods.microsoft.manual=<b>If your internet connection is bad, it may cause web pages to load slowly or fail to load altogether.\nYou may try again later or switch to a different internet connection.</b>
account.methods.microsoft.profile=Account Profile
account.methods.microsoft.purchase=Buy Minecraft
account.methods.microsoft.snapshot=You are using an unofficial build of HMCL. Please download the <a href="https://hmcl.huangyuhui.net/download">official build</a> to log in.

View File

@@ -117,9 +117,14 @@ account.methods.microsoft.error.wrong_verify_method=登入失敗。請在 Micros
account.methods.microsoft.logging_in=登入中……
account.methods.microsoft.makegameidsettings=建立檔案 / 編輯檔案名稱
account.methods.microsoft.hint=點擊「登入」按鈕開始新增 Microsoft 帳戶。
account.methods.microsoft.manual=請在彈出的網頁中輸入上方顯示的代碼以完成登入。\n\
若網站未能顯示,請手動在瀏覽器中開啟:%s\n\
<b>若網路環境不佳,可能會導致網頁載入緩慢甚至無法載入,請稍後再試或更換網路環境後再試。</b>\n
account.methods.microsoft.methods.or=
account.methods.microsoft.methods.device=在其他裝置登入
account.methods.microsoft.methods.device.hint=在其他裝置掃描 QR Code或開啟 \n %s \n 完成登入
account.methods.microsoft.methods.device.copy=複製代碼
account.methods.microsoft.methods.browser=在瀏覽器登入 (推薦)
account.methods.microsoft.methods.browser.copy_open=複製連結並在瀏覽器中開啟
account.methods.microsoft.methods.browser.hint=點擊下方按鈕複製連結並開啟瀏覽器以登入
account.methods.microsoft.manual=<b>若網路環境不佳,可能會導致網頁載入緩慢甚至無法載入,請稍後再試或更換網路環境後再試。</b>
account.methods.microsoft.profile=編輯帳戶個人資訊
account.methods.microsoft.purchase=購買 Minecraft
account.methods.forgot_password=忘記密碼

View File

@@ -118,9 +118,14 @@ account.methods.microsoft.error.wrong_verify_method=登录失败。请在微软
account.methods.microsoft.logging_in=登录中……
account.methods.microsoft.makegameidsettings=创建档案 / 编辑档案名称
account.methods.microsoft.hint=点击“登录”按钮开始添加微软账户。
account.methods.microsoft.manual=请在弹出的网页中输入上方显示的代码以完成登录。\n\
若网站未能显示,请手动在浏览器中打开:%s\n\
<b>若网络环境不佳,可能会导致网页加载缓慢甚至无法加载,请使用网络代理并重试。</b>\n\
account.methods.microsoft.methods.or=
account.methods.microsoft.methods.device=在其他设备登录
account.methods.microsoft.methods.device.hint=在其他设备扫描二维码,或打开 \n %s \n 完成登录
account.methods.microsoft.methods.device.copy=复制代码
account.methods.microsoft.methods.browser=在浏览器登录 (推荐)
account.methods.microsoft.methods.browser.copy_open=复制链接并打开浏览器
account.methods.microsoft.methods.browser.hint=点击下方按钮复制链接并打开浏览器登录
account.methods.microsoft.manual=<b>若网络环境不佳,可能会导致网页加载缓慢甚至无法加载,请使用网络代理并重试。</b>\n\
如遇到问题,你可以点击右上角帮助按钮进行求助。
account.methods.microsoft.profile=编辑账户个人信息
account.methods.microsoft.purchase=购买 Minecraft

View File

@@ -19,20 +19,134 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<html lang="%lang%">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello Minecraft! Launcher</title>
<style>
%style%
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: var(--monet-surface);
color: var(--monet-on-surface);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
user-select: none;
cursor: default;
}
.dialog-card {
background-color: var(--monet-surface-container-high);
padding: 24px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.14), 0 0 0 1px rgba(0, 0, 0, 0.02);
max-width: 560px;
width: 90%;
display: flex;
flex-direction: row;
gap: 16px;
}
.icon-column {
flex-shrink: 0;
padding-top: 4px;
}
.icon-column svg {
width: 40px;
height: 40px;
display: block;
}
.content-column {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
.title {
font-size: 20px;
line-height: 1.4;
font-weight: 500;
color: var(--monet-on-surface);
margin-bottom: 8px;
}
.subtitle {
font-size: 14px;
line-height: 1.5;
color: var(--monet-on-surface-variant);
word-wrap: break-word;
margin-bottom: 24px;
}
.actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.btn-confirm {
background-color: transparent;
color: var(--monet-primary);
border: none;
padding: 0 16px;
height: 40px;
font-size: 14px;
font-weight: 500;
font-family: inherit;
cursor: pointer;
transition: background-color 0.2s;
outline: none;
min-width: 64px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.btn-confirm:hover {
background-color: rgba(128, 128, 128, 0.08);
}
.btn-confirm:active {
background-color: rgba(128, 128, 128, 0.12);
}
</style>
</head>
<body>
<div>
%close-page%
<div class="dialog-card">
<div class="icon-column">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="m10.6 16.6l7.05-7.05l-1.4-1.4l-5.65 5.65l-2.85-2.85l-1.4 1.4zM12 22q-2.075 0-3.9-.788t-3.175-2.137T2.788 15.9T2 12t.788-3.9t2.137-3.175T8.1 2.788T12 2t3.9.788t3.175 2.137T21.213 8.1T22 12t-.788 3.9t-2.137 3.175t-3.175 2.138T12 22m0-2q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4T6.325 6.325T4 12t2.325 5.675T12 20m0-8"/>
</svg>
</div>
<script>
setTimeout(function() {
open("about:blank","_self").close();
}, 5000);
</script>
</body>
<div class="content-column">
<div class="title">%success%</div>
<div class="subtitle">%close_page%</div>
<div class="actions">
<button class="btn-confirm" onclick="closeWindow()">%ok%</button>
</div>
</div>
</div>
<script>
function closeWindow() {
window.close();
window.open("about:blank", "_self").close();
}
setTimeout(closeWindow, 3000);
</script>
</body>
</html>

View File

@@ -77,7 +77,7 @@ public class OAuth {
private Result authenticateAuthorizationCode(Options options) throws IOException, InterruptedException, JsonParseException, ExecutionException, AuthenticationException {
Session session = options.callback.startServer();
options.callback.openBrowser(NetworkUtils.withQuery(authorizationURL,
options.callback.openBrowser(GrantFlow.AUTHORIZATION_CODE, NetworkUtils.withQuery(authorizationURL,
mapOf(pair("client_id", options.callback.getClientId()), pair("response_type", "code"),
pair("redirect_uri", session.getRedirectURI()), pair("scope", options.scope),
pair("prompt", "select_account"))));
@@ -106,7 +106,7 @@ public class OAuth {
options.callback.grantDeviceCode(deviceTokenResponse.userCode, deviceTokenResponse.verificationURI);
// Microsoft OAuth Flow
options.callback.openBrowser(deviceTokenResponse.verificationURI);
options.callback.openBrowser(GrantFlow.DEVICE, deviceTokenResponse.verificationURI);
long startTime = System.nanoTime();
long interval = TimeUnit.MILLISECONDS.convert(deviceTokenResponse.interval, TimeUnit.SECONDS);
@@ -237,9 +237,10 @@ public class OAuth {
/**
* Open browser
*
* @param grantFlow the grant flow.
* @param url OAuth url.
*/
void openBrowser(String url) throws IOException;
void openBrowser(GrantFlow grantFlow, String url) throws IOException;
String getClientId();

View File

@@ -47,10 +47,10 @@ public final class MicrosoftAccount extends OAuthAccount {
this.characterUUID = requireNonNull(session.getProfile().getId());
}
protected MicrosoftAccount(MicrosoftService service, CharacterSelector characterSelector) throws AuthenticationException {
protected MicrosoftAccount(MicrosoftService service, OAuth.GrantFlow flow) throws AuthenticationException {
this.service = requireNonNull(service);
MicrosoftSession acquiredSession = service.authenticate();
MicrosoftSession acquiredSession = service.authenticate(flow);
if (acquiredSession.getProfile() == null) {
session = service.refresh(acquiredSession);
} else {
@@ -105,7 +105,7 @@ public final class MicrosoftAccount extends OAuthAccount {
@Override
public AuthInfo logInWhenCredentialsExpired() throws AuthenticationException {
MicrosoftSession acquiredSession = service.authenticate();
MicrosoftSession acquiredSession = service.authenticate(OAuth.GrantFlow.DEVICE);
if (!Objects.equals(characterUUID, acquiredSession.getProfile().getId())) {
throw new WrongAccountException(characterUUID, acquiredSession.getProfile().getId());
}

View File

@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.auth.microsoft;
import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.CharacterSelector;
import org.jackhuang.hmcl.auth.OAuth;
import java.util.Map;
import java.util.Objects;
@@ -39,9 +40,7 @@ public class MicrosoftAccountFactory extends AccountFactory<MicrosoftAccount> {
@Override
public MicrosoftAccount create(CharacterSelector selector, String username, String password, ProgressCallback progressCallback, Object additionalData) throws AuthenticationException {
Objects.requireNonNull(selector);
return new MicrosoftAccount(service, selector);
return new MicrosoftAccount(service, (OAuth.GrantFlow) additionalData);
}
@Override

View File

@@ -69,9 +69,9 @@ public class MicrosoftService {
return profileRepository;
}
public MicrosoftSession authenticate() throws AuthenticationException {
public MicrosoftSession authenticate(OAuth.GrantFlow flow) throws AuthenticationException {
try {
OAuth.Result result = OAuth.MICROSOFT.authenticate(OAuth.GrantFlow.DEVICE, new OAuth.Options(SCOPE, callback));
OAuth.Result result = OAuth.MICROSOFT.authenticate(flow, new OAuth.Options(SCOPE, callback));
return authenticateViaLiveAccessToken(result.getAccessToken(), result.getRefreshToken());
} catch (IOException e) {
throw new ServerDisconnectException(e);