GetAuthorizationCodeAsync(Uri authorizationUrl, CancellationToken cancellationToken)
+ {
+ var listenerPrefix = _redirectUri.GetLeftPart(UriPartial.Authority);
+ if (!listenerPrefix.EndsWith("/")) listenerPrefix += "/";
+
+ using var listener = new System.Net.HttpListener();
+ listener.Prefixes.Add(listenerPrefix);
+
+ try
+ {
+ listener.Start();
+
+ OpenBrowser(authorizationUrl);
+
+ var context = await listener.GetContextAsync();
+
+ var query = HttpUtility.ParseQueryString(context.Request.Url?.Query ?? string.Empty);
+ var code = query["code"];
+ var error = query["error"];
+
+ string responseHtml = "Authentication complete
You can close this window now.
";
+ byte[] buffer = Encoding.UTF8.GetBytes(responseHtml);
+ context.Response.ContentLength64 = buffer.Length;
+ context.Response.ContentType = "text/html";
+ context.Response.OutputStream.Write(buffer, 0, buffer.Length);
+ context.Response.Close();
+
+ if (!string.IsNullOrEmpty(error))
+ {
+ Console.WriteLine($"Auth error: {error}");
+ return null;
+ }
+
+ return code;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error getting auth code: {ex.Message}");
+ return null;
+ }
+ finally
+ {
+ if (listener.IsListening) listener.Stop();
+ }
+ }
+
+ private async Task ExchangeCodeForTokenAsync(
+ 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"] = _clientId,
+ ["code_verifier"] = codeVerifier
+ });
+
+ try
+ {
+ using var request = new HttpRequestMessage(HttpMethod.Post, authServerMetadata.TokenEndpoint)
+ {
+ Content = requestContent
+ };
+
+ if (!string.IsNullOrEmpty(_clientSecret))
+ {
+ var authValue = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{_clientId}:{_clientSecret}"));
+ request.Headers.Authorization = new AuthenticationHeaderValue("Basic", authValue);
+ }
+
+ var response = await _httpClient.SendAsync(request, cancellationToken);
+ if (response.IsSuccessStatusCode)
+ {
+ var json = await response.Content.ReadAsStringAsync(cancellationToken);
+ var tokenResponse = JsonSerializer.Deserialize(
+ json, _jsonOptions);
+
+ if (tokenResponse != null)
+ {
+ tokenResponse.ObtainedAt = DateTimeOffset.UtcNow;
+ return tokenResponse;
+ }
+ }
+ else
+ {
+ Console.WriteLine($"Token exchange failed: {response.StatusCode}");
+ var error = await response.Content.ReadAsStringAsync(cancellationToken);
+ Console.WriteLine($"Error: {error}");
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Exception during token exchange: {ex.Message}");
+ }
+
+ return null;
+ }
+
+ private void OpenBrowser(Uri url)
+ {
+ try
+ {
+ var psi = new System.Diagnostics.ProcessStartInfo
+ {
+ FileName = url.ToString(),
+ UseShellExecute = true
+ };
+ System.Diagnostics.Process.Start(psi);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error opening browser: {ex.Message}");
+ Console.WriteLine($"Please manually navigate to: {url}");
+ }
+ }
+
+ private string GenerateCodeVerifier()
+ {
+ var bytes = new byte[32];
+ using var rng = RandomNumberGenerator.Create();
+ rng.GetBytes(bytes);
+ return Convert.ToBase64String(bytes)
+ .TrimEnd('=')
+ .Replace('+', '-')
+ .Replace('/', '_');
+ }
+
+ private string GenerateCodeChallenge(string codeVerifier)
+ {
+ using var sha256 = SHA256.Create();
+ var challengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier));
+ return Convert.ToBase64String(challengeBytes)
+ .TrimEnd('=')
+ .Replace('+', '-')
+ .Replace('/', '_');
+ }
+}
\ No newline at end of file
diff --git a/samples/ProtectedMCPClient/Program.cs b/samples/ProtectedMCPClient/Program.cs
new file mode 100644
index 00000000..c76967bb
--- /dev/null
+++ b/samples/ProtectedMCPClient/Program.cs
@@ -0,0 +1,94 @@
+using Microsoft.Extensions.DependencyInjection;
+using ModelContextProtocol.Authentication;
+using ModelContextProtocol.Client;
+using ModelContextProtocol.Protocol.Transport;
+
+namespace ProtectedMCPClient;
+
+class Program
+{
+ static async Task Main(string[] args)
+ {
+ Console.WriteLine("Protected MCP Weather Server");
+ Console.WriteLine();
+
+ var serverUrl = "http://localhost:7071/sse";
+
+ // We can customize a shared HttpClient with a custom handler if desired
+ var sharedHandler = new SocketsHttpHandler
+ {
+ PooledConnectionLifetime = TimeSpan.FromMinutes(2),
+ PooledConnectionIdleTimeout = TimeSpan.FromMinutes(1)
+ };
+
+ var httpClient = new HttpClient(sharedHandler);
+
+ // Create the token provider with our custom HttpClient,
+ // letting the AuthorizationHelpers be created automatically
+ var tokenProvider = new BasicOAuthProvider(
+ new Uri(serverUrl),
+ httpClient,
+ null, // AuthorizationHelpers will be created automatically
+ clientId: "6ad97b5f-7a7b-413f-8603-7a3517d4adb8",
+ redirectUri: new Uri("http://localhost:1179/callback"),
+ scopes: ["api://167b4284-3f92-4436-92ed-38b38f83ae08/weather.read"]
+ );
+
+ Console.WriteLine();
+ Console.WriteLine($"Connecting to weather server at {serverUrl}...");
+
+ try
+ {
+ var transportOptions = new SseClientTransportOptions
+ {
+ Endpoint = new Uri(serverUrl),
+ Name = "Secure Weather Client"
+ };
+
+ // Create a transport with authentication support using the correct constructor parameters
+ var transport = new SseClientTransport(
+ transportOptions,
+ tokenProvider
+ );
+ var client = await McpClientFactory.CreateAsync(transport);
+
+ var tools = await client.ListToolsAsync();
+ if (tools.Count == 0)
+ {
+ Console.WriteLine("No tools available on the server.");
+ return;
+ }
+
+ Console.WriteLine($"Found {tools.Count} tools on the server.");
+ Console.WriteLine();
+
+ if (tools.Any(t => t.Name == "GetAlerts"))
+ {
+ Console.WriteLine("Calling GetAlerts tool...");
+
+ var result = await client.CallToolAsync(
+ "GetAlerts",
+ new Dictionary { { "state", "WA" } }
+ );
+
+ Console.WriteLine("Result: " + result.Content[0].Text);
+ Console.WriteLine();
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error: {ex.Message}");
+ if (ex.InnerException != null)
+ {
+ Console.WriteLine($"Inner error: {ex.InnerException.Message}");
+ }
+
+ #if DEBUG
+ Console.WriteLine($"Stack trace: {ex.StackTrace}");
+ #endif
+ }
+
+ Console.WriteLine("Press any key to exit...");
+ Console.ReadKey();
+ }
+}
\ No newline at end of file
diff --git a/samples/ProtectedMCPClient/ProtectedMCPClient.csproj b/samples/ProtectedMCPClient/ProtectedMCPClient.csproj
new file mode 100644
index 00000000..cc60ff80
--- /dev/null
+++ b/samples/ProtectedMCPClient/ProtectedMCPClient.csproj
@@ -0,0 +1,14 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/ProtectedMCPClient/Types/AuthorizationServerMetadata.cs b/samples/ProtectedMCPClient/Types/AuthorizationServerMetadata.cs
new file mode 100644
index 00000000..c5fd6253
--- /dev/null
+++ b/samples/ProtectedMCPClient/Types/AuthorizationServerMetadata.cs
@@ -0,0 +1,69 @@
+using System.Text.Json.Serialization;
+
+namespace ModelContextProtocol.Types.Authentication;
+
+///
+/// Represents the metadata about an OAuth authorization server.
+///
+public class AuthorizationServerMetadata
+{
+ ///
+ /// The authorization endpoint URI.
+ ///
+ [JsonPropertyName("authorization_endpoint")]
+ public Uri AuthorizationEndpoint { get; set; } = null!;
+
+ ///
+ /// The token endpoint URI.
+ ///
+ [JsonPropertyName("token_endpoint")]
+ public Uri TokenEndpoint { get; set; } = null!;
+
+ ///
+ /// The registration endpoint URI.
+ ///
+ [JsonPropertyName("registration_endpoint")]
+ public Uri? RegistrationEndpoint { get; set; }
+
+ ///
+ /// The revocation endpoint URI.
+ ///
+ [JsonPropertyName("revocation_endpoint")]
+ public Uri? RevocationEndpoint { get; set; }
+
+ ///
+ /// The response types supported by the authorization server.
+ ///
+ [JsonPropertyName("response_types_supported")]
+ public List? ResponseTypesSupported { get; set; }
+
+ ///
+ /// The grant types supported by the authorization server.
+ ///
+ [JsonPropertyName("grant_types_supported")]
+ public List? GrantTypesSupported { get; set; }
+
+ ///
+ /// The token endpoint authentication methods supported by the authorization server.
+ ///
+ [JsonPropertyName("token_endpoint_auth_methods_supported")]
+ public List? TokenEndpointAuthMethodsSupported { get; set; }
+
+ ///
+ /// The code challenge methods supported by the authorization server.
+ ///
+ [JsonPropertyName("code_challenge_methods_supported")]
+ public List? CodeChallengeMethodsSupported { get; set; }
+
+ ///
+ /// The issuer URI of the authorization server.
+ ///
+ [JsonPropertyName("issuer")]
+ public Uri? Issuer { get; set; }
+
+ ///
+ /// The scopes supported by the authorization server.
+ ///
+ [JsonPropertyName("scopes_supported")]
+ public List? ScopesSupported { get; set; }
+}
\ No newline at end of file
diff --git a/samples/ProtectedMCPClient/Types/TokenContainer.cs b/samples/ProtectedMCPClient/Types/TokenContainer.cs
new file mode 100644
index 00000000..34deb859
--- /dev/null
+++ b/samples/ProtectedMCPClient/Types/TokenContainer.cs
@@ -0,0 +1,39 @@
+using System.Text.Json.Serialization;
+
+namespace ProtectedMCPClient.Types;
+
+///
+/// Represents a token response from the OAuth server.
+///
+internal class TokenContainer
+{
+ [JsonPropertyName("access_token")]
+ public string AccessToken { get; set; } = string.Empty;
+
+ [JsonPropertyName("refresh_token")]
+ public string? RefreshToken { get; set; }
+
+ [JsonPropertyName("expires_in")]
+ public int ExpiresIn { get; set; }
+
+ [JsonPropertyName("ext_expires_in")]
+ public int ExtExpiresIn { get; set; }
+
+ [JsonPropertyName("token_type")]
+ public string TokenType { get; set; } = string.Empty;
+
+ [JsonPropertyName("scope")]
+ public string Scope { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the timestamp when the token was obtained.
+ ///
+ [JsonIgnore]
+ public DateTimeOffset ObtainedAt { get; set; }
+
+ ///
+ /// Gets the timestamp when the token expires, calculated from ObtainedAt and ExpiresIn.
+ ///
+ [JsonIgnore]
+ public DateTimeOffset ExpiresAt => ObtainedAt.AddSeconds(ExpiresIn);
+}
diff --git a/samples/ProtectedMCPServer/Program.cs b/samples/ProtectedMCPServer/Program.cs
new file mode 100644
index 00000000..54d21a9d
--- /dev/null
+++ b/samples/ProtectedMCPServer/Program.cs
@@ -0,0 +1,104 @@
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.IdentityModel.Tokens;
+using ModelContextProtocol.AspNetCore.Authentication;
+using ModelContextProtocol.Authentication;
+using ProtectedMCPServer.Tools;
+using System.Net.Http.Headers;
+using System.Security.Claims;
+
+var builder = WebApplication.CreateBuilder(args);
+
+var serverUrl = "http://localhost:7071/";
+var tenantId = "a2213e1c-e51e-4304-9a0d-effe57f31655";
+var instance = "https://login.microsoftonline.com/";
+
+builder.Services.AddAuthentication(options =>
+{
+ options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme;
+ options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
+})
+.AddJwtBearer(options =>
+{
+ options.Authority = $"{instance}{tenantId}/v2.0";
+ options.TokenValidationParameters = new TokenValidationParameters
+ {
+ ValidateIssuer = true,
+ ValidateAudience = true,
+ ValidateLifetime = true,
+ ValidateIssuerSigningKey = true,
+ ValidAudience = "167b4284-3f92-4436-92ed-38b38f83ae08",
+ ValidIssuer = $"{instance}{tenantId}/v2.0",
+ NameClaimType = "name",
+ RoleClaimType = "roles"
+ };
+
+ options.MetadataAddress = $"{instance}{tenantId}/v2.0/.well-known/openid-configuration";
+
+ options.Events = new JwtBearerEvents
+ {
+ OnTokenValidated = context =>
+ {
+ var name = context.Principal?.Identity?.Name ?? "unknown";
+ var email = context.Principal?.FindFirstValue("preferred_username") ?? "unknown";
+ Console.WriteLine($"Token validated for: {name} ({email})");
+ return Task.CompletedTask;
+ },
+ OnAuthenticationFailed = context =>
+ {
+ Console.WriteLine($"Authentication failed: {context.Exception.Message}");
+ return Task.CompletedTask;
+ },
+ OnChallenge = context =>
+ {
+ Console.WriteLine($"Challenging client to authenticate with Entra ID");
+ return Task.CompletedTask;
+ }
+ };
+})
+.AddMcp(options =>
+{
+ options.ProtectedResourceMetadataProvider = context =>
+ {
+ var metadata = new ProtectedResourceMetadata
+ {
+ Resource = new Uri("http://localhost"),
+ BearerMethodsSupported = { "header" },
+ ResourceDocumentation = new Uri("https://docs.example.com/api/weather"),
+ AuthorizationServers = { new Uri($"{instance}{tenantId}/v2.0") }
+ };
+
+ metadata.ScopesSupported.AddRange([
+ "api://167b4284-3f92-4436-92ed-38b38f83ae08/weather.read"
+ ]);
+
+ return metadata;
+ };
+});
+
+builder.Services.AddAuthorization();
+
+builder.Services.AddHttpContextAccessor();
+builder.Services.AddMcpServer()
+.WithTools()
+.WithHttpTransport();
+
+// Configure HttpClientFactory for weather.gov API
+builder.Services.AddHttpClient("WeatherApi", client =>
+{
+ client.BaseAddress = new Uri("https://api.weather.gov");
+ client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("weather-tool", "1.0"));
+});
+
+var app = builder.Build();
+
+app.UseAuthentication();
+app.UseAuthorization();
+
+// Use the default MCP policy name that we've configured
+app.MapMcp().RequireAuthorization();
+
+Console.WriteLine($"Starting MCP server with authorization at {serverUrl}");
+Console.WriteLine($"PRM Document URL: {serverUrl}.well-known/oauth-protected-resource");
+Console.WriteLine("Press Ctrl+C to stop the server");
+
+app.Run(serverUrl);
diff --git a/samples/ProtectedMCPServer/Properties/launchSettings.json b/samples/ProtectedMCPServer/Properties/launchSettings.json
new file mode 100644
index 00000000..03646532
--- /dev/null
+++ b/samples/ProtectedMCPServer/Properties/launchSettings.json
@@ -0,0 +1,12 @@
+{
+ "profiles": {
+ "ProtectedMCPServer": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "https://localhost:55598;http://localhost:55599"
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/ProtectedMCPServer/ProtectedMCPServer.csproj b/samples/ProtectedMCPServer/ProtectedMCPServer.csproj
new file mode 100644
index 00000000..e3d680fd
--- /dev/null
+++ b/samples/ProtectedMCPServer/ProtectedMCPServer.csproj
@@ -0,0 +1,14 @@
+
+
+
+ net9.0
+ enable
+ enable
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/ProtectedMCPServer/Tools/HttpClientExt.cs b/samples/ProtectedMCPServer/Tools/HttpClientExt.cs
new file mode 100644
index 00000000..f7b2b549
--- /dev/null
+++ b/samples/ProtectedMCPServer/Tools/HttpClientExt.cs
@@ -0,0 +1,13 @@
+using System.Text.Json;
+
+namespace ModelContextProtocol;
+
+internal static class HttpClientExt
+{
+ public static async Task ReadJsonDocumentAsync(this HttpClient client, string requestUri)
+ {
+ using var response = await client.GetAsync(requestUri);
+ response.EnsureSuccessStatusCode();
+ return await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
+ }
+}
\ No newline at end of file
diff --git a/samples/ProtectedMCPServer/Tools/WeatherTools.cs b/samples/ProtectedMCPServer/Tools/WeatherTools.cs
new file mode 100644
index 00000000..7c8c0851
--- /dev/null
+++ b/samples/ProtectedMCPServer/Tools/WeatherTools.cs
@@ -0,0 +1,67 @@
+using ModelContextProtocol;
+using ModelContextProtocol.Server;
+using System.ComponentModel;
+using System.Globalization;
+using System.Text.Json;
+
+namespace ProtectedMCPServer.Tools;
+
+[McpServerToolType]
+public sealed class WeatherTools
+{
+ private readonly IHttpClientFactory _httpClientFactory;
+
+ public WeatherTools(IHttpClientFactory httpClientFactory)
+ {
+ _httpClientFactory = httpClientFactory;
+ }
+
+ [McpServerTool, Description("Get weather alerts for a US state.")]
+ public async Task GetAlerts(
+ [Description("The US state to get alerts for. Use the 2 letter abbreviation for the state (e.g. NY).")] string state)
+ {
+ var client = _httpClientFactory.CreateClient("WeatherApi");
+ using var jsonDocument = await client.ReadJsonDocumentAsync($"/alerts/active/area/{state}");
+ var jsonElement = jsonDocument.RootElement;
+ var alerts = jsonElement.GetProperty("features").EnumerateArray();
+
+ if (!alerts.Any())
+ {
+ return "No active alerts for this state.";
+ }
+
+ return string.Join("\n--\n", alerts.Select(alert =>
+ {
+ JsonElement properties = alert.GetProperty("properties");
+ return $"""
+ Event: {properties.GetProperty("event").GetString()}
+ Area: {properties.GetProperty("areaDesc").GetString()}
+ Severity: {properties.GetProperty("severity").GetString()}
+ Description: {properties.GetProperty("description").GetString()}
+ Instruction: {properties.GetProperty("instruction").GetString()}
+ """;
+ }));
+ }
+
+ [McpServerTool, Description("Get weather forecast for a location.")]
+ public async Task GetForecast(
+ [Description("Latitude of the location.")] double latitude,
+ [Description("Longitude of the location.")] double longitude)
+ {
+ var client = _httpClientFactory.CreateClient("WeatherApi");
+ var pointUrl = string.Create(CultureInfo.InvariantCulture, $"/points/{latitude},{longitude}");
+ using var jsonDocument = await client.ReadJsonDocumentAsync(pointUrl);
+ var forecastUrl = jsonDocument.RootElement.GetProperty("properties").GetProperty("forecast").GetString()
+ ?? throw new Exception($"No forecast URL provided by {client.BaseAddress}points/{latitude},{longitude}");
+
+ using var forecastDocument = await client.ReadJsonDocumentAsync(forecastUrl);
+ var periods = forecastDocument.RootElement.GetProperty("properties").GetProperty("periods").EnumerateArray();
+
+ return string.Join("\n---\n", periods.Select(period => $"""
+ {period.GetProperty("name").GetString()}
+ Temperature: {period.GetProperty("temperature").GetInt32()}°F
+ Wind: {period.GetProperty("windSpeed").GetString()} {period.GetProperty("windDirection").GetString()}
+ Forecast: {period.GetProperty("detailedForecast").GetString()}
+ """));
+ }
+}
diff --git a/src/ModelContextProtocol.AspNetCore/Authentication/McpAuthenticationDefaults.cs b/src/ModelContextProtocol.AspNetCore/Authentication/McpAuthenticationDefaults.cs
new file mode 100644
index 00000000..4c720c65
--- /dev/null
+++ b/src/ModelContextProtocol.AspNetCore/Authentication/McpAuthenticationDefaults.cs
@@ -0,0 +1,17 @@
+namespace ModelContextProtocol.AspNetCore.Authentication;
+
+///
+/// Default values used by MCP authentication.
+///
+public static class McpAuthenticationDefaults
+{
+ ///
+ /// The default value used for authentication scheme name.
+ ///
+ public const string AuthenticationScheme = "McpAuth";
+
+ ///
+ /// The default value used for authentication scheme display name.
+ ///
+ public const string DisplayName = "MCP Authentication";
+}
\ No newline at end of file
diff --git a/src/ModelContextProtocol.AspNetCore/Authentication/McpAuthenticationEvents.cs b/src/ModelContextProtocol.AspNetCore/Authentication/McpAuthenticationEvents.cs
new file mode 100644
index 00000000..7a7eb29b
--- /dev/null
+++ b/src/ModelContextProtocol.AspNetCore/Authentication/McpAuthenticationEvents.cs
@@ -0,0 +1,9 @@
+namespace ModelContextProtocol.AspNetCore.Authentication
+{
+ ///
+ /// Represents the authentication events for Model Context Protocol.
+ ///
+ public class McpAuthenticationEvents
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/ModelContextProtocol.AspNetCore/Authentication/McpAuthenticationExceptions.cs b/src/ModelContextProtocol.AspNetCore/Authentication/McpAuthenticationExceptions.cs
new file mode 100644
index 00000000..96f200da
--- /dev/null
+++ b/src/ModelContextProtocol.AspNetCore/Authentication/McpAuthenticationExceptions.cs
@@ -0,0 +1,59 @@
+using Microsoft.AspNetCore.Authentication;
+using ModelContextProtocol.AspNetCore.Authentication;
+
+namespace Microsoft.Extensions.DependencyInjection;
+
+///
+/// Extension methods for adding MCP authorization support to ASP.NET Core applications.
+///
+public static class McpAuthenticationExtensions
+{
+ ///
+ /// Adds MCP authorization support to the application.
+ ///
+ /// The authentication builder.
+ /// An action to configure MCP authentication options.
+ /// The authentication builder for chaining.
+ public static AuthenticationBuilder AddMcp(
+ this AuthenticationBuilder builder,
+ Action? configureOptions = null)
+ {
+ return AddMcp(
+ builder,
+ McpAuthenticationDefaults.AuthenticationScheme,
+ McpAuthenticationDefaults.DisplayName,
+ configureOptions);
+ }
+
+ ///
+ /// Adds MCP authorization support to the application with a custom scheme name.
+ ///
+ /// The authentication builder.
+ /// The authentication scheme name to use.
+ /// The display name for the authentication scheme.
+ /// An action to configure MCP authentication options.
+ /// The authentication builder for chaining.
+ public static AuthenticationBuilder AddMcp(
+ this AuthenticationBuilder builder,
+ string authenticationScheme,
+ string displayName,
+ Action? configureOptions = null)
+ {
+ if (configureOptions != null)
+ {
+ if (authenticationScheme == McpAuthenticationDefaults.AuthenticationScheme)
+ {
+ builder.Services.Configure(configureOptions);
+ }
+ else
+ {
+ builder.Services.Configure(authenticationScheme, configureOptions);
+ }
+ }
+
+ return builder.AddScheme(
+ authenticationScheme,
+ displayName,
+ options => { }); // No-op to avoid overriding
+ }
+}
diff --git a/src/ModelContextProtocol.AspNetCore/Authentication/McpAuthenticationHandler.cs b/src/ModelContextProtocol.AspNetCore/Authentication/McpAuthenticationHandler.cs
new file mode 100644
index 00000000..e0d87687
--- /dev/null
+++ b/src/ModelContextProtocol.AspNetCore/Authentication/McpAuthenticationHandler.cs
@@ -0,0 +1,152 @@
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using ModelContextProtocol.Authentication;
+using ModelContextProtocol.Utils.Json;
+using System.Text.Encodings.Web;
+using System.Text.Json;
+
+namespace ModelContextProtocol.AspNetCore.Authentication;
+
+///
+/// Authentication handler for MCP protocol that adds resource metadata to challenge responses
+/// and handles resource metadata endpoint requests.
+///
+public class McpAuthenticationHandler : AuthenticationHandler, IAuthenticationRequestHandler
+{
+ private readonly IOptionsMonitor _optionsMonitor;
+ private string _resourceMetadataPath;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public McpAuthenticationHandler(
+ IOptionsMonitor options,
+ ILoggerFactory logger,
+ UrlEncoder encoder)
+ : base(options, logger, encoder)
+ {
+ _optionsMonitor = options;
+ _resourceMetadataPath = options.CurrentValue.ResourceMetadataUri.ToString();
+ }
+
+ ///
+ public async Task HandleRequestAsync()
+ {
+ // Check if the request is for the resource metadata endpoint
+ string requestPath = Request.Path.Value ?? string.Empty;
+
+ // If the path doesn't match, let the request continue through the pipeline
+ if (!string.Equals(requestPath, _resourceMetadataPath, StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ // Use the request's cancellation token if available
+ var cancellationToken = Request.HttpContext.RequestAborted;
+ await HandleResourceMetadataRequestAsync(cancellationToken);
+ return true;
+ }
+
+ ///
+ /// Gets the base URL from the current request, including scheme, host, and path base.
+ ///
+ private string GetBaseUrl() => $"{Request.Scheme}://{Request.Host}{Request.PathBase}";
+
+ ///
+ /// Gets the absolute URI for the resource metadata endpoint.
+ ///
+ private string GetAbsoluteResourceMetadataUri()
+ {
+ var options = _optionsMonitor.CurrentValue;
+ var resourceMetadataUri = options.ResourceMetadataUri;
+
+ // If the options have changed, update the cached path
+ string currentPath = resourceMetadataUri.ToString();
+ if (_resourceMetadataPath != currentPath)
+ {
+ _resourceMetadataPath = currentPath;
+ }
+
+ if (resourceMetadataUri.IsAbsoluteUri)
+ {
+ return resourceMetadataUri.ToString();
+ }
+
+ // For relative URIs, combine with the base URL
+ string baseUrl = GetBaseUrl();
+
+ if (!Uri.TryCreate(baseUrl + _resourceMetadataPath, UriKind.Absolute, out var absoluteUri))
+ {
+ throw new InvalidOperationException("Could not create absolute URI for resource metadata.");
+ }
+
+ return absoluteUri.ToString();
+ }
+
+ ///
+ /// Handles the resource metadata request.
+ ///
+ /// A token to cancel the operation.
+ private Task HandleResourceMetadataRequestAsync(CancellationToken cancellationToken = default)
+ {
+ // Get resource metadata from options, using the dynamic provider if available
+ var options = _optionsMonitor.CurrentValue;
+ var resourceMetadata = options.GetResourceMetadata(Request.HttpContext);
+
+ // Create a copy to avoid modifying the original
+ var metadata = new ProtectedResourceMetadata
+ {
+ Resource = resourceMetadata.Resource ?? new Uri(GetBaseUrl()),
+ AuthorizationServers = [.. resourceMetadata.AuthorizationServers],
+ BearerMethodsSupported = [.. resourceMetadata.BearerMethodsSupported],
+ ScopesSupported = [.. resourceMetadata.ScopesSupported],
+ ResourceDocumentation = resourceMetadata.ResourceDocumentation
+ };
+
+ Response.StatusCode = StatusCodes.Status200OK;
+ Response.ContentType = "application/json";
+
+ var json = JsonSerializer.Serialize(
+ metadata,
+ McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ProtectedResourceMetadata)));
+
+ return Response.WriteAsync(json, cancellationToken);
+ }
+
+ ///
+ protected override async Task HandleAuthenticateAsync()
+ {
+ // If ForwardAuthenticate is set, forward the authentication to the specified scheme
+ if (!string.IsNullOrEmpty(Options.ForwardAuthenticate) &&
+ Options.ForwardAuthenticate != Scheme.Name)
+ {
+ // Simply forward the authentication request to the specified scheme and return its result
+ // This ensures we don't interfere with the authentication process
+ return await Context.AuthenticateAsync(Options.ForwardAuthenticate);
+ }
+
+ // If no forwarding is configured, this handler doesn't perform authentication
+ return AuthenticateResult.NoResult();
+ }
+
+ ///
+ protected override Task HandleChallengeAsync(AuthenticationProperties properties)
+ {
+ // Get the absolute URI for the resource metadata
+ string rawPrmDocumentUri = GetAbsoluteResourceMetadataUri();
+
+ // Initialize properties if null
+ properties ??= new AuthenticationProperties();
+
+ // Store the resource_metadata in properties in case other handlers need it
+ properties.Items["resource_metadata"] = rawPrmDocumentUri;
+
+ // Add the WWW-Authenticate header with Bearer scheme and resource metadata
+ string headerValue = $"Bearer realm=\"{Scheme.Name}\", resource_metadata=\"{rawPrmDocumentUri}\"";
+ Response.Headers.Append("WWW-Authenticate", headerValue);
+
+ return base.HandleChallengeAsync(properties);
+ }
+}
diff --git a/src/ModelContextProtocol.AspNetCore/Authentication/McpAuthenticationOptions.cs b/src/ModelContextProtocol.AspNetCore/Authentication/McpAuthenticationOptions.cs
new file mode 100644
index 00000000..6434fe06
--- /dev/null
+++ b/src/ModelContextProtocol.AspNetCore/Authentication/McpAuthenticationOptions.cs
@@ -0,0 +1,119 @@
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Http;
+using ModelContextProtocol.Authentication;
+
+namespace ModelContextProtocol.AspNetCore.Authentication;
+
+///
+/// Options for the MCP authentication handler.
+///
+public class McpAuthenticationOptions : AuthenticationSchemeOptions
+{
+ private static readonly Uri DefaultResourceMetadataUri = new("/.well-known/oauth-protected-resource", UriKind.Relative);
+ private Func? _resourceMetadataProvider;
+ private ProtectedResourceMetadata _resourceMetadata;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public McpAuthenticationOptions()
+ {
+ base.ForwardAuthenticate = "Bearer";
+ ResourceMetadataUri = DefaultResourceMetadataUri;
+ _resourceMetadata = new ProtectedResourceMetadata() { Resource = new Uri("http://localhost") };
+ Events = new McpAuthenticationEvents();
+ }
+
+ ///
+ /// Gets or sets the events used to handle authentication events.
+ ///
+ public new McpAuthenticationEvents Events
+ {
+ get { return (McpAuthenticationEvents)base.Events!; }
+ set { base.Events = value; }
+ }
+
+ ///
+ /// The URI to the resource metadata document.
+ ///
+ ///
+ /// This URI will be included in the WWW-Authenticate header when a 401 response is returned.
+ ///
+ public Uri ResourceMetadataUri { get; set; }
+
+ ///
+ /// Gets or sets the static protected resource metadata.
+ ///
+ ///
+ /// This contains the OAuth metadata for the protected resource, including authorization servers,
+ /// supported scopes, and other information needed for clients to authenticate.
+ /// Setting this property will automatically update the
+ /// to return this static instance.
+ ///
+ public ProtectedResourceMetadata ResourceMetadata
+ {
+ get => _resourceMetadata;
+ set
+ {
+ _resourceMetadata = value ?? new ProtectedResourceMetadata() { Resource = new Uri("http://localhost") };
+ // When static metadata is set, update the provider to use it
+ _resourceMetadataProvider = _ => _resourceMetadata;
+ }
+ }
+
+ ///
+ /// Gets or sets a delegate that dynamically provides resource metadata based on the HTTP context.
+ ///
+ ///
+ /// When set, this delegate will be called to generate resource metadata for each request,
+ /// allowing dynamic customization based on the caller or other contextual information.
+ /// This takes precedence over the static property.
+ ///
+ public Func? ProtectedResourceMetadataProvider
+ {
+ get => _resourceMetadataProvider;
+ set => _resourceMetadataProvider = value ?? (_ => _resourceMetadata);
+ }
+
+ ///
+ /// Sets a static resource metadata instance that will be returned for all requests.
+ ///
+ /// The static resource metadata to use.
+ /// The current options instance for method chaining.
+ ///
+ /// This is a convenience method equivalent to setting the property.
+ ///
+ public McpAuthenticationOptions UseStaticResourceMetadata(ProtectedResourceMetadata metadata)
+ {
+ ResourceMetadata = metadata ?? new ProtectedResourceMetadata() { Resource = new Uri("http://localhost") };
+ return this;
+ }
+
+ ///
+ /// Sets a delegate to dynamically provide resource metadata for each request.
+ ///
+ /// A delegate that returns resource metadata for a given HTTP context.
+ /// The current options instance for method chaining.
+ ///
+ /// This is a convenience method equivalent to setting the property.
+ ///
+ public McpAuthenticationOptions UseDynamicResourceMetadata(Func provider)
+ {
+ ProtectedResourceMetadataProvider = provider ?? throw new ArgumentNullException(nameof(provider));
+ return this;
+ }
+
+ ///
+ /// Gets the resource metadata for the current request.
+ ///
+ /// The HTTP context for the current request.
+ /// The resource metadata to use for the current request.
+ internal ProtectedResourceMetadata GetResourceMetadata(HttpContext context)
+ {
+ var provider = _resourceMetadataProvider;
+
+ return provider != null
+ ? provider(context)
+ : _resourceMetadata;
+ }
+}
\ No newline at end of file
diff --git a/src/ModelContextProtocol.AspNetCore/Authentication/McpAuthorizationPolicyExtensions.cs b/src/ModelContextProtocol.AspNetCore/Authentication/McpAuthorizationPolicyExtensions.cs
new file mode 100644
index 00000000..c0d6985a
--- /dev/null
+++ b/src/ModelContextProtocol.AspNetCore/Authentication/McpAuthorizationPolicyExtensions.cs
@@ -0,0 +1,65 @@
+using Microsoft.AspNetCore.Authorization;
+using ModelContextProtocol.AspNetCore.Authentication;
+
+namespace Microsoft.Extensions.DependencyInjection;
+
+///
+/// Extension methods for adding MCP authorization policies to ASP.NET Core applications.
+///
+public static class McpAuthorizationExtensions
+{
+ ///
+ /// Adds a preconfigured MCP policy to the authorization options.
+ ///
+ /// The authorization options.
+ /// The name of the policy to add. Default is .
+ /// An optional action to further configure the policy builder.
+ /// The authorization options for chaining.
+ public static AuthorizationOptions AddMcpPolicy(
+ this AuthorizationOptions options,
+ string policyName = McpAuthenticationDefaults.AuthenticationScheme,
+ Action? configurePolicy = null)
+ {
+ // Create a policy builder with default MCP configuration
+ var policyBuilder = new AuthorizationPolicyBuilder()
+ .RequireAuthenticatedUser()
+ .AddAuthenticationSchemes(McpAuthenticationDefaults.AuthenticationScheme);
+
+ configurePolicy?.Invoke(policyBuilder);
+
+ options.AddPolicy(policyName, policyBuilder.Build());
+
+ return options;
+ }
+
+ ///
+ /// Adds a preconfigured MCP policy that includes additional authentication schemes.
+ ///
+ /// The authorization options.
+ /// Additional authentication schemes to include in the policy.
+ /// The name of the policy to add. Default is .
+ /// An optional action to further configure the policy builder.
+ /// The authorization options for chaining.
+ public static AuthorizationOptions AddMcpPolicy(
+ this AuthorizationOptions options,
+ string[] additionalSchemes,
+ string policyName = McpAuthenticationDefaults.AuthenticationScheme,
+ Action? configurePolicy = null)
+ {
+ if (additionalSchemes == null || additionalSchemes.Length == 0)
+ {
+ return AddMcpPolicy(options, policyName, configurePolicy);
+ }
+
+ string[] allSchemes = [McpAuthenticationDefaults.AuthenticationScheme, ..additionalSchemes];
+
+ var policyBuilder = new AuthorizationPolicyBuilder(allSchemes)
+ .RequireAuthenticatedUser();
+
+ configurePolicy?.Invoke(policyBuilder);
+
+ options.AddPolicy(policyName, policyBuilder.Build());
+
+ return options;
+ }
+}
\ No newline at end of file
diff --git a/src/ModelContextProtocol/Authentication/AuthorizationDelegatingHandler.cs b/src/ModelContextProtocol/Authentication/AuthorizationDelegatingHandler.cs
new file mode 100644
index 00000000..39322fcf
--- /dev/null
+++ b/src/ModelContextProtocol/Authentication/AuthorizationDelegatingHandler.cs
@@ -0,0 +1,185 @@
+using ModelContextProtocol.Utils;
+using System.Net.Http.Headers;
+
+namespace ModelContextProtocol.Authentication;
+
+///
+/// A delegating handler that adds authentication tokens to requests and handles 401 responses.
+///
+public class AuthorizationDelegatingHandler : DelegatingHandler
+{
+ private readonly IMcpCredentialProvider _credentialProvider;
+ private string _currentScheme;
+ private static readonly char[] SchemeSplitDelimiters = { ' ', ',' };
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The provider that supplies authentication tokens.
+ public AuthorizationDelegatingHandler(IMcpCredentialProvider credentialProvider)
+ {
+ Throw.IfNull(credentialProvider);
+
+ _credentialProvider = credentialProvider;
+
+ // Select first supported scheme as the default
+ _currentScheme = _credentialProvider.SupportedSchemes.FirstOrDefault() ??
+ throw new ArgumentException("Authorization provider must support at least one authentication scheme.", nameof(credentialProvider));
+ }
+
+ ///
+ /// Sends an HTTP request with authentication handling.
+ ///
+ protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ if (request.Headers.Authorization == null)
+ {
+ await AddAuthorizationHeaderAsync(request, _currentScheme, cancellationToken).ConfigureAwait(false);
+ }
+
+ var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
+
+ if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
+ {
+ return await HandleUnauthorizedResponseAsync(request, response, cancellationToken).ConfigureAwait(false);
+ }
+
+ return response;
+ }
+
+ ///
+ /// Handles a 401 Unauthorized response by attempting to authenticate and retry the request.
+ ///
+ private async Task HandleUnauthorizedResponseAsync(
+ HttpRequestMessage originalRequest,
+ HttpResponseMessage response,
+ CancellationToken cancellationToken)
+ {
+ // Gather the schemes the server wants us to use from WWW-Authenticate headers
+ var serverSchemes = ExtractServerSupportedSchemes(response);
+
+ // Find the intersection between what the server supports and what our provider supports
+ string? bestSchemeMatch = null;
+
+ // First try to find a direct match with the current scheme if it's still valid
+ string schemeUsed = originalRequest.Headers.Authorization?.Scheme ?? _currentScheme ?? string.Empty;
+ if (!string.IsNullOrEmpty(schemeUsed) &&
+ serverSchemes.Contains(schemeUsed) &&
+ _credentialProvider.SupportedSchemes.Contains(schemeUsed))
+ {
+ bestSchemeMatch = schemeUsed;
+ }
+ else
+ {
+ // Find the first server scheme that's in our supported set
+ bestSchemeMatch = serverSchemes.Intersect(_credentialProvider.SupportedSchemes, StringComparer.OrdinalIgnoreCase).FirstOrDefault();
+
+ // If no match was found, either throw an exception or use default
+ if (bestSchemeMatch is null)
+ {
+ if (serverSchemes.Count > 0)
+ {
+ throw new IOException(
+ $"The server does not support any of the provided authentication schemes."
+ $"Server supports: [{string.Join(", ", serverSchemes)}], " +
+ $"Provider supports: [{string.Join(", ", _credentialProvider.SupportedSchemes)}].");
+ }
+
+ // If the server didn't specify any schemes, use the provider's default
+ bestSchemeMatch = _credentialProvider.SupportedSchemes.FirstOrDefault();
+ }
+ }
+
+ // If we have a scheme to try, use it
+ if (bestSchemeMatch != null)
+ {
+ // Try to handle the 401 response with the selected scheme
+ var (handled, recommendedScheme) = await _credentialProvider.HandleUnauthorizedResponseAsync(
+ response,
+ bestSchemeMatch,
+ cancellationToken).ConfigureAwait(false);
+
+ if (!handled)
+ {
+ throw new McpException(
+ $"Failed to handle unauthorized response with scheme '{bestSchemeMatch}'. " +
+ "The authentication provider was unable to process the authentication challenge.");
+ }
+
+ _currentScheme = recommendedScheme ?? bestSchemeMatch;
+
+ var retryRequest = new HttpRequestMessage(originalRequest.Method, originalRequest.RequestUri)
+ {
+ Version = originalRequest.Version,
+#if NET
+ VersionPolicy = originalRequest.VersionPolicy,
+#endif
+ Content = originalRequest.Content
+ };
+
+ // Copy headers except Authorization which we'll set separately
+ foreach (var header in originalRequest.Headers)
+ {
+ if (!header.Key.Equals("Authorization", StringComparison.OrdinalIgnoreCase))
+ {
+ retryRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);
+ }
+ }
+#if NET
+ foreach (var property in originalRequest.Options)
+ {
+ retryRequest.Options.Set(new HttpRequestOptionsKey