Skip to content

Comments

迁移微软授权代码流 client_secret 到 PKCE #5575

Merged
Glavo merged 17 commits intoHMCL-dev:mainfrom
CiiLu:pkce
Feb 21, 2026
Merged

迁移微软授权代码流 client_secret 到 PKCE #5575
Glavo merged 17 commits intoHMCL-dev:mainfrom
CiiLu:pkce

Conversation

@CiiLu
Copy link
Contributor

@CiiLu CiiLu commented Feb 19, 2026

resolves #5566

https://learn.microsoft.com/zh-cn/entra/identity-platform/v2-oauth2-auth-code-flow

请不要在本机应用或单页应用中使用应用程序机密,因为 client_secret 无法可靠地存储在设备上。

兑换授权代码时,公共客户端(包括本机应用程序和单页应用)不得使用机密或证书。

目前建议将参数(code_challenge)用于所有应用程序类型(公共和机密客户端)

需要黄鱼在 entra 进行一些配置

@CiiLu CiiLu changed the title 迁移微软授权代码流到 PKCE 迁移微软授权代码流 client_secret 到 PKCE Feb 19, 2026
@CiiLu CiiLu marked this pull request as ready for review February 19, 2026 14:38
@huanghongxun
Copy link
Collaborator

更新后,Microsoft Auth Server 是如何验证 client 是 HMCL?我看请求参数内只传递 client_id,不再传递 client_secret 了。
看起来生成codeVerifier和应用本身没啥关系?

@CiiLu
Copy link
Contributor Author

CiiLu commented Feb 19, 2026

看起来生成codeVerifier和应用本身没啥关系?

是。

本地应用不能安全保存 client_secret,所以不靠 secret 认证,而是用 PKCE。Authorization Code 会和 code_challenge 绑定,只有能提供匹配 code_verifier 的一方才能换到 token。

这里目的不是用来证明 client 就是 HMCL,而是用来证明当前换 token 的 client 是最初发起请求的 client ,目的是解决 Authorization Code 被截获后被他人兑换的问题。

@huanghongxun
Copy link
Collaborator

需要在entra上配置什么呢?client_secret不需要的话,已经不需要再换客户端证书了吧?

@CiiLu
Copy link
Contributor Author

CiiLu commented Feb 20, 2026

image

这个位置 ,HMCL 现在使用的应该是 Web / 单页应用程序 ,是需要 secret 的,换成 移动和桌面应用程序 应该就可以使用 PKCE 了。

可能需要注意一下旧版本兼容问题。

@huanghongxun
Copy link
Collaborator

所以是给移动桌面应用程序分类,将原来Web的RedirectURI重新添加一遍?

@CiiLu
Copy link
Contributor Author

CiiLu commented Feb 21, 2026

是,但是这样会导致旧版本在登录时直接报错(目前的问题是在之后刷新时报错)
image
所以本 PR 中同时更改了端口,目前为这几个 URI
http://localhost:29712/auth-response
http://localhost:29713/auth-response
http://localhost:29714/auth-response
http://localhost:29715/auth-response
http://localhost:29716/auth-response
需要保留旧版本登录兼容性的话原来的 Web URI 不用删除,只需要再 移动和桌面应用程序 添加这几个就行

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

