diff --git a/colaflow-api/src/ColaFlow.API/Program.cs b/colaflow-api/src/ColaFlow.API/Program.cs index 8c8303a..018a6b8 100644 --- a/colaflow-api/src/ColaFlow.API/Program.cs +++ b/colaflow-api/src/ColaFlow.API/Program.cs @@ -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("/hubs/notification"); app.MapHub("/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 // ============================================ diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Domain/ValueObjects/ApiKeyPermissions.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Domain/ValueObjects/ApiKeyPermissions.cs index c67f92b..2f22fba 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Domain/ValueObjects/ApiKeyPermissions.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Domain/ValueObjects/ApiKeyPermissions.cs @@ -28,9 +28,9 @@ public sealed class ApiKeyPermissions public List AllowedTools { get; set; } = new(); /// - /// Private constructor for EF Core + /// Parameterless constructor for EF Core and JSON deserialization /// - private ApiKeyPermissions() + public ApiKeyPermissions() { } diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Authentication/McpApiKeyAuthenticationHandler.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Authentication/McpApiKeyAuthenticationHandler.cs new file mode 100644 index 0000000..00a2b8f --- /dev/null +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Authentication/McpApiKeyAuthenticationHandler.cs @@ -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; + +/// +/// Authentication handler for MCP API Key authentication. +/// This handler validates API keys in the Authorization header +/// and creates claims for the authenticated user. +/// +public class McpApiKeyAuthenticationHandler : AuthenticationHandler +{ + private readonly IMcpApiKeyService _apiKeyService; + + public McpApiKeyAuthenticationHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + IMcpApiKeyService apiKeyService) + : base(options, logger, encoder) + { + _apiKeyService = apiKeyService; + } + + protected override async Task HandleAuthenticateAsync() + { + // Extract API Key from Authorization header + var apiKey = ExtractApiKey(); + if (string.IsNullOrEmpty(apiKey)) + { + return AuthenticateResult.Fail("Missing API Key. Please provide Authorization: Bearer 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 + { + 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 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); + } + + /// + /// Extract API Key from Authorization header + /// Supports: Authorization: Bearer + /// + 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 " 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(); + } +} + +/// +/// Options for MCP API Key authentication +/// +public class McpApiKeyAuthenticationOptions : AuthenticationSchemeOptions +{ + /// + /// The authentication scheme name + /// + public const string DefaultScheme = "McpApiKey"; +} diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Extensions/McpServiceExtensions.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Extensions/McpServiceExtensions.cs index 4abc090..ee86d06 100644 --- a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Extensions/McpServiceExtensions.cs +++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Infrastructure/Extensions/McpServiceExtensions.cs @@ -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; } + /// + /// Adds MCP API Key authentication scheme to the authentication builder. + /// This enables the /mcp-sdk endpoint to use API Key authentication. + /// + public static AuthenticationBuilder AddMcpApiKeyAuthentication( + this AuthenticationBuilder builder, + Action? configureOptions = null) + { + return builder.AddScheme( + McpApiKeyAuthenticationOptions.DefaultScheme, + configureOptions ?? (_ => { })); + } + /// /// Adds MCP middleware to the application pipeline /// IMPORTANT: Middleware order matters - must be in this sequence: