8000 Fix openid scope loss on token refresh by himanshu7745 · Pull Request #52622 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content

Fix openid scope loss on token refresh - #52622

Merged
mposolda merged 8 commits into
keycloak:mainfrom
himanshu7745:fix/52574-openid-refresh-scope
Sep 16, 2026
Merged

mposolda merged 8 commits into
keycloak:mainfrom
himanshu7745:fix/52574-openid-refresh-scope

Conversation

@himanshu7745
Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes #52574, where the openid scope is lost from a refreshed access token when the refresh request specifies a narrower scope without openid.

For example:

Initial authorization:
scope=openid profile

Refresh request:
scope=profile

The refreshed access token should retain the openid scope.

Why is this needed?

The regression was introduced by b49a51bfd3, which introduced requestedScopeString to prevent scope values from being mixed between concurrent token requests.

During token refresh, requestedScopeString is populated from the narrowed oldTokenScope. When the refresh request contains only scope=profile, openid is removed from oldTokenScope.

DefaultClientSessionContext.getScopeString() then uses requestedScopeString to determine whether the request is an OIDC request. Since openid is no longer present, it is not added to the refreshed access token scope.

The original authorization scope is still available from clientSession.getNote(OAuth2Constants.SCOPE).

What changed?

Use clientSession.getNote(OAuth2Constants.SCOPE) for the OIDC scope check in DefaultClientSessionContext.getScopeString().

The existing requestedScopeString behavior is preserved for per-request scope processing and parameterized scope isolation.

A regression test was added to verify that openid is retained when refreshing a token with scope=profile.

Testing

The regression test covers:

  • Initial authorization with scope=openid profile
  • openid present in the initial access token
  • openid present in the initial refresh token
  • Refresh request containing only scope=profile
  • openid retained in the refreshed access token
  • UserInfo succeeding with the refreshed access token
  • openid retained in the refreshed refresh token

The existing parameterized scope isolation behavior is also verified to ensure the fix does not regress the changes introduced by #48955.

Closes keycloak#52574

Signed-off-by: Himanshu Mishra <mishrahimanshu0076@gmail.com>
@himanshu7745
himanshu7745 requested a review from a team as a code owner September 10, 2026 12:11
Copilot AI balanced review requested due to automatic review settings September 10, 2026 12:11
Copilot AI left 8000 a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings September 10, 2026 19:19
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings September 11, 2026 02:52
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

A critical scope-handling race remains, and the regression test lacks the initial refresh-token scope assertion.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (2)

