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