feat(backend): Implement MCP Protocol Handler (Story 5.1)
Implemented JSON-RPC 2.0 protocol handler for MCP communication, enabling AI agents to communicate with ColaFlow using the Model Context Protocol. **Implementation:** - JSON-RPC 2.0 data models (Request, Response, Error, ErrorCode) - MCP protocol models (Initialize, Capabilities, ClientInfo, ServerInfo) - McpProtocolHandler with method routing and error handling - Method handlers: initialize, resources/list, tools/list, tools/call - ASP.NET Core middleware for /mcp endpoint - Service registration and dependency injection setup **Testing:** - 28 unit tests covering protocol parsing, validation, and error handling - Integration tests for initialize handshake and error responses - All tests passing with >80% coverage **Changes:** - Created ColaFlow.Modules.Mcp.Contracts project - Created ColaFlow.Modules.Mcp.Domain project - Created ColaFlow.Modules.Mcp.Application project - Created ColaFlow.Modules.Mcp.Infrastructure project - Created ColaFlow.Modules.Mcp.Tests project - Registered MCP module in ColaFlow.API Program.cs - Added /mcp endpoint via middleware **Acceptance Criteria Met:** ✅ JSON-RPC 2.0 messages correctly parsed ✅ Request validation (jsonrpc: "2.0", method, params, id) ✅ Error responses conform to JSON-RPC 2.0 spec ✅ Invalid requests return proper error codes (-32700, -32600, -32601, -32602) ✅ MCP initialize method implemented ✅ Server capabilities returned (resources, tools, prompts) ✅ Protocol version negotiation works (1.0) ✅ Request routing to method handlers ✅ Unit test coverage > 80% ✅ All tests passing **Story**: docs/stories/sprint_5/story_5_1.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
431
docs/stories/sprint_5/story_5_2.md
Normal file
431
docs/stories/sprint_5/story_5_2.md
Normal file
@@ -0,0 +1,431 @@
|
||||
---
|
||||
story_id: story_5_2
|
||||
sprint_id: sprint_5
|
||||
phase: Phase 1 - Foundation
|
||||
status: not_started
|
||||
priority: P0
|
||||
story_points: 5
|
||||
assignee: backend
|
||||
estimated_days: 2
|
||||
created_date: 2025-11-06
|
||||
dependencies: []
|
||||
---
|
||||
|
||||
# Story 5.2: API Key Management System
|
||||
|
||||
**Phase**: Phase 1 - Foundation (Week 1-2)
|
||||
**Priority**: P0 CRITICAL
|
||||
**Estimated Effort**: 5 Story Points (2 days)
|
||||
|
||||
## User Story
|
||||
|
||||
**As a** System Administrator
|
||||
**I want** to securely manage API Keys for AI agents
|
||||
**So that** only authorized AI agents can access ColaFlow through MCP protocol
|
||||
|
||||
## Business Value
|
||||
|
||||
Security is critical for M2. API Key management ensures only authorized AI agents can read/write ColaFlow data. Without this, MCP Server would be vulnerable to unauthorized access.
|
||||
|
||||
**Impact**:
|
||||
- Prevents unauthorized AI access
|
||||
- Enables multi-tenant security isolation
|
||||
- Supports audit trail for all AI operations
|
||||
- Foundation for rate limiting and IP whitelisting
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
### AC1: API Key Generation
|
||||
- [ ] Generate unique 40-character random API Keys
|
||||
- [ ] Format: `cola_` prefix + 36 random characters (e.g., `cola_abc123...xyz`)
|
||||
- [ ] API Keys are cryptographically secure (using `RandomNumberGenerator`)
|
||||
- [ ] No duplicate keys in database (unique constraint)
|
||||
|
||||
### AC2: Secure Storage
|
||||
- [ ] API Key hashed with BCrypt before storage
|
||||
- [ ] Only first 8 characters stored as `key_prefix` for lookup
|
||||
- [ ] Full hash stored in `key_hash` column
|
||||
- [ ] Original API Key never stored (shown once at creation)
|
||||
|
||||
### AC3: API Key Validation
|
||||
- [ ] Middleware validates `Authorization: Bearer <api_key>` header
|
||||
- [ ] API Key lookup by prefix (fast index)
|
||||
- [ ] Hash verification with BCrypt.Verify
|
||||
- [ ] Expired keys rejected (check `expires_at`)
|
||||
- [ ] Revoked keys rejected (check `revoked_at`)
|
||||
- [ ] Update `last_used_at` and `usage_count` on successful validation
|
||||
|
||||
### AC4: Multi-Tenant Isolation
|
||||
- [ ] API Key linked to specific `tenant_id`
|
||||
- [ ] All MCP operations scoped to API Key's tenant
|
||||
- [ ] Cross-tenant access blocked (Global Query Filters)
|
||||
- [ ] TenantContext service extracts tenant from API Key
|
||||
|
||||
### AC5: CRUD API Endpoints
|
||||
- [ ] `POST /api/mcp/keys` - Create API Key (returns plain key once)
|
||||
- [ ] `GET /api/mcp/keys` - List tenant's API Keys (no key_hash)
|
||||
- [ ] `GET /api/mcp/keys/{id}` - Get API Key details
|
||||
- [ ] `PATCH /api/mcp/keys/{id}` - Update name/permissions
|
||||
- [ ] `DELETE /api/mcp/keys/{id}` - Revoke API Key (soft delete)
|
||||
|
||||
### AC6: Testing
|
||||
- [ ] Unit tests for key generation (uniqueness, format)
|
||||
- [ ] Unit tests for BCrypt hashing/verification
|
||||
- [ ] Integration tests for CRUD operations
|
||||
- [ ] Integration tests for multi-tenant isolation
|
||||
- [ ] Integration tests for authentication middleware
|
||||
|
||||
## Technical Design
|
||||
|
||||
### Database Schema
|
||||
|
||||
```sql
|
||||
CREATE TABLE mcp_api_keys (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
|
||||
-- Security
|
||||
key_hash VARCHAR(64) NOT NULL UNIQUE,
|
||||
key_prefix VARCHAR(16) NOT NULL,
|
||||
|
||||
-- Metadata
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
|
||||
-- Permissions (JSONB for flexibility)
|
||||
permissions JSONB NOT NULL DEFAULT '{"read": true, "write": false}',
|
||||
ip_whitelist JSONB,
|
||||
|
||||
-- Status
|
||||
status INT NOT NULL DEFAULT 1, -- 1=Active, 2=Revoked
|
||||
last_used_at TIMESTAMP NULL,
|
||||
usage_count BIGINT NOT NULL DEFAULT 0,
|
||||
|
||||
-- Lifecycle
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
expires_at TIMESTAMP NOT NULL, -- Default: 90 days
|
||||
revoked_at TIMESTAMP NULL,
|
||||
revoked_by UUID REFERENCES users(id),
|
||||
|
||||
-- Indexes
|
||||
INDEX idx_key_prefix (key_prefix),
|
||||
INDEX idx_tenant_user (tenant_id, user_id),
|
||||
INDEX idx_expires_at (expires_at),
|
||||
INDEX idx_status (status)
|
||||
);
|
||||
```
|
||||
|
||||
### Domain Model
|
||||
|
||||
```csharp
|
||||
public class McpApiKey : AggregateRoot
|
||||
{
|
||||
public Guid TenantId { get; private set; }
|
||||
public Guid UserId { get; private set; }
|
||||
|
||||
public string KeyHash { get; private set; }
|
||||
public string KeyPrefix { get; private set; }
|
||||
|
||||
public string Name { get; private set; }
|
||||
public string? Description { get; private set; }
|
||||
|
||||
public ApiKeyPermissions Permissions { get; private set; }
|
||||
public List<string>? IpWhitelist { get; private set; }
|
||||
|
||||
public ApiKeyStatus Status { get; private set; }
|
||||
public DateTime? LastUsedAt { get; private set; }
|
||||
public long UsageCount { get; private set; }
|
||||
|
||||
public DateTime ExpiresAt { get; private set; }
|
||||
public DateTime? RevokedAt { get; private set; }
|
||||
public Guid? RevokedBy { get; private set; }
|
||||
|
||||
// Factory method
|
||||
public static (McpApiKey apiKey, string plainKey) Create(
|
||||
string name,
|
||||
Guid tenantId,
|
||||
Guid userId,
|
||||
ApiKeyPermissions permissions,
|
||||
int expirationDays = 90)
|
||||
{
|
||||
var plainKey = GenerateApiKey();
|
||||
var keyHash = BCrypt.Net.BCrypt.HashPassword(plainKey);
|
||||
var keyPrefix = plainKey.Substring(0, 12); // "cola_abc123..."
|
||||
|
||||
var apiKey = new McpApiKey
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenantId,
|
||||
UserId = userId,
|
||||
KeyHash = keyHash,
|
||||
KeyPrefix = keyPrefix,
|
||||
Name = name,
|
||||
Permissions = permissions,
|
||||
Status = ApiKeyStatus.Active,
|
||||
ExpiresAt = DateTime.UtcNow.AddDays(expirationDays)
|
||||
};
|
||||
|
||||
apiKey.AddDomainEvent(new ApiKeyCreatedEvent(apiKey.Id, name));
|
||||
return (apiKey, plainKey);
|
||||
}
|
||||
|
||||
public void Revoke(Guid revokedBy)
|
||||
{
|
||||
if (Status == ApiKeyStatus.Revoked)
|
||||
throw new InvalidOperationException("API Key already revoked");
|
||||
|
||||
Status = ApiKeyStatus.Revoked;
|
||||
RevokedAt = DateTime.UtcNow;
|
||||
RevokedBy = revokedBy;
|
||||
|
||||
AddDomainEvent(new ApiKeyRevokedEvent(Id, Name));
|
||||
}
|
||||
|
||||
public void RecordUsage()
|
||||
{
|
||||
LastUsedAt = DateTime.UtcNow;
|
||||
UsageCount++;
|
||||
}
|
||||
|
||||
private static string GenerateApiKey()
|
||||
{
|
||||
var bytes = new byte[27]; // 36 chars in base64
|
||||
using var rng = RandomNumberGenerator.Create();
|
||||
rng.GetBytes(bytes);
|
||||
return "cola_" + Convert.ToBase64String(bytes)
|
||||
.Replace("+", "").Replace("/", "").Replace("=", "")
|
||||
.Substring(0, 36);
|
||||
}
|
||||
}
|
||||
|
||||
public class ApiKeyPermissions
|
||||
{
|
||||
public bool Read { get; set; } = true;
|
||||
public bool Write { get; set; } = false;
|
||||
public List<string> AllowedResources { get; set; } = new();
|
||||
public List<string> AllowedTools { get; set; } = new();
|
||||
}
|
||||
|
||||
public enum ApiKeyStatus
|
||||
{
|
||||
Active = 1,
|
||||
Revoked = 2
|
||||
}
|
||||
```
|
||||
|
||||
### Authentication Middleware
|
||||
|
||||
```csharp
|
||||
public class McpApiKeyMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
|
||||
public async Task InvokeAsync(
|
||||
HttpContext context,
|
||||
IMcpApiKeyService apiKeyService)
|
||||
{
|
||||
// Only apply to /mcp endpoints
|
||||
if (!context.Request.Path.StartsWithSegments("/mcp"))
|
||||
{
|
||||
await _next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract API Key from Authorization header
|
||||
var apiKey = ExtractApiKey(context.Request.Headers);
|
||||
if (string.IsNullOrEmpty(apiKey))
|
||||
{
|
||||
context.Response.StatusCode = 401;
|
||||
await context.Response.WriteAsJsonAsync(new McpError
|
||||
{
|
||||
Code = McpErrorCode.Unauthorized,
|
||||
Message = "Missing API Key"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate API Key
|
||||
var validationResult = await apiKeyService.ValidateAsync(apiKey);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
context.Response.StatusCode = 401;
|
||||
await context.Response.WriteAsJsonAsync(new McpError
|
||||
{
|
||||
Code = McpErrorCode.Unauthorized,
|
||||
Message = validationResult.ErrorMessage
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Set TenantContext for downstream handlers
|
||||
context.Items["TenantId"] = validationResult.TenantId;
|
||||
context.Items["ApiKeyId"] = validationResult.ApiKeyId;
|
||||
context.Items["ApiKeyPermissions"] = validationResult.Permissions;
|
||||
|
||||
await _next(context);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Tasks
|
||||
|
||||
### Task 1: Domain Layer - McpApiKey Aggregate (4 hours)
|
||||
- [ ] Create `McpApiKey` entity class
|
||||
- [ ] Create `ApiKeyPermissions` value object
|
||||
- [ ] Implement `Create()` factory method with key generation
|
||||
- [ ] Implement `Revoke()` method
|
||||
- [ ] Create domain events (ApiKeyCreated, ApiKeyRevoked)
|
||||
|
||||
**Files to Create**:
|
||||
- `ColaFlow.Modules.Mcp.Domain/Entities/McpApiKey.cs`
|
||||
- `ColaFlow.Modules.Mcp.Domain/ValueObjects/ApiKeyPermissions.cs`
|
||||
- `ColaFlow.Modules.Mcp.Domain/Events/ApiKeyCreatedEvent.cs`
|
||||
- `ColaFlow.Modules.Mcp.Domain/Events/ApiKeyRevokedEvent.cs`
|
||||
|
||||
### Task 2: Infrastructure - Repository & Database (4 hours)
|
||||
- [ ] Create `IMcpApiKeyRepository` interface
|
||||
- [ ] Implement `McpApiKeyRepository` (EF Core)
|
||||
- [ ] Create database migration for `mcp_api_keys` table
|
||||
- [ ] Configure EF Core entity (fluent API)
|
||||
- [ ] Add indexes (key_prefix, tenant_id)
|
||||
|
||||
**Files to Create**:
|
||||
- `ColaFlow.Modules.Mcp.Domain/Repositories/IMcpApiKeyRepository.cs`
|
||||
- `ColaFlow.Modules.Mcp.Infrastructure/Repositories/McpApiKeyRepository.cs`
|
||||
- `ColaFlow.Modules.Mcp.Infrastructure/EntityConfigurations/McpApiKeyConfiguration.cs`
|
||||
- `ColaFlow.Modules.Mcp.Infrastructure/Migrations/YYYYMMDDHHMMSS_AddMcpApiKeys.cs`
|
||||
|
||||
### Task 3: Application Layer - API Key Service (4 hours)
|
||||
- [ ] Create `IMcpApiKeyService` interface
|
||||
- [ ] Implement `CreateApiKeyAsync()` method
|
||||
- [ ] Implement `ValidateAsync()` method (BCrypt verification)
|
||||
- [ ] Implement `RevokeApiKeyAsync()` method
|
||||
- [ ] Implement `GetApiKeysAsync()` query
|
||||
|
||||
**Files to Create**:
|
||||
- `ColaFlow.Modules.Mcp.Application/Contracts/IMcpApiKeyService.cs`
|
||||
- `ColaFlow.Modules.Mcp.Application/Services/McpApiKeyService.cs`
|
||||
|
||||
### Task 4: Authentication Middleware (3 hours)
|
||||
- [ ] Create `McpApiKeyMiddleware` class
|
||||
- [ ] Extract API Key from `Authorization: Bearer` header
|
||||
- [ ] Validate API Key (call `IMcpApiKeyService.ValidateAsync`)
|
||||
- [ ] Set TenantContext in HttpContext.Items
|
||||
- [ ] Return 401 for invalid/expired keys
|
||||
|
||||
**Files to Create**:
|
||||
- `ColaFlow.Modules.Mcp/Middleware/McpApiKeyMiddleware.cs`
|
||||
- `ColaFlow.Modules.Mcp/Extensions/McpMiddlewareExtensions.cs`
|
||||
|
||||
### Task 5: REST API Endpoints (3 hours)
|
||||
- [ ] Create `McpKeysController`
|
||||
- [ ] `POST /api/mcp/keys` - Create API Key
|
||||
- [ ] `GET /api/mcp/keys` - List API Keys
|
||||
- [ ] `GET /api/mcp/keys/{id}` - Get API Key details
|
||||
- [ ] `DELETE /api/mcp/keys/{id}` - Revoke API Key
|
||||
- [ ] Add [Authorize] attribute (require JWT)
|
||||
|
||||
**Files to Create**:
|
||||
- `ColaFlow.Modules.Mcp/Controllers/McpKeysController.cs`
|
||||
- `ColaFlow.Modules.Mcp/DTOs/CreateApiKeyRequest.cs`
|
||||
- `ColaFlow.Modules.Mcp/DTOs/ApiKeyResponse.cs`
|
||||
|
||||
### Task 6: Unit Tests (3 hours)
|
||||
- [ ] Test API Key generation (format, uniqueness)
|
||||
- [ ] Test BCrypt hashing/verification
|
||||
- [ ] Test McpApiKey domain logic (Create, Revoke)
|
||||
- [ ] Test validation logic (expired, revoked)
|
||||
|
||||
**Files to Create**:
|
||||
- `ColaFlow.Modules.Mcp.Tests/Domain/McpApiKeyTests.cs`
|
||||
- `ColaFlow.Modules.Mcp.Tests/Services/McpApiKeyServiceTests.cs`
|
||||
|
||||
### Task 7: Integration Tests (3 hours)
|
||||
- [ ] Test CRUD API endpoints
|
||||
- [ ] Test multi-tenant isolation (Tenant A cannot see Tenant B keys)
|
||||
- [ ] Test authentication middleware (valid/invalid keys)
|
||||
- [ ] Test expired key rejection
|
||||
|
||||
**Files to Create**:
|
||||
- `ColaFlow.Modules.Mcp.Tests/Integration/McpApiKeyIntegrationTests.cs`
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests (Target: > 80% coverage)
|
||||
- API Key generation logic
|
||||
- BCrypt hashing/verification
|
||||
- Domain entity methods (Create, Revoke)
|
||||
- Validation logic (expiration, revocation)
|
||||
|
||||
### Integration Tests
|
||||
- CRUD operations end-to-end
|
||||
- Authentication middleware flow
|
||||
- Multi-tenant isolation verification
|
||||
- Database queries (repository)
|
||||
|
||||
### Manual Testing Checklist
|
||||
- [ ] Create API Key → returns plain key once
|
||||
- [ ] Use API Key in Authorization header → success
|
||||
- [ ] Use expired API Key → 401 Unauthorized
|
||||
- [ ] Use revoked API Key → 401 Unauthorized
|
||||
- [ ] Tenant A cannot see Tenant B's keys
|
||||
- [ ] API Key usage count increments
|
||||
|
||||
## Dependencies
|
||||
|
||||
**Prerequisites**:
|
||||
- .NET 9 ASP.NET Core
|
||||
- BCrypt.Net NuGet package
|
||||
- EF Core 9
|
||||
- PostgreSQL database
|
||||
|
||||
**Blocks**:
|
||||
- All MCP operations (Story 5.5, 5.11) - Need API Key authentication
|
||||
|
||||
## Risks & Mitigation
|
||||
|
||||
| Risk | Impact | Probability | Mitigation |
|
||||
|------|--------|-------------|------------|
|
||||
| API Key brute force | High | Low | BCrypt hashing (slow), rate limiting |
|
||||
| Key prefix collision | Medium | Very Low | 12-char prefix, crypto RNG |
|
||||
| BCrypt performance | Medium | Low | Prefix lookup optimization |
|
||||
| Key storage security | High | Low | Hash only, audit access |
|
||||
|
||||
## Definition of Done
|
||||
|
||||
- [ ] Code compiles without warnings
|
||||
- [ ] All unit tests passing (> 80% coverage)
|
||||
- [ ] All integration tests passing
|
||||
- [ ] Code reviewed and approved
|
||||
- [ ] Database migration created and tested
|
||||
- [ ] XML documentation for public APIs
|
||||
- [ ] Authentication middleware works end-to-end
|
||||
- [ ] Multi-tenant isolation verified (100%)
|
||||
- [ ] API Key never logged in plain text
|
||||
|
||||
## Notes
|
||||
|
||||
### Why This Story Matters
|
||||
- **Security Foundation**: All MCP operations depend on secure authentication
|
||||
- **Multi-Tenant Isolation**: Prevents cross-tenant data access
|
||||
- **Audit Trail**: Tracks which AI agent performed which operation
|
||||
- **Compliance**: Supports GDPR, SOC2 requirements
|
||||
|
||||
### Key Design Decisions
|
||||
1. **BCrypt Hashing**: Slow by design, prevents brute force
|
||||
2. **Prefix Lookup**: Fast lookup without full hash scan
|
||||
3. **JSONB Permissions**: Flexible permission model for future
|
||||
4. **Soft Delete**: Revocation preserves audit trail
|
||||
|
||||
### Security Best Practices
|
||||
- API Key shown only once at creation
|
||||
- BCrypt with default cost factor (10)
|
||||
- 90-day expiration by default
|
||||
- Rate limiting (to be added in Phase 6)
|
||||
- IP whitelisting (to be added in Phase 4)
|
||||
|
||||
### Reference Materials
|
||||
- Sprint 5 Plan: `docs/plans/sprint_5.md`
|
||||
- Architecture Design: `docs/M2-MCP-SERVER-ARCHITECTURE.md`
|
||||
- BCrypt.Net Documentation: https://github.com/BcryptNet/bcrypt.net
|
||||
Reference in New Issue
Block a user