Skip to content

auth: refresh_token() requests offline_access that was never granted, so refreshes fail with invalid_scope #1330

Open

Description

@erkiaas

Summary

Since #897, AuthorizationManager::refresh_token() adds offline_access to the refresh request whenever the authorization server lists it in scopes_supported, even if the stored grant doesn't include it. An authorization server that didn't grant offline_access refuses that refresh with invalid_scope. The client then has to re-authorize every time the access token expires.

Where

crates/rmcp/src/transport/auth.rs L2309-L2313:

let mut refresh_scopes = stored_credentials.granted_scopes.clone();
self.add_offline_access_if_supported(&mut refresh_scopes);
let requested_scopes = refresh_scopes.clone();
for scope in refresh_scopes {
    refresh_request = refresh_request.add_scope(Scope::new(scope));

The test added in #897, refresh_token_adds_offline_access_when_as_supports_it, expects a refresh of a grant holding only read to send offline_access read.

Why this breaks

RFC 6749 §6: the refresh request's scope "MUST NOT include any scope not originally granted by the resource owner". The authorization server is right to answer invalid_scope.

Requesting offline_access at authorization time doesn't guarantee it's granted. OIDC Core §11 requires prompt=consent alongside offline_access "unless other conditions for processing the request permitting offline access … are in place", and rmcp adds the scope without the prompt. node-oidc-provider, for one, drops offline_access from the grant by default. An OP can still issue a refresh token without the scope (node-oidc-provider allows this through its issueRefreshToken hook), and SEP-2207's own "No guarantee" item allows for that. Every refresh rmcp then sends is rejected:

POST /token
grant_type=refresh_token&refresh_token=…&scope=openid+profile+email+offline_access

HTTP/1.1 400 Bad Request
{"error":"invalid_scope","error_description":"refresh token missing requested scope","scope":"offline_access"}

SEP-2207, which #676/#897 implement, only covers authorization requests. Client guidance item 2: the client "MAY add the offline_access scope to the list of scopes from the resource server before making authorization requests to the Authorization Server". It doesn't extend that to refresh requests.

Reproduction

  1. Run an authorization server that strips offline_access without prompt=consent (node-oidc-provider 9.x does by default), still issues refresh tokens to the client (for node-oidc-provider, an issueRefreshToken that returns true for clients allowed the refresh_token grant), and lists offline_access in scopes_supported.
  2. Authorize with rmcp. The token response's scope doesn't include offline_access, but a refresh token is issued.
  3. Let the access token expire, or call refresh_token(). The token endpoint returns invalid_scope for offline_access.

Seen in the wild with Codex CLI 0.155.0 and 0.161.0 (codex-mcp-client, rmcp 3.3.0). Restricting Codex's configured scopes doesn't help: the login requests only the configured scopes, but every refresh still gets offline_access added. Still present on main (08e0211).

Suggested fix

Don't add offline_access on the refresh path. Send the stored granted_scopes, or omit scope entirely, which RFC 6749 §6 defines as "equal to the scope originally granted". If the grant does include offline_access, it's already in granted_scopes, so nothing is lost. #897's scope-upgrade change is a re-authorization, so SEP-2207 does cover it and it can stay.

Activity

  1. added
    bugSomething is not working
    P1High: significant functionality gap or spec violation
    T-securitySecurity-related changes
    T-transportTransport layer changes
    ready for workIssue is well-defined and ready to be picked up
    on Oct 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High: significant functionality gap or spec violationT-securitySecurity-related changesT-transportTransport layer changesbugSomething is not workingready for workIssue is well-defined and ready to be picked up

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions