feat(backend): Add API Key authentication to /mcp-sdk endpoint
This commit adds API Key authentication support for the Microsoft MCP SDK endpoint at /mcp-sdk, ensuring secure access control. Changes: - Fix ApiKeyPermissions deserialization bug by making constructor public - Create McpApiKeyAuthenticationHandler for ASP.NET Core authentication - Add AddMcpApiKeyAuthentication extension method for scheme registration - Configure RequireMcpApiKey authorization policy in Program.cs - Apply authentication to /mcp-sdk endpoint with RequireAuthorization() The authentication validates API keys from Authorization header (Bearer token), sets user context (TenantId, UserId, Permissions), and returns 401 JSON-RPC error on failure. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -128,7 +128,8 @@ builder.Services.AddAuthentication(options =>
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
};
|
||||
});
|
||||
})
|
||||
.AddMcpApiKeyAuthentication(); // Add MCP API Key authentication scheme
|
||||
|
||||
// Configure Authorization Policies for RBAC
|
||||
builder.Services.AddAuthorization(options =>
|
||||
@@ -153,6 +154,11 @@ builder.Services.AddAuthorization(options =>
|
||||
// AI Agent only (for MCP integration testing)
|
||||
options.AddPolicy("RequireAIAgent", policy =>
|
||||
policy.RequireRole("AIAgent"));
|
||||
|
||||
// MCP API Key authentication policy (for /mcp-sdk endpoint)
|
||||
options.AddPolicy("RequireMcpApiKey", policy =>
|
||||
policy.AddAuthenticationSchemes(ColaFlow.Modules.Mcp.Infrastructure.Authentication.McpApiKeyAuthenticationOptions.DefaultScheme)
|
||||
.RequireAuthenticatedUser());
|
||||
});
|
||||
|
||||
// Configure CORS for frontend (SignalR requires AllowCredentials)
|
||||
@@ -238,9 +244,10 @@ app.MapHub<NotificationHub>("/hubs/notification");
|
||||
app.MapHub<McpNotificationHub>("/hubs/mcp-notifications");
|
||||
|
||||
// ============================================
|
||||
// Map MCP SDK Endpoint
|
||||
// Map MCP SDK Endpoint with API Key Authentication
|
||||
// ============================================
|
||||
app.MapMcp("/mcp-sdk"); // Official SDK endpoint at /mcp-sdk
|
||||
app.MapMcp("/mcp-sdk")
|
||||
.RequireAuthorization("RequireMcpApiKey"); // Require MCP API Key authentication
|
||||
// Note: Legacy /mcp endpoint still handled by UseMcpMiddleware() above
|
||||
|
||||
// ============================================
|
||||
|
||||
@@ -28,9 +28,9 @@ public sealed class ApiKeyPermissions
|
||||
public List<string> AllowedTools { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Private constructor for EF Core
|
||||
/// Parameterless constructor for EF Core and JSON deserialization
|
||||
/// </summary>
|
||||
private ApiKeyPermissions()
|
||||
public ApiKeyPermissions()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using ColaFlow.Modules.Mcp.Application.Services;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace ColaFlow.Modules.Mcp.Infrastructure.Authentication;
|
||||
|
||||
/// <summary>
|
||||
/// Authentication handler for MCP API Key authentication.
|
||||
/// This handler validates API keys in the Authorization header
|
||||
/// and creates claims for the authenticated user.
|
||||
/// </summary>
|
||||
public class McpApiKeyAuthenticationHandler : AuthenticationHandler<McpApiKeyAuthenticationOptions>
|
||||
{
|
||||
private readonly IMcpApiKeyService _apiKeyService;
|
||||
|
||||
public McpApiKeyAuthenticationHandler(
|
||||
IOptionsMonitor<McpApiKeyAuthenticationOptions> options,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder,
|
||||
IMcpApiKeyService apiKeyService)
|
||||
: base(options, logger, encoder)
|
||||
{
|
||||
_apiKeyService = apiKeyService;
|
||||
}
|
||||
|
||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
// Extract API Key from Authorization header
|
||||
var apiKey = ExtractApiKey();
|
||||
if (string.IsNullOrEmpty(apiKey))
|
||||
{
|
||||
return AuthenticateResult.Fail("Missing API Key. Please provide Authorization: Bearer <api_key> header.");
|
||||
}
|
||||
|
||||
// Get client IP address
|
||||
var ipAddress = Context.Connection.RemoteIpAddress?.ToString();
|
||||
|
||||
// Validate API Key
|
||||
var validationResult = await _apiKeyService.ValidateAsync(apiKey, ipAddress, Context.RequestAborted);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
Logger.LogWarning("MCP SDK request rejected - Invalid API Key: {ErrorMessage}", validationResult.ErrorMessage);
|
||||
return AuthenticateResult.Fail(validationResult.ErrorMessage ?? "Invalid API Key");
|
||||
}
|
||||
|
||||
// Create claims from validation result
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(ClaimTypes.NameIdentifier, validationResult.UserId.ToString()),
|
||||
new("ApiKeyId", validationResult.ApiKeyId.ToString()),
|
||||
new("TenantId", validationResult.TenantId.ToString()),
|
||||
new("UserId", validationResult.UserId.ToString()),
|
||||
};
|
||||
|
||||
// Add permission claims
|
||||
if (validationResult.Permissions != null)
|
||||
{
|
||||
claims.Add(new Claim("McpPermissions:Read", validationResult.Permissions.Read.ToString()));
|
||||
claims.Add(new Claim("McpPermissions:Write", validationResult.Permissions.Write.ToString()));
|
||||
}
|
||||
|
||||
var identity = new ClaimsIdentity(claims, Scheme.Name);
|
||||
var principal = new ClaimsPrincipal(identity);
|
||||
var ticket = new AuthenticationTicket(principal, Scheme.Name);
|
||||
|
||||
// Store validation result in HttpContext.Items for downstream use
|
||||
Context.Items["McpAuthType"] = "ApiKey";
|
||||
Context.Items["McpApiKeyId"] = validationResult.ApiKeyId;
|
||||
Context.Items["McpTenantId"] = validationResult.TenantId;
|
||||
Context.Items["McpUserId"] = validationResult.UserId;
|
||||
Context.Items["McpPermissions"] = validationResult.Permissions;
|
||||
|
||||
Logger.LogDebug("MCP SDK request authenticated - ApiKey: {ApiKeyId}, Tenant: {TenantId}, User: {UserId}",
|
||||
validationResult.ApiKeyId, validationResult.TenantId, validationResult.UserId);
|
||||
|
||||
return AuthenticateResult.Success(ticket);
|
||||
}
|
||||
|
||||
protected override Task HandleChallengeAsync(AuthenticationProperties properties)
|
||||
{
|
||||
Response.StatusCode = 401;
|
||||
Response.ContentType = "application/json";
|
||||
|
||||
var errorResponse = new
|
||||
{
|
||||
jsonrpc = "2.0",
|
||||
error = new
|
||||
{
|
||||
code = -32001, // Custom error code for authentication failure
|
||||
message = "Unauthorized",
|
||||
data = new { details = "Missing or invalid API Key. Please provide Authorization: Bearer <api_key> header." }
|
||||
},
|
||||
id = (object?)null
|
||||
};
|
||||
|
||||
var json = System.Text.Json.JsonSerializer.Serialize(errorResponse, new System.Text.Json.JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
|
||||
});
|
||||
|
||||
return Response.WriteAsync(json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract API Key from Authorization header
|
||||
/// Supports: Authorization: Bearer <api_key>
|
||||
/// </summary>
|
||||
private string? ExtractApiKey()
|
||||
{
|
||||
if (!Request.Headers.TryGetValue("Authorization", out var authHeader))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var authHeaderValue = authHeader.ToString();
|
||||
if (string.IsNullOrWhiteSpace(authHeaderValue))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Support "Bearer <api_key>" format
|
||||
const string bearerPrefix = "Bearer ";
|
||||
if (authHeaderValue.StartsWith(bearerPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return authHeaderValue.Substring(bearerPrefix.Length).Trim();
|
||||
}
|
||||
|
||||
// Also support direct API key without "Bearer " prefix (for compatibility)
|
||||
return authHeaderValue.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for MCP API Key authentication
|
||||
/// </summary>
|
||||
public class McpApiKeyAuthenticationOptions : AuthenticationSchemeOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// The authentication scheme name
|
||||
/// </summary>
|
||||
public const string DefaultScheme = "McpApiKey";
|
||||
}
|
||||
@@ -3,11 +3,13 @@ using ColaFlow.Modules.Mcp.Application.Resources;
|
||||
using ColaFlow.Modules.Mcp.Application.Services;
|
||||
using ColaFlow.Modules.Mcp.Contracts.Resources;
|
||||
using ColaFlow.Modules.Mcp.Domain.Repositories;
|
||||
using ColaFlow.Modules.Mcp.Infrastructure.Authentication;
|
||||
using ColaFlow.Modules.Mcp.Infrastructure.BackgroundServices;
|
||||
using ColaFlow.Modules.Mcp.Infrastructure.Middleware;
|
||||
using ColaFlow.Modules.Mcp.Infrastructure.Persistence;
|
||||
using ColaFlow.Modules.Mcp.Infrastructure.Persistence.Repositories;
|
||||
using ColaFlow.Modules.Mcp.Infrastructure.Services;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
@@ -79,6 +81,19 @@ public static class McpServiceExtensions
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds MCP API Key authentication scheme to the authentication builder.
|
||||
/// This enables the /mcp-sdk endpoint to use API Key authentication.
|
||||
/// </summary>
|
||||
public static AuthenticationBuilder AddMcpApiKeyAuthentication(
|
||||
this AuthenticationBuilder builder,
|
||||
Action<McpApiKeyAuthenticationOptions>? configureOptions = null)
|
||||
{
|
||||
return builder.AddScheme<McpApiKeyAuthenticationOptions, McpApiKeyAuthenticationHandler>(
|
||||
McpApiKeyAuthenticationOptions.DefaultScheme,
|
||||
configureOptions ?? (_ => { }));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds MCP middleware to the application pipeline
|
||||
/// IMPORTANT: Middleware order matters - must be in this sequence:
|
||||
|
||||
Reference in New Issue
Block a user