Skip to content

feat: re-check the app allowlist at OAuth2 code redemption - #28740

Draft
BobbyHo wants to merge 2 commits into
plat480-1-apply-negotiated-scopefrom
plat480-2-stale-scope-recheck
Draft

feat: re-check the app allowlist at OAuth2 code redemption#28740
BobbyHo wants to merge 2 commits into
plat480-1-apply-negotiated-scopefrom
plat480-2-stale-scope-recheck

Conversation

@BobbyHo

@BobbyHo BobbyHo commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

TL;DR

Second of the PLAT-480 stack, stacked on #28237. #28237 made the negotiated scope bound the issued token; this PR makes an administrator's narrowing of an app's registered scopes apply to codes that were already issued.

PR What it does
#28237 Mints access tokens with the negotiated scope, so a grant for one scope stops behaving as full access, and reports the granted scope per RFC 6749 §5.1.
#28740 (this) Re-checks the code's scope against the app's registered scopes at redemption, closing the ten minute window in which a narrowing was ignored.
#28744 Makes code redemption single-use under concurrency. Carries no scope logic and is independent of this PR.

An authorization code is valid for ten minutes, and its scope is checked against the app's registered scopes exactly once, when the code is issued. An administrator who narrowed the registration inside that window had the narrowing ignored: the code still redeemed for a token carrying the wider scope.

  • The token exchange re-checks the code's scope against the registration as it stands at redemption and refuses with invalid_scope, per RFC 6749 §5.2.
  • The check is coverage rather than membership, and reuses the comparison the authorize endpoint already runs instead of a second copy of it.
  • Refresh keeps the scope originally granted, per RFC 6749 §6, so a narrowing takes effect at the next authorization rather than mid-session.

Depends on #28237

Stack: #28237, this PR, #28744.

An authorization code's scope is checked against the app's registered
scopes when the code is issued, and the code stays valid for ten minutes
after that. An admin who narrows the registration in that window had the
narrowing ignored: the code still minted a token carrying the wider
scope.

authorizationCodeGrant now re-checks the code's stored scope against the
registration as it stands at redemption, and refuses with invalid_scope
when it is no longer covered. The catalog filter and the coverage loop
move out of negotiateScope into grantableScopes and
firstScopeOutsideAllowlist so both sides run the same comparison; each
caller picks its own rejection reason.

Refresh is deliberately left alone. RFC 6749 section 6 bounds a refresh
by the scope originally granted, so a narrowing takes effect at the next
authorization instead of dropping capability from a live session.
@github-actions

Copy link
Copy Markdown
Contributor

Docs preview

Check off each page once it's been reviewed. If a page changes in a later push, its checkbox clears automatically so it gets a fresh look. Pages not yet wired into the docs navigation aren't listed here.

Drop what the code already says and keep the parts that are not visible
from it: the canonical precondition on firstScopeOutsideAllowlist, why
refresh is exempt, and why the stored scope is canonicalized again.

ReasonStaleScope moves into the existing export block in
authorize_internal_test.go rather than repeating its comment.
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.

1 participant