modelcontextprotocol / modelcontextprotocol/csharp-sdk

Can't implement a CIMD client if auth provider doesn't put "none" first in it's list of supported token auth methods

Open
#1,612 1 comment 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

area-auth bug P2
Dominant language
C#
Stars
4.5k
Forks
814
Avg merge
9d 19h
Merged PRs (30d)
4

Description

Describe the bug

When authenticating as a public client identified by a Client ID Metadata Document (CIMD), ClientOAuthProvider can choose the wrong token-endpoint authentication method and the token exchange fails. This is the case when using Auth0 as an authentication provider.

The auth method is selected in GetAccessTokenAsync with:

_tokenEndpointAuthMethod ??= authServerMetadata.TokenEndpointAuthMethodsSupported?.FirstOrDefault();

Since it just takes the first method the authorization server advertises and ClientOAuthOptions exposes no way to override the method, if you don't have control over the authentication servers response, you can't get a token.

A CIMD client is a public client and must authenticate with none (proven via PKCE). But when the authorization server lists client_secret_basic ahead of none — as Auth0 does, advertising ["client_secret_basic","client_secret_post","private_key_jwt","none"], the provider picks client_secret_basic. CreateTokenRequest then sends the token request with an Authorization: Basic header built from the CIMD URL and an empty secret, and omits client_id from the body. A public/CIMD client has no client secret, so the authorization server rejects the exchange with 401 access_denied, and McpClient.CreateAsync throws.

Because the only place _tokenEndpointAuthMethod is set from client-specific data is the dynamic client registration (DCR) response, switching a working client from DCR to CIMD silently regresses the token-endpoint auth method.

To Reproduce
Steps to reproduce the behavior:

  1. Use an authorization server that supports CIMD (client_id_metadata_document_supported: true) and advertises client_secret_basic before none in token_endpoint_auth_methods_supported (e.g. Auth0).
  2. Host a CIMD document that declares "token_endpoint_auth_method": "none".
  3. Create a transport configured for CIMD only — no ClientId/ClientSecret, no DCR:
    await using var transport = new HttpClientTransport(new HttpClientTransportOptions
    {
        Endpoint = new Uri(mcpServerUrl),
        OAuth = new ClientOAuthOptions
        {
            RedirectUri = new Uri("https://localhost/auth/callback"),
            ClientMetadataDocumentUri = new Uri("https://example.com/client-metadata.json"),
            AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
        },
    }, httpClient);
    
    await using var client = await McpClient.CreateAsync(transport);
    
  4. Complete the authorization-code flow. The authorize leg succeeds and returns a code, but the token-exchange POST /token is sent as Authorization: Basic base64(<cimd-url>:) with no client_id in the body.
  5. The authorization server responds 401 access_denied and CreateAsync throws.

Expected behavior
A CIMD public client should authenticate at the token endpoint with none (client id in the request body, PKCE as the proof of possession), matching the token_endpoint_auth_method declared in its metadata document, and the token exchange should succeed — regardless of which method the authorization server lists first. Pragmatically, it probably doesn't make sense for the client to read it's own CIMD, and the SDK should let the caller specify the token-endpoint authentication method.

Logs

End processing HTTP request after 126ms - 401   POST https://<issuer>/oauth/token
System.Net.Http.HttpRequestException: Response status code does not indicate success: 401 (Unauthorized).
Response body: {"error":"access_denied","error_description":"Unauthorized"}
   at ModelContextProtocol.HttpResponseMessageExtensions.EnsureSuccessStatusCodeWithResponseBodyAsync(...)
   at ModelContextProtocol.Authentication.ClientOAuthProvider.ExchangeCodeForTokenAsync(...)
   at ModelContextProtocol.Authentication.ClientOAuthProvider.InitiateAuthorizationCodeFlowAsync(...)
   at ModelContextProtocol.Authentication.ClientOAuthProvider.GetAccessTokenAsync(...)

The corresponding token request omits client_id from the form body (it is Basic-encoded in the header with an empty secret), so the authorization server cannot identify the client.

Additional context

  • SDK version: 1.3.0 (also reproduces in 1.2.0 and on main — the TokenEndpointAuthMethodsSupported?.FirstOrDefault() selection and the CIMD handling are unchanged).
  • Root causes: (1) ApplyClientIdMetadataDocument discards the metadata document's token_endpoint_auth_method; (2) there is no option to set the token-endpoint auth method explicitly; so the provider falls back to the server's first-advertised method.
  • The confidential CIMD profile (private_key_jwt) is also not expressible — CreateTokenRequest only emits client_secret_basic, client_secret_post, or none — so none is the only CIMD profile the SDK can currently produce.

Proposed fix
Add an opt-in ClientOAuthOptions.TokenEndpointAuthMethod (used with precedence: explicit → DCR response → server-advertised first) and/or have ApplyClientIdMetadataDocument honor the document's declared method. I have a branch with the explicit-option fix plus before/after tests and am happy to open a PR.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in ClientOAuthProvider.GetAccessTokenAsync, ApplyClientIdMetadataDocument, and CreateTokenRequest, then trace how DCR and server metadata set _tokenEndpointAuthMethod. Use the reported before/after tests as a starting point and add coverage for CIMD with none, verifying explicit → DCR → server-advertised precedence and a token request that includes client_id rather than Basic credentials.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
api, authentication, backend-api-design
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
62/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.