Repository navigation
Conversation
11d7b49 to
7194f1e
Compare
|
|
||
| effective_scope = resolve_scope(scope: scope, prm: prm) | ||
| effective_scope = normalize_offline_access_scope(effective_scope, as_metadata: as_metadata) | ||
| effective_scope = select_scope(effective_scope, as_metadata: as_metadata) |
There was a problem hiding this comment.
One design question before the detailed review.
scope_selector receives the final list, so it cannot tell a challenged scope from a default one. On a 403 step-up, an allowlist selector that leaves out the challenged scope makes the flow re-authorize for what is already granted, and the retry fails with 403 again. Without a callback_handler, each such request ends in AuthorizationPendingError, so the user is redirected in a loop.
The spec treats the two sources differently: challenged scopes are authoritative for the operation, while scopes_supported is a default. I would like the selector to cover only that default:
- Called only when the challenge names no scope, with PRM
scopes_supported. It returns a subset, and[]requests none of them. - Challenged scopes skip it.
authorization_request_validatoralready accepts or refuses them. offline_accessstays controlled bygrant_types. The step-up union and the endpoint URL's query are left as they are.
That keeps each hook to one job, and a narrow contract can be widened after release while a wide one cannot be narrowed.
It does not let a host narrow the scopes of a 401 challenge, or drop offline_access through the hook. If your use case needs either of those, could you describe it? If I'm misunderstanding anything here, please feel free to correct me.
There was a problem hiding this comment.
Agreed. The selector now narrows only unchallenged PRM defaults, with subset-only results. Challenged scopes and the step-up union bypass it; the validator accepts or refuses them. offline_access retains the existing grant_types policy. I removed the selector-specific endpoint cleanup, preserving the existing endpoint-query behavior.
[] means no PRM scopes, not necessarily no scope parameter. The docs and examples now describe that narrower contract.
Why
The MCP 2026-07-28 scope-selection strategy recommends the challenge's
scope, then PRMscopes_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_validatorcan 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 PRMscopes_supportedwhen the SDK selects those defaults instead of a challenged scope, and returns an array containing a subset;[]requests none of those defaults.nilresults and custom additions are refused. Challenged scopes and the step-up union bypass it, and provider fallback is unchanged. It runs beforeoffline_accessaugmentation, authorization-request validation and client registration. The existingoffline_accesspolicy and endpoint-query handling are unchanged; unsupportedoffline_accessremains stripped. Returning[]does not guarantee an omittedscopeparameter or prevent the AS from applying defaults (RFC 6749 §3.3).With no selector, the SDK's spec-aligned default and other OAuth grants are unchanged. This follows the application-policy hook pattern in the C# SDK and Go SDK.
Scope selection, illustrated
These examples assume no challenged scope, a Protected Resource Metadata (PRM) document advertising
read write admin, and an authorization server (AS) supportingoffline_accesswith a client declaring therefresh_tokengrant. The authorization-request validator shown here is optional.Default versus application policy
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 addsoffline_access. It does not prevent AS defaults.Placement in a hosted OAuth flow
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. The selector does not change endpoint-query handling: a nonempty effective scope replaces a prefilled scope, while a prefilled scope survives when the flow has none.Validation
git diff --checkpass.