Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/ModelContextProtocol.Core/Authentication/ClientOAuthOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,21 @@ public sealed class ClientOAuthOptions
/// </remarks>
public string? ClientSecret { get; set; }

/// <summary>
/// Gets or sets the token endpoint authentication method (<c>token_endpoint_auth_method</c>) to use when
/// requesting tokens.
/// </summary>
/// <remarks>
/// Supported values are <c>none</c>, <c>client_secret_basic</c>, and <c>client_secret_post</c>.
/// When not set, the method is inferred from the dynamic client registration response (when DCR is used),
/// and otherwise from the first entry in the authorization server's
/// <c>token_endpoint_auth_methods_supported</c>. For DCR, this value is requested during registration,
/// but the method returned by the authorization server is authoritative.
/// Set this explicitly when inference is incorrect, most notably for a public client identified by a
/// <see cref="ClientMetadataDocumentUri">Client ID Metadata Document</see>, which commonly uses <c>none</c>.
/// </remarks>
public string? TokenEndpointAuthMethod { get; set; }

/// <summary>
/// Gets or sets the HTTPS URL pointing to this client's metadata document.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ internal sealed partial class ClientOAuthProvider : McpHttpClient
private readonly bool _validateAuthorizationResponseIssuer;
private readonly Uri? _clientMetadataDocumentUri;
private readonly string? _configuredClientId;
private readonly string? _configuredTokenEndpointAuthMethod;

// _dcrClientName, _dcrClientUri, _dcrInitialAccessToken, _dcrConfiguredApplicationType and _dcrResponseDelegate are used for dynamic client registration (RFC 7591)
private readonly string? _dcrClientName;
Expand Down Expand Up @@ -97,9 +98,19 @@ public ClientOAuthProvider(
throw new ArgumentNullException(nameof(options));
}

if (options.TokenEndpointAuthMethod is not null &&
!IsSupportedTokenEndpointAuthMethod(options.TokenEndpointAuthMethod))
{
throw new ArgumentException(
$"{nameof(options.TokenEndpointAuthMethod)} must be 'client_secret_basic', 'client_secret_post', or 'none'.",
$"{nameof(options)}.{nameof(options.TokenEndpointAuthMethod)}");
}

_clientId = options.ClientId;
_configuredClientId = options.ClientId;
_clientSecret = options.ClientSecret;
_configuredTokenEndpointAuthMethod = options.TokenEndpointAuthMethod;
_tokenEndpointAuthMethod = _configuredTokenEndpointAuthMethod;
_redirectUri = options.RedirectUri ?? throw new ArgumentException("ClientOAuthOptions.RedirectUri must configured.", nameof(options));
_configuredScopes = options.Scopes is null ? null : string.Join(" ", options.Scopes);
_scopeSelector = options.ScopeSelector;
Expand Down Expand Up @@ -847,12 +858,17 @@ private HttpRequestMessage CreateTokenRequest(Uri tokenEndpoint, Dictionary<stri
// Public client: include client_id in the body but no secret.
formFields["client_id"] = clientId;
}
else
else if (_tokenEndpointAuthMethod is null or "client_secret_post")
{
// Default to client_secret_post: include credentials in the body.
formFields["client_id"] = clientId;
formFields["client_secret"] = _clientSecret ?? string.Empty;
}
else
{
ThrowFailedToHandleUnauthorizedResponse(
$"Token endpoint authentication method '{_tokenEndpointAuthMethod}' is not supported.");
}

request.Content = new FormUrlEncodedContent(formFields);
return request;
Expand Down Expand Up @@ -934,7 +950,7 @@ private async Task PerformDynamicClientRegistrationAsync(
RedirectUris = [_redirectUri.ToString()],
GrantTypes = ["authorization_code", "refresh_token"],
ResponseTypes = ["code"],
TokenEndpointAuthMethod = "client_secret_post",
TokenEndpointAuthMethod = _configuredTokenEndpointAuthMethod ?? "client_secret_post",
ClientName = _dcrClientName,
ClientUri = _dcrClientUri?.ToString(),
Scope = ComputeEffectiveScope(protectedResourceMetadata, authServerMetadata),
Expand Down Expand Up @@ -983,9 +999,23 @@ private async Task PerformDynamicClientRegistrationAsync(
_clientSecret = registrationResponse.ClientSecret;
}

if (!string.IsNullOrEmpty(registrationResponse.TokenEndpointAuthMethod))
// The response describes the registration the server created and is authoritative. Some servers omit the
// field, in which case use the method requested during registration.
var registeredTokenEndpointAuthMethod = registrationResponse.TokenEndpointAuthMethod;
if (!string.IsNullOrEmpty(registeredTokenEndpointAuthMethod))
{
if (!IsSupportedTokenEndpointAuthMethod(registeredTokenEndpointAuthMethod!))
{
ThrowFailedToHandleUnauthorizedResponse(
$"Dynamic client registration returned unsupported token endpoint authentication method " +
$"'{registeredTokenEndpointAuthMethod}'.");
}

_tokenEndpointAuthMethod = registeredTokenEndpointAuthMethod;
}
else
{
_tokenEndpointAuthMethod = registrationResponse.TokenEndpointAuthMethod;
_tokenEndpointAuthMethod = registrationRequest.TokenEndpointAuthMethod;
}

LogDynamicClientRegistrationSuccessful(_clientId!);
Expand All @@ -1008,6 +1038,9 @@ private static string InferApplicationType(Uri redirectUri)
return "native";
}

private static bool IsSupportedTokenEndpointAuthMethod(string tokenEndpointAuthMethod) =>
tokenEndpointAuthMethod is "client_secret_basic" or "client_secret_post" or "none";

private static string? GetResourceUri(ProtectedResourceMetadata protectedResourceMetadata)
=> protectedResourceMetadata.Resource;

Expand Down Expand Up @@ -1525,7 +1558,16 @@ private void RestoreCachedClientCredentials(TokenContainer? tokens, Uri selected
// Assign _clientId last. Callers treat a non-empty _clientId as "registration complete", so the
// secret and auth method must already be in place before _clientId becomes observable.
_clientSecret ??= cached.ClientSecret;
_tokenEndpointAuthMethod ??= cached.TokenEndpointAuthMethod;
if (string.Equals(cached.ClientId, _clientMetadataDocumentUri?.AbsoluteUri, StringComparison.Ordinal))
{
// Configured intent controls CIMD clients.
_tokenEndpointAuthMethod ??= cached.TokenEndpointAuthMethod;
}
else
{
// A DCR response is authoritative for the registration and must survive a cold start.
_tokenEndpointAuthMethod = cached.TokenEndpointAuthMethod ?? _configuredTokenEndpointAuthMethod;
}
_clientId = cached.ClientId;
_clientCredentialsAuthorizationServer = cached.AuthorizationServer;
}
Expand Down Expand Up @@ -1554,7 +1596,7 @@ private void BindClientCredentialsToAuthorizationServer(Uri selectedAuthServer)

_clientId = null;
_clientSecret = null;
_tokenEndpointAuthMethod = null;
_tokenEndpointAuthMethod = _configuredTokenEndpointAuthMethod;
_authServerMetadata = null;
_clientCredentialsAuthorizationServer = null;
}
Expand Down
Loading