Skip to content

Add a scope selector for OAuth authorization-code clients - #579

Open
atesgoral wants to merge 1 commit into
modelcontextprotocol:mainfrom
atesgoral:ag/oauth-explicit-empty-scope
Open

atesgoral wants to merge 1 commit into
modelcontextprotocol:mainfrom
atesgoral:ag/oauth-explicit-empty-scope

Conversation

@atesgoral

@atesgoral atesgoral commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Why

The MCP 2026-07-28 scope-selection strategy recommends the challenge's scope, then PRM scopes_supported (intended as a minimal basic set). The Ruby SDK follows that default. Some hosted clients need a narrower per-user policy, including a first authorization request with no PRM defaults; authorization_request_validator can refuse but cannot change the selected scopes.

Fixes #578.

What changes

Add optional Provider.new(scope_selector:) for authorization-code clients. It receives a read-only array of valid PRM scopes_supported tokens when the SDK selects those defaults instead of a challenged scope, and returns an array containing a subset; [] requests none of those defaults. Malformed PRM tokens raise Flow::AuthorizationError before the callback; nil results and custom additions raise ArgumentError. One Hash membership index avoids repeated candidate-list scans. Challenged scopes and the step-up union bypass the selector, and provider fallback is unchanged. It runs before offline_access augmentation, authorization-request validation and client registration. The existing offline_access policy is unchanged; unsupported offline_access remains stripped from the flow's resolved scope. Returning [] does not guarantee an omitted scope parameter or prevent the authorization server from applying defaults (RFC 6749 Section 3.3).

With no selector, the SDK's spec-aligned scope selection and other OAuth grants are unchanged. This follows the application-policy hook pattern in the C# SDK and Go SDK.

Before client registration, the authorization-request validator sees the explicit scopes that will actually be sent, including a single prefilled endpoint scope when the flow has none. Endpoint query names are decoded once and reused by policy and URL assembly. Repeated scope parameters that would survive are rejected before validation/registration, even without a selector or validator; a nonempty flow scope still replaces endpoint scopes normally. This constrains the client request, not the authorization server's final grant.

Scope selection, illustrated

These examples assume no challenged scope, a Protected Resource Metadata (PRM) document advertising read write admin, and an authorization server (AS) supporting offline_access with a client declaring the refresh_token grant. The authorization-request validator shown here is optional.

Default versus application policy

PRM-only selection followed by endpoint query preparation and validation of outgoing scopes before registration

The hook changes what the client requests, not what the AS grants. Returning [] requests no PRM defaults; in this example, the existing refresh-token policy still adds offline_access. It does not prevent AS defaults.

Placement in a hosted OAuth flow

Hosted OAuth sequence preparing endpoint scopes before validator approval, registration and browser redirect

This uses the existing two-leg OAuth flow from PR 573. The scope selector is the addition here; callback persistence and Flow#finish! are existing behavior. The host must inspect the actual granted scopes. Endpoint scope handling is prepared before policy approval: a nonempty flow scope replaces prefills; a single surviving endpoint scope is shown to the validator. Repeated scopes that would survive fail before registration.

Validation

  • All 11 OAuth test files pass locally: 441 tests, 1,452 assertions.
  • The full client suite, including OAuth, passes locally: 744 tests, 2,374 assertions.
  • Ruby syntax checks and git diff --check pass.
  • Direct Ruby RuboCop passes for the five changed Ruby files; no dependency or native-extension rebuild.

@atesgoral atesgoral changed the title Let OAuth clients select only explicit scopes Add a scope selector for OAuth authorization-code clients Sep 28, 2026
@atesgoral
atesgoral force-pushed the ag/oauth-explicit-empty-scope branch from 11d7b49 to 7194f1e Compare October 1, 2026 19:39
@atesgoral
atesgoral marked this pull request as ready for review October 1, 2026 19:45
@atesgoral
atesgoral requested a review from koic October 1, 2026 19:48
Comment thread lib/mcp/client/oauth/flow.rb Outdated
Comment thread docs/_client/authorization.md Outdated
This hook does not control `offline_access` augmentation or alter authorization-endpoint query parameters. Returning `[]`
does not guarantee an omitted `scope` parameter: refresh policy can add `offline_access`, and a prefilled endpoint scope
survives when the flow has no scope of its own. The AS can also apply defaults or reject the request
([RFC 6749 §3.3](https://www.rfc-editor.org/rfc/rfc6749#section-3.3)); inspect the granted scopes.

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.

Nit: the rest of this page cites sections as "RFC 6749 Section 3.3" and spells out "authorization server" rather than "AS" (lines 111 and 114); § is also the only non-ASCII character in the docs source.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 3b9e57d, thanks!

@atesgoral
atesgoral force-pushed the ag/oauth-explicit-empty-scope branch from c5a7ce9 to 3b9e57d Compare October 7, 2026 15:54
@atesgoral
atesgoral force-pushed the ag/oauth-explicit-empty-scope branch from 3b9e57d to 75c6c24 Compare October 7, 2026 18:22
@atesgoral

Copy link
Copy Markdown
Contributor Author

Updated in 75c6c24. The selector is still PRM-only/subset-only. The validator now sees any surviving endpoint scope before registration; surviving repeated scope parameters are rejected. Subset checks use one hash index, and malformed PRM candidates fail before the callback. Tests, docs and diagrams are updated.

@atesgoral
atesgoral requested a review from koic October 8, 2026 03:11

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Let OAuth clients filter requested scopes

2 participants