1+ using ModelContextProtocol . Authentication . Types ;
12using System . Net . Http . Headers ;
23
34namespace ModelContextProtocol . Authentication ;
@@ -8,6 +9,7 @@ namespace ModelContextProtocol.Authentication;
89public class AuthorizationDelegatingHandler : DelegatingHandler
910{
1011 private readonly IMcpAuthorizationProvider _authorizationProvider ;
12+ private string ? _currentScheme ;
1113
1214 /// <summary>
1315 /// Initializes a new instance of the <see cref="AuthorizationDelegatingHandler"/> class.
@@ -16,50 +18,137 @@ public class AuthorizationDelegatingHandler : DelegatingHandler
1618 public AuthorizationDelegatingHandler ( IMcpAuthorizationProvider authorizationProvider )
1719 {
1820 _authorizationProvider = authorizationProvider ?? throw new ArgumentNullException ( nameof ( authorizationProvider ) ) ;
21+
22+ // Select first supported scheme as the default
23+ _currentScheme = _authorizationProvider . SupportedSchemes . FirstOrDefault ( ) ;
24+ if ( _currentScheme == null )
25+ {
26+ throw new ArgumentException ( "Authorization provider must support at least one authentication scheme." , nameof ( authorizationProvider ) ) ;
27+ }
1928 }
2029
2130 /// <summary>
2231 /// Sends an HTTP request with authentication handling.
2332 /// </summary>
2433 protected override async Task < HttpResponseMessage > SendAsync ( HttpRequestMessage request , CancellationToken cancellationToken )
2534 {
26- if ( request . Headers . Authorization == null )
35+ if ( request . Headers . Authorization == null && _currentScheme != null )
2736 {
28- await AddAuthorizationHeaderAsync ( request , cancellationToken ) ;
37+ await AddAuthorizationHeaderAsync ( request , _currentScheme , cancellationToken ) ;
2938 }
3039
3140 var response = await base . SendAsync ( request , cancellationToken ) ;
3241
3342 if ( response . StatusCode == System . Net . HttpStatusCode . Unauthorized )
3443 {
35- var handled = await _authorizationProvider . HandleUnauthorizedResponseAsync (
36- response ,
37- cancellationToken ) ;
38-
39- if ( handled )
44+ // Gather the schemes the server wants us to use from WWW-Authenticate headers
45+ var serverSchemes = ExtractServerSupportedSchemes ( response ) ;
46+
47+ // Find the intersection between what the server supports and what our provider supports
48+ var supportedSchemes = _authorizationProvider . SupportedSchemes . ToList ( ) ;
49+ string ? bestSchemeMatch = null ;
50+
51+ // First try to find a direct match with the current scheme if it's still valid
52+ string schemeUsed = request . Headers . Authorization ? . Scheme ?? _currentScheme ?? string . Empty ;
53+ if ( serverSchemes . Contains ( schemeUsed ) && supportedSchemes . Contains ( schemeUsed ) )
4054 {
41- var retryRequest = await CloneHttpRequestMessageAsync ( request ) ;
42-
43- await AddAuthorizationHeaderAsync ( retryRequest , cancellationToken ) ;
44-
45- return await base . SendAsync ( retryRequest , cancellationToken ) ;
55+ bestSchemeMatch = schemeUsed ;
56+ }
57+ else
58+ {
59+ // Try to find any matching scheme between server and provider
60+ bestSchemeMatch = serverSchemes . FirstOrDefault ( scheme => supportedSchemes . Contains ( scheme ) ) ;
61+
62+ // If still no match, default to the provider's preferred scheme
63+ if ( bestSchemeMatch == null && serverSchemes . Count > 0 )
64+ {
65+ throw new AuthenticationSchemeMismatchException (
66+ $ "No matching authentication scheme found. Server supports: [{ string . Join ( ", " , serverSchemes ) } ], " +
67+ $ "Provider supports: [{ string . Join ( ", " , supportedSchemes ) } ].",
68+ serverSchemes ,
69+ supportedSchemes ) ;
70+ }
71+ else if ( bestSchemeMatch == null )
72+ {
73+ // If the server didn't specify any schemes, use the provider's default
74+ bestSchemeMatch = supportedSchemes . FirstOrDefault ( ) ;
75+ }
76+ }
77+
78+ // If we have a scheme to try, use it
79+ if ( bestSchemeMatch != null )
80+ {
81+ // Try to handle the 401 response with the selected scheme
82+ var ( handled , recommendedScheme ) = await _authorizationProvider . HandleUnauthorizedResponseAsync (
83+ response ,
84+ bestSchemeMatch ,
85+ cancellationToken ) ;
86+
87+ if ( handled )
88+ {
89+ var retryRequest = await CloneHttpRequestMessageAsync ( request ) ;
90+
91+ // Use the recommended scheme if provided, otherwise use our best match
92+ string schemeToUse = recommendedScheme ?? bestSchemeMatch ;
93+ if ( ! string . IsNullOrEmpty ( recommendedScheme ) )
94+ {
95+ _currentScheme = recommendedScheme ;
96+ }
97+ else
98+ {
99+ _currentScheme = bestSchemeMatch ;
100+ }
101+
102+ await AddAuthorizationHeaderAsync ( retryRequest , schemeToUse , cancellationToken ) ;
103+ return await base . SendAsync ( retryRequest , cancellationToken ) ;
104+ }
105+ else
106+ {
107+ throw new McpException (
108+ $ "Failed to handle unauthorized response with scheme '{ bestSchemeMatch } '. " +
109+ "The authentication provider was unable to process the authentication challenge." ) ;
110+ }
46111 }
47112 }
48113
49114 return response ;
50115 }
51116
117+ /// <summary>
118+ /// Extracts the authentication schemes that the server supports from the WWW-Authenticate headers.
119+ /// </summary>
120+ private static List < string > ExtractServerSupportedSchemes ( HttpResponseMessage response )
121+ {
122+ var serverSchemes = new List < string > ( ) ;
123+
124+ if ( response . Headers . Contains ( "WWW-Authenticate" ) )
125+ {
126+ foreach ( var authHeader in response . Headers . GetValues ( "WWW-Authenticate" ) )
127+ {
128+ // Extract the scheme from the WWW-Authenticate header
129+ // Format is typically: "Scheme param1=value1, param2=value2"
130+ string scheme = authHeader . Split ( new [ ] { ' ' , ',' } , StringSplitOptions . RemoveEmptyEntries ) [ 0 ] ;
131+ if ( ! string . IsNullOrEmpty ( scheme ) )
132+ {
133+ serverSchemes . Add ( scheme ) ;
134+ }
135+ }
136+ }
137+
138+ return serverSchemes ;
139+ }
140+
52141 /// <summary>
53142 /// Adds an authorization header to the request.
54143 /// </summary>
55- private async Task AddAuthorizationHeaderAsync ( HttpRequestMessage request , CancellationToken cancellationToken )
144+ private async Task AddAuthorizationHeaderAsync ( HttpRequestMessage request , string scheme , CancellationToken cancellationToken )
56145 {
57146 if ( request . RequestUri != null )
58147 {
59- var token = await _authorizationProvider . GetCredentialAsync ( request . RequestUri , cancellationToken ) ;
148+ var token = await _authorizationProvider . GetCredentialAsync ( scheme , request . RequestUri , cancellationToken ) ;
60149 if ( ! string . IsNullOrEmpty ( token ) )
61150 {
62- request . Headers . Authorization = new AuthenticationHeaderValue ( _authorizationProvider . AuthorizationScheme , token ) ;
151+ request . Headers . Authorization = new AuthenticationHeaderValue ( scheme , token ) ;
63152 }
64153 }
65154 }
0 commit comments