modelcontextprotocol / modelcontextprotocol/csharp-sdk
Cached-credential reuse via ClientOAuthOptions.ClientId fails: no way to specify token_endpoint_auth_method
@halter73 is already working on this.
Since Jun 22, 2026.
- Dominant language
- C#
- Stars
- 4.5k
- Forks
- 814
- Avg merge
- 9d 19h
- Merged PRs (30d)
- 4
Description
Summary
When an MCP client persists the client_id / client_secret returned from Dynamic Client Registration and then provides them via ClientOAuthOptions.ClientId / ClientSecret on a subsequent session (to skip DCR), the token exchange step fails with 401 Unauthorized against authorization servers whose token_endpoint_auth_methods_supported does not list "client_secret_post" first.
ClientOAuthOptions has no TokenEndpointAuthMethod setting, so there's no supported way to tell the SDK which method the client was registered with — the SDK falls back to the AS metadata's first supported method, which may not match how the client was registered.
Prior discussion
This failure mode was originally flagged as conformance test auth/pre-registration in #1177, which was closed by #1254 ("Bring up to 0.1.13 conformance"). PR #1254 introduced ClientOAuthOptions.ClientId / ClientSecret, but the matching TokenEndpointAuthMethod was not exposed — so reusing DCR-acquired credentials still fails at token exchange on ASes that don't list client_secret_post first in their metadata. Reproducible on 1.3.0.
Reproduction
-
Configure a client against an AS (e.g. WorkOS AuthKit) whose
token_endpoint_auth_methods_supportedlistsnonebeforeclient_secret_post:"token_endpoint_auth_methods_supported": ["none", "client_secret_post", "client_secret_basic"] -
First run — let the SDK do DCR:
OAuth = new ClientOAuthOptions { RedirectUri = new Uri("http://localhost:5556/callback"), AuthorizationRedirectDelegate = ..., DynamicClientRegistration = new DynamicClientRegistrationOptions { ClientName = "Sample MCP Client", ResponseDelegate = (response, ct) => { // persist response.ClientId and response.ClientSecret to disk return Task.CompletedTask; }, }, }DCR succeeds, MCP
initializesucceeds, tool calls succeed. ✅ -
Second run — read cached credentials from disk and pass them back:
OAuth = new ClientOAuthOptions { ClientId = cachedClientId, ClientSecret = cachedClientSecret, RedirectUri = new Uri("http://localhost:5556/callback"), AuthorizationRedirectDelegate = ..., // No DynamicClientRegistration — we want to skip DCR }Token exchange fails:
System.Net.Http.HttpRequestException: Response status code does not indicate success: 401 (Unauthorized). Response body: {"error":"unauthorized"}
Root cause
In ClientOAuthProvider.cs:655, PerformDynamicClientRegistrationAsync hardcodes the DCR request's TokenEndpointAuthMethod:
var registrationRequest = new DynamicClientRegistrationRequest
{
// ...
TokenEndpointAuthMethod = "client_secret_post",
// ...
};
So every DCR'd client is registered as client_secret_post (confidential). The provider stores this on its private _tokenEndpointAuthMethod field when DCR completes — but only in memory for the current session.
On a subsequent session where ClientOAuthOptions.ClientId is set (skipping DCR), the provider's _tokenEndpointAuthMethod starts null and falls back via:
_tokenEndpointAuthMethod ??= authServerMetadata.TokenEndpointAuthMethodsSupported?.FirstOrDefault();
For an AS that lists none first (e.g. WorkOS AuthKit), the provider selects "none". CreateTokenRequest then takes the none branch:
else if (string.Equals(_tokenEndpointAuthMethod, "none", StringComparison.Ordinal))
{
// Public client: include client_id in the body but no secret.
formFields["client_id"] = clientId;
}
…and sends client_id only — no client_secret. The AS rejects with 401 because the registered client expects client_secret_post. The cached ClientSecret we worked to capture is never used.
Expected behavior
Reusing a DCR'd client via cached credentials should work without falling out of the supported configuration surface. Either:
- The SDK persists / accepts the auth method alongside
ClientId/ClientSecret, or - The DCR step doesn't hardcode
client_secret_post(e.g., negotiates or defaults tononefor PKCE-only flows).
Suggested fix
Add TokenEndpointAuthMethod to ClientOAuthOptions:
public sealed class ClientOAuthOptions
{
// ...existing properties...
/// <summary>
/// Gets or sets the token endpoint authentication method to use when <see cref="ClientId"/>
/// is supplied externally (e.g. from a persisted DCR registration). When omitted, the SDK
/// falls back to the authorization server's metadata.
/// </summary>
public string? TokenEndpointAuthMethod { get; set; }
}
Initialize ClientOAuthProvider._tokenEndpointAuthMethod from this option in the constructor. The existing fallback to AS metadata stays as a default.
Environment
- SDK:
ModelContextProtocol/ModelContextProtocol.AspNetCore1.3.0 - Authorization server: WorkOS AuthKit
- .NET 10.0
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.