Skip to content

fix(auth): refresh with granted scopes - #1331

Open
jstar0 wants to merge 1 commit into
modelcontextprotocol:mainfrom
jstar0:fix/1330-preserve-granted-refresh-scopes
Open

jstar0 wants to merge 1 commit into
modelcontextprotocol:mainfrom
jstar0:fix/1330-preserve-granted-refresh-scopes

Conversation

@jstar0

@jstar0 jstar0 commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Send only the scopes recorded in the stored grant on OAuth refresh. Authorization and 403 scope-upgrade requests continue to add offline_access when supported.

Fixes #1330

Testing

  • cargo test -p rmcp --features "auth transport-io" passed: 417 unit tests and 39 doctests; 9 doctests and 4 live Keycloak tests were ignored as configured.
  • cargo +nightly fmt --all -- --check passed.
  • cargo clippy -p rmcp --features auth --lib passed with three existing dead_code warnings.

@jstar0
jstar0 requested a review from a team as a code owner October 9, 2026 00:01
@github-actions github-actions Bot added T-core Core library changes T-transport Transport layer changes labels Oct 9, 2026
@jstar0

jstar0 commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

All relevant checks now pass on head 18f9856. Could a maintainer review the OAuth refresh-scope fix when convenient? I am happy to address any feedback.

@DaleSeo DaleSeo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I left a couple of small suggestions. Nothing blocking.

Comment on lines +9086 to +9088
let mut scope_parts: Vec<&str> = scope.split_whitespace().collect();
scope_parts.sort_unstable();
assert_eq!(scope_parts, vec!["read"]);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The test token server leaves scope out of its response, so resolve_granted_scopes falls back to requested_scopes. That means the old code also saved the ungranted offline_access scope in granted_scopes, which caused every later refresh to fail. Can we assert what gets saved to the credential store too?

Comment on lines +2309 to 2312
let requested_scopes = stored_credentials.granted_scopes.clone();
for scope in requested_scopes.iter().cloned() {
refresh_request = refresh_request.add_scope(Scope::new(scope));
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

granted_scopes is cloned into a new Vec, and then each element is cloned again. So the resolve_granted_scopes call below can take requested_scopes directly instead of &requested_scopes.

Suggested change
let requested_scopes = stored_credentials.granted_scopes.clone();
for scope in requested_scopes.iter().cloned() {
refresh_request = refresh_request.add_scope(Scope::new(scope));
}
let requested_scopes = &stored_credentials.granted_scopes;
refresh_request =
refresh_request.add_scopes(requested_scopes.iter().cloned().map(Scope::new));

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-core Core library changes T-transport Transport layer changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants