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:
- 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).
- Host a CIMD document that declares
"token_endpoint_auth_method": "none".
- 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);
- 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.
- 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.
Describe the bug
When authenticating as a public client identified by a Client ID Metadata Document (CIMD),
ClientOAuthProvidercan 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
GetAccessTokenAsyncwith:Since it just takes the first method the authorization server advertises and
ClientOAuthOptionsexposes 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 listsclient_secret_basicahead ofnone— as Auth0 does, advertising["client_secret_basic","client_secret_post","private_key_jwt","none"], the provider picksclient_secret_basic.CreateTokenRequestthen sends the token request with anAuthorization: Basicheader built from the CIMD URL and an empty secret, and omitsclient_idfrom the body. A public/CIMD client has no client secret, so the authorization server rejects the exchange with401 access_denied, andMcpClient.CreateAsyncthrows.Because the only place
_tokenEndpointAuthMethodis 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:
client_id_metadata_document_supported: true) and advertisesclient_secret_basicbeforenoneintoken_endpoint_auth_methods_supported(e.g. Auth0)."token_endpoint_auth_method": "none".ClientId/ClientSecret, no DCR:POST /tokenis sent asAuthorization: Basic base64(<cimd-url>:)with noclient_idin the body.401 access_deniedandCreateAsyncthrows.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 thetoken_endpoint_auth_methoddeclared 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
The corresponding token request omits
client_idfrom the form body (it is Basic-encoded in the header with an empty secret), so the authorization server cannot identify the client.Additional context
main— theTokenEndpointAuthMethodsSupported?.FirstOrDefault()selection and the CIMD handling are unchanged).ApplyClientIdMetadataDocumentdiscards the metadata document'stoken_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.private_key_jwt) is also not expressible —CreateTokenRequestonly emitsclient_secret_basic,client_secret_post, ornone— sononeis 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 haveApplyClientIdMetadataDocumenthonor 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.