@@ -8,15 +8,9 @@ namespace ModelContextProtocol.Authentication;
88/// Provides utility methods for handling authentication in MCP clients.
99/// </summary>
1010public class AuthorizationHelpers
11- {
12- private readonly HttpClient _httpClient ;
11+ { private readonly HttpClient _httpClient ;
1312 private readonly ILogger _logger ;
1413 private static readonly Lazy < HttpClient > _defaultHttpClient = new ( ( ) => new HttpClient ( ) ) ;
15-
16- /// <summary>
17- /// The well-known path prefix for resource metadata.
18- /// </summary>
19- private const string WellKnownPathPrefix = "/.well-known/" ;
2014
2115 /// <summary>
2216 /// Initializes a new instance of the <see cref="AuthorizationHelpers"/> class.
@@ -57,11 +51,12 @@ public AuthorizationHelpers(HttpClient? httpClient = null, ILogger? logger = nul
5751 }
5852
5953 /// <summary>
60- /// Verifies that the resource URI in the metadata exactly matches the server URL as required by the RFC.
54+ /// Verifies that the resource URI in the metadata exactly matches the original request URL as required by the RFC.
55+ /// Per RFC: The resource value must be identical to the URL that the client used to make the request to the resource server.
6156 /// </summary>
6257 /// <param name="protectedResourceMetadata">The metadata to verify.</param>
63- /// <param name="resourceLocation">The server URL to compare against .</param>
64- /// <returns>True if the resource URI exactly matches the server , otherwise false.</returns>
58+ /// <param name="resourceLocation">The original URL the client used to make the request to the resource server .</param>
59+ /// <returns>True if the resource URI exactly matches the original request URL , otherwise false.</returns>
6560 private static bool VerifyResourceMatch ( ProtectedResourceMetadata protectedResourceMetadata , Uri resourceLocation )
6661 {
6762 if ( protectedResourceMetadata . Resource == null || resourceLocation == null )
@@ -71,11 +66,11 @@ private static bool VerifyResourceMatch(ProtectedResourceMetadata protectedResou
7166
7267 // Per RFC: The resource value must be identical to the URL that the client used
7368 // to make the request to the resource server. Compare entire URIs, not just the host.
74-
69+
7570 // Normalize the URIs to ensure consistent comparison
7671 string normalizedMetadataResource = NormalizeUri ( protectedResourceMetadata . Resource ) ;
7772 string normalizedResourceLocation = NormalizeUri ( resourceLocation ) ;
78-
73+
7974 return string . Equals ( normalizedMetadataResource , normalizedResourceLocation , StringComparison . OrdinalIgnoreCase ) ;
8075 }
8176
@@ -100,44 +95,7 @@ private static string NormalizeUri(Uri uri)
10095 builder . Path = builder . Path . TrimEnd ( '/' ) ;
10196 }
10297
103- return builder . Uri . ToString ( ) ;
104- }
105-
106- /// <summary>
107- /// Extracts the base resource URI from a well-known path URL.
108- /// </summary>
109- /// <param name="metadataUri">The metadata URI containing a well-known path.</param>
110- /// <returns>The base URI without the well-known path component.</returns>
111- /// <exception cref="InvalidOperationException">Thrown when the URI does not contain a valid well-known path.</exception>
112- private Uri ExtractBaseResourceUri ( Uri metadataUri )
113- {
114- // Check for well-known path
115- int wellKnownIndex = metadataUri . AbsolutePath . IndexOf ( WellKnownPathPrefix , StringComparison . OrdinalIgnoreCase ) ;
116-
117- // Validate the URL contains a valid well-known path
118- if ( wellKnownIndex < 0 )
119- {
120- throw new InvalidOperationException (
121- $ "Resource metadata URL '{ metadataUri } ' does not contain a valid well-known path format (/.well-known/)") ;
122- }
123-
124- // Create URI with just the base part
125- var baseUriBuilder = new UriBuilder ( metadataUri )
126- {
127- Path = wellKnownIndex > 0 ? metadataUri . AbsolutePath . Substring ( 0 , wellKnownIndex ) : "/" ,
128- Fragment = string . Empty ,
129- Query = string . Empty ,
130- Port = - 1 // Remove port
131- } ;
132-
133- // Ensure path ends with a slash
134- if ( ! baseUriBuilder . Path . EndsWith ( "/" ) )
135- {
136- baseUriBuilder . Path += "/" ;
137- }
138-
139- return baseUriBuilder . Uri ;
140- }
98+ return builder . Uri . ToString ( ) ; }
14199
142100 /// <summary>
143101 /// Responds to a 401 challenge by parsing the WWW-Authenticate header, fetching the resource metadata,
@@ -185,22 +143,20 @@ internal async Task<ProtectedResourceMetadata> ExtractProtectedResourceMetadata(
185143 throw new InvalidOperationException ( "The WWW-Authenticate header does not contain a resource_metadata parameter" ) ;
186144 }
187145
188- Uri metadataUri = new ( resourceMetadataUrl ) ;
189-
190- var metadata = await FetchProtectedResourceMetadataAsync ( metadataUri , cancellationToken ) . ConfigureAwait ( false ) ;
146+ Uri metadataUri = new ( resourceMetadataUrl ) ; var metadata = await FetchProtectedResourceMetadataAsync ( metadataUri , cancellationToken ) . ConfigureAwait ( false ) ;
191147 if ( metadata == null )
192148 {
193149 throw new InvalidOperationException ( $ "Failed to fetch resource metadata from { resourceMetadataUrl } ") ;
194150 }
195151
196- // Extract the base URI from the metadata URL
197- Uri urlToValidate = ExtractBaseResourceUri ( metadataUri ) ;
198- _logger . LogDebug ( $ "Validating resource metadata against base URL: { urlToValidate } ") ;
152+ // Per RFC: The resource value must be identical to the URL that the client used
153+ // to make the request to the resource server
154+ _logger . LogDebug ( $ "Validating resource metadata against original server URL: { serverUrl } ") ;
199155
200- if ( ! VerifyResourceMatch ( metadata , urlToValidate ) )
156+ if ( ! VerifyResourceMatch ( metadata , serverUrl ) )
201157 {
202158 throw new InvalidOperationException (
203- $ "Resource URI in metadata ({ metadata . Resource } ) does not match the expected URI ({ urlToValidate } )") ;
159+ $ "Resource URI in metadata ({ metadata . Resource } ) does not match the expected URI ({ serverUrl } )") ;
204160 }
205161
206162 return metadata ;
@@ -245,7 +201,9 @@ internal async Task<ProtectedResourceMetadata> ExtractProtectedResourceMetadata(
245201 }
246202
247203 return null ;
248- } /// <summary>
204+ }
205+
206+ /// <summary>
249207 /// Handles a 401 Unauthorized response and returns all available authorization servers.
250208 /// This is the primary method for OAuth discovery - use this when you want full control
251209 /// over authorization server selection.
@@ -261,7 +219,7 @@ public async Task<IReadOnlyList<Uri>> GetAvailableAuthorizationServersAsync(
261219 CancellationToken cancellationToken = default )
262220 {
263221 if ( response == null ) throw new ArgumentNullException ( nameof ( response ) ) ;
264-
222+
265223 try
266224 {
267225 // Extract resource metadata behind the scenes
0 commit comments