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:
Yaojia Wang
2025-11-23 15:14:09 +01:00
parent 34a379750f
commit b38a9d16fa
4 changed files with 174 additions and 5 deletions

View File

@@ -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
// ============================================

View File

@@ -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()
{
}

View File

@@ -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";
}

View File

@@ -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: