@@ -236,34 +236,48 @@ private async Task<string> GetAccessTokenAsync(HttpResponseMessage response, boo
236236 {
237237 // Get available authorization servers from the 401 or 403 response
238238 var protectedResourceMetadata = await ExtractProtectedResourceMetadata ( response , cancellationToken ) . ConfigureAwait ( false ) ;
239- var availableAuthorizationServers = protectedResourceMetadata . AuthorizationServers ;
239+ var selectedAuthServer = default ( Uri ) ;
240240
241- if ( availableAuthorizationServers . Count == 0 )
241+ // If the MCP server does not provide protected resource metadata, fallback to using the MCP server URL as the
242+ // authorization server.
243+ //
244+ // This is to maintain compatibility with MCP version 2025-03-26.
245+ if ( protectedResourceMetadata is null )
242246 {
243- ThrowFailedToHandleUnauthorizedResponse ( "No authorization servers found in authentication challenge" ) ;
247+ selectedAuthServer = new Uri ( _serverUrl , "/" ) ;
248+
249+ LogSelectedFallbackAuthorizationServer ( selectedAuthServer ) ;
244250 }
251+ else
252+ {
253+ var availableAuthorizationServers = protectedResourceMetadata . AuthorizationServers ;
245254
246- // Select authorization server using configured strategy
247- var selectedAuthServer = _authServerSelector ( availableAuthorizationServers ) ;
255+ if ( availableAuthorizationServers . Count == 0 )
256+ {
257+ ThrowFailedToHandleUnauthorizedResponse ( "No authorization servers found in authentication challenge" ) ;
258+ }
248259
249- if ( selectedAuthServer is null )
250- {
251- ThrowFailedToHandleUnauthorizedResponse ( $ "Authorization server selection returned null. Available servers: { string . Join ( ", " , availableAuthorizationServers ) } ") ;
252- }
260+ // Select authorization server using configured strategy
261+ selectedAuthServer = _authServerSelector ( availableAuthorizationServers ) ;
253262
254- if ( ! availableAuthorizationServers . Contains ( selectedAuthServer ) )
255- {
256- ThrowFailedToHandleUnauthorizedResponse ( $ "Authorization server selector returned a server not in the available list: { selectedAuthServer } . Available servers: { string . Join ( ", " , availableAuthorizationServers ) } ") ;
257- }
263+ if ( selectedAuthServer is null )
264+ {
265+ ThrowFailedToHandleUnauthorizedResponse ( $ "Authorization server selection returned null . Available servers: { string . Join ( ", " , availableAuthorizationServers ) } ") ;
266+ }
258267
259- LogSelectedAuthorizationServer ( selectedAuthServer , availableAuthorizationServers . Count ) ;
268+ if ( ! availableAuthorizationServers . Contains ( selectedAuthServer ) )
269+ {
270+ ThrowFailedToHandleUnauthorizedResponse ( $ "Authorization server selector returned a server not in the available list: { selectedAuthServer } . Available servers: { string . Join ( ", " , availableAuthorizationServers ) } ") ;
271+ }
272+
273+ LogSelectedAuthorizationServer ( selectedAuthServer , availableAuthorizationServers . Count ) ;
274+ }
260275
261276 // Get auth server metadata
262277 var authServerMetadata = await GetAuthServerMetadataAsync ( selectedAuthServer , cancellationToken ) . ConfigureAwait ( false ) ;
278+ var resourceUri = protectedResourceMetadata ? . Resource ;
263279
264280 // The existing access token must be invalid to have resulted in a 401 response, but refresh might still work.
265- var resourceUri = GetRequiredResourceUri ( protectedResourceMetadata ) ;
266-
267281 // Only attempt a token refresh if we haven't attempted to already for this request.
268282 // Also only attempt a token refresh for a 401 Unauthorized responses. Other response status codes
269283 // should not be used for expired access tokens. This is important because 403 forbiden responses can
@@ -387,15 +401,19 @@ private static IEnumerable<Uri> GetWellKnownAuthorizationServerMetadataUris(Uri
387401 }
388402 }
389403
390- private async Task < string ? > RefreshTokensAsync ( string refreshToken , Uri resourceUri , AuthorizationServerMetadata authServerMetadata , CancellationToken cancellationToken )
404+ private async Task < string ? > RefreshTokensAsync ( string refreshToken , Uri ? resourceUri , AuthorizationServerMetadata authServerMetadata , CancellationToken cancellationToken )
391405 {
392406 Dictionary < string , string > formFields = new ( )
393407 {
394408 [ "grant_type" ] = "refresh_token" ,
395409 [ "refresh_token" ] = refreshToken ,
396- [ "resource" ] = resourceUri . ToString ( ) ,
397410 } ;
398411
412+ if ( resourceUri is not null )
413+ {
414+ formFields . Add ( "resource" , resourceUri . ToString ( ) ) ;
415+ }
416+
399417 using var request = CreateTokenRequest ( authServerMetadata . TokenEndpoint , formFields ) ;
400418
401419 using var httpResponse = await _httpClient . SendAsync ( request , cancellationToken ) . ConfigureAwait ( false ) ;
@@ -411,7 +429,7 @@ private static IEnumerable<Uri> GetWellKnownAuthorizationServerMetadataUris(Uri
411429 }
412430
413431 private async Task < string > InitiateAuthorizationCodeFlowAsync (
414- ProtectedResourceMetadata protectedResourceMetadata ,
432+ ProtectedResourceMetadata ? protectedResourceMetadata ,
415433 AuthorizationServerMetadata authServerMetadata ,
416434 CancellationToken cancellationToken )
417435 {
@@ -430,22 +448,24 @@ private async Task<string> InitiateAuthorizationCodeFlowAsync(
430448 }
431449
432450 private Uri BuildAuthorizationUrl (
433- ProtectedResourceMetadata protectedResourceMetadata ,
451+ ProtectedResourceMetadata ? protectedResourceMetadata ,
434452 AuthorizationServerMetadata authServerMetadata ,
435453 string codeChallenge )
436454 {
437- var resourceUri = GetRequiredResourceUri ( protectedResourceMetadata ) ;
438-
439455 var queryParamsDictionary = new Dictionary < string , string >
440456 {
441457 [ "client_id" ] = GetClientIdOrThrow ( ) ,
442458 [ "redirect_uri" ] = _redirectUri . ToString ( ) ,
443459 [ "response_type" ] = "code" ,
444460 [ "code_challenge" ] = codeChallenge ,
445461 [ "code_challenge_method" ] = "S256" ,
446- [ "resource" ] = resourceUri . ToString ( ) ,
447462 } ;
448463
464+ if ( protectedResourceMetadata ? . Resource is { } resourceUri )
465+ {
466+ queryParamsDictionary . Add ( "resource" , resourceUri . ToString ( ) ) ;
467+ }
468+
449469 var scope = GetScopeParameter ( protectedResourceMetadata ) ;
450470 if ( ! string . IsNullOrEmpty ( scope ) )
451471 {
@@ -473,23 +493,25 @@ private Uri BuildAuthorizationUrl(
473493 }
474494
475495 private async Task < string > ExchangeCodeForTokenAsync (
476- ProtectedResourceMetadata protectedResourceMetadata ,
496+ ProtectedResourceMetadata ? protectedResourceMetadata ,
477497 AuthorizationServerMetadata authServerMetadata ,
478498 string authorizationCode ,
479499 string codeVerifier ,
480500 CancellationToken cancellationToken )
481501 {
482- var resourceUri = GetRequiredResourceUri ( protectedResourceMetadata ) ;
483-
484502 Dictionary < string , string > formFields = new ( )
485503 {
486504 [ "grant_type" ] = "authorization_code" ,
487505 [ "code" ] = authorizationCode ,
488506 [ "redirect_uri" ] = _redirectUri . ToString ( ) ,
489507 [ "code_verifier" ] = codeVerifier ,
490- [ "resource" ] = resourceUri . ToString ( ) ,
491508 } ;
492509
510+ if ( protectedResourceMetadata ? . Resource is { } resourceUri )
511+ {
512+ formFields . Add ( "resource" , resourceUri . ToString ( ) ) ;
513+ }
514+
493515 using var request = CreateTokenRequest ( authServerMetadata . TokenEndpoint , formFields ) ;
494516
495517 using var httpResponse = await _httpClient . SendAsync ( request , cancellationToken ) . ConfigureAwait ( false ) ;
@@ -585,7 +607,7 @@ private async Task<TokenContainer> HandleSuccessfulTokenResponseAsync(HttpRespon
585607 /// Performs dynamic client registration with the authorization server.
586608 /// </summary>
587609 private async Task PerformDynamicClientRegistrationAsync (
588- ProtectedResourceMetadata protectedResourceMetadata ,
610+ ProtectedResourceMetadata ? protectedResourceMetadata ,
589611 AuthorizationServerMetadata authServerMetadata ,
590612 CancellationToken cancellationToken )
591613 {
@@ -659,19 +681,13 @@ private async Task PerformDynamicClientRegistrationAsync(
659681 }
660682 }
661683
662- private static Uri GetRequiredResourceUri ( ProtectedResourceMetadata protectedResourceMetadata )
684+ private string ? GetScopeParameter ( ProtectedResourceMetadata ? protectedResourceMetadata )
663685 {
664- if ( protectedResourceMetadata . Resource is null )
686+ if ( protectedResourceMetadata is null )
665687 {
666- ThrowFailedToHandleUnauthorizedResponse ( "Protected resource metadata did not include a 'resource' value." ) ;
688+ return null ;
667689 }
668-
669- return protectedResourceMetadata . Resource ;
670- }
671-
672- private string ? GetScopeParameter ( ProtectedResourceMetadata protectedResourceMetadata )
673- {
674- if ( ! string . IsNullOrEmpty ( protectedResourceMetadata . WwwAuthenticateScope ) )
690+ else if ( ! string . IsNullOrEmpty ( protectedResourceMetadata . WwwAuthenticateScope ) )
675691 {
676692 return protectedResourceMetadata . WwwAuthenticateScope ;
677693 }
@@ -740,7 +756,7 @@ private static string NormalizeUri(Uri uri)
740756 /// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
741757 /// <returns>The resource metadata if the resource matches the server, otherwise throws an exception.</returns>
742758 /// <exception cref="InvalidOperationException">Thrown when the response is not a 401, the metadata can't be fetched, or the resource URI doesn't match the server URL.</exception>
743- private async Task < ProtectedResourceMetadata > ExtractProtectedResourceMetadata ( HttpResponseMessage response , CancellationToken cancellationToken )
759+ private async Task < ProtectedResourceMetadata ? > ExtractProtectedResourceMetadata ( HttpResponseMessage response , CancellationToken cancellationToken )
744760 {
745761 Uri resourceUri = _serverUrl ;
746762 string ? wwwAuthenticateScope = null ;
@@ -786,23 +802,21 @@ private async Task<ProtectedResourceMetadata> ExtractProtectedResourceMetadata(H
786802 break ;
787803 }
788804 }
789-
790- if ( metadata is null )
791- {
792- throw new McpException ( $ "Failed to find protected resource metadata at a well-known location for { _serverUrl } ") ;
793- }
794805 }
795806
796- // The WWW-Authenticate header parameter should be preferred over using the scopes_supported metadata property.
797- // https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#protected-resource-metadata-discovery-requirements
798- metadata . WwwAuthenticateScope = wwwAuthenticateScope ;
807+ if ( metadata is not null )
808+ {
809+ // The WWW-Authenticate header parameter should be preferred over using the scopes_supported metadata property.
810+ // https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#protected-resource-metadata-discovery-requirements
811+ metadata . WwwAuthenticateScope = wwwAuthenticateScope ;
799812
800- // Per RFC: The resource value must be identical to the URL that the client used to make the request to the resource server
801- LogValidatingResourceMetadata ( resourceUri ) ;
813+ // Per RFC: The resource value must be identical to the URL that the client used to make the request to the resource server
814+ LogValidatingResourceMetadata ( resourceUri ) ;
802815
803- if ( ! VerifyResourceMatch ( metadata , resourceUri ) )
804- {
805- throw new McpException ( $ "Resource URI in metadata ({ metadata . Resource } ) does not match the expected URI ({ resourceUri } )") ;
816+ if ( ! VerifyResourceMatch ( metadata , resourceUri ) )
817+ {
818+ throw new McpException ( $ "Resource URI in metadata ({ metadata . Resource } ) does not match the expected URI ({ resourceUri } )") ;
819+ }
806820 }
807821
808822 return metadata ;
@@ -908,6 +922,9 @@ private static void ThrowFailedToHandleUnauthorizedResponse(string message) =>
908922 [ LoggerMessage ( Level = LogLevel . Information , Message = "Selected authorization server: {Server} from {Count} available servers" ) ]
909923 partial void LogSelectedAuthorizationServer ( Uri server , int count ) ;
910924
925+ [ LoggerMessage ( Level = LogLevel . Information , Message = "Selected fallback authorization server: {Server}" ) ]
926+ partial void LogSelectedFallbackAuthorizationServer ( Uri server ) ;
927+
911928 [ LoggerMessage ( Level = LogLevel . Information , Message = "OAuth authorization completed successfully" ) ]
912929 partial void LogOAuthAuthorizationCompleted ( ) ;
913930
0 commit comments