@@ -32,6 +32,7 @@ internal sealed partial class ClientOAuthProvider : McpHttpClient
3232 private readonly IDictionary < string , string > _additionalAuthorizationParameters ;
3333 private readonly Func < IReadOnlyList < Uri > , Uri ? > _authServerSelector ;
3434 private readonly AuthorizationRedirectDelegate _authorizationRedirectDelegate ;
35+ private readonly Func < Uri , Uri , CancellationToken , Task < AuthorizationResult ? > > ? _authorizationCallbackHandler ;
3536 private readonly Uri ? _clientMetadataDocumentUri ;
3637
3738 // _dcrClientName, _dcrClientUri, _dcrInitialAccessToken and _dcrResponseDelegate are used for dynamic client registration (RFC 7591)
@@ -84,6 +85,9 @@ public ClientOAuthProvider(
8485 // Set up authorization server selection strategy
8586 _authServerSelector = options . AuthServerSelector ?? DefaultAuthServerSelector ;
8687
88+ // Set up authorization callback handler (new RFC 9207-aware handler takes precedence)
89+ _authorizationCallbackHandler = options . AuthorizationCallbackHandler ;
90+
8791 // Set up authorization URL handler (use default if not provided)
8892 _authorizationRedirectDelegate = options . AuthorizationRedirectDelegate ?? DefaultAuthorizationUrlHandler ;
8993
@@ -370,6 +374,16 @@ private async Task<AuthorizationServerMetadata> GetAuthServerMetadataAsync(Uri a
370374 metadata . TokenEndpointAuthMethodsSupported ??= [ "client_secret_post" ] ;
371375 metadata . CodeChallengeMethodsSupported ??= [ "S256" ] ;
372376
377+ // Validate the issuer in the metadata document per RFC 8414 Section 3.3:
378+ // the issuer value MUST be identical to the issuer identifier used to construct
379+ // the well-known URL.
380+ if ( metadata . Issuer is not null &&
381+ ! string . Equals ( metadata . Issuer . OriginalString , authServerUri . OriginalString , StringComparison . Ordinal ) )
382+ {
383+ ThrowFailedToHandleUnauthorizedResponse (
384+ $ "Authorization server metadata issuer '{ metadata . Issuer } ' does not match the expected issuer '{ authServerUri } ' (RFC 8414 Section 3.3).") ;
385+ }
386+
373387 return metadata ;
374388 }
375389 catch ( Exception ex )
@@ -462,14 +476,33 @@ private async Task<string> InitiateAuthorizationCodeFlowAsync(
462476 var codeChallenge = GenerateCodeChallenge ( codeVerifier ) ;
463477
464478 var authUrl = BuildAuthorizationUrl ( protectedResourceMetadata , authServerMetadata , codeChallenge ) ;
465- var authCode = await _authorizationRedirectDelegate ( authUrl , _redirectUri , cancellationToken ) . ConfigureAwait ( false ) ;
466479
467- if ( string . IsNullOrEmpty ( authCode ) )
480+ string ? authorizationCode ;
481+ string ? iss = null ;
482+
483+ if ( _authorizationCallbackHandler is not null )
468484 {
469- ThrowFailedToHandleUnauthorizedResponse ( $ "The { nameof ( AuthorizationRedirectDelegate ) } returned a null or empty authorization code.") ;
485+ var authResult = await _authorizationCallbackHandler ( authUrl , _redirectUri , cancellationToken ) . ConfigureAwait ( false ) ;
486+ if ( authResult is null || string . IsNullOrEmpty ( authResult . Code ) )
487+ {
488+ ThrowFailedToHandleUnauthorizedResponse ( $ "The { nameof ( ClientOAuthOptions . AuthorizationCallbackHandler ) } returned a null or empty authorization code.") ;
489+ }
490+
491+ authorizationCode = authResult ! . Code ! ;
492+ iss = authResult . Iss ;
493+ }
494+ else
495+ {
496+ authorizationCode = await _authorizationRedirectDelegate ( authUrl , _redirectUri , cancellationToken ) . ConfigureAwait ( false ) ;
497+ if ( string . IsNullOrEmpty ( authorizationCode ) )
498+ {
499+ ThrowFailedToHandleUnauthorizedResponse ( $ "The { nameof ( AuthorizationRedirectDelegate ) } returned a null or empty authorization code.") ;
500+ }
470501 }
471502
472- return await ExchangeCodeForTokenAsync ( protectedResourceMetadata , authServerMetadata , authCode ! , codeVerifier , cancellationToken ) . ConfigureAwait ( false ) ;
503+ ValidateIssuerResponse ( iss , authServerMetadata ) ;
504+
505+ return await ExchangeCodeForTokenAsync ( protectedResourceMetadata , authServerMetadata , authorizationCode ! , codeVerifier , cancellationToken ) . ConfigureAwait ( false ) ;
473506 }
474507
475508 private Uri BuildAuthorizationUrl (
@@ -773,6 +806,47 @@ private async Task PerformDynamicClientRegistrationAsync(
773806 return scope + " " + OfflineAccess ;
774807 }
775808
809+ /// <summary>
810+ /// Validates the <c>iss</c> parameter from an authorization response per
811+ /// <see href="https://datatracker.ietf.org/doc/html/rfc9207">RFC 9207</see>.
812+ /// </summary>
813+ /// <param name="iss">The issuer identifier received in the authorization response, or null if absent.</param>
814+ /// <param name="authServerMetadata">The authorization server metadata containing the expected issuer.</param>
815+ private void ValidateIssuerResponse ( string ? iss , AuthorizationServerMetadata authServerMetadata )
816+ {
817+ var expectedIssuer = authServerMetadata . Issuer ? . OriginalString ;
818+
819+ if ( authServerMetadata . AuthorizationResponseIssParameterSupported )
820+ {
821+ // Server advertises iss support: iss MUST be present and match.
822+ if ( string . IsNullOrEmpty ( iss ) )
823+ {
824+ ThrowFailedToHandleUnauthorizedResponse (
825+ "Authorization server advertises RFC 9207 iss parameter support but none was received in the authorization response." ) ;
826+ }
827+
828+ // Use exact string comparison per RFC 9207 / RFC 3986 §6.2.1.
829+ if ( ! string . Equals ( iss , expectedIssuer , StringComparison . Ordinal ) )
830+ {
831+ ThrowFailedToHandleUnauthorizedResponse (
832+ $ "Authorization response issuer '{ iss } ' does not match expected issuer '{ expectedIssuer } '.") ;
833+ }
834+ }
835+ else
836+ {
837+ // Server does not advertise iss support: if iss is present, still validate it.
838+ if ( ! string . IsNullOrEmpty ( iss ) )
839+ {
840+ if ( ! string . Equals ( iss , expectedIssuer , StringComparison . Ordinal ) )
841+ {
842+ ThrowFailedToHandleUnauthorizedResponse (
843+ $ "Authorization response issuer '{ iss } ' does not match expected issuer '{ expectedIssuer } '.") ;
844+ }
845+ }
846+ // If iss is absent and not advertised, proceed normally.
847+ }
848+ }
849+
776850 /// <summary>
777851 /// Verifies that the resource URI in the metadata exactly matches the original request URL as required by the RFC.
778852 /// Per RFC: The resource value must be identical to the URL that the client used to make the request to the resource server.
0 commit comments