@@ -24,8 +24,8 @@ internal sealed class ClientOAuthProvider
2424 private readonly Uri _serverUrl ;
2525 private readonly Uri _redirectUri ;
2626 private readonly string [ ] ? _scopes ;
27- private readonly string _clientId ;
28- private readonly string ? _clientSecret ;
27+ private string ? _clientId ;
28+ private string ? _clientSecret ;
2929 private readonly Func < IReadOnlyList < Uri > , Uri ? > _authServerSelector ;
3030 private readonly AuthorizationRedirectDelegate _authorizationRedirectDelegate ;
3131
@@ -34,6 +34,7 @@ internal sealed class ClientOAuthProvider
3434
3535 private TokenContainer ? _token ;
3636 private AuthorizationServerMetadata ? _authServerMetadata ;
37+ private readonly ClientOAuthOptions _options ;
3738
3839 /// <summary>
3940 /// Initializes a new instance of the <see cref="ClientOAuthProvider"/> class using the specified options.
@@ -49,20 +50,18 @@ public ClientOAuthProvider(
4950 HttpClient ? httpClient = null ,
5051 ILoggerFactory ? loggerFactory = null )
5152 {
52- _serverUrl = serverUrl ?? throw new ArgumentNullException ( nameof ( serverUrl ) ) ;
53+ _serverUrl = serverUrl ;
5354 _httpClient = httpClient ?? new HttpClient ( ) ;
5455 _logger = ( ILogger ? ) loggerFactory ? . CreateLogger < ClientOAuthProvider > ( ) ?? NullLogger . Instance ;
5556
56- if ( options is null )
57- {
58- throw new ArgumentNullException ( nameof ( options ) ) ;
59- }
60-
6157 _clientId = options . ClientId ;
6258 _redirectUri = options . RedirectUri ;
6359 _clientSecret = options . ClientSecret ;
6460 _scopes = options . Scopes ? . ToArray ( ) ;
6561
62+ // Store options for potential dynamic registration
63+ _options = options ;
64+
6665 // Set up authorization server selection strategy
6766 _authServerSelector = options . AuthServerSelector ?? DefaultAuthServerSelector ;
6867
@@ -94,10 +93,29 @@ public ClientOAuthProvider(
9493 return Task . FromResult < string ? > ( authorizationCode ) ;
9594 }
9695
97- /// <inheritdoc />
96+ /// <summary>
97+ /// Gets the collection of authentication schemes supported by this provider.
98+ /// </summary>
99+ /// <remarks>
100+ /// <para>
101+ /// This property returns all authentication schemes that this provider can handle,
102+ /// allowing clients to select the appropriate scheme based on server capabilities.
103+ /// </para>
104+ /// <para>
105+ /// Common values include "Bearer" for JWT tokens, "Basic" for username/password authentication,
106+ /// and "Negotiate" for integrated Windows authentication.
107+ /// </para>
108+ /// </remarks>
98109 public IEnumerable < string > SupportedSchemes => [ BearerScheme ] ;
99110
100- /// <inheritdoc />
111+ /// <summary>
112+ /// Gets an authentication token or credential for authenticating requests to a resource
113+ /// using the specified authentication scheme.
114+ /// </summary>
115+ /// <param name="scheme">The authentication scheme to use.</param>
116+ /// <param name="resourceUri">The URI of the resource requiring authentication.</param>
117+ /// <param name="cancellationToken">A token to cancel the operation.</param>
118+ /// <returns>An authentication token string or null if no token could be obtained for the specified scheme.</returns>
101119 public async Task < string ? > GetCredentialAsync ( string scheme , Uri resourceUri , CancellationToken cancellationToken = default )
102120 {
103121 ThrowIfNotBearerScheme ( scheme ) ;
@@ -125,7 +143,16 @@ public ClientOAuthProvider(
125143 return null ;
126144 }
127145
128- /// <inheritdoc />
146+ /// <summary>
147+ /// Handles a 401 Unauthorized response from a resource.
148+ /// </summary>
149+ /// <param name="scheme">The authentication scheme that was used when the unauthorized response was received.</param>
150+ /// <param name="response">The HTTP response that contained the 401 status code.</param>
151+ /// <param name="cancellationToken">A token to cancel the operation.</param>
152+ /// <returns>
153+ /// A result object indicating if the provider was able to handle the unauthorized response,
154+ /// and the authentication scheme that should be used for the next attempt, if any.
155+ /// </returns>
129156 public async Task HandleUnauthorizedResponseAsync (
130157 string scheme ,
131158 HttpResponseMessage response ,
@@ -185,6 +212,12 @@ private async Task PerformOAuthAuthorizationAsync(
185212 // Store auth server metadata for future refresh operations
186213 _authServerMetadata = authServerMetadata ;
187214
215+ // Perform dynamic client registration if needed
216+ if ( string . IsNullOrEmpty ( _clientId ) )
217+ {
218+ await PerformDynamicClientRegistrationAsync ( authServerMetadata , cancellationToken ) . ConfigureAwait ( false ) ;
219+ }
220+
188221 // Perform the OAuth flow
189222 var token = await InitiateAuthorizationCodeFlowAsync ( protectedResourceMetadata , authServerMetadata , cancellationToken ) . ConfigureAwait ( false ) ;
190223
@@ -242,7 +275,7 @@ private async Task<TokenContainer> RefreshTokenAsync(string refreshToken, Author
242275 {
243276 [ "grant_type" ] = "refresh_token" ,
244277 [ "refresh_token" ] = refreshToken ,
245- [ "client_id" ] = _clientId
278+ [ "client_id" ] = GetClientIdOrThrow ( ) ,
246279 } ) ;
247280
248281 using var request = new HttpRequestMessage ( HttpMethod . Post , authServerMetadata . TokenEndpoint )
@@ -322,8 +355,8 @@ private async Task<TokenContainer> ExchangeCodeForTokenAsync(
322355 [ "grant_type" ] = "authorization_code" ,
323356 [ "code" ] = authorizationCode ,
324357 [ "redirect_uri" ] = _redirectUri . ToString ( ) ,
325- [ "client_id" ] = _clientId ,
326- [ "code_verifier" ] = codeVerifier
358+ [ "client_id" ] = GetClientIdOrThrow ( ) ,
359+ [ "code_verifier" ] = codeVerifier ,
327360 } ) ;
328361
329362 using var request = new HttpRequestMessage ( HttpMethod . Post , authServerMetadata . TokenEndpoint )
@@ -372,6 +405,60 @@ private async Task<TokenContainer> FetchTokenAsync(HttpRequestMessage request, C
372405 return await JsonSerializer . DeserializeAsync ( stream , McpJsonUtilities . JsonContext . Default . ProtectedResourceMetadata , cancellationToken ) . ConfigureAwait ( false ) ;
373406 }
374407
408+ private async Task PerformDynamicClientRegistrationAsync (
409+ AuthorizationServerMetadata authServerMetadata ,
410+ CancellationToken cancellationToken )
411+ {
412+ if ( authServerMetadata . RegistrationEndpoint is null )
413+ {
414+ ThrowFailedToHandleUnauthorizedResponse ( "Authorization server does not support dynamic client registration" ) ;
415+ }
416+
417+ _logger . LogInformation ( "Performing dynamic client registration with {RegistrationEndpoint}" , authServerMetadata . RegistrationEndpoint ) ;
418+
419+ var registrationRequest = new DynamicClientRegistrationRequest
420+ {
421+ RedirectUris = [ _redirectUri . ToString ( ) ] ,
422+ GrantTypes = [ "authorization_code" , "refresh_token" ] ,
423+ ResponseTypes = [ "code" ] ,
424+ TokenEndpointAuthMethod = "client_secret_basic" ,
425+ ClientName = _options . ClientName ,
426+ ClientUri = _options . ClientUri ? . ToString ( ) ,
427+ Scope = _scopes != null ? string . Join ( " " , _scopes ) : null
428+ } ;
429+
430+ var requestJson = JsonSerializer . Serialize ( registrationRequest , McpJsonUtilities . JsonContext . Default . DynamicClientRegistrationRequest ) ;
431+ var requestContent = new StringContent ( requestJson , Encoding . UTF8 , "application/json" ) ;
432+
433+ using var request = new HttpRequestMessage ( HttpMethod . Post , authServerMetadata . RegistrationEndpoint )
434+ {
435+ Content = requestContent
436+ } ;
437+
438+ using var httpResponse = await _httpClient . SendAsync ( request , cancellationToken ) . ConfigureAwait ( false ) ;
439+ httpResponse . EnsureSuccessStatusCode ( ) ;
440+
441+ using var responseStream = await httpResponse . Content . ReadAsStreamAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
442+ var registrationResponse = await JsonSerializer . DeserializeAsync (
443+ responseStream ,
444+ McpJsonUtilities . JsonContext . Default . DynamicClientRegistrationResponse ,
445+ cancellationToken ) . ConfigureAwait ( false ) ;
446+
447+ if ( registrationResponse is null )
448+ {
449+ ThrowFailedToHandleUnauthorizedResponse ( "Dynamic client registration returned empty response" ) ;
450+ }
451+
452+ // Update client credentials
453+ _clientId = registrationResponse . ClientId ;
454+ if ( ! string . IsNullOrEmpty ( registrationResponse . ClientSecret ) )
455+ {
456+ _clientSecret = registrationResponse . ClientSecret ;
457+ }
458+
459+ _logger . LogInformation ( "Dynamic client registration successful. Client ID: {ClientId}" , _clientId ) ;
460+ }
461+
375462 /// <summary>
376463 /// Verifies that the resource URI in the metadata exactly matches the original request URL as required by the RFC.
377464 /// Per RFC: The resource value must be identical to the URL that the client used to make the request to the resource server.
@@ -540,15 +627,17 @@ private static string GenerateCodeChallenge(string codeVerifier)
540627 . Replace ( '/' , '_' ) ;
541628 }
542629
630+ private string GetClientIdOrThrow ( ) => _clientId ?? throw new InvalidOperationException ( $ "_clientId is uninitialized! This should be unreachable from public API.") ;
631+
543632 private static void ThrowIfNotBearerScheme ( string scheme )
544633 {
545634 if ( ! string . Equals ( scheme , BearerScheme , StringComparison . OrdinalIgnoreCase ) )
546635 {
547- throw new InvalidOperationException ( $ "The '{ scheme } ' is not supported. This credential provider only supports the '{ BearerScheme } ' scheme") ;
636+ throw new InvalidOperationException ( $ "The '{ scheme } ' is not supported. This credential provider only supports the '{ BearerScheme } ' scheme. ") ;
548637 }
549638 }
550639
551640 [ DoesNotReturn ]
552- private static void ThrowFailedToHandleUnauthorizedResponse ( string message ) =>
553- throw new McpException ( $ "Failed to handle unauthorized response with 'Bearer' scheme. { message } ") ;
641+ private static void ThrowFailedToHandleUnauthorizedResponse ( string message , Exception ? innerException = null ) =>
642+ throw new McpException ( $ "Failed to handle unauthorized response with 'Bearer' scheme. { message } ", innerException ) ;
554643}
0 commit comments