@@ -44,6 +44,7 @@ internal sealed partial class ClientOAuthProvider : McpHttpClient
4444
4545 private string ? _clientId ;
4646 private string ? _clientSecret ;
47+ private string ? _tokenEndpointAuthMethod ;
4748 private ITokenCache _tokenCache ;
4849 private AuthorizationServerMetadata ? _authServerMetadata ;
4950
@@ -293,6 +294,9 @@ await _tokenCache.GetTokensAsync(cancellationToken).ConfigureAwait(false) is { R
293294 }
294295 }
295296
297+ // Determine the token endpoint auth method from server metadata if not already set by DCR.
298+ _tokenEndpointAuthMethod ??= authServerMetadata . TokenEndpointAuthMethodsSupported ? . FirstOrDefault ( ) ;
299+
296300 // Store auth server metadata for future refresh operations
297301 _authServerMetadata = authServerMetadata ;
298302
@@ -385,20 +389,15 @@ private static IEnumerable<Uri> GetWellKnownAuthorizationServerMetadataUris(Uri
385389
386390 private async Task < string ? > RefreshTokensAsync ( string refreshToken , Uri resourceUri , AuthorizationServerMetadata authServerMetadata , CancellationToken cancellationToken )
387391 {
388- var requestContent = new FormUrlEncodedContent ( new Dictionary < string , string >
392+ Dictionary < string , string > formFields = new ( )
389393 {
390394 [ "grant_type" ] = "refresh_token" ,
391395 [ "refresh_token" ] = refreshToken ,
392- [ "client_id" ] = GetClientIdOrThrow ( ) ,
393- [ "client_secret" ] = _clientSecret ?? string . Empty ,
394396 [ "resource" ] = resourceUri . ToString ( ) ,
395- } ) ;
396-
397- using var request = new HttpRequestMessage ( HttpMethod . Post , authServerMetadata . TokenEndpoint )
398- {
399- Content = requestContent
400397 } ;
401398
399+ using var request = CreateTokenRequest ( authServerMetadata . TokenEndpoint , formFields ) ;
400+
402401 using var httpResponse = await _httpClient . SendAsync ( request , cancellationToken ) . ConfigureAwait ( false ) ;
403402
404403 if ( ! httpResponse . IsSuccessStatusCode )
@@ -482,22 +481,17 @@ private async Task<string> ExchangeCodeForTokenAsync(
482481 {
483482 var resourceUri = GetRequiredResourceUri ( protectedResourceMetadata ) ;
484483
485- var requestContent = new FormUrlEncodedContent ( new Dictionary < string , string >
484+ Dictionary < string , string > formFields = new ( )
486485 {
487486 [ "grant_type" ] = "authorization_code" ,
488487 [ "code" ] = authorizationCode ,
489488 [ "redirect_uri" ] = _redirectUri . ToString ( ) ,
490- [ "client_id" ] = GetClientIdOrThrow ( ) ,
491489 [ "code_verifier" ] = codeVerifier ,
492- [ "client_secret" ] = _clientSecret ?? string . Empty ,
493490 [ "resource" ] = resourceUri . ToString ( ) ,
494- } ) ;
495-
496- using var request = new HttpRequestMessage ( HttpMethod . Post , authServerMetadata . TokenEndpoint )
497- {
498- Content = requestContent
499491 } ;
500492
493+ using var request = CreateTokenRequest ( authServerMetadata . TokenEndpoint , formFields ) ;
494+
501495 using var httpResponse = await _httpClient . SendAsync ( request , cancellationToken ) . ConfigureAwait ( false ) ;
502496 await httpResponse . EnsureSuccessStatusCodeWithResponseBodyAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
503497
@@ -506,6 +500,38 @@ private async Task<string> ExchangeCodeForTokenAsync(
506500 return tokens . AccessToken ;
507501 }
508502
503+ /// <summary>
504+ /// Creates an HTTP request to the token endpoint, applying the appropriate authentication
505+ /// method based on <see cref="_tokenEndpointAuthMethod"/>.
506+ /// </summary>
507+ private HttpRequestMessage CreateTokenRequest ( Uri tokenEndpoint , Dictionary < string , string > formFields )
508+ {
509+ HttpRequestMessage request = new ( HttpMethod . Post , tokenEndpoint ) ;
510+
511+ var clientId = GetClientIdOrThrow ( ) ;
512+ if ( string . Equals ( _tokenEndpointAuthMethod , "client_secret_basic" , StringComparison . Ordinal ) )
513+ {
514+ // Per RFC 6749 §2.3.1: send client_id:client_secret as HTTP Basic auth.
515+ request . Headers . Authorization = new (
516+ "Basic" ,
517+ Convert . ToBase64String ( Encoding . UTF8 . GetBytes ( $ "{ Uri . EscapeDataString ( clientId ) } :{ Uri . EscapeDataString ( _clientSecret ?? string . Empty ) } ") ) ) ;
518+ }
519+ else if ( string . Equals ( _tokenEndpointAuthMethod , "none" , StringComparison . Ordinal ) )
520+ {
521+ // Public client: include client_id in the body but no secret.
522+ formFields [ "client_id" ] = clientId ;
523+ }
524+ else
525+ {
526+ // Default to client_secret_post: include credentials in the body.
527+ formFields [ "client_id" ] = clientId ;
528+ formFields [ "client_secret" ] = _clientSecret ?? string . Empty ;
529+ }
530+
531+ request . Content = new FormUrlEncodedContent ( formFields ) ;
532+ return request ;
533+ }
534+
509535 private async Task < TokenContainer > HandleSuccessfulTokenResponseAsync ( HttpResponseMessage response , CancellationToken cancellationToken )
510536 {
511537 using var stream = await response . Content . ReadAsStreamAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
@@ -620,6 +646,11 @@ private async Task PerformDynamicClientRegistrationAsync(
620646 _clientSecret = registrationResponse . ClientSecret ;
621647 }
622648
649+ if ( ! string . IsNullOrEmpty ( registrationResponse . TokenEndpointAuthMethod ) )
650+ {
651+ _tokenEndpointAuthMethod = registrationResponse . TokenEndpointAuthMethod ;
652+ }
653+
623654 LogDynamicClientRegistrationSuccessful ( _clientId ! ) ;
624655
625656 if ( _dcrResponseDelegate is not null )
0 commit comments