将微软 OAuth 授权码流程从依赖 client_secret 迁移到 PKCE(S256),以符合公共客户端最佳实践并避免刷新时必须携带机密导致的失败(#5566)。

Changes:

  • 在授权码登录流程中加入 PKCE:生成 code_challenge(S256)并在换取 token 时提交 code_verifier
  • 刷新 token 请求中移除 client_secret 相关逻辑,并同步删除构建时注入的 secret 配置
  • 在本地回调服务器 OAuthServer 中生成并保存 code_verifier

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
HMCLCore/src/main/java/org/jackhuang/hmcl/auth/OAuth.java 授权码流程增加 PKCE 参数;刷新流程移除 client_secret;新增 generateCodeChallengeSession#getCodeVerifier
HMCL/src/main/java/org/jackhuang/hmcl/game/OAuthServer.java 生成并提供 PKCE code_verifier(通过 OAuth.Session
HMCL/build.gradle.kts 删除微软授权 secret 的环境变量/属性注入

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 88 to 96
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"))));
mapOf(pair("client_id", options.callback.getClientId()),
pair("response_type", "code"),
pair("redirect_uri", session.getRedirectURI()),
pair("scope", options.scope),
pair("prompt", "select_account"),
pair("code_challenge", codeChallenge),
pair("code_challenge_method", "S256")
)));
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Authorization code flow adds PKCE, but the request still omits the OAuth state parameter. Without generating and validating state, the localhost callback can be vulnerable to CSRF/login-swapping (accepting a code not initiated by this client). Consider generating a random state per auth attempt, include it in the authorize URL, and verify it in the callback handler before accepting the code.

Copilot uses AI. Check for mistakes.
Comment on lines 48 to 72
@@ -52,6 +55,20 @@ private OAuthServer(int port) {
super(port);

this.port = port;
this.codeVerifier = generateCodeVerifier();
}

// https://datatracker.ietf.org/doc/html/rfc7636#section-4.1
private String generateCodeVerifier() {
SecureRandom sr = new SecureRandom();
byte[] code = new byte[64];
sr.nextBytes(code);
return Base64.getUrlEncoder().withoutPadding().encodeToString(code);
}

@Override
public String getCodeVerifier() {
return codeVerifier;
}
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PKCE verifier is generated and stored, but there is still no per-request state nonce stored on the session and validated in serve() before completing the future. Adding a state field here (and exposing it via OAuth.Session, e.g. getState()) would allow the authorize URL to include state and the callback to reject mismatches.

Copilot uses AI. Check for mistakes.

// https://datatracker.ietf.org/doc/html/rfc7636#section-4.1
private String generateCodeVerifier() {
SecureRandom sr = new SecureRandom();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

为什么这里两个随机数用两种风格的写法?另外 generateStategenerateCodeVerifier 是不是共享一个 SecureRandom 实例更好?

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +193 to +202
try {
byte[] bytes = codeVerifier.getBytes(StandardCharsets.US_ASCII);
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
messageDigest.update(bytes, 0, bytes.length);
byte[] digest = messageDigest.digest();
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
} catch (Exception e) {
LOG.warning("Failed to generate code challenge", e);
throw new RuntimeException(e);
}
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generateCodeChallenge catches a broad Exception, logs, and then throws a generic RuntimeException. Since this is deterministic code (SHA-256 should always be available), consider narrowing the catch to NoSuchAlgorithmException (or treating it as an AssertionError/IllegalStateException) so unexpected runtime errors aren’t accidentally swallowed and rethrown with less context.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 126 to 128
if (!this.state.equals(query.get("state")))
future.completeExceptionally(new AuthenticationException(
query.containsKey("state") ? "invalid state" : "missing state"));
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code continues executing after state validation fails. When the state doesn't match, the code should return early instead of continuing to process the request. Currently, after calling future.completeExceptionally, the code continues to lines 130-136 which may call future.complete() or future.completeExceptionally() again, causing unpredictable behavior. Add a return statement after the state validation block to prevent further execution when validation fails.

Suggested change
if (!this.state.equals(query.get("state")))
future.completeExceptionally(new AuthenticationException(
query.containsKey("state") ? "invalid state" : "missing state"));
if (!this.state.equals(query.get("state"))) {
future.completeExceptionally(new AuthenticationException(
query.containsKey("state") ? "invalid state" : "missing state"));
return newFixedLengthResponse(Response.Status.BAD_REQUEST, MIME_PLAINTEXT,
query.containsKey("state") ? "invalid state" : "missing state");
}

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@Glavo Glavo merged commit 917d110 into HMCL-dev:main Feb 21, 2026
2 checks passed
@CiiLu CiiLu deleted the pkce branch February 22, 2026 15:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] 无法刷新微软账户

3 participants