services/src/main/java/org/keycloak/services/util/DefaultClientSessionContext.java:212

  • The same shared client-session note is used in the non-parameterized path, so concurrent/sequential authorization-code requests can likewise make a token's OIDC decision depend on whichever request updated the note last instead of the code's saved scope. Preserve requestedScopeString for code/token requests and special-case refresh handling rather than switching this branch to the mutable note.
        String scopeSent = clientSession.getNote(OAuth2Constants.SCOPE);
        if (TokenUtil.isOIDCRequest(scopeSent)) {
            scopeParam = TokenUtil.attachOIDCScope(scopeParam);

tests/base/src/test/java/org/keycloak/tests/oauth/RefreshTokenTest.java:784

  • The refreshed refresh token is also only checked for non-null, so the test does not verify that openid remains in its scope as claimed. Add a scope assertion on this parsed token.
            RefreshToken refreshedRefreshToken = oauth.parseRefreshToken(refreshResponse.getRefreshToken());
            assertNotNull(refreshedRefreshToken);
  • Files reviewed: 2/2 changed files
  • Comments generated: 2
  • Review effort level: Lite

Copilot AI review requested due to automatic review settings September 11, 2026 03:13
… DefaultClientSessionContext and verify with test assertions

Signed-off-by: Himanshu Mishra <mishrahimanshu0076@gmail.com>
@himanshu7745
himanshu7745 force-pushed the fix/52574-openid-refresh-scope branch from a90bacf to 3ba30a8 Compare September 11, 2026 03:16
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

OIDC detection still relies on a mutable client-session note and must use refresh-context state to avoid concurrent authorization races.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Lite

Copilot AI review requested due to automatic review settings September 11, 2026 03:17
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Use the original refresh-token scope instead of the mutable client-session note for the scope decision.

Review details

Suppressed comments (1)

services/src/main/java/org/keycloak/services/util/DefaultClientSessionContext.java:221

  • This fallback reintroduces the shared client-session note as the source of a per-token scope decision. TokenManager.attachAuthenticationSession() overwrites that note when another authorization request uses the same client session, so an older refresh token that was issued without openid can gain it after a later OIDC request—or lose it after a later non-OIDC request—despite oldRefreshToken.getScope() being the authoritative grant. Carry the original refresh-token scope into the context (before narrowing) and use that for this check instead of reading the mutable note.
        if (OAuth2Constants.REFRESH_TOKEN.equals(getAttribute(Constants.GRANT_TYPE, String.class))) {
            return TokenUtil.isOIDCRequest(clientSession.getNote(OAuth2Constants.SCOPE));
  • Files reviewed: 2/2 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

…ding test case

Signed-off-by: Himanshu Mishra <mishrahimanshu0076@gmail.com>
Copilot AI review requested due to automatic review settings September 11, 2026 03:51
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Extend the regression test to assert the refreshed ID token and its access-token hash.

Review details

Suppressed comments (1)

tests/base/src/test/java/org/keycloak/tests/oauth/RefreshTokenTest.java:823

  • This regression test exercises the new oldRefreshToken-based OIDC decision, but it only checks the access-token scope and UserInfo. If the change at AbstractRefreshTokenProvider.java:178 failed to generate the ID token (or its access-token hash) after the client-session scope note was overwritten, this test would still pass; assert the refreshed ID token and its access-token hash here as well.
            assertScopes("openid profile", refreshResponse.getScope());
  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

…refresh

Signed-off-by: Himanshu Mishra <mishrahimanshu0076@gmail.com>
Copilot AI review requested due to automatic review settings September 11, 2026 04:02
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The fix is covered by regression tests and no unresolved blocking issues were identified.

Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@mposolda mposolda self-assigned this Sep 14, 2026
Signed-off-by: Himanshu Mishra <mishrahimanshu0076@gmail.com>
Copilot AI review requested due to automatic review settings September 14, 2026 16:47
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The implementation and regression coverage address the scope-loss regression; the remaining documentation nit is non-blocking.

Review details

Suppressed comments (1)

services/src/main/java/org/keycloak/services/util/DefaultClientSessionContext.java:222

  • The PR description says this fallback uses clientSession.getNote(OAuth2Constants.SCOPE), but the implementation reads ORIGINAL_REFRESH_TOKEN_SCOPE, populated from oldRefreshToken.getScope(). That distinction is material—the latter is what lets the second regression test work after a later authorization overwrites the mutable client-session note—so please update the description to match the implementation (or change the implementation if note-based behavior is intended).
            return TokenUtil.isOIDCRequest(getAttribute(ORIGINAL_REFRESH_TOKEN_SCOPE, String.class));
  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new
  • Review effort level: Lite (auto)

Note

Copilot is running an experiment and ran this review at Lite.

Copilot AI review requested due to automatic review settings September 14, 2026 18:29
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The fix and regression coverage address the reported scope-loss issue without unresolved blocking concerns.

Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new
  • Review effort level: Lite (auto)

Note

Copilot is running an experiment and ran this review at Lite.

@mposolda mposolda left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@himanshu7745 Thanks!

@mposolda
mposolda merged commit 917d83b into keycloak:main Sep 16, 2026
94 checks passed
@himanshu7745
himanshu7745 deleted the fix/52574-openid-refresh-scope branch September 16, 2026 09:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

3 participants

0