SEP-2352: Clarify authorization server binding and migration#2352
Merged
Conversation
Add guidance for clients on binding persisted client credentials to specific authorization servers and handling AS changes. Clarifies multi-AS behavior when multiple servers are listed in authorization_servers. Closes #1349 Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
pcarleton
reviewed
Mar 5, 2026
pcarleton
left a comment
Member
There was a problem hiding this comment.
This is a good change. Both typescript and python will need to adjustments.
Should be straightforward to conformance test as well, and key it on DCR support.
KevinLuo2000
reviewed
Mar 5, 2026
Co-authored-by: Kaixuan Luo <[email protected]>
Co-authored-by: Kaixuan Luo <[email protected]>
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
3 tasks
max-stytch
reviewed
Mar 6, 2026
Co-authored-by: Max Gerber <[email protected]>
wdawson
reviewed
Mar 6, 2026
Co-authored-by: Wils Dawson <[email protected]>
Co-authored-by: Wils Dawson <[email protected]>
Co-authored-by: Wils Dawson <[email protected]>
|
I am implementing this SEP in the Go SDK and would like to clarify a couple of cases regarding client credential management:
|
This was referenced May 26, 2026
5 tasks
This was referenced Jun 6, 2026
Closed
Open
Closed
9 tasks
9 tasks
8 tasks
9 tasks
9 tasks
koic
added a commit
to modelcontextprotocol/ruby-sdk
that referenced
this pull request
Jul 11, 2026
… SEP-2352 ## Motivation and Context SEP-2352 (modelcontextprotocol/modelcontextprotocol#2352, merged for the 2026-07-28 spec release) clarifies authorization server binding and migration: clients MUST key persisted client credentials by the issuing authorization server's issuer identifier, MUST NOT reuse credentials across different authorization servers, and MUST re-register when the server's authorization server changes. CIMD `client_id`s (HTTPS URLs) are explicitly portable across authorization servers, and a mismatch SHOULD surface an error rather than silently use mismatched credentials. The Ruby flow previously reused any stored `client_information` with a `client_id` unconditionally, so a server that migrated to a new authorization server would have the old server's credentials (and its dead tokens) replayed forever. This change follows the detect-change, invalidate, and re-register approach that both reference SDKs converged on (typescript-sdk#2348/#2358, merged 2026-06-24; python-sdk#2933/#2936/#2946, which uses the same proactive compare-before-reuse design as this change) while keeping the 4-method storage contract intact by stamping the binding into the persisted hash: - Successful Dynamic Client Registration now persists the credentials with an `"issuer"` member set to the validated AS metadata `issuer` (which `ensure_issuer_matches!` has already pinned to the discovery URL). - `Flow#ensure_client_registered` reuses stored credentials only when the stored issuer matches the current one. Credentials without an issuer binding (data persisted by older SDK versions, or user-supplied pre-registered credentials) are bound to the current issuer on first use. A portable CIMD `client_id` is reused and re-bound. Any other issuer mismatch discards the stale registration and its tokens (tokens minted by the old AS are dead at the new one) and re-registers via CIMD/DCR. - `Flow#refresh!` raises `AuthorizationError` instead of replaying another authorization server's credentials at the token endpoint; `MCP::Client::HTTP#attempt_refresh` already rescues that error and falls back to the full flow, which performs the discard-and-re-register. - The `client_credentials` grant (machine-to-machine) reads static pre-registered credentials without going through `ensure_client_registered`, so it would have silently sent another authorization server's credentials after a migration. `run_client_credentials!` now surfaces an `AuthorizationError` when the stored `issuer` mismatches the current authorization server, implementing the spec's "SHOULD surface an error" (the TypeScript SDK raises `AuthorizationServerMismatchError` here). Re-registration is not an option for pre-registered m2m credentials, so the operator must update the stored credentials; credentials without a recorded issuer keep working unchanged. Two adjacent hardenings from the reference implementations were checked and need no change here: the legacy-fallback issuer stamp is self-consistent because this SDK's fallback registers at the same origin it uses as the issuer (the cross-origin never-reached-AS binding python-sdk#2946 closed cannot arise), and preserving a non-rotated `refresh_token` across refreshes already exists via `preserve_refresh_token`. The TypeScript SDK's redirected-to-issuer callback gate is not adopted: the RFC 9207 `iss` validation (SEP-2468) covers that mix-up leg. Resolves #385. ## How Has This Been Tested? New tests in `test/mcp/client/oauth/flow_test.rb`: - stored credentials with a matching issuer skip DCR - an issuer mismatch re-registers and persists the new `client_id` with the new issuer - the stale registration's tokens are cleared before re-registration (observed by aborting the flow at the DCR step) - legacy credentials without an issuer (string- and symbol-keyed) are reused and bound to the current issuer on first use - a CIMD `client_id` with a mismatched issuer is reused without DCR and re-bound - fresh DCR persists the issuer binding - `refresh!` succeeds with a matching stored issuer and raises (without contacting the token endpoint) with a mismatched one - the `client_credentials` grant succeeds with a matching stored issuer and raises (without contacting the token endpoint) with a mismatched one All existing flow, HTTP OAuth, and provider tests pass unchanged. ## Breaking Changes None for the documented storage contract: `client_information` hashes gain an additional `"issuer"` member that storages persist opaquely, and credentials stored without one keep working via first-use binding. Behavior changes only in the previously broken case where the authorization server changed, which now re-registers (or, for static machine-to-machine credentials, surfaces an error) instead of replaying the old server's credentials.
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.
Summary
Test plan
npx prettier --checkpassesnpm run check:docs:js-commentspasses🤖 Generated with Claude Code