Skip to content

Commit c4ba02e

Browse files
committed
Add legacy authorization URL fallback
This patch adds support for legacy behavior defined in MCP 2025-03-26, where by if the MCP server does not provide a PRM, the MCP client uses the MCP server's base URL as the authorization server. Since a PRM is not specified, a `resource` indicator is also not specified, this means the code has to updated to handle this optionality.
1 parent 27b5eb8 commit c4ba02e

1 file changed

Lines changed: 69 additions & 52 deletions

File tree

src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs

Lines changed: 69 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)