From 0bb5fb7f9f107b869dce4ad1cbaf4b3ada7dc974 Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Wed, 27 May 2026 14:07:13 -0700 Subject: [PATCH 1/4] Implement RFC 9207 issuer validation in ClientOAuthProvider --- samples/ProtectedMcpClient/Program.cs | 10 ++- .../AuthorizationRedirectDelegate.cs | 6 +- .../Authentication/AuthorizationResult.cs | 39 +++++++++ .../AuthorizationServerMetadata.cs | 7 ++ .../Authentication/ClientOAuthOptions.cs | 18 ++++ .../Authentication/ClientOAuthProvider.cs | 82 ++++++++++++++++++- .../OAuth/AuthEventTests.cs | 4 +- .../OAuth/AuthTests.cs | 70 ++++++++-------- .../OAuth/OAuthTestBase.cs | 8 +- .../OAuth/TokenCacheTests.cs | 12 +-- .../Program.cs | 25 ++++-- 11 files changed, 222 insertions(+), 59 deletions(-) create mode 100644 src/ModelContextProtocol.Core/Authentication/AuthorizationResult.cs diff --git a/samples/ProtectedMcpClient/Program.cs b/samples/ProtectedMcpClient/Program.cs index eba792556..ade93da96 100644 --- a/samples/ProtectedMcpClient/Program.cs +++ b/samples/ProtectedMcpClient/Program.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Logging; +using ModelContextProtocol.Authentication; using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; using System.Diagnostics; @@ -32,7 +33,7 @@ OAuth = new() { RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + AuthorizationCallbackHandler = HandleAuthorizationUrlAsync, DynamicClientRegistration = new() { ClientName = "ProtectedMcpClient", @@ -71,8 +72,8 @@ /// The authorization URL to open in the browser. /// The redirect URI where the authorization code will be sent. /// The to monitor for cancellation requests. The default is . -/// The authorization code extracted from the callback, or null if the operation failed. -static async Task HandleAuthorizationUrlAsync(Uri authorizationUrl, Uri redirectUri, CancellationToken cancellationToken) +/// The authorization result extracted from the callback, or null if the operation failed. +static async Task HandleAuthorizationUrlAsync(Uri authorizationUrl, Uri redirectUri, CancellationToken cancellationToken) { Console.WriteLine("Starting OAuth authorization flow..."); Console.WriteLine($"Opening browser to: {authorizationUrl}"); @@ -93,6 +94,7 @@ var context = await listener.GetContextAsync(); var query = HttpUtility.ParseQueryString(context.Request.Url?.Query ?? string.Empty); var code = query["code"]; + var iss = query["iss"]; var error = query["error"]; string responseHtml = "

Authentication complete

You can close this window now.

"; @@ -115,7 +117,7 @@ } Console.WriteLine("Authorization code received successfully."); - return code; + return new AuthorizationResult { Code = code, Iss = iss }; } catch (Exception ex) { diff --git a/src/ModelContextProtocol.Core/Authentication/AuthorizationRedirectDelegate.cs b/src/ModelContextProtocol.Core/Authentication/AuthorizationRedirectDelegate.cs index a811e51cc..c268a47fd 100644 --- a/src/ModelContextProtocol.Core/Authentication/AuthorizationRedirectDelegate.cs +++ b/src/ModelContextProtocol.Core/Authentication/AuthorizationRedirectDelegate.cs @@ -22,7 +22,11 @@ namespace ModelContextProtocol.Authentication; /// /// The implementation should handle user interaction to visit the authorization URL and extract /// the authorization code from the callback. The authorization code is typically provided as -/// a query parameter in the redirect URI callback. +/// a code query parameter in the redirect URI callback. +/// +/// +/// For RFC 9207 issuer validation support, use +/// instead, which allows returning both the authorization code and the iss parameter. /// /// public delegate Task AuthorizationRedirectDelegate(Uri authorizationUri, Uri redirectUri, CancellationToken cancellationToken); \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Authentication/AuthorizationResult.cs b/src/ModelContextProtocol.Core/Authentication/AuthorizationResult.cs new file mode 100644 index 000000000..37da9087a --- /dev/null +++ b/src/ModelContextProtocol.Core/Authentication/AuthorizationResult.cs @@ -0,0 +1,39 @@ +namespace ModelContextProtocol.Authentication; + +/// +/// Represents the result of an OAuth authorization redirect, containing the authorization code +/// and optionally the issuer identifier from the authorization response. +/// +/// +/// +/// The property should be populated from the iss query parameter in the +/// redirect URI when present, as specified by +/// RFC 9207. +/// This enables the SDK to validate that the authorization response originated from the expected +/// authorization server, mitigating mix-up attacks. +/// +/// +public sealed class AuthorizationResult +{ + /// + /// Gets the authorization code returned by the authorization server. + /// + public string? Code { get; init; } + + /// + /// Gets the issuer identifier returned in the authorization response per + /// RFC 9207. + /// + /// + /// + /// This value should be extracted from the iss query parameter of the redirect URI. + /// When present, the SDK validates it against the expected authorization server issuer to + /// prevent mix-up attacks. + /// + /// + /// Implementations of should populate this property + /// whenever the iss parameter is present in the redirect URI callback. + /// + /// + public string? Iss { get; init; } +} diff --git a/src/ModelContextProtocol.Core/Authentication/AuthorizationServerMetadata.cs b/src/ModelContextProtocol.Core/Authentication/AuthorizationServerMetadata.cs index 87df29636..d8d684a3b 100644 --- a/src/ModelContextProtocol.Core/Authentication/AuthorizationServerMetadata.cs +++ b/src/ModelContextProtocol.Core/Authentication/AuthorizationServerMetadata.cs @@ -72,4 +72,11 @@ internal sealed class AuthorizationServerMetadata /// [JsonPropertyName("client_id_metadata_document_supported")] public bool ClientIdMetadataDocumentSupported { get; set; } + + /// + /// Indicates whether the authorization server includes the iss parameter in authorization responses + /// as defined in RFC 9207. + /// + [JsonPropertyName("authorization_response_iss_parameter_supported")] + public bool AuthorizationResponseIssParameterSupported { get; set; } } diff --git a/src/ModelContextProtocol.Core/Authentication/ClientOAuthOptions.cs b/src/ModelContextProtocol.Core/Authentication/ClientOAuthOptions.cs index 0bfb19a59..08cbee53c 100644 --- a/src/ModelContextProtocol.Core/Authentication/ClientOAuthOptions.cs +++ b/src/ModelContextProtocol.Core/Authentication/ClientOAuthOptions.cs @@ -84,6 +84,24 @@ public sealed class ClientOAuthOptions /// public AuthorizationRedirectDelegate? AuthorizationRedirectDelegate { get; set; } + /// + /// Gets or sets a callback that handles the full OAuth authorization flow, returning both the + /// authorization code and the issuer identifier for RFC 9207 validation. + /// + /// + /// + /// When set, this handler takes precedence over . + /// It enables the SDK to validate the iss parameter in the authorization response per + /// RFC 9207, which mitigates + /// mix-up attacks. + /// + /// + /// Implementations should extract both the code and iss query parameters from + /// the redirect URI callback and return them in an . + /// + /// + public Func>? AuthorizationCallbackHandler { get; set; } + /// /// Gets or sets the authorization server selector function. /// diff --git a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs index 662e436eb..a4139f132 100644 --- a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs +++ b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs @@ -32,6 +32,7 @@ internal sealed partial class ClientOAuthProvider : McpHttpClient private readonly IDictionary _additionalAuthorizationParameters; private readonly Func, Uri?> _authServerSelector; private readonly AuthorizationRedirectDelegate _authorizationRedirectDelegate; + private readonly Func>? _authorizationCallbackHandler; private readonly Uri? _clientMetadataDocumentUri; // _dcrClientName, _dcrClientUri, _dcrInitialAccessToken and _dcrResponseDelegate are used for dynamic client registration (RFC 7591) @@ -84,6 +85,9 @@ public ClientOAuthProvider( // Set up authorization server selection strategy _authServerSelector = options.AuthServerSelector ?? DefaultAuthServerSelector; + // Set up authorization callback handler (new RFC 9207-aware handler takes precedence) + _authorizationCallbackHandler = options.AuthorizationCallbackHandler; + // Set up authorization URL handler (use default if not provided) _authorizationRedirectDelegate = options.AuthorizationRedirectDelegate ?? DefaultAuthorizationUrlHandler; @@ -370,6 +374,16 @@ private async Task GetAuthServerMetadataAsync(Uri a metadata.TokenEndpointAuthMethodsSupported ??= ["client_secret_post"]; metadata.CodeChallengeMethodsSupported ??= ["S256"]; + // Validate the issuer in the metadata document per RFC 8414 Section 3.3: + // the issuer value MUST be identical to the issuer identifier used to construct + // the well-known URL. + if (metadata.Issuer is not null && + !string.Equals(metadata.Issuer.OriginalString, authServerUri.OriginalString, StringComparison.Ordinal)) + { + ThrowFailedToHandleUnauthorizedResponse( + $"Authorization server metadata issuer '{metadata.Issuer}' does not match the expected issuer '{authServerUri}' (RFC 8414 Section 3.3)."); + } + return metadata; } catch (Exception ex) @@ -462,14 +476,33 @@ private async Task InitiateAuthorizationCodeFlowAsync( var codeChallenge = GenerateCodeChallenge(codeVerifier); var authUrl = BuildAuthorizationUrl(protectedResourceMetadata, authServerMetadata, codeChallenge); - var authCode = await _authorizationRedirectDelegate(authUrl, _redirectUri, cancellationToken).ConfigureAwait(false); - if (string.IsNullOrEmpty(authCode)) + string? authorizationCode; + string? iss = null; + + if (_authorizationCallbackHandler is not null) { - ThrowFailedToHandleUnauthorizedResponse($"The {nameof(AuthorizationRedirectDelegate)} returned a null or empty authorization code."); + var authResult = await _authorizationCallbackHandler(authUrl, _redirectUri, cancellationToken).ConfigureAwait(false); + if (authResult is null || string.IsNullOrEmpty(authResult.Code)) + { + ThrowFailedToHandleUnauthorizedResponse($"The {nameof(ClientOAuthOptions.AuthorizationCallbackHandler)} returned a null or empty authorization code."); + } + + authorizationCode = authResult!.Code!; + iss = authResult.Iss; + } + else + { + authorizationCode = await _authorizationRedirectDelegate(authUrl, _redirectUri, cancellationToken).ConfigureAwait(false); + if (string.IsNullOrEmpty(authorizationCode)) + { + ThrowFailedToHandleUnauthorizedResponse($"The {nameof(AuthorizationRedirectDelegate)} returned a null or empty authorization code."); + } } - return await ExchangeCodeForTokenAsync(protectedResourceMetadata, authServerMetadata, authCode!, codeVerifier, cancellationToken).ConfigureAwait(false); + ValidateIssuerResponse(iss, authServerMetadata); + + return await ExchangeCodeForTokenAsync(protectedResourceMetadata, authServerMetadata, authorizationCode!, codeVerifier, cancellationToken).ConfigureAwait(false); } private Uri BuildAuthorizationUrl( @@ -773,6 +806,47 @@ private async Task PerformDynamicClientRegistrationAsync( return scope + " " + OfflineAccess; } + /// + /// Validates the iss parameter from an authorization response per + /// RFC 9207. + /// + /// The issuer identifier received in the authorization response, or null if absent. + /// The authorization server metadata containing the expected issuer. + private void ValidateIssuerResponse(string? iss, AuthorizationServerMetadata authServerMetadata) + { + var expectedIssuer = authServerMetadata.Issuer?.OriginalString; + + if (authServerMetadata.AuthorizationResponseIssParameterSupported) + { + // Server advertises iss support: iss MUST be present and match. + if (string.IsNullOrEmpty(iss)) + { + ThrowFailedToHandleUnauthorizedResponse( + "Authorization server advertises RFC 9207 iss parameter support but none was received in the authorization response."); + } + + // Use exact string comparison per RFC 9207 / RFC 3986 §6.2.1. + if (!string.Equals(iss, expectedIssuer, StringComparison.Ordinal)) + { + ThrowFailedToHandleUnauthorizedResponse( + $"Authorization response issuer '{iss}' does not match expected issuer '{expectedIssuer}'."); + } + } + else + { + // Server does not advertise iss support: if iss is present, still validate it. + if (!string.IsNullOrEmpty(iss)) + { + if (!string.Equals(iss, expectedIssuer, StringComparison.Ordinal)) + { + ThrowFailedToHandleUnauthorizedResponse( + $"Authorization response issuer '{iss}' does not match expected issuer '{expectedIssuer}'."); + } + } + // If iss is absent and not advertised, proceed normally. + } + } + /// /// Verifies that the resource URI in the metadata exactly matches the original request URL as required by the RFC. /// Per RFC: The resource value must be identical to the URL that the client used to make the request to the resource server. diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthEventTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthEventTests.cs index 7aafd312e..1866dfe13 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthEventTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthEventTests.cs @@ -48,7 +48,7 @@ public async Task CanAuthenticate_WithResourceMetadataFromEvent() ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + AuthorizationCallbackHandler = HandleAuthorizationUrlAsync, }, }, HttpClient, @@ -76,7 +76,7 @@ public async Task CanAuthenticate_WithDynamicClientRegistration_FromEvent() OAuth = new ClientOAuthOptions() { RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + AuthorizationCallbackHandler = HandleAuthorizationUrlAsync, Scopes = ["mcp:tools"], DynamicClientRegistration = new() { diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthTests.cs index 1ec6fddc6..29fe29483 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthTests.cs @@ -41,7 +41,7 @@ public async Task CanAuthenticate() ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + AuthorizationCallbackHandler = HandleAuthorizationUrlAsync, }, }, HttpClient, LoggerFactory); @@ -78,7 +78,7 @@ public async Task CannotAuthenticate_WithUnregisteredClient() ClientId = "unregistered-demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + AuthorizationCallbackHandler = HandleAuthorizationUrlAsync, }, }, HttpClient, LoggerFactory); @@ -98,7 +98,7 @@ public async Task CanAuthenticate_WithDynamicClientRegistration() OAuth = new ClientOAuthOptions() { RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + AuthorizationCallbackHandler = HandleAuthorizationUrlAsync, DynamicClientRegistration = new() { ClientName = "Test MCP Client", @@ -122,7 +122,7 @@ public async Task CanAuthenticate_WithClientMetadataDocument() OAuth = new ClientOAuthOptions() { RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + AuthorizationCallbackHandler = HandleAuthorizationUrlAsync, ClientMetadataDocumentUri = new Uri(ClientMetadataDocumentUrl) }, }, HttpClient, LoggerFactory); @@ -147,7 +147,7 @@ public async Task UsesDynamicClientRegistration_WhenCimdNotSupported() OAuth = new ClientOAuthOptions() { RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + AuthorizationCallbackHandler = HandleAuthorizationUrlAsync, ClientMetadataDocumentUri = new Uri("http://invalid-cimd.example.com"), DynamicClientRegistration = new() { @@ -176,7 +176,7 @@ public async Task DoesNotUseClientMetadataDocument_WhenClientIdIsSpecified() ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + AuthorizationCallbackHandler = HandleAuthorizationUrlAsync, ClientMetadataDocumentUri = new Uri("http://invalid-cimd.example.com"), }, }, HttpClient, LoggerFactory); @@ -198,7 +198,7 @@ public async Task CannotAuthenticate_WithInvalidClientMetadataDocument(string ur OAuth = new ClientOAuthOptions() { RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + AuthorizationCallbackHandler = HandleAuthorizationUrlAsync, ClientMetadataDocumentUri = new Uri(uri), }, }, HttpClient, LoggerFactory); @@ -263,7 +263,7 @@ public async Task CanAuthenticate_WithTokenRefresh() ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + AuthorizationCallbackHandler = HandleAuthorizationUrlAsync, }, }, HttpClient, LoggerFactory); @@ -290,7 +290,7 @@ public async Task CanAuthenticate_WithExtraParams() ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = (uri, redirect, ct) => + AuthorizationCallbackHandler = (uri, redirect, ct) => { lastAuthorizationUri = uri; return HandleAuthorizationUrlAsync(uri, redirect, ct); @@ -322,7 +322,7 @@ public async Task CannotOverrideExistingParameters_WithExtraParams() ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + AuthorizationCallbackHandler = HandleAuthorizationUrlAsync, AdditionalAuthorizationParameters = new Dictionary { ["redirect_uri"] = "custom_value", @@ -347,7 +347,7 @@ public async Task CanAuthenticate_WithoutResourceInWwwAuthenticateHeader() ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + AuthorizationCallbackHandler = HandleAuthorizationUrlAsync, }, }, HttpClient, LoggerFactory); @@ -369,7 +369,7 @@ public async Task CanAuthenticate_WithoutResourceInWwwAuthenticateHeader_WithPat ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + AuthorizationCallbackHandler = HandleAuthorizationUrlAsync, }, }, HttpClient, LoggerFactory); @@ -397,7 +397,7 @@ public async Task AuthorizationFlow_UsesScopeFromProtectedResourceMetadata() ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = (uri, redirect, ct) => + AuthorizationCallbackHandler = (uri, redirect, ct) => { var query = QueryHelpers.ParseQuery(uri.Query); requestedScope = query["scope"].ToString(); @@ -448,7 +448,7 @@ public async Task AuthorizationFlow_UsesScopeFromChallengeHeader() ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = (uri, redirect, ct) => + AuthorizationCallbackHandler = (uri, redirect, ct) => { var query = QueryHelpers.ParseQuery(uri.Query); requestedScope = query["scope"].ToString(); @@ -537,7 +537,7 @@ public async Task AuthorizationFlow_UsesScopeFromForbiddenHeader() ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = (uri, redirect, ct) => + AuthorizationCallbackHandler = (uri, redirect, ct) => { var query = QueryHelpers.ParseQuery(uri.Query); requestedScope = query["scope"].ToString(); @@ -575,7 +575,7 @@ public async Task AuthorizationFails_WhenResourceMetadataPortDiffers() ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + AuthorizationCallbackHandler = HandleAuthorizationUrlAsync, }, }, HttpClient, LoggerFactory); @@ -614,7 +614,7 @@ public async Task CannotAuthenticate_WhenProtectedResourceMetadataMissingResourc ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + AuthorizationCallbackHandler = HandleAuthorizationUrlAsync, }, }, HttpClient, LoggerFactory); @@ -642,7 +642,7 @@ public async Task CanAuthenticate_WithAuthorizationServerPathInsertionMetadata() ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + AuthorizationCallbackHandler = HandleAuthorizationUrlAsync, }, }, HttpClient, LoggerFactory); @@ -675,7 +675,7 @@ public async Task CanAuthenticate_WithAuthorizationServerPathFallbacks() ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + AuthorizationCallbackHandler = HandleAuthorizationUrlAsync, }, }, HttpClient, LoggerFactory); @@ -739,7 +739,7 @@ public async Task CanAuthenticate_WithResourceMetadataPathFallbacks() ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + AuthorizationCallbackHandler = HandleAuthorizationUrlAsync, }, }, HttpClient, LoggerFactory); @@ -794,7 +794,7 @@ public async Task CannotAuthenticate_WhenResourceMetadataResourceIsNonRootParent ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + AuthorizationCallbackHandler = HandleAuthorizationUrlAsync, }, }, HttpClient, LoggerFactory); @@ -835,7 +835,7 @@ public async Task CannotAuthenticate_WhenWwwAuthenticateResourceMetadataIsRootPa ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + AuthorizationCallbackHandler = HandleAuthorizationUrlAsync, }, }, HttpClient, LoggerFactory); @@ -883,7 +883,7 @@ public async Task ResourceMetadata_DoesNotAddTrailingSlash() ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + AuthorizationCallbackHandler = HandleAuthorizationUrlAsync, }, }, HttpClient, LoggerFactory); @@ -1035,7 +1035,7 @@ public async Task ResourceMetadata_PreservesExplicitTrailingSlash() ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + AuthorizationCallbackHandler = HandleAuthorizationUrlAsync, }, }, HttpClient, LoggerFactory); @@ -1152,7 +1152,7 @@ await context.Response.WriteAsync($$""" ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + AuthorizationCallbackHandler = HandleAuthorizationUrlAsync, }, }, HttpClient, LoggerFactory); @@ -1254,7 +1254,7 @@ public async Task CanAuthenticate_WithLegacyServerUsingDefaultEndpointFallback() ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + AuthorizationCallbackHandler = HandleAuthorizationUrlAsync, }, }, HttpClient, LoggerFactory); @@ -1278,7 +1278,7 @@ public async Task AuthorizationFlow_AppendsOfflineAccess_WhenServerAdvertisesIt( ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = (uri, redirect, ct) => + AuthorizationCallbackHandler = (uri, redirect, ct) => { var query = QueryHelpers.ParseQuery(uri.Query); requestedScope = query["scope"].ToString(); @@ -1310,7 +1310,7 @@ public async Task AuthorizationFlow_DoesNotAppendOfflineAccess_WhenServerDoesNot ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = (uri, redirect, ct) => + AuthorizationCallbackHandler = (uri, redirect, ct) => { var query = QueryHelpers.ParseQuery(uri.Query); requestedScope = query["scope"].ToString(); @@ -1349,7 +1349,7 @@ public async Task AuthorizationFlow_DoesNotDuplicateOfflineAccess_WhenAlreadyPre ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = (uri, redirect, ct) => + AuthorizationCallbackHandler = (uri, redirect, ct) => { var query = QueryHelpers.ParseQuery(uri.Query); requestedScope = query["scope"].ToString(); @@ -1386,7 +1386,7 @@ public async Task AuthorizationFlow_ScopeSelector_CanFilterServerProposedScopes( ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = (uri, redirect, ct) => + AuthorizationCallbackHandler = (uri, redirect, ct) => { var query = QueryHelpers.ParseQuery(uri.Query); requestedScope = query["scope"].ToString(); @@ -1417,7 +1417,7 @@ public async Task AuthorizationFlow_ScopeSelector_CanAddCustomScope() ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = (uri, redirect, ct) => + AuthorizationCallbackHandler = (uri, redirect, ct) => { var query = QueryHelpers.ParseQuery(uri.Query); requestedScope = query["scope"].ToString(); @@ -1455,7 +1455,7 @@ public async Task AuthorizationFlow_ScopeSelector_ReceivesNull_WhenServerProvide ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + AuthorizationCallbackHandler = HandleAuthorizationUrlAsync, ScopeSelector = scopes => { capturedInput = scopes; @@ -1485,7 +1485,7 @@ public async Task AuthorizationFlow_ScopeSelector_ReturningNull_OmitsScopeParame ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = (uri, redirect, ct) => + AuthorizationCallbackHandler = (uri, redirect, ct) => { scopePresent = QueryHelpers.ParseQuery(uri.Query).ContainsKey("scope"); return HandleAuthorizationUrlAsync(uri, redirect, ct); @@ -1515,7 +1515,7 @@ public async Task AuthorizationFlow_ScopeSelector_ReturningEmpty_OmitsScopeParam ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = (uri, redirect, ct) => + AuthorizationCallbackHandler = (uri, redirect, ct) => { scopePresent = QueryHelpers.ParseQuery(uri.Query).ContainsKey("scope"); return HandleAuthorizationUrlAsync(uri, redirect, ct); @@ -1546,7 +1546,7 @@ public async Task DynamicClientRegistration_ScopeSelector_AppliesToDcrScope() OAuth = new ClientOAuthOptions() { RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + AuthorizationCallbackHandler = HandleAuthorizationUrlAsync, DynamicClientRegistration = new() { ClientName = "Test MCP Client" }, ScopeSelector = scopes => scopes?.Where(s => s == "mcp:tools"), }, diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/OAuthTestBase.cs b/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/OAuthTestBase.cs index 3c1919b0b..6d3321677 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/OAuthTestBase.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/OAuthTestBase.cs @@ -106,7 +106,7 @@ protected async Task StartMcpServerAsync(string path = "", strin return app; } - protected async Task HandleAuthorizationUrlAsync(Uri authorizationUri, Uri redirectUri, CancellationToken cancellationToken) + protected async Task HandleAuthorizationUrlAsync(Uri authorizationUri, Uri redirectUri, CancellationToken cancellationToken) { using var redirectResponse = await HttpClient.GetAsync(authorizationUri, cancellationToken); Assert.Equal(HttpStatusCode.Redirect, redirectResponse.StatusCode); @@ -115,7 +115,11 @@ protected async Task StartMcpServerAsync(string path = "", strin if (location is not null && !string.IsNullOrEmpty(location.Query)) { var queryParams = QueryHelpers.ParseQuery(location.Query); - return queryParams["code"]; + return new ModelContextProtocol.Authentication.AuthorizationResult + { + Code = queryParams["code"], + Iss = queryParams.TryGetValue("iss", out var iss) ? (string?)iss : null, + }; } return null; diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/TokenCacheTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/TokenCacheTests.cs index fb9e2bfda..0582394ad 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/TokenCacheTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/TokenCacheTests.cs @@ -26,7 +26,7 @@ public async Task GetTokenAsync_CachedAccessTokenIsUsedForOutgoingRequests() ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = (uri, redirect, ct) => + AuthorizationCallbackHandler = (uri, redirect, ct) => { authDelegateCalledInitially = true; return HandleAuthorizationUrlAsync(uri, redirect, ct); @@ -53,7 +53,7 @@ public async Task GetTokenAsync_CachedAccessTokenIsUsedForOutgoingRequests() ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = (uri, redirect, ct) => + AuthorizationCallbackHandler = (uri, redirect, ct) => { authDelegateCalledAgain = true; return HandleAuthorizationUrlAsync(uri, redirect, ct); @@ -82,7 +82,7 @@ public async Task StoreTokenAsync_NewlyAcquiredAccessTokenIsCached() ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + AuthorizationCallbackHandler = HandleAuthorizationUrlAsync, TokenCache = tokenCache }, }, HttpClient, LoggerFactory); @@ -109,7 +109,7 @@ public async Task GetTokenAsync_InvalidCachedTokenTriggersAuthDelegate() ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = (uri, redirect, ct) => + AuthorizationCallbackHandler = (uri, redirect, ct) => { authDelegateCalled = true; return HandleAuthorizationUrlAsync(uri, redirect, ct); @@ -141,7 +141,7 @@ public async Task GetTokenAsync_InvalidAccessTokenTriggersRefresh() ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = (uri, redirect, ct) => + AuthorizationCallbackHandler = (uri, redirect, ct) => { authDelegateCalledInitially = true; return HandleAuthorizationUrlAsync(uri, redirect, ct); @@ -171,7 +171,7 @@ public async Task GetTokenAsync_InvalidAccessTokenTriggersRefresh() ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = (uri, redirect, ct) => + AuthorizationCallbackHandler = (uri, redirect, ct) => { authDelegateCalledAgain = true; return HandleAuthorizationUrlAsync(uri, redirect, ct); diff --git a/tests/ModelContextProtocol.ConformanceClient/Program.cs b/tests/ModelContextProtocol.ConformanceClient/Program.cs index 7ce848907..eafbbac4d 100644 --- a/tests/ModelContextProtocol.ConformanceClient/Program.cs +++ b/tests/ModelContextProtocol.ConformanceClient/Program.cs @@ -3,6 +3,7 @@ using System.Text.Json; using System.Web; using Microsoft.Extensions.Logging; +using ModelContextProtocol.Authentication; using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; @@ -81,7 +82,7 @@ RedirectUri = clientRedirectUri, // Configure the metadata document URI for CIMD. ClientMetadataDocumentUri = new Uri("https://conformance-test.local/client-metadata.json"), - AuthorizationRedirectDelegate = (authUrl, redirectUri, ct) => HandleAuthorizationUrlAsync(authUrl, redirectUri, ct), + AuthorizationCallbackHandler = (authUrl, redirectUri, ct) => HandleAuthorizationUrlAsync(authUrl, redirectUri, ct), }; if (preRegisteredClientId is not null) @@ -329,7 +330,7 @@ // Copied from ProtectedMcpClient sample // Simulate a user opening the browser and logging in // Copied from OAuthTestBase -static async Task HandleAuthorizationUrlAsync(Uri authorizationUrl, Uri redirectUri, CancellationToken cancellationToken) +static async Task HandleAuthorizationUrlAsync(Uri authorizationUrl, Uri redirectUri, CancellationToken cancellationToken) { Console.WriteLine("Starting OAuth authorization flow..."); Console.WriteLine($"Simulating opening browser to: {authorizationUrl}"); @@ -344,16 +345,30 @@ if (location is not null && !string.IsNullOrEmpty(location.Query)) { - // Parse query string to extract "code" parameter + // Parse query string to extract "code" and "iss" parameters var query = location.Query.TrimStart('?'); + string? code = null; + string? iss = null; foreach (var pair in query.Split('&')) { var parts = pair.Split('=', 2); - if (parts.Length == 2 && parts[0] == "code") + if (parts.Length == 2) { - return HttpUtility.UrlDecode(parts[1]); + if (parts[0] == "code") + { + code = HttpUtility.UrlDecode(parts[1]); + } + else if (parts[0] == "iss") + { + iss = HttpUtility.UrlDecode(parts[1]); + } } } + + if (code is not null) + { + return new AuthorizationResult { Code = code, Iss = iss }; + } } return null; From 83f95f4fb9194bf33da933a927d798a8148fdc12 Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Thu, 28 May 2026 08:31:41 -0700 Subject: [PATCH 2/4] Skip RFC 8414 issuer validation in legacy 2025-03-26 backcompat path --- .../Authentication/ClientOAuthProvider.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs index a4139f132..f03a2c34b 100644 --- a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs +++ b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs @@ -377,7 +377,15 @@ private async Task GetAuthServerMetadataAsync(Uri a // Validate the issuer in the metadata document per RFC 8414 Section 3.3: // the issuer value MUST be identical to the issuer identifier used to construct // the well-known URL. - if (metadata.Issuer is not null && + // Skip validation in legacy backcompat mode (resourceUri is null) because the + // authServerUri was derived from the server origin rather than from Protected + // Resource Metadata, so it may not match the server's canonical issuer. + // Note: resourceUri is null exclusively in the 2025-03-26 legacy path. For newer + // protocol versions, ExtractProtectedResourceMetadata throws if the PRM document + // omits the resource field (VerifyResourceMatch returns false for null Resource), + // so we never reach this point with resourceUri == null in non-legacy flows. + if (resourceUri is not null && + metadata.Issuer is not null && !string.Equals(metadata.Issuer.OriginalString, authServerUri.OriginalString, StringComparison.Ordinal)) { ThrowFailedToHandleUnauthorizedResponse( From dc79995350ccff0e5742836e5f3e4700f7001272 Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Tue, 14 Jul 2026 11:53:19 -0700 Subject: [PATCH 3/4] Simplify OAuth authorization callback Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- samples/ProtectedMcpClient/Program.cs | 10 +- .../AuthorizationCallbackContext.cs | 17 ++++ .../AuthorizationRedirectDelegate.cs | 32 ------ .../Authentication/AuthorizationResult.cs | 4 +- .../Authentication/ClientOAuthOptions.cs | 28 ++---- .../Authentication/ClientOAuthProvider.cs | 58 +++++------ .../OAuth/AuthTests.cs | 97 ++++++++++++------- .../OAuth/OAuthTestBase.cs | 6 +- .../OAuth/TokenCacheTests.cs | 30 +++--- .../Program.cs | 8 +- 10 files changed, 147 insertions(+), 143 deletions(-) create mode 100644 src/ModelContextProtocol.Core/Authentication/AuthorizationCallbackContext.cs delete mode 100644 src/ModelContextProtocol.Core/Authentication/AuthorizationRedirectDelegate.cs diff --git a/samples/ProtectedMcpClient/Program.cs b/samples/ProtectedMcpClient/Program.cs index ade93da96..9b805ad36 100644 --- a/samples/ProtectedMcpClient/Program.cs +++ b/samples/ProtectedMcpClient/Program.cs @@ -69,12 +69,16 @@ /// Handles the OAuth authorization URL by starting a local HTTP server and opening a browser. /// This implementation demonstrates how SDK consumers can provide their own authorization flow. /// -/// The authorization URL to open in the browser. -/// The redirect URI where the authorization code will be sent. +/// The context containing the authorization and redirect URIs. /// The to monitor for cancellation requests. The default is . /// The authorization result extracted from the callback, or null if the operation failed. -static async Task HandleAuthorizationUrlAsync(Uri authorizationUrl, Uri redirectUri, CancellationToken cancellationToken) +static async Task HandleAuthorizationUrlAsync( + AuthorizationCallbackContext authorizationContext, + CancellationToken cancellationToken) { + var authorizationUrl = authorizationContext.AuthorizationUri; + var redirectUri = authorizationContext.RedirectUri; + Console.WriteLine("Starting OAuth authorization flow..."); Console.WriteLine($"Opening browser to: {authorizationUrl}"); diff --git a/src/ModelContextProtocol.Core/Authentication/AuthorizationCallbackContext.cs b/src/ModelContextProtocol.Core/Authentication/AuthorizationCallbackContext.cs new file mode 100644 index 000000000..cbc453802 --- /dev/null +++ b/src/ModelContextProtocol.Core/Authentication/AuthorizationCallbackContext.cs @@ -0,0 +1,17 @@ +namespace ModelContextProtocol.Authentication; + +/// +/// Provides the information needed to complete an OAuth authorization request. +/// +public sealed class AuthorizationCallbackContext +{ + /// + /// Gets the authorization URI that the user needs to visit. + /// + public required Uri AuthorizationUri { get; init; } + + /// + /// Gets the redirect URI where the authorization response will be sent. + /// + public required Uri RedirectUri { get; init; } +} diff --git a/src/ModelContextProtocol.Core/Authentication/AuthorizationRedirectDelegate.cs b/src/ModelContextProtocol.Core/Authentication/AuthorizationRedirectDelegate.cs deleted file mode 100644 index c268a47fd..000000000 --- a/src/ModelContextProtocol.Core/Authentication/AuthorizationRedirectDelegate.cs +++ /dev/null @@ -1,32 +0,0 @@ - -namespace ModelContextProtocol.Authentication; - -/// -/// Represents a method that handles the OAuth authorization URL and returns the authorization code. -/// -/// The authorization URL that the user needs to visit. -/// The redirect URI where the authorization code will be sent. -/// The to monitor for cancellation requests. The default is . -/// A task that represents the asynchronous operation. The task result contains the authorization code if successful, or null if the operation failed or was cancelled. -/// -/// -/// This delegate provides SDK consumers with full control over how the OAuth authorization flow is handled. -/// Implementers can choose to: -/// -/// -/// Start a local HTTP server and open a browser (default behavior) -/// Display the authorization URL to the user for manual handling -/// Integrate with a custom UI or authentication flow -/// Use a different redirect mechanism altogether -/// -/// -/// The implementation should handle user interaction to visit the authorization URL and extract -/// the authorization code from the callback. The authorization code is typically provided as -/// a code query parameter in the redirect URI callback. -/// -/// -/// For RFC 9207 issuer validation support, use -/// instead, which allows returning both the authorization code and the iss parameter. -/// -/// -public delegate Task AuthorizationRedirectDelegate(Uri authorizationUri, Uri redirectUri, CancellationToken cancellationToken); \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Authentication/AuthorizationResult.cs b/src/ModelContextProtocol.Core/Authentication/AuthorizationResult.cs index 37da9087a..d5bebc502 100644 --- a/src/ModelContextProtocol.Core/Authentication/AuthorizationResult.cs +++ b/src/ModelContextProtocol.Core/Authentication/AuthorizationResult.cs @@ -31,8 +31,8 @@ public sealed class AuthorizationResult /// prevent mix-up attacks. /// /// - /// Implementations of should populate this property - /// whenever the iss parameter is present in the redirect URI callback. + /// Implementations of should populate this + /// property whenever the iss parameter is present in the redirect URI callback. /// /// public string? Iss { get; init; } diff --git a/src/ModelContextProtocol.Core/Authentication/ClientOAuthOptions.cs b/src/ModelContextProtocol.Core/Authentication/ClientOAuthOptions.cs index 08cbee53c..5d0cfaea5 100644 --- a/src/ModelContextProtocol.Core/Authentication/ClientOAuthOptions.cs +++ b/src/ModelContextProtocol.Core/Authentication/ClientOAuthOptions.cs @@ -70,37 +70,23 @@ public sealed class ClientOAuthOptions public ScopeSelectorDelegate? ScopeSelector { get; set; } /// - /// Gets or sets the authorization redirect delegate for handling the OAuth authorization flow. + /// Gets or sets the callback that handles the OAuth authorization flow. /// /// /// - /// This delegate is responsible for handling the OAuth authorization URL and obtaining the authorization code. - /// If not specified, a default implementation will be used that prompts the user to enter the code manually. + /// This callback receives the authorization and redirect URIs in an + /// and returns the authorization response. + /// If not specified, a default implementation prompts the user to enter the authorization code manually. /// /// /// Custom implementations might open a browser, start an HTTP listener, or use other mechanisms to capture - /// the authorization code from the OAuth redirect. - /// - /// - public AuthorizationRedirectDelegate? AuthorizationRedirectDelegate { get; set; } - - /// - /// Gets or sets a callback that handles the full OAuth authorization flow, returning both the - /// authorization code and the issuer identifier for RFC 9207 validation. - /// - /// - /// - /// When set, this handler takes precedence over . - /// It enables the SDK to validate the iss parameter in the authorization response per + /// the authorization response. They should return both the code and iss query parameters + /// from the redirect URI callback. This enables the SDK to validate the iss parameter per /// RFC 9207, which mitigates /// mix-up attacks. /// - /// - /// Implementations should extract both the code and iss query parameters from - /// the redirect URI callback and return them in an . - /// /// - public Func>? AuthorizationCallbackHandler { get; set; } + public Func>? AuthorizationCallbackHandler { get; set; } /// /// Gets or sets the authorization server selector function. diff --git a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs index 6359aa06f..458e7ed76 100644 --- a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs +++ b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs @@ -31,8 +31,7 @@ internal sealed partial class ClientOAuthProvider : McpHttpClient private readonly ScopeSelectorDelegate? _scopeSelector; private readonly IDictionary _additionalAuthorizationParameters; private readonly Func, Uri?> _authServerSelector; - private readonly AuthorizationRedirectDelegate _authorizationRedirectDelegate; - private readonly Func>? _authorizationCallbackHandler; + private readonly Func> _authorizationCallbackHandler; private readonly Uri? _clientMetadataDocumentUri; // _dcrClientName, _dcrClientUri, _dcrInitialAccessToken and _dcrResponseDelegate are used for dynamic client registration (RFC 7591) @@ -85,11 +84,8 @@ public ClientOAuthProvider( // Set up authorization server selection strategy _authServerSelector = options.AuthServerSelector ?? DefaultAuthServerSelector; - // Set up authorization callback handler (new RFC 9207-aware handler takes precedence) - _authorizationCallbackHandler = options.AuthorizationCallbackHandler; - - // Set up authorization URL handler (use default if not provided) - _authorizationRedirectDelegate = options.AuthorizationRedirectDelegate ?? DefaultAuthorizationUrlHandler; + // Set up authorization callback handler (use default if not provided) + _authorizationCallbackHandler = options.AuthorizationCallbackHandler ?? DefaultAuthorizationUrlHandler; _dcrClientName = options.DynamicClientRegistration?.ClientName; _dcrClientUri = options.DynamicClientRegistration?.ClientUri; @@ -108,18 +104,19 @@ public ClientOAuthProvider( /// /// Default authorization URL handler that displays the URL to the user for manual input. /// - /// The authorization URL to handle. - /// The redirect URI where the authorization code will be sent. + /// The context containing the authorization and redirect URIs. /// The to monitor for cancellation requests. - /// The authorization code entered by the user, or null if none was provided. - private static Task DefaultAuthorizationUrlHandler(Uri authorizationUrl, Uri redirectUri, CancellationToken cancellationToken) + /// The authorization result entered by the user, or null if none was provided. + private static Task DefaultAuthorizationUrlHandler( + AuthorizationCallbackContext context, + CancellationToken cancellationToken) { Console.WriteLine($"Please open the following URL in your browser to authorize the application:"); - Console.WriteLine($"{authorizationUrl}"); + Console.WriteLine($"{context.AuthorizationUri}"); Console.WriteLine(); Console.Write("Enter the authorization code from the redirect URL: "); var authorizationCode = Console.ReadLine(); - return Task.FromResult(authorizationCode); + return Task.FromResult(new() { Code = authorizationCode }); } internal override async Task SendAsync(HttpRequestMessage request, JsonRpcMessage? message, CancellationToken cancellationToken) @@ -485,32 +482,27 @@ private async Task InitiateAuthorizationCodeFlowAsync( var authUrl = BuildAuthorizationUrl(protectedResourceMetadata, authServerMetadata, codeChallenge); - string? authorizationCode; - string? iss = null; - - if (_authorizationCallbackHandler is not null) - { - var authResult = await _authorizationCallbackHandler(authUrl, _redirectUri, cancellationToken).ConfigureAwait(false); - if (authResult is null || string.IsNullOrEmpty(authResult.Code)) + var authResult = await _authorizationCallbackHandler( + new AuthorizationCallbackContext { - ThrowFailedToHandleUnauthorizedResponse($"The {nameof(ClientOAuthOptions.AuthorizationCallbackHandler)} returned a null or empty authorization code."); - } + AuthorizationUri = authUrl, + RedirectUri = _redirectUri, + }, + cancellationToken).ConfigureAwait(false); - authorizationCode = authResult!.Code!; - iss = authResult.Iss; - } - else + if (authResult is null || string.IsNullOrEmpty(authResult.Code)) { - authorizationCode = await _authorizationRedirectDelegate(authUrl, _redirectUri, cancellationToken).ConfigureAwait(false); - if (string.IsNullOrEmpty(authorizationCode)) - { - ThrowFailedToHandleUnauthorizedResponse($"The {nameof(AuthorizationRedirectDelegate)} returned a null or empty authorization code."); - } + ThrowFailedToHandleUnauthorizedResponse($"The {nameof(ClientOAuthOptions.AuthorizationCallbackHandler)} returned a null or empty authorization code."); } - ValidateIssuerResponse(iss, authServerMetadata); + ValidateIssuerResponse(authResult!.Iss, authServerMetadata); - return await ExchangeCodeForTokenAsync(protectedResourceMetadata, authServerMetadata, authorizationCode!, codeVerifier, cancellationToken).ConfigureAwait(false); + return await ExchangeCodeForTokenAsync( + protectedResourceMetadata, + authServerMetadata, + authResult.Code!, + codeVerifier, + cancellationToken).ConfigureAwait(false); } private Uri BuildAuthorizationUrl( diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthTests.cs index 6e6b8b06a..d804a8f29 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthTests.cs @@ -49,6 +49,37 @@ public async Task CanAuthenticate() transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken); } + [Fact] + public async Task AuthorizationCallbackHandler_ReceivesConfiguredRedirectUri() + { + await using var app = await StartMcpServerAsync(); + + var redirectUri = new Uri("http://localhost:1179/callback"); + AuthorizationCallbackContext? callbackContext = null; + + await using var transport = new HttpClientTransport(new() + { + Endpoint = new(McpServerUrl), + OAuth = new() + { + ClientId = "demo-client", + ClientSecret = "demo-secret", + RedirectUri = redirectUri, + AuthorizationCallbackHandler = (context, cancellationToken) => + { + callbackContext = context; + return HandleAuthorizationUrlAsync(context, cancellationToken); + }, + }, + }, HttpClient, LoggerFactory); + + await using var client = await McpClient.CreateAsync( + transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken); + + Assert.NotNull(callbackContext); + Assert.Equal(redirectUri, callbackContext.RedirectUri); + } + [Fact] public async Task CannotAuthenticate_WithoutOAuthConfiguration() { @@ -290,10 +321,10 @@ public async Task CanAuthenticate_WithExtraParams() ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationCallbackHandler = (uri, redirect, ct) => + AuthorizationCallbackHandler = (context, ct) => { - lastAuthorizationUri = uri; - return HandleAuthorizationUrlAsync(uri, redirect, ct); + lastAuthorizationUri = context.AuthorizationUri; + return HandleAuthorizationUrlAsync(context, ct); }, AdditionalAuthorizationParameters = new Dictionary { @@ -397,11 +428,11 @@ public async Task AuthorizationFlow_UsesScopeFromProtectedResourceMetadata() ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationCallbackHandler = (uri, redirect, ct) => + AuthorizationCallbackHandler = (context, ct) => { - var query = QueryHelpers.ParseQuery(uri.Query); + var query = QueryHelpers.ParseQuery(context.AuthorizationUri.Query); requestedScope = query["scope"].ToString(); - return HandleAuthorizationUrlAsync(uri, redirect, ct); + return HandleAuthorizationUrlAsync(context, ct); }, }, }, HttpClient, LoggerFactory); @@ -448,11 +479,11 @@ public async Task AuthorizationFlow_UsesScopeFromChallengeHeader() ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationCallbackHandler = (uri, redirect, ct) => + AuthorizationCallbackHandler = (context, ct) => { - var query = QueryHelpers.ParseQuery(uri.Query); + var query = QueryHelpers.ParseQuery(context.AuthorizationUri.Query); requestedScope = query["scope"].ToString(); - return HandleAuthorizationUrlAsync(uri, redirect, ct); + return HandleAuthorizationUrlAsync(context, ct); }, }, }, HttpClient, LoggerFactory); @@ -537,11 +568,11 @@ public async Task AuthorizationFlow_UsesScopeFromForbiddenHeader() ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationCallbackHandler = (uri, redirect, ct) => + AuthorizationCallbackHandler = (context, ct) => { - var query = QueryHelpers.ParseQuery(uri.Query); + var query = QueryHelpers.ParseQuery(context.AuthorizationUri.Query); requestedScope = query["scope"].ToString(); - return HandleAuthorizationUrlAsync(uri, redirect, ct); + return HandleAuthorizationUrlAsync(context, ct); }, }, }, HttpClient, LoggerFactory); @@ -1371,11 +1402,11 @@ public async Task AuthorizationFlow_AppendsOfflineAccess_WhenServerAdvertisesIt( ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationCallbackHandler = (uri, redirect, ct) => + AuthorizationCallbackHandler = (context, ct) => { - var query = QueryHelpers.ParseQuery(uri.Query); + var query = QueryHelpers.ParseQuery(context.AuthorizationUri.Query); requestedScope = query["scope"].ToString(); - return HandleAuthorizationUrlAsync(uri, redirect, ct); + return HandleAuthorizationUrlAsync(context, ct); }, }, }, HttpClient, LoggerFactory); @@ -1403,11 +1434,11 @@ public async Task AuthorizationFlow_DoesNotAppendOfflineAccess_WhenServerDoesNot ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationCallbackHandler = (uri, redirect, ct) => + AuthorizationCallbackHandler = (context, ct) => { - var query = QueryHelpers.ParseQuery(uri.Query); + var query = QueryHelpers.ParseQuery(context.AuthorizationUri.Query); requestedScope = query["scope"].ToString(); - return HandleAuthorizationUrlAsync(uri, redirect, ct); + return HandleAuthorizationUrlAsync(context, ct); }, }, }, HttpClient, LoggerFactory); @@ -1442,11 +1473,11 @@ public async Task AuthorizationFlow_DoesNotDuplicateOfflineAccess_WhenAlreadyPre ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationCallbackHandler = (uri, redirect, ct) => + AuthorizationCallbackHandler = (context, ct) => { - var query = QueryHelpers.ParseQuery(uri.Query); + var query = QueryHelpers.ParseQuery(context.AuthorizationUri.Query); requestedScope = query["scope"].ToString(); - return HandleAuthorizationUrlAsync(uri, redirect, ct); + return HandleAuthorizationUrlAsync(context, ct); }, }, }, HttpClient, LoggerFactory); @@ -1479,11 +1510,11 @@ public async Task AuthorizationFlow_ScopeSelector_CanFilterServerProposedScopes( ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationCallbackHandler = (uri, redirect, ct) => + AuthorizationCallbackHandler = (context, ct) => { - var query = QueryHelpers.ParseQuery(uri.Query); + var query = QueryHelpers.ParseQuery(context.AuthorizationUri.Query); requestedScope = query["scope"].ToString(); - return HandleAuthorizationUrlAsync(uri, redirect, ct); + return HandleAuthorizationUrlAsync(context, ct); }, ScopeSelector = scopes => scopes?.Where(s => s == "mcp:tools"), }, @@ -1510,11 +1541,11 @@ public async Task AuthorizationFlow_ScopeSelector_CanAddCustomScope() ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationCallbackHandler = (uri, redirect, ct) => + AuthorizationCallbackHandler = (context, ct) => { - var query = QueryHelpers.ParseQuery(uri.Query); + var query = QueryHelpers.ParseQuery(context.AuthorizationUri.Query); requestedScope = query["scope"].ToString(); - return HandleAuthorizationUrlAsync(uri, redirect, ct); + return HandleAuthorizationUrlAsync(context, ct); }, ScopeSelector = scopes => scopes?.Append("custom:scope") ?? ["custom:scope"], }, @@ -1578,10 +1609,10 @@ public async Task AuthorizationFlow_ScopeSelector_ReturningNull_OmitsScopeParame ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationCallbackHandler = (uri, redirect, ct) => + AuthorizationCallbackHandler = (context, ct) => { - scopePresent = QueryHelpers.ParseQuery(uri.Query).ContainsKey("scope"); - return HandleAuthorizationUrlAsync(uri, redirect, ct); + scopePresent = QueryHelpers.ParseQuery(context.AuthorizationUri.Query).ContainsKey("scope"); + return HandleAuthorizationUrlAsync(context, ct); }, ScopeSelector = _ => null, }, @@ -1608,10 +1639,10 @@ public async Task AuthorizationFlow_ScopeSelector_ReturningEmpty_OmitsScopeParam ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationCallbackHandler = (uri, redirect, ct) => + AuthorizationCallbackHandler = (context, ct) => { - scopePresent = QueryHelpers.ParseQuery(uri.Query).ContainsKey("scope"); - return HandleAuthorizationUrlAsync(uri, redirect, ct); + scopePresent = QueryHelpers.ParseQuery(context.AuthorizationUri.Query).ContainsKey("scope"); + return HandleAuthorizationUrlAsync(context, ct); }, ScopeSelector = _ => [], }, diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/OAuthTestBase.cs b/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/OAuthTestBase.cs index 6d3321677..48e609f9d 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/OAuthTestBase.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/OAuthTestBase.cs @@ -106,9 +106,11 @@ protected async Task StartMcpServerAsync(string path = "", strin return app; } - protected async Task HandleAuthorizationUrlAsync(Uri authorizationUri, Uri redirectUri, CancellationToken cancellationToken) + protected async Task HandleAuthorizationUrlAsync( + ModelContextProtocol.Authentication.AuthorizationCallbackContext authorizationContext, + CancellationToken cancellationToken) { - using var redirectResponse = await HttpClient.GetAsync(authorizationUri, cancellationToken); + using var redirectResponse = await HttpClient.GetAsync(authorizationContext.AuthorizationUri, cancellationToken); Assert.Equal(HttpStatusCode.Redirect, redirectResponse.StatusCode); var location = redirectResponse.Headers.Location; diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/TokenCacheTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/TokenCacheTests.cs index 0582394ad..09aa165c1 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/TokenCacheTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/TokenCacheTests.cs @@ -26,10 +26,10 @@ public async Task GetTokenAsync_CachedAccessTokenIsUsedForOutgoingRequests() ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationCallbackHandler = (uri, redirect, ct) => + AuthorizationCallbackHandler = (context, ct) => { authDelegateCalledInitially = true; - return HandleAuthorizationUrlAsync(uri, redirect, ct); + return HandleAuthorizationUrlAsync(context, ct); }, TokenCache = tokenCache, }, @@ -40,7 +40,7 @@ public async Task GetTokenAsync_CachedAccessTokenIsUsedForOutgoingRequests() // Just connecting should trigger auth and storage. } - Assert.True(authDelegateCalledInitially, "AuthorizationRedirectDelegate should be called to get initial token"); + Assert.True(authDelegateCalledInitially, "AuthorizationCallbackHandler should be called to get initial token"); Assert.NotNull(tokenCache.LastStoredToken); var authDelegateCalledAgain = false; @@ -53,10 +53,10 @@ public async Task GetTokenAsync_CachedAccessTokenIsUsedForOutgoingRequests() ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationCallbackHandler = (uri, redirect, ct) => + AuthorizationCallbackHandler = (context, ct) => { authDelegateCalledAgain = true; - return HandleAuthorizationUrlAsync(uri, redirect, ct); + return HandleAuthorizationUrlAsync(context, ct); }, TokenCache = tokenCache }, @@ -64,7 +64,7 @@ public async Task GetTokenAsync_CachedAccessTokenIsUsedForOutgoingRequests() await using var client = await McpClient.CreateAsync(transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken); - Assert.False(authDelegateCalledAgain, "AuthorizationRedirectDelegate should not be called when token is valid"); + Assert.False(authDelegateCalledAgain, "AuthorizationCallbackHandler should not be called when token is valid"); } [Fact] @@ -109,10 +109,10 @@ public async Task GetTokenAsync_InvalidCachedTokenTriggersAuthDelegate() ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationCallbackHandler = (uri, redirect, ct) => + AuthorizationCallbackHandler = (context, ct) => { authDelegateCalled = true; - return HandleAuthorizationUrlAsync(uri, redirect, ct); + return HandleAuthorizationUrlAsync(context, ct); }, TokenCache = tokenCache, }, @@ -120,7 +120,7 @@ public async Task GetTokenAsync_InvalidCachedTokenTriggersAuthDelegate() await using var client = await McpClient.CreateAsync(transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken); - Assert.True(authDelegateCalled, "AuthorizationRedirectDelegate should be called when cached token is invalid"); + Assert.True(authDelegateCalled, "AuthorizationCallbackHandler should be called when cached token is invalid"); Assert.NotNull(tokenCache.LastStoredToken); Assert.NotEqual("invalid-token", tokenCache.LastStoredToken.AccessToken); } @@ -141,10 +141,10 @@ public async Task GetTokenAsync_InvalidAccessTokenTriggersRefresh() ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationCallbackHandler = (uri, redirect, ct) => + AuthorizationCallbackHandler = (context, ct) => { authDelegateCalledInitially = true; - return HandleAuthorizationUrlAsync(uri, redirect, ct); + return HandleAuthorizationUrlAsync(context, ct); }, TokenCache = tokenCache, }, @@ -155,7 +155,7 @@ public async Task GetTokenAsync_InvalidAccessTokenTriggersRefresh() // Just connecting should trigger auth and storage. } - Assert.True(authDelegateCalledInitially, "AuthorizationRedirectDelegate should be called to get initial token"); + Assert.True(authDelegateCalledInitially, "AuthorizationCallbackHandler should be called to get initial token"); Assert.False(TestOAuthServer.HasRefreshedToken, "Token should not have been refreshed yet"); Assert.NotNull(tokenCache.LastStoredToken); @@ -171,10 +171,10 @@ public async Task GetTokenAsync_InvalidAccessTokenTriggersRefresh() ClientId = "demo-client", ClientSecret = "demo-secret", RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationCallbackHandler = (uri, redirect, ct) => + AuthorizationCallbackHandler = (context, ct) => { authDelegateCalledAgain = true; - return HandleAuthorizationUrlAsync(uri, redirect, ct); + return HandleAuthorizationUrlAsync(context, ct); }, TokenCache = tokenCache }, @@ -182,7 +182,7 @@ public async Task GetTokenAsync_InvalidAccessTokenTriggersRefresh() await using var client = await McpClient.CreateAsync(transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken); - Assert.False(authDelegateCalledAgain, "AuthorizationRedirectDelegate should not be called when refresh token is valid"); + Assert.False(authDelegateCalledAgain, "AuthorizationCallbackHandler should not be called when refresh token is valid"); Assert.True(TestOAuthServer.HasRefreshedToken, "Token should have been refreshed"); Assert.NotEqual("invalid-token", tokenCache.LastStoredToken.AccessToken); } diff --git a/tests/ModelContextProtocol.ConformanceClient/Program.cs b/tests/ModelContextProtocol.ConformanceClient/Program.cs index eafbbac4d..dbc02a5fe 100644 --- a/tests/ModelContextProtocol.ConformanceClient/Program.cs +++ b/tests/ModelContextProtocol.ConformanceClient/Program.cs @@ -82,7 +82,7 @@ RedirectUri = clientRedirectUri, // Configure the metadata document URI for CIMD. ClientMetadataDocumentUri = new Uri("https://conformance-test.local/client-metadata.json"), - AuthorizationCallbackHandler = (authUrl, redirectUri, ct) => HandleAuthorizationUrlAsync(authUrl, redirectUri, ct), + AuthorizationCallbackHandler = HandleAuthorizationUrlAsync, }; if (preRegisteredClientId is not null) @@ -330,8 +330,12 @@ // Copied from ProtectedMcpClient sample // Simulate a user opening the browser and logging in // Copied from OAuthTestBase -static async Task HandleAuthorizationUrlAsync(Uri authorizationUrl, Uri redirectUri, CancellationToken cancellationToken) +static async Task HandleAuthorizationUrlAsync( + AuthorizationCallbackContext authorizationContext, + CancellationToken cancellationToken) { + var authorizationUrl = authorizationContext.AuthorizationUri; + Console.WriteLine("Starting OAuth authorization flow..."); Console.WriteLine($"Simulating opening browser to: {authorizationUrl}"); From ea8c1e067316b40cc35881eaed4ea913c4574bf7 Mon Sep 17 00:00:00 2001 From: Tarek Mahmoud Sayed Date: Tue, 14 Jul 2026 14:50:36 -0700 Subject: [PATCH 4/4] Suppress ApiCompat baseline errors for removed AuthorizationRedirectDelegate The AuthorizationRedirectDelegate type and ClientOAuthOptions.AuthorizationRedirectDelegate property were removed in favor of the new AuthorizationCallbackHandler API. Add baseline suppressions (CP0001/CP0002) so the Release pack's package validation against 1.3.0 passes. --- .../CompatibilitySuppressions.xml | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/src/ModelContextProtocol.Core/CompatibilitySuppressions.xml b/src/ModelContextProtocol.Core/CompatibilitySuppressions.xml index e686b454f..e322bed26 100644 --- a/src/ModelContextProtocol.Core/CompatibilitySuppressions.xml +++ b/src/ModelContextProtocol.Core/CompatibilitySuppressions.xml @@ -1,6 +1,13 @@  + + CP0001 + T:ModelContextProtocol.Authentication.AuthorizationRedirectDelegate + lib/net10.0/ModelContextProtocol.Core.dll + lib/net10.0/ModelContextProtocol.Core.dll + true + CP0001 T:ModelContextProtocol.IMcpTaskStore @@ -155,6 +162,13 @@ lib/net10.0/ModelContextProtocol.Core.dll true + + CP0001 + T:ModelContextProtocol.Authentication.AuthorizationRedirectDelegate + lib/net8.0/ModelContextProtocol.Core.dll + lib/net8.0/ModelContextProtocol.Core.dll + true + CP0001 T:ModelContextProtocol.IMcpTaskStore @@ -309,6 +323,13 @@ lib/net8.0/ModelContextProtocol.Core.dll true + + CP0001 + T:ModelContextProtocol.Authentication.AuthorizationRedirectDelegate + lib/net9.0/ModelContextProtocol.Core.dll + lib/net9.0/ModelContextProtocol.Core.dll + true + CP0001 T:ModelContextProtocol.IMcpTaskStore @@ -463,6 +484,13 @@ lib/net9.0/ModelContextProtocol.Core.dll true + + CP0001 + T:ModelContextProtocol.Authentication.AuthorizationRedirectDelegate + lib/netstandard2.0/ModelContextProtocol.Core.dll + lib/netstandard2.0/ModelContextProtocol.Core.dll + true + CP0001 T:ModelContextProtocol.IMcpTaskStore @@ -638,6 +666,20 @@ lib/net10.0/ModelContextProtocol.Core.dll true + + CP0002 + M:ModelContextProtocol.Authentication.ClientOAuthOptions.get_AuthorizationRedirectDelegate + lib/net10.0/ModelContextProtocol.Core.dll + lib/net10.0/ModelContextProtocol.Core.dll + true + + + CP0002 + M:ModelContextProtocol.Authentication.ClientOAuthOptions.set_AuthorizationRedirectDelegate(ModelContextProtocol.Authentication.AuthorizationRedirectDelegate) + lib/net10.0/ModelContextProtocol.Core.dll + lib/net10.0/ModelContextProtocol.Core.dll + true + CP0002 M:ModelContextProtocol.Client.McpClient.CallToolAsTaskAsync(System.String,System.Collections.Generic.IReadOnlyDictionary{System.String,System.Object},ModelContextProtocol.Protocol.McpTaskMetadata,System.IProgress{ModelContextProtocol.ProgressNotificationValue},ModelContextProtocol.RequestOptions,System.Threading.CancellationToken) @@ -1009,6 +1051,20 @@ lib/net8.0/ModelContextProtocol.Core.dll true + + CP0002 + M:ModelContextProtocol.Authentication.ClientOAuthOptions.get_AuthorizationRedirectDelegate + lib/net8.0/ModelContextProtocol.Core.dll + lib/net8.0/ModelContextProtocol.Core.dll + true + + + CP0002 + M:ModelContextProtocol.Authentication.ClientOAuthOptions.set_AuthorizationRedirectDelegate(ModelContextProtocol.Authentication.AuthorizationRedirectDelegate) + lib/net8.0/ModelContextProtocol.Core.dll + lib/net8.0/ModelContextProtocol.Core.dll + true + CP0002 M:ModelContextProtocol.Client.McpClient.CallToolAsTaskAsync(System.String,System.Collections.Generic.IReadOnlyDictionary{System.String,System.Object},ModelContextProtocol.Protocol.McpTaskMetadata,System.IProgress{ModelContextProtocol.ProgressNotificationValue},ModelContextProtocol.RequestOptions,System.Threading.CancellationToken) @@ -1380,6 +1436,20 @@ lib/net9.0/ModelContextProtocol.Core.dll true + + CP0002 + M:ModelContextProtocol.Authentication.ClientOAuthOptions.get_AuthorizationRedirectDelegate + lib/net9.0/ModelContextProtocol.Core.dll + lib/net9.0/ModelContextProtocol.Core.dll + true + + + CP0002 + M:ModelContextProtocol.Authentication.ClientOAuthOptions.set_AuthorizationRedirectDelegate(ModelContextProtocol.Authentication.AuthorizationRedirectDelegate) + lib/net9.0/ModelContextProtocol.Core.dll + lib/net9.0/ModelContextProtocol.Core.dll + true + CP0002 M:ModelContextProtocol.Client.McpClient.CallToolAsTaskAsync(System.String,System.Collections.Generic.IReadOnlyDictionary{System.String,System.Object},ModelContextProtocol.Protocol.McpTaskMetadata,System.IProgress{ModelContextProtocol.ProgressNotificationValue},ModelContextProtocol.RequestOptions,System.Threading.CancellationToken) @@ -1751,6 +1821,20 @@ lib/netstandard2.0/ModelContextProtocol.Core.dll true + + CP0002 + M:ModelContextProtocol.Authentication.ClientOAuthOptions.get_AuthorizationRedirectDelegate + lib/netstandard2.0/ModelContextProtocol.Core.dll + lib/netstandard2.0/ModelContextProtocol.Core.dll + true + + + CP0002 + M:ModelContextProtocol.Authentication.ClientOAuthOptions.set_AuthorizationRedirectDelegate(ModelContextProtocol.Authentication.AuthorizationRedirectDelegate) + lib/netstandard2.0/ModelContextProtocol.Core.dll + lib/netstandard2.0/ModelContextProtocol.Core.dll + true + CP0002 M:ModelContextProtocol.Client.McpClient.CallToolAsTaskAsync(System.String,System.Collections.Generic.IReadOnlyDictionary{System.String,System.Object},ModelContextProtocol.Protocol.McpTaskMetadata,System.IProgress{ModelContextProtocol.ProgressNotificationValue},ModelContextProtocol.RequestOptions,System.Threading.CancellationToken)