Skip to content

[Phase 2] API key authentication for server endpoints #324

Description

@frankbria

Overview

Implement API key authentication for server endpoints. This provides a simpler alternative to JWT tokens for programmatic API access while maintaining security.

Background

The current server uses JWT-based authentication via FastAPI Users. For Phase 2, we need to support:

  1. API keys for programmatic/automation access (CI/CD, scripts, integrations)
  2. JWT tokens retained for web UI sessions

API keys are preferred for server-to-server communication because:

  • No expiration/refresh complexity
  • Easier to rotate
  • Can be scoped to specific permissions
  • Works better with webhooks and integrations

Deliverables

1. API Key Model

class APIKey(BaseModel):
    id: str
    name: str  # Human-readable name (e.g., "CI Pipeline", "Slack Bot")
    key_hash: str  # bcrypt hash of the actual key
    prefix: str  # First 8 chars for identification (e.g., "cf_live_")
    scopes: list[str]  # ["read", "write", "admin"]
    created_at: datetime
    last_used_at: datetime | None
    expires_at: datetime | None  # Optional expiration
    is_active: bool

2. Key Generation

# Format: cf_{environment}_{random_32_chars}
# Examples:
#   cf_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
#   cf_test_x9y8z7w6v5u4t3s2r1q0p9o8n7m6l5k4

def generate_api_key(environment: str = "live") -> tuple[str, str]:
    """Returns (full_key, key_hash) - full_key shown once, hash stored."""
    random_part = secrets.token_hex(16)
    full_key = f"cf_{environment}_{random_part}"
    key_hash = bcrypt.hash(full_key)
    return full_key, key_hash

3. Authentication Middleware

from fastapi import Security, HTTPException
from fastapi.security import APIKeyHeader

api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)

async def get_api_key(
    api_key: str = Security(api_key_header),
) -> APIKey | None:
    if not api_key:
        return None
    
    # Look up by prefix for efficiency
    prefix = api_key[:12]  # "cf_live_" + first 4 of random
    key_record = await db.get_api_key_by_prefix(prefix)
    
    if key_record and bcrypt.verify(api_key, key_record.key_hash):
        await db.update_last_used(key_record.id)
        return key_record
    
    return None

async def require_auth(
    api_key: APIKey | None = Depends(get_api_key),
    jwt_user: User | None = Depends(get_current_user_optional),
):
    """Accept either API key or JWT token."""
    if api_key:
        return {"type": "api_key", "principal": api_key}
    if jwt_user:
        return {"type": "jwt", "principal": jwt_user}
    raise HTTPException(status_code=401, detail="Authentication required")

4. CLI Commands

# Create API key
cf auth api-key create --name "CI Pipeline" --scopes read,write
# Output: Your API key (shown once): cf_live_a1b2c3d4...

# List API keys
cf auth api-key list
# Output: 
#   ID          NAME           SCOPES        LAST USED
#   cf_live_a1  CI Pipeline    read,write    2 hours ago
#   cf_live_x9  Slack Bot      read          Never

# Revoke API key
cf auth api-key revoke cf_live_a1

# Rotate API key (revoke old, create new with same name/scopes)
cf auth api-key rotate cf_live_a1

5. Server Endpoints

@router.post("/api/auth/api-keys")
async def create_api_key(name: str, scopes: list[str], auth = Depends(require_auth)):
    """Create a new API key (requires admin or JWT auth)."""

@router.get("/api/auth/api-keys")
async def list_api_keys(auth = Depends(require_auth)):
    """List API keys (hashes not included)."""

@router.delete("/api/auth/api-keys/{key_id}")
async def revoke_api_key(key_id: str, auth = Depends(require_auth)):
    """Revoke an API key."""

6. Scope Definitions

Scope Permissions
read List tasks, view status, stream output
write Create tasks, answer blockers, execute
admin Manage API keys, delete projects

Acceptance Criteria

  • API keys can be created, listed, and revoked via CLI
  • API keys authenticate server requests via X-API-Key header
  • API keys support scope-based permissions
  • API keys work alongside existing JWT authentication
  • Keys are stored as bcrypt hashes (never plaintext)
  • Last-used timestamp tracked for auditing
  • Documentation for API key usage

Security Considerations

Related Issues

References

  • docs/authentication.md - Current auth documentation
  • Stripe API keys as reference design

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestphase-2Phase 2: Server Layer as Thin Adaptersecurity

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions