Implement authorization server binding and credential isolation (MCP SEP-2352)#1474
Draft
Implement authorization server binding and credential isolation (MCP SEP-2352)#1474
Conversation
Agent-Logs-Url: https://github.com/modelcontextprotocol/csharp-sdk/sessions/101fdc47-ecb5-4163-829b-0b320592a4be Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
Agent-Logs-Url: https://github.com/modelcontextprotocol/csharp-sdk/sessions/101fdc47-ecb5-4163-829b-0b320592a4be Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
Copilot
AI
changed the title
[WIP] Update ClientOAuthProvider to comply with SEP-2352 requirements
Implement authorization server binding and credential isolation (MCP SEP-2352)
Mar 29, 2026
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.
MCP SEP-2352 adds MUST-level requirements for clients to maintain per-AS credential state and detect AS changes, preventing accidental reuse of credentials issued by one authorization server with a different one.
Core changes —
ClientOAuthProviderAS binding tracking
_boundAuthServerIssuer— records the issuer URI of the AS that issued current credentials_credentialsArePreRegistered— set at construction ifoptions.ClientIdis provided_isCimdClientId— marks client IDs from CIMD (self-hosted HTTPS URLs, portable across ASes)HandleAuthorizationServerChange(Uri currentIssuer)— called after fetching AS metadata, before any token refresh attempt:ClientIdMcpException(credentials are AS-specific)_clientId/_clientSecret/auth method → triggers re-registrationInvalidatableTokenCache— internal wrapper aroundITokenCachethat addsInvalidate(). ReturnsnullfromGetTokensAsyncwhen invalidated (reset on nextStoreTokensAsync). KeepsITokenCacheinterface stable._boundAuthServerIssueris updated only after a successful token acquisition (refresh or auth code flow).Tests —
AuthServerBindingTestsThree integration tests using
McpAuthenticationOptions.Events.OnResourceMetadataRequestto dynamically swap the reported AS URL, plus middleware to force re-authentication:AuthServerChange_WithPreRegisteredCredentials_ThrowsMcpExceptionAuthServerChange_WithDcrCredentials_TriggersReregistration— assertsdcrCallCount == 2AuthServerChange_WithCimdCredentials_ClearsTokensButKeepsPortableClientId— asserts CIMD URL appears asclient_idin the second auth requestOriginal prompt
Summary
The MCP specification PR modelcontextprotocol/modelcontextprotocol#2352 ("SEP-2352: Clarify authorization server binding and migration") has been merged. It adds new MUST/MUST NOT requirements for client OAuth implementations around authorization server binding and credential isolation. The C# SDK's
ClientOAuthProviderneeds to be updated to comply.Spec Requirements (from the merged spec change)
1. Per-AS credential isolation (MUST)
2. Authorization Server Binding (MUST)
3. Pre-registered credential mismatch (SHOULD)
4. CIMD portability
Current Gaps in the C# SDK
All relevant code is in
src/ModelContextProtocol.Core/Authentication/.Gap 1:
ClientOAuthProviderstores credentials as single instance fieldsThe fields
_clientId,_clientSecret,_tokenEndpointAuthMethod, and_authServerMetadataare single values with no association to a specific authorization server issuer. When a different AS is selected (or the AS changes), credentials from the old AS could be incorrectly reused.Fix: Track the issuer URI that credentials were obtained from. Before reusing
_clientId/_clientSecret, verify the current AS issuer matches the one that issued those credentials. If it doesn't match:_clientMetadataDocumentUriis set and was used), the client ID is portable — keep it, but discard tokens._clientId,_clientSecret, and_tokenEndpointAuthMethodso re-registration happens with the new AS.ClientIdinClientOAuthOptions), surface an error (throwMcpException) indicating the AS has changed and the pre-registered credentials are no longer valid for the new AS.Gap 2:
ITokenCacheand token storage are not keyed by AS issuerITokenCache.StoreTokensAsync/GetTokensAsynchave no concept of which authorization server the tokens belong to. This means tokens from one AS could be returned when a different AS is now in use.Fix: Since
ITokenCacheis a public interface and changing its signature would be a breaking change, the preferred approach is to haveClientOAuthProviderinternally invalidate/clear the token cache when an AS change is detected (before attempting to use cached tokens). This way theITokenCacheinterface stays stable, but the provider ensures tokens from a stale AS are not reused.Gap 3: No AS change detection
ClientOAuthProvider.GetAccessTokenAsyncdoes not compare the currently-selected authorization server against a previously-used one. There is no mechanism to detect that the AS has changed between calls.Fix: Store the issuer URI of the AS that was used for the most recent successful authentication. On subsequent calls to
GetAccessTokenAsync, after selecting the AS and fetching its metadata, compare the new AS issuer against the stored one. If they differ, trigger the credential invalidation logic described in Gap 1.Implementation Guidance
private Uri? _boundAuthServerIssuertoClientOAuthProviderto track which AS issuer the current credentials are bound to.private bool _credentialsFromPreRegistrationto distinguish pre-registered credentials from DCR-obtained ones. This can be determined at construction time: if `options....This pull request was created from Copilot chat.
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.