diff --git a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs
index 785e3cc2e..774fe4365 100644
--- a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs
+++ b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs
@@ -494,6 +494,13 @@ private void ApplyClientIdMetadataDocument(Uri metadataUri)
_clientId = metadataUri.AbsoluteUri;
+ // A CIMD client is a public client (it has no client secret, and its identifier is a URL),
+ // so it authenticates at the token endpoint with "none" and proves possession via PKCE.
+ // Without this, GetAccessTokenAsync falls through to the first method the authorization
+ // server advertises (e.g. client_secret_basic on Auth0), which fails with 401 access_denied.
+ // See https://github.com/modelcontextprotocol/csharp-sdk/issues/1612.
+ _tokenEndpointAuthMethod = "none";
+
// See: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-client-id-metadata-document-00#section-3
static bool IsValidClientMetadataDocumentUri(Uri uri)
=> uri.IsAbsoluteUri
diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthTests.cs
index 693c77943..3b770fc6c 100644
--- a/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthTests.cs
+++ b/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthTests.cs
@@ -360,6 +360,34 @@ public async Task CanAuthenticate_WithClientMetadataDocument()
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
}
+ [Fact]
+ public async Task CanAuthenticate_WithClientMetadataDocument_WhenServerAdvertisesClientSecretBasicFirst()
+ {
+ // Auth0 advertises client_secret_basic ahead of none. A CIMD client is a public client and
+ // must use "none" (PKCE) regardless of the advertised order, otherwise the token exchange
+ // sends an empty-secret Basic header and is rejected with 401 access_denied (#1612).
+ TestOAuthServer.TokenEndpointAuthMethodsSupported = ["client_secret_basic", "client_secret_post", "private_key_jwt", "none"];
+ await using var app = await StartMcpServerAsync();
+
+ await using var transport = new HttpClientTransport(new()
+ {
+ Endpoint = new(McpServerUrl),
+ OAuth = new ClientOAuthOptions()
+ {
+ RedirectUri = new Uri("http://localhost:1179/callback"),
+ AuthorizationCallbackHandler = HandleAuthorizationUrlAsync,
+ ClientMetadataDocumentUri = new Uri(ClientMetadataDocumentUrl),
+ DynamicClientRegistration = new()
+ {
+ ApplicationType = "web",
+ },
+ },
+ }, HttpClient, LoggerFactory);
+
+ await using var client = await McpClient.CreateAsync(
+ transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
+ }
+
[Fact]
public async Task CannotAuthenticate_WhenMetadataOmitsPkceMethods()
{
diff --git a/tests/ModelContextProtocol.TestOAuthServer/Program.cs b/tests/ModelContextProtocol.TestOAuthServer/Program.cs
index 73663dc94..68239186b 100644
--- a/tests/ModelContextProtocol.TestOAuthServer/Program.cs
+++ b/tests/ModelContextProtocol.TestOAuthServer/Program.cs
@@ -80,6 +80,17 @@ public Program(ILoggerProvider? loggerProvider = null, IConnectionListenerFactor
///
public bool ClientIdMetadataDocumentSupported { get; set; } = true;
+ ///
+ /// Gets or sets the token_endpoint_auth_methods_supported values the authorization server
+ /// advertises in its discovery document. Tests set this to a list that does not lead with
+ /// none (e.g. ["client_secret_basic", "none"], mirroring Auth0) to verify that a
+ /// CIMD public client still authenticates with none rather than the first advertised method.
+ ///
+ ///
+ /// The default value is ["client_secret_post"].
+ ///
+ public List TokenEndpointAuthMethodsSupported { get; set; } = ["client_secret_post"];
+
///
/// Gets or sets a value indicating whether the authorization server expects a resource parameter.
/// When true, the resource parameter must be present and match a valid resource.
@@ -276,7 +287,7 @@ IResult HandleMetadataRequest(HttpContext context, string? issuerPath = null)
ScopesSupported = IncludeOfflineAccessInMetadata
? ["openid", "profile", "email", "mcp:tools", "offline_access"]
: ["openid", "profile", "email", "mcp:tools"],
- TokenEndpointAuthMethodsSupported = ["client_secret_post"],
+ TokenEndpointAuthMethodsSupported = TokenEndpointAuthMethodsSupported,
ClaimsSupported = ["sub", "iss", "name", "email", "aud"],
CodeChallengeMethodsSupported = MetadataPathsWithoutPkceSupport.Contains(context.Request.Path)
? null