feat: let an OAuth2 refresh narrow the granted scope - #28751
Draft
BobbyHo wants to merge 1 commit into
Draft
Conversation
RFC 6749 section 6 bounds a refresh by the scope originally granted, so the scope parameter may only give authority up. A refresh that names a scope now mints and persists that narrower scope, and later refreshes are bounded by it in turn; an omitted parameter keeps the grant as it stands. The comparison is coverage rather than membership, matching negotiateScope, so a grant of coder:workspaces.access can narrow to workspace:ssh and an unrestricted coder:all grant can narrow at all. A request beyond the grant is refused with invalid_scope before anything is read or issued, leaving the refresh token usable.
Contributor
Docs previewCheck 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. |
This was referenced Aug 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TL;DR
First of three in PLAT-481, the last phase of PLAT-470.
POST /oauth2/tokensparsed the refresh request'sscopeand discarded it, so a client asking for less was silently handed the broader token it tried to give up. A refresh may now narrow, and may still not widen.coder:all.scope. It is minted, persisted, and stated back per RFC 6749 §5.1.coder:allcovers every scope by wildcard but is a member of no set but its own, and every app without a configured allowlist holds exactly that grant, so those clients could never ask for less. Coverage also reuses the authorize-side comparison rather than introducing a third.scopestill keeps the grant as it stands.The narrowing (
narrowGrantedScope,tokens.go)rbac.ExpandScope, fail there, and tell a client that typo'd a scope that the server could not decide, rather than that the name is unknown.APIKeyScopes, soscopeStringToAPIKeyScopesconverts forward forapikey.Generateand nothing converts backward. TherbacScopesToStringshelper the plan called for is not needed.GetAPIKeyByID, so a rejection costs no further reads and issues nothing. The refresh token stays redeemable.firstScopeOutsideAllowlistbecomesfirstScopeNotCoveredand takes its ceiling explicitly. On refresh the ceiling is the token's own grant, so anapp_scopelog field would have named something the comparison never read.Tests
TestNarrowGrantedScope: a genuine subset, a composite narrowed to a constituent it covers,coder:allnarrowed to a real scope (the case coverage exists for), an expansion rejected, an unknown name rejected as unknown rather than undecidable, an omitted request left untouched.RefreshNarrowsTheScopeasserts all three of the minted key, the persistedoauth2_provider_app_tokens.scopecolumn and the response scope. The next refresh inherits that column, so a widened one would undo the narrowing.RefreshCannotWidenTheScopeasserts the 400 and that the original token is still redeemable.ResponseStatesTheScopeGrantedpins RFC 6749 §5.1 for both an omitted-scope exchange and a narrowed refresh, which no test asserted on purpose before.Docs: the Limitations bullet saying a client cannot narrow on refresh is now false and is deleted; the token-endpoint
invalid_scopesection documents the narrowing and both rejection wordings.Stack: #28237, #28740, #28744, this PR.