using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; #if NET9_0_OR_GREATER using System.Buffers.Text; #endif using System.Diagnostics.CodeAnalysis; using System.Net.Http.Headers; using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Web; namespace ModelContextProtocol.Authentication; /// /// A generic implementation of an OAuth authorization provider. /// internal sealed partial class ClientOAuthProvider { /// /// The Bearer authentication scheme. /// private const string BearerScheme = "Bearer"; private static readonly string[] s_wellKnownPaths = [".well-known/openid-configuration", ".well-known/oauth-authorization-server"]; private readonly Uri _serverUrl; private readonly Uri _redirectUri; private readonly string[]? _scopes; private readonly IDictionary _additionalAuthorizationParameters; private readonly Func, Uri?> _authServerSelector; private readonly AuthorizationRedirectDelegate _authorizationRedirectDelegate; private readonly Uri? _clientMetadataDocumentUri; // _dcrClientName, _dcrClientUri, _dcrInitialAccessToken and _dcrResponseDelegate are used for dynamic client registration (RFC 7591) private readonly string? _dcrClientName; private readonly Uri? _dcrClientUri; private readonly string? _dcrInitialAccessToken; private readonly Func? _dcrResponseDelegate; private readonly HttpClient _httpClient; private readonly ILogger _logger; private string? _clientId; private string? _clientSecret; private ITokenCache _tokenCache; private AuthorizationServerMetadata? _authServerMetadata; /// /// Initializes a new instance of the class using the specified options. /// /// The MCP server URL. /// The OAuth provider configuration options. /// The HTTP client to use for OAuth requests. If null, a default HttpClient is used. /// A logger factory to handle diagnostic messages. /// or is null. public ClientOAuthProvider( Uri serverUrl, ClientOAuthOptions options, HttpClient httpClient, ILoggerFactory? loggerFactory = null) { _serverUrl = serverUrl ?? throw new ArgumentNullException(nameof(serverUrl)); _httpClient = httpClient; _logger = (ILogger?)loggerFactory?.CreateLogger() ?? NullLogger.Instance; if (options is null) { throw new ArgumentNullException(nameof(options)); } _clientId = options.ClientId; _clientSecret = options.ClientSecret; _redirectUri = options.RedirectUri ?? throw new ArgumentException("ClientOAuthOptions.RedirectUri must configured.", nameof(options)); _scopes = options.Scopes?.ToArray(); _additionalAuthorizationParameters = options.AdditionalAuthorizationParameters; _clientMetadataDocumentUri = options.ClientMetadataDocumentUri; // Set up authorization server selection strategy _authServerSelector = options.AuthServerSelector ?? DefaultAuthServerSelector; // Set up authorization URL handler (use default if not provided) _authorizationRedirectDelegate = options.AuthorizationRedirectDelegate ?? DefaultAuthorizationUrlHandler; _dcrClientName = options.DynamicClientRegistration?.ClientName; _dcrClientUri = options.DynamicClientRegistration?.ClientUri; _dcrInitialAccessToken = options.DynamicClientRegistration?.InitialAccessToken; _dcrResponseDelegate = options.DynamicClientRegistration?.ResponseDelegate; _tokenCache = options.TokenCache ?? new InMemoryTokenCache(); } /// /// Default authorization server selection strategy that selects the first available server. /// /// List of available authorization servers. /// The selected authorization server, or null if none are available. private static Uri? DefaultAuthServerSelector(IReadOnlyList availableServers) => availableServers.FirstOrDefault(); /// /// 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 to monitor for cancellation requests. The default is . /// The authorization code entered by the user, or null if none was provided. private static Task DefaultAuthorizationUrlHandler(Uri authorizationUrl, Uri redirectUri, CancellationToken cancellationToken) { Console.WriteLine($"Please open the following URL in your browser to authorize the application:"); Console.WriteLine($"{authorizationUrl}"); Console.WriteLine(); Console.Write("Enter the authorization code from the redirect URL: "); var authorizationCode = Console.ReadLine(); return Task.FromResult(authorizationCode); } /// /// Gets the collection of authentication schemes supported by this provider. /// /// /// /// This property returns all authentication schemes that this provider can handle, /// allowing clients to select the appropriate scheme based on server capabilities. /// /// /// Common values include "Bearer" for JWT tokens, "Basic" for username/password authentication, /// and "Negotiate" for integrated Windows authentication. /// /// public IEnumerable SupportedSchemes => [BearerScheme]; /// /// Gets an authentication token or credential for authenticating requests to a resource /// using the specified authentication scheme. /// /// The authentication scheme to use. /// The URI of the resource requiring authentication. /// The to monitor for cancellation requests. The default is . /// An authentication token string, or null if no token could be obtained for the specified scheme. public async Task GetCredentialAsync(string scheme, Uri resourceUri, CancellationToken cancellationToken = default) { ThrowIfNotBearerScheme(scheme); var tokens = await _tokenCache.GetTokensAsync(cancellationToken).ConfigureAwait(false); // Return the token if it's valid if (tokens is not null && !tokens.IsExpired) { return tokens.AccessToken; } // Try to refresh the access token if it is invalid and we have a refresh token. if (tokens?.RefreshToken != null && _authServerMetadata != null) { var newTokens = await RefreshTokenAsync(tokens.RefreshToken, resourceUri, _authServerMetadata, cancellationToken).ConfigureAwait(false); if (newTokens is not null) { return newTokens.AccessToken; } } // No valid token - auth handler will trigger the 401 flow return null; } /// /// Handles a 401 Unauthorized response from a resource. /// /// The authentication scheme that was used when the unauthorized response was received. /// The HTTP response that contained the 401 status code. /// The to monitor for cancellation requests. The default is . /// /// A result object indicating if the provider was able to handle the unauthorized response, /// and the authentication scheme that should be used for the next attempt, if any. /// public async Task HandleUnauthorizedResponseAsync( string scheme, HttpResponseMessage response, CancellationToken cancellationToken = default) { ThrowIfNotBearerScheme(scheme); await PerformOAuthAuthorizationAsync(response, cancellationToken).ConfigureAwait(false); } /// /// Performs OAuth authorization by selecting an appropriate authorization server and completing the OAuth flow. /// /// The 401 Unauthorized response containing authentication challenge. /// The to monitor for cancellation requests. The default is . /// A result object indicating whether authorization was successful. private async Task PerformOAuthAuthorizationAsync( HttpResponseMessage response, CancellationToken cancellationToken) { // Get available authorization servers from the 401 response var protectedResourceMetadata = await ExtractProtectedResourceMetadata(response, _serverUrl, cancellationToken).ConfigureAwait(false); var availableAuthorizationServers = protectedResourceMetadata.AuthorizationServers; if (availableAuthorizationServers.Count == 0) { ThrowFailedToHandleUnauthorizedResponse("No authorization servers found in authentication challenge"); } // Select authorization server using configured strategy var selectedAuthServer = _authServerSelector(availableAuthorizationServers); if (selectedAuthServer is null) { ThrowFailedToHandleUnauthorizedResponse($"Authorization server selection returned null. Available servers: {string.Join(", ", availableAuthorizationServers)}"); } if (!availableAuthorizationServers.Contains(selectedAuthServer)) { ThrowFailedToHandleUnauthorizedResponse($"Authorization server selector returned a server not in the available list: {selectedAuthServer}. Available servers: {string.Join(", ", availableAuthorizationServers)}"); } LogSelectedAuthorizationServer(selectedAuthServer, availableAuthorizationServers.Count); // Get auth server metadata var authServerMetadata = await GetAuthServerMetadataAsync(selectedAuthServer, cancellationToken).ConfigureAwait(false); // Store auth server metadata for future refresh operations _authServerMetadata = authServerMetadata; // The existing access token must be invalid to have resulted in a 401 response, but refresh might still work. if (await _tokenCache.GetTokensAsync(cancellationToken).ConfigureAwait(false) is { RefreshToken: { } refreshToken }) { var refreshedTokens = await RefreshTokenAsync(refreshToken, protectedResourceMetadata.Resource, authServerMetadata, cancellationToken).ConfigureAwait(false); if (refreshedTokens is not null) { // A non-null result indicates the refresh succeeded and the new tokens have been stored. return; } } // Assign a client ID if necessary if (string.IsNullOrEmpty(_clientId)) { // Try using a client metadata document before falling back to dynamic client registration if (authServerMetadata.ClientIdMetadataDocumentSupported && _clientMetadataDocumentUri is not null) { ApplyClientIdMetadataDocument(_clientMetadataDocumentUri); } else { await PerformDynamicClientRegistrationAsync(authServerMetadata, cancellationToken).ConfigureAwait(false); } } // Perform the OAuth flow await InitiateAuthorizationCodeFlowAsync(protectedResourceMetadata, authServerMetadata, cancellationToken).ConfigureAwait(false); LogOAuthAuthorizationCompleted(); } private void ApplyClientIdMetadataDocument(Uri metadataUri) { if (!IsValidClientMetadataDocumentUri(metadataUri)) { ThrowFailedToHandleUnauthorizedResponse( $"{nameof(ClientOAuthOptions.ClientMetadataDocumentUri)} must be an HTTPS URL with a non-root absolute path. Value: '{metadataUri}'."); } _clientId = metadataUri.AbsoluteUri; // See: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-client-id-metadata-document-00#section-3 static bool IsValidClientMetadataDocumentUri(Uri uri) => uri.IsAbsoluteUri && string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) && uri.AbsolutePath.Length > 1; // AbsolutePath always starts with "/" } private async Task GetAuthServerMetadataAsync(Uri authServerUri, CancellationToken cancellationToken) { if (authServerUri.OriginalString.Length == 0 || authServerUri.OriginalString[authServerUri.OriginalString.Length - 1] != '/') { authServerUri = new Uri($"{authServerUri.OriginalString}/"); } foreach (var path in s_wellKnownPaths) { try { var wellKnownEndpoint = new Uri(authServerUri, path); var response = await _httpClient.GetAsync(wellKnownEndpoint, cancellationToken).ConfigureAwait(false); if (!response.IsSuccessStatusCode) { continue; } using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); var metadata = await JsonSerializer.DeserializeAsync(stream, McpJsonUtilities.JsonContext.Default.AuthorizationServerMetadata, cancellationToken).ConfigureAwait(false); if (metadata is null) { continue; } if (metadata.AuthorizationEndpoint is null) { ThrowFailedToHandleUnauthorizedResponse($"No authorization_endpoint was provided via '{wellKnownEndpoint}'."); } if (metadata.AuthorizationEndpoint.Scheme != Uri.UriSchemeHttp && metadata.AuthorizationEndpoint.Scheme != Uri.UriSchemeHttps) { ThrowFailedToHandleUnauthorizedResponse($"AuthorizationEndpoint must use HTTP or HTTPS. '{metadata.AuthorizationEndpoint}' does not meet this requirement."); } metadata.ResponseTypesSupported ??= ["code"]; metadata.GrantTypesSupported ??= ["authorization_code", "refresh_token"]; metadata.TokenEndpointAuthMethodsSupported ??= ["client_secret_post"]; metadata.CodeChallengeMethodsSupported ??= ["S256"]; return metadata; } catch (Exception ex) { LogErrorFetchingAuthServerMetadata(ex, path); } } throw new McpException($"Failed to find .well-known/openid-configuration or .well-known/oauth-authorization-server metadata for authorization server: '{authServerUri}'"); } private async Task RefreshTokenAsync(string refreshToken, Uri resourceUri, AuthorizationServerMetadata authServerMetadata, CancellationToken cancellationToken) { var requestContent = new FormUrlEncodedContent(new Dictionary { ["grant_type"] = "refresh_token", ["refresh_token"] = refreshToken, ["client_id"] = GetClientIdOrThrow(), ["client_secret"] = _clientSecret ?? string.Empty, ["resource"] = resourceUri.ToString(), }); using var request = new HttpRequestMessage(HttpMethod.Post, authServerMetadata.TokenEndpoint) { Content = requestContent }; using var httpResponse = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); if (!httpResponse.IsSuccessStatusCode) { return null; } return await HandleSuccessfulTokenResponseAsync(httpResponse, cancellationToken).ConfigureAwait(false); } private async Task InitiateAuthorizationCodeFlowAsync( ProtectedResourceMetadata protectedResourceMetadata, AuthorizationServerMetadata authServerMetadata, CancellationToken cancellationToken) { var codeVerifier = GenerateCodeVerifier(); var codeChallenge = GenerateCodeChallenge(codeVerifier); var authUrl = BuildAuthorizationUrl(protectedResourceMetadata, authServerMetadata, codeChallenge); var authCode = await _authorizationRedirectDelegate(authUrl, _redirectUri, cancellationToken).ConfigureAwait(false); if (string.IsNullOrEmpty(authCode)) { ThrowFailedToHandleUnauthorizedResponse($"The {nameof(AuthorizationRedirectDelegate)} returned a null or empty authorization code."); } await ExchangeCodeForTokenAsync(protectedResourceMetadata, authServerMetadata, authCode!, codeVerifier, cancellationToken).ConfigureAwait(false); } private Uri BuildAuthorizationUrl( ProtectedResourceMetadata protectedResourceMetadata, AuthorizationServerMetadata authServerMetadata, string codeChallenge) { var queryParamsDictionary = new Dictionary { ["client_id"] = GetClientIdOrThrow(), ["redirect_uri"] = _redirectUri.ToString(), ["response_type"] = "code", ["code_challenge"] = codeChallenge, ["code_challenge_method"] = "S256", ["resource"] = protectedResourceMetadata.Resource.ToString(), }; var scopesSupported = protectedResourceMetadata.ScopesSupported; if (_scopes is not null || scopesSupported.Count > 0) { queryParamsDictionary["scope"] = string.Join(" ", _scopes ?? scopesSupported.ToArray()); } // Add extra parameters if provided. Load into a dictionary before constructing to avoid overwiting values. foreach (var kvp in _additionalAuthorizationParameters) { queryParamsDictionary.Add(kvp.Key, kvp.Value); } var queryParams = HttpUtility.ParseQueryString(string.Empty); foreach (var kvp in queryParamsDictionary) { queryParams[kvp.Key] = kvp.Value; } var uriBuilder = new UriBuilder(authServerMetadata.AuthorizationEndpoint) { Query = queryParams.ToString() }; return uriBuilder.Uri; } private async Task ExchangeCodeForTokenAsync( ProtectedResourceMetadata protectedResourceMetadata, AuthorizationServerMetadata authServerMetadata, string authorizationCode, string codeVerifier, CancellationToken cancellationToken) { var requestContent = new FormUrlEncodedContent(new Dictionary { ["grant_type"] = "authorization_code", ["code"] = authorizationCode, ["redirect_uri"] = _redirectUri.ToString(), ["client_id"] = GetClientIdOrThrow(), ["code_verifier"] = codeVerifier, ["client_secret"] = _clientSecret ?? string.Empty, ["resource"] = protectedResourceMetadata.Resource.ToString(), }); using var request = new HttpRequestMessage(HttpMethod.Post, authServerMetadata.TokenEndpoint) { Content = requestContent }; using var httpResponse = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); httpResponse.EnsureSuccessStatusCode(); await HandleSuccessfulTokenResponseAsync(httpResponse, cancellationToken).ConfigureAwait(false); } private async Task HandleSuccessfulTokenResponseAsync(HttpResponseMessage response, CancellationToken cancellationToken) { using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); var tokenResponse = await JsonSerializer.DeserializeAsync(stream, McpJsonUtilities.JsonContext.Default.TokenResponse, cancellationToken).ConfigureAwait(false); if (tokenResponse is null) { ThrowFailedToHandleUnauthorizedResponse($"The token endpoint '{response.RequestMessage?.RequestUri}' returned an empty response."); } if (tokenResponse.TokenType is null || !string.Equals(tokenResponse.TokenType, BearerScheme, StringComparison.OrdinalIgnoreCase)) { ThrowFailedToHandleUnauthorizedResponse($"The token endpoint '{response.RequestMessage?.RequestUri}' returned an unsupported token type: '{tokenResponse.TokenType ?? ""}'. Only 'Bearer' tokens are supported."); } TokenContainer tokens = new() { AccessToken = tokenResponse.AccessToken, RefreshToken = tokenResponse.RefreshToken, ExpiresIn = tokenResponse.ExpiresIn, TokenType = tokenResponse.TokenType, Scope = tokenResponse.Scope, ObtainedAt = DateTimeOffset.UtcNow, }; await _tokenCache.StoreTokensAsync(tokens, cancellationToken).ConfigureAwait(false); return tokens; } /// /// Fetches the protected resource metadata from the provided URL. /// /// The URL to fetch the metadata from. /// The to monitor for cancellation requests. The default is . /// The fetched ProtectedResourceMetadata, or null if it couldn't be fetched. private async Task FetchProtectedResourceMetadataAsync(Uri metadataUrl, CancellationToken cancellationToken = default) { using var httpResponse = await _httpClient.GetAsync(metadataUrl, cancellationToken).ConfigureAwait(false); httpResponse.EnsureSuccessStatusCode(); using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); return await JsonSerializer.DeserializeAsync(stream, McpJsonUtilities.JsonContext.Default.ProtectedResourceMetadata, cancellationToken).ConfigureAwait(false); } /// /// Performs dynamic client registration with the authorization server. /// /// The authorization server metadata. /// The to monitor for cancellation requests. The default is . /// A task representing the asynchronous operation. private async Task PerformDynamicClientRegistrationAsync( AuthorizationServerMetadata authServerMetadata, CancellationToken cancellationToken) { if (authServerMetadata.RegistrationEndpoint is null) { ThrowFailedToHandleUnauthorizedResponse("Authorization server does not support dynamic client registration"); } LogPerformingDynamicClientRegistration(authServerMetadata.RegistrationEndpoint); var registrationRequest = new DynamicClientRegistrationRequest { RedirectUris = [_redirectUri.ToString()], GrantTypes = ["authorization_code", "refresh_token"], ResponseTypes = ["code"], TokenEndpointAuthMethod = "client_secret_post", ClientName = _dcrClientName, ClientUri = _dcrClientUri?.ToString(), Scope = _scopes is not null ? string.Join(" ", _scopes) : null }; var requestJson = JsonSerializer.Serialize(registrationRequest, McpJsonUtilities.JsonContext.Default.DynamicClientRegistrationRequest); using var requestContent = new StringContent(requestJson, Encoding.UTF8, "application/json"); using var request = new HttpRequestMessage(HttpMethod.Post, authServerMetadata.RegistrationEndpoint) { Content = requestContent }; if (!string.IsNullOrEmpty(_dcrInitialAccessToken)) { request.Headers.Authorization = new AuthenticationHeaderValue(BearerScheme, _dcrInitialAccessToken); } using var httpResponse = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); if (!httpResponse.IsSuccessStatusCode) { var errorContent = await httpResponse.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); ThrowFailedToHandleUnauthorizedResponse($"Dynamic client registration failed with status {httpResponse.StatusCode}: {errorContent}"); } using var responseStream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); var registrationResponse = await JsonSerializer.DeserializeAsync( responseStream, McpJsonUtilities.JsonContext.Default.DynamicClientRegistrationResponse, cancellationToken).ConfigureAwait(false); if (registrationResponse is null) { ThrowFailedToHandleUnauthorizedResponse("Dynamic client registration returned empty response"); } // Update client credentials _clientId = registrationResponse.ClientId; if (!string.IsNullOrEmpty(registrationResponse.ClientSecret)) { _clientSecret = registrationResponse.ClientSecret; } LogDynamicClientRegistrationSuccessful(_clientId!); if (_dcrResponseDelegate is not null) { await _dcrResponseDelegate(registrationResponse, cancellationToken).ConfigureAwait(false); } } /// /// 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. /// /// The metadata to verify. /// The original URL the client used to make the request to the resource server. /// True if the resource URI exactly matches the original request URL, otherwise false. private static bool VerifyResourceMatch(ProtectedResourceMetadata protectedResourceMetadata, Uri resourceLocation) { if (protectedResourceMetadata.Resource == null || resourceLocation == null) { return false; } // Per RFC: The resource value must be identical to the URL that the client used // to make the request to the resource server. Compare entire URIs, not just the host. // Normalize the URIs to ensure consistent comparison string normalizedMetadataResource = NormalizeUri(protectedResourceMetadata.Resource); string normalizedResourceLocation = NormalizeUri(resourceLocation); return string.Equals(normalizedMetadataResource, normalizedResourceLocation, StringComparison.OrdinalIgnoreCase); } /// /// Normalizes a URI for consistent comparison. /// /// The URI to normalize. /// A normalized string representation of the URI. private static string NormalizeUri(Uri uri) { var builder = new UriBuilder(uri) { Port = -1 // Always remove port }; if (builder.Path.Length > 0 && builder.Path[builder.Path.Length - 1] == '/') { builder.Path = builder.Path.TrimEnd('/'); } return builder.Uri.ToString(); } /// /// 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. /// /// The HTTP response containing the WWW-Authenticate header. /// The server URL to verify against the resource metadata. /// The to monitor for cancellation requests. The default is . /// The resource metadata if the resource matches the server, otherwise throws an exception. /// Thrown when the response is not a 401, lacks a WWW-Authenticate header, /// lacks a resource_metadata parameter, the metadata can't be fetched, or the resource URI doesn't match the server URL. private async Task ExtractProtectedResourceMetadata(HttpResponseMessage response, Uri serverUrl, CancellationToken cancellationToken = default) { if (response.StatusCode != System.Net.HttpStatusCode.Unauthorized) { throw new InvalidOperationException($"Expected a 401 Unauthorized response, but received {(int)response.StatusCode} {response.StatusCode}"); } // Extract the WWW-Authenticate header if (response.Headers.WwwAuthenticate.Count == 0) { throw new McpException("The 401 response does not contain a WWW-Authenticate header"); } // Look for the Bearer authentication scheme with resource_metadata parameter string? resourceMetadataUrl = null; foreach (var header in response.Headers.WwwAuthenticate) { if (string.Equals(header.Scheme, BearerScheme, StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(header.Parameter)) { resourceMetadataUrl = ParseWwwAuthenticateParameters(header.Parameter, "resource_metadata"); if (resourceMetadataUrl != null) { break; } } } if (resourceMetadataUrl == null) { throw new McpException("The WWW-Authenticate header does not contain a resource_metadata parameter"); } Uri metadataUri = new(resourceMetadataUrl); var metadata = await FetchProtectedResourceMetadataAsync(metadataUri, cancellationToken).ConfigureAwait(false) ?? throw new McpException($"Failed to fetch resource metadata from {resourceMetadataUrl}"); // Per RFC: The resource value must be identical to the URL that the client used // to make the request to the resource server LogValidatingResourceMetadata(serverUrl); if (!VerifyResourceMatch(metadata, serverUrl)) { throw new McpException($"Resource URI in metadata ({metadata.Resource}) does not match the expected URI ({serverUrl})"); } return metadata; } /// /// Parses the WWW-Authenticate header parameters to extract a specific parameter. /// /// The parameter string from the WWW-Authenticate header. /// The name of the parameter to extract. /// The value of the parameter, or null if not found. private static string? ParseWwwAuthenticateParameters(string parameters, string parameterName) { if (parameters.IndexOf(parameterName, StringComparison.OrdinalIgnoreCase) == -1) { return null; } foreach (var part in parameters.Split(',')) { string trimmedPart = part.Trim(); int equalsIndex = trimmedPart.IndexOf('='); if (equalsIndex <= 0) { continue; } ReadOnlySpan key = trimmedPart.AsSpan().Slice(0, equalsIndex).Trim(); if (key.Equals(parameterName, StringComparison.OrdinalIgnoreCase)) { ReadOnlySpan value = trimmedPart.AsSpan(equalsIndex + 1).Trim(); if (value.Length > 0 && value[0] == '"' && value[value.Length - 1] == '"') { value = value.Slice(1, value.Length - 2); } return value.ToString(); } } return null; } private static string GenerateCodeVerifier() { var bytes = new byte[32]; using var rng = RandomNumberGenerator.Create(); rng.GetBytes(bytes); return Convert.ToBase64String(bytes) .TrimEnd('=') .Replace('+', '-') .Replace('/', '_'); } private static string GenerateCodeChallenge(string codeVerifier) { #if NET9_0_OR_GREATER Span hash = stackalloc byte[SHA256.HashSizeInBytes]; SHA256.HashData(Encoding.UTF8.GetBytes(codeVerifier), hash); return Base64Url.EncodeToString(hash); #else using var sha256 = SHA256.Create(); var challengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier)); return Convert.ToBase64String(challengeBytes) .TrimEnd('=') .Replace('+', '-') .Replace('/', '_'); #endif } private string GetClientIdOrThrow() => _clientId ?? throw new InvalidOperationException("Client ID is not available. This may indicate an issue with dynamic client registration."); private static void ThrowIfNotBearerScheme(string scheme) { if (!string.Equals(scheme, BearerScheme, StringComparison.OrdinalIgnoreCase)) { throw new InvalidOperationException($"The '{scheme}' is not supported. This credential provider only supports the '{BearerScheme}' scheme"); } } [DoesNotReturn] private static void ThrowFailedToHandleUnauthorizedResponse(string message) => throw new McpException($"Failed to handle unauthorized response with 'Bearer' scheme. {message}"); [LoggerMessage(Level = LogLevel.Information, Message = "Selected authorization server: {Server} from {Count} available servers")] partial void LogSelectedAuthorizationServer(Uri server, int count); [LoggerMessage(Level = LogLevel.Information, Message = "OAuth authorization completed successfully")] partial void LogOAuthAuthorizationCompleted(); [LoggerMessage(Level = LogLevel.Error, Message = "Error fetching auth server metadata from {Path}")] partial void LogErrorFetchingAuthServerMetadata(Exception ex, string path); [LoggerMessage(Level = LogLevel.Information, Message = "Performing dynamic client registration with {RegistrationEndpoint}")] partial void LogPerformingDynamicClientRegistration(Uri registrationEndpoint); [LoggerMessage(Level = LogLevel.Information, Message = "Dynamic client registration successful. Client ID: {ClientId}")] partial void LogDynamicClientRegistrationSuccessful(string clientId); [LoggerMessage(Level = LogLevel.Debug, Message = "Validating resource metadata against original server URL: {ServerUrl}")] partial void LogValidatingResourceMetadata(Uri serverUrl); }