-
Notifications
You must be signed in to change notification settings - Fork 830
迁移微软授权代码流 client_secret 到 PKCE
#5575
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1dfbb11
c0da4d3
20a7580
3dac94a
8fd626c
78c9241
e247143
6965e0c
1385ce9
3a27464
4606367
87111ba
a042318
e5264e2
def58f9
c7f3312
62e906b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,12 +24,16 @@ | |
| import org.jackhuang.hmcl.util.io.NetworkUtils; | ||
|
|
||
| import java.io.IOException; | ||
| import java.nio.charset.StandardCharsets; | ||
| import java.security.MessageDigest; | ||
| import java.util.Base64; | ||
| import java.util.Map; | ||
| import java.util.concurrent.ExecutionException; | ||
| import java.util.concurrent.TimeUnit; | ||
|
|
||
| import static org.jackhuang.hmcl.util.Lang.mapOf; | ||
| import static org.jackhuang.hmcl.util.Pair.pair; | ||
| import static org.jackhuang.hmcl.util.logging.Logger.LOG; | ||
|
|
||
| public class OAuth { | ||
| public static final OAuth MICROSOFT = new OAuth( | ||
|
|
@@ -77,17 +81,31 @@ public Result authenticate(GrantFlow grantFlow, Options options) throws Authenti | |
|
|
||
| private Result authenticateAuthorizationCode(Options options) throws IOException, InterruptedException, JsonParseException, ExecutionException, AuthenticationException { | ||
| Session session = options.callback.startServer(); | ||
|
|
||
| String codeVerifier = session.getCodeVerifier(); | ||
| String state = session.getState(); | ||
| String codeChallenge = generateCodeChallenge(codeVerifier); | ||
|
|
||
| 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("state", state), | ||
| pair("code_challenge_method", "S256") | ||
| ))); | ||
|
Comment on lines
89
to
98
|
||
| String code = session.waitFor(); | ||
|
|
||
| // Authorization Code -> Token | ||
| AuthorizationResponse response = HttpRequest.POST(accessTokenURL) | ||
| .form(pair("client_id", options.callback.getClientId()), pair("code", code), | ||
| pair("grant_type", "authorization_code"), pair("client_secret", options.callback.getClientSecret()), | ||
| pair("redirect_uri", session.getRedirectURI()), pair("scope", options.scope)) | ||
| .form(pair("client_id", options.callback.getClientId()), | ||
| pair("code", code), | ||
| pair("grant_type", "authorization_code"), | ||
| pair("code_verifier", codeVerifier), | ||
| pair("redirect_uri", session.getRedirectURI()), | ||
| pair("scope", options.scope)) | ||
| .ignoreHttpCode() | ||
| .retry(5) | ||
| .getJson(AuthorizationResponse.class); | ||
|
|
@@ -153,10 +171,6 @@ public Result refresh(String refreshToken, Options options) throws Authenticatio | |
| pair("grant_type", "refresh_token") | ||
| ); | ||
|
|
||
| if (!options.callback.isPublicClient()) { | ||
| query.put("client_secret", options.callback.getClientSecret()); | ||
| } | ||
|
|
||
| RefreshResponse response = HttpRequest.POST(tokenURL) | ||
| .form(query) | ||
| .accept("application/json") | ||
|
|
@@ -174,6 +188,20 @@ public Result refresh(String refreshToken, Options options) throws Authenticatio | |
| } | ||
| } | ||
|
|
||
| private static String generateCodeChallenge(String codeVerifier) { | ||
| // https://datatracker.ietf.org/doc/html/rfc7636#section-4.2 | ||
| 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); | ||
| } | ||
|
Comment on lines
+193
to
+202
|
||
| } | ||
|
|
||
| private static void handleErrorResponse(ErrorResponse response) throws AuthenticationException { | ||
| if (response.error == null || response.errorDescription == null) { | ||
| return; | ||
|
|
@@ -207,6 +235,9 @@ public Options setUserAgent(String userAgent) { | |
| } | ||
|
|
||
| public interface Session { | ||
| String getState(); | ||
|
|
||
| String getCodeVerifier(); | ||
|
|
||
| String getRedirectURI(); | ||
|
|
||
|
|
@@ -243,10 +274,6 @@ public interface Callback { | |
| void openBrowser(GrantFlow grantFlow, String url) throws IOException; | ||
|
|
||
| String getClientId(); | ||
|
|
||
| String getClientSecret(); | ||
|
|
||
| boolean isPublicClient(); | ||
| } | ||
|
|
||
| public enum GrantFlow { | ||
|
|
||
There was a problem hiding this comment.
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
statenonce stored on the session and validated inserve()before completing the future. Adding astatefield here (and exposing it viaOAuth.Session, e.g.getState()) would allow the authorize URL to includestateand the callback to reject mismatches.