Change ProtectedResourceMetadata URI properties to strings and build resource strings directly (#1264)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: halter73 <54385+halter73@users.noreply.github.com> Co-authored-by: Stephen Halter <halter73@gmail.com>
This commit is contained in:
parent
b4c7ded398
commit
dbf91bcb1a
2 changed files with 209 additions and 55 deletions
|
|
@ -154,7 +154,7 @@ internal sealed partial class ClientOAuthProvider : McpHttpClient
|
|||
// Try to refresh the access token if it is invalid and we have a refresh token.
|
||||
if (_authServerMetadata is not null && tokens?.RefreshToken is { Length: > 0 } refreshToken)
|
||||
{
|
||||
var accessToken = await RefreshTokensAsync(refreshToken, resourceUri, _authServerMetadata, cancellationToken).ConfigureAwait(false);
|
||||
var accessToken = await RefreshTokensAsync(refreshToken, resourceUri.ToString(), _authServerMetadata, cancellationToken).ConfigureAwait(false);
|
||||
return (accessToken, true);
|
||||
}
|
||||
|
||||
|
|
@ -243,15 +243,26 @@ internal sealed partial class ClientOAuthProvider : McpHttpClient
|
|||
ThrowFailedToHandleUnauthorizedResponse("No authorization servers found in authentication challenge");
|
||||
}
|
||||
|
||||
// Convert string URIs to Uri objects for the selector
|
||||
List<Uri> authServerUris = [];
|
||||
foreach (var serverUriString in availableAuthorizationServers)
|
||||
{
|
||||
if (!Uri.TryCreate(serverUriString, UriKind.Absolute, out var serverUri))
|
||||
{
|
||||
ThrowFailedToHandleUnauthorizedResponse($"Invalid authorization server URI: '{serverUriString}'. Available servers: {string.Join(", ", availableAuthorizationServers)}");
|
||||
}
|
||||
authServerUris.Add(serverUri);
|
||||
}
|
||||
|
||||
// Select authorization server using configured strategy
|
||||
var selectedAuthServer = _authServerSelector(availableAuthorizationServers);
|
||||
var selectedAuthServer = _authServerSelector(authServerUris);
|
||||
|
||||
if (selectedAuthServer is null)
|
||||
{
|
||||
ThrowFailedToHandleUnauthorizedResponse($"Authorization server selection returned null. Available servers: {string.Join(", ", availableAuthorizationServers)}");
|
||||
}
|
||||
|
||||
if (!availableAuthorizationServers.Contains(selectedAuthServer))
|
||||
if (!authServerUris.Contains(selectedAuthServer))
|
||||
{
|
||||
ThrowFailedToHandleUnauthorizedResponse($"Authorization server selector returned a server not in the available list: {selectedAuthServer}. Available servers: {string.Join(", ", availableAuthorizationServers)}");
|
||||
}
|
||||
|
|
@ -387,13 +398,13 @@ internal sealed partial class ClientOAuthProvider : McpHttpClient
|
|||
}
|
||||
}
|
||||
|
||||
private async Task<string?> RefreshTokensAsync(string refreshToken, Uri resourceUri, AuthorizationServerMetadata authServerMetadata, CancellationToken cancellationToken)
|
||||
private async Task<string?> RefreshTokensAsync(string refreshToken, string resourceUri, AuthorizationServerMetadata authServerMetadata, CancellationToken cancellationToken)
|
||||
{
|
||||
Dictionary<string, string> formFields = new()
|
||||
{
|
||||
["grant_type"] = "refresh_token",
|
||||
["refresh_token"] = refreshToken,
|
||||
["resource"] = resourceUri.ToString(),
|
||||
["resource"] = resourceUri,
|
||||
};
|
||||
|
||||
using var request = CreateTokenRequest(authServerMetadata.TokenEndpoint, formFields);
|
||||
|
|
@ -443,7 +454,7 @@ internal sealed partial class ClientOAuthProvider : McpHttpClient
|
|||
["response_type"] = "code",
|
||||
["code_challenge"] = codeChallenge,
|
||||
["code_challenge_method"] = "S256",
|
||||
["resource"] = resourceUri.ToString(),
|
||||
["resource"] = resourceUri,
|
||||
};
|
||||
|
||||
var scope = GetScopeParameter(protectedResourceMetadata);
|
||||
|
|
@ -487,7 +498,7 @@ internal sealed partial class ClientOAuthProvider : McpHttpClient
|
|||
["code"] = authorizationCode,
|
||||
["redirect_uri"] = _redirectUri.ToString(),
|
||||
["code_verifier"] = codeVerifier,
|
||||
["resource"] = resourceUri.ToString(),
|
||||
["resource"] = resourceUri,
|
||||
};
|
||||
|
||||
using var request = CreateTokenRequest(authServerMetadata.TokenEndpoint, formFields);
|
||||
|
|
@ -659,7 +670,7 @@ internal sealed partial class ClientOAuthProvider : McpHttpClient
|
|||
}
|
||||
}
|
||||
|
||||
private static Uri GetRequiredResourceUri(ProtectedResourceMetadata protectedResourceMetadata)
|
||||
private static string GetRequiredResourceUri(ProtectedResourceMetadata protectedResourceMetadata)
|
||||
{
|
||||
if (protectedResourceMetadata.Resource is null)
|
||||
{
|
||||
|
|
@ -732,6 +743,27 @@ internal sealed partial class ClientOAuthProvider : McpHttpClient
|
|||
return builder.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes a URI string for consistent comparison.
|
||||
/// </summary>
|
||||
/// <param name="uriString">The URI string to normalize.</param>
|
||||
/// <returns>
|
||||
/// A normalized string representation of the URI. If the string is a valid absolute URI,
|
||||
/// it is parsed and normalized (scheme, host, port, and path without trailing slash).
|
||||
/// If the string is not a valid absolute URI, only the trailing slash is removed.
|
||||
/// </returns>
|
||||
private static string NormalizeUri(string uriString)
|
||||
{
|
||||
// Parse the string as a URI to normalize it
|
||||
if (!Uri.TryCreate(uriString, UriKind.Absolute, out var uri))
|
||||
{
|
||||
// If it's not a valid URI, return the string with trailing slash removed
|
||||
return uriString.TrimEnd('/');
|
||||
}
|
||||
|
||||
return NormalizeUri(uri);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Responds to a 401 challenge by parsing the WWW-Authenticate header, fetching the resource metadata,
|
||||
/// verifying the resource match, and returning the metadata if valid.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ModelContextProtocol.Authentication;
|
||||
|
|
@ -20,7 +21,8 @@ public sealed class ProtectedResourceMetadata
|
|||
/// <b>Resource</b> must be explicitly set. Automatic inference only works with the default endpoint pattern.
|
||||
/// </remarks>
|
||||
[JsonPropertyName("resource")]
|
||||
public Uri? Resource { get; set; }
|
||||
[StringSyntax(StringSyntaxAttribute.Uri)]
|
||||
public string? Resource { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of authorization server URIs.
|
||||
|
|
@ -33,7 +35,7 @@ public sealed class ProtectedResourceMetadata
|
|||
/// OPTIONAL.
|
||||
/// </remarks>
|
||||
[JsonPropertyName("authorization_servers")]
|
||||
public List<Uri> AuthorizationServers { get; set; } = [];
|
||||
public List<string> AuthorizationServers { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the supported bearer token methods.
|
||||
|
|
@ -69,7 +71,8 @@ public sealed class ProtectedResourceMetadata
|
|||
/// that the resource server uses to sign resource responses. This URL MUST use the HTTPS scheme.
|
||||
/// </remarks>
|
||||
[JsonPropertyName("jwks_uri")]
|
||||
public Uri? JwksUri { get; set; }
|
||||
[StringSyntax(StringSyntaxAttribute.Uri)]
|
||||
public string? JwksUri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of the JWS signing algorithms supported by the protected resource for signing resource responses.
|
||||
|
|
@ -105,7 +108,8 @@ public sealed class ProtectedResourceMetadata
|
|||
/// OPTIONAL.
|
||||
/// </remarks>
|
||||
[JsonPropertyName("resource_documentation")]
|
||||
public Uri? ResourceDocumentation { get; set; }
|
||||
[StringSyntax(StringSyntaxAttribute.Uri)]
|
||||
public string? ResourceDocumentation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the URL of a page containing human-readable information about the protected resource's requirements.
|
||||
|
|
@ -117,7 +121,8 @@ public sealed class ProtectedResourceMetadata
|
|||
/// OPTIONAL.
|
||||
/// </remarks>
|
||||
[JsonPropertyName("resource_policy_uri")]
|
||||
public Uri? ResourcePolicyUri { get; set; }
|
||||
[StringSyntax(StringSyntaxAttribute.Uri)]
|
||||
public string? ResourcePolicyUri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the URL of a page containing human-readable information about the protected resource's terms of service.
|
||||
|
|
@ -126,7 +131,8 @@ public sealed class ProtectedResourceMetadata
|
|||
/// OPTIONAL. The value of this field MAY be internationalized.
|
||||
/// </remarks>
|
||||
[JsonPropertyName("resource_tos_uri")]
|
||||
public Uri? ResourceTosUri { get; set; }
|
||||
[StringSyntax(StringSyntaxAttribute.Uri)]
|
||||
public string? ResourceTosUri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether there is protected resource support for mutual-TLS client certificate-bound access tokens.
|
||||
|
|
@ -195,13 +201,13 @@ public sealed class ProtectedResourceMetadata
|
|||
/// <summary>
|
||||
/// Creates a deep copy of this <see cref="ProtectedResourceMetadata"/> instance, optionally overriding the Resource property.
|
||||
/// </summary>
|
||||
/// <param name="derivedResourceUri">Optional URI to use for the Resource property if the original Resource is null.</param>
|
||||
/// <param name="derivedResource">Optional resource URI string to use for the Resource property if the original Resource is null.</param>
|
||||
/// <returns>A new instance of <see cref="ProtectedResourceMetadata"/> with cloned values.</returns>
|
||||
public ProtectedResourceMetadata Clone(Uri? derivedResourceUri = null)
|
||||
public ProtectedResourceMetadata Clone(string? derivedResource = null)
|
||||
{
|
||||
return new ProtectedResourceMetadata
|
||||
{
|
||||
Resource = Resource ?? derivedResourceUri,
|
||||
Resource = Resource ?? derivedResource,
|
||||
AuthorizationServers = [.. AuthorizationServers],
|
||||
BearerMethodsSupported = [.. BearerMethodsSupported],
|
||||
ScopesSupported = [.. ScopesSupported],
|
||||
|
|
|
|||
Loading…
Reference in a new issue