From d867d787c1cd0347fd08a6b403dfec8e444d2231 Mon Sep 17 00:00:00 2001 From: elmorem Date: Thu, 11 Dec 2025 14:29:48 -0800 Subject: [PATCH] feat: Add authentication middleware and integration documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Provide authentication middleware for automatic enforcement, configuration examples, and comprehensive integration guide for enabling auth in services. Changes: - Add AuthenticationMiddleware for automatic auth enforcement - Create generate_auth_config.py utility script - Add .env.auth.example configuration template - Add docs/AUTHENTICATION.md integration guide Authentication Middleware (shared/auth/middleware.py): - Automatic JWT and API key validation - Configurable exempt paths (health, docs, metrics) - Attaches UserIdentity to request.state.user - Returns 401 for missing/invalid credentials - Easy integration with FastAPI applications Configuration Generator (scripts/generate_auth_config.py): - Generate secure JWT secrets (32-byte entropy) - Generate example API keys - Provides setup instructions - Helps developers get started quickly Environment Template (.env.auth.example): - All authentication settings documented - Clear descriptions and defaults - Security best practices included - Example values for development Integration Guide (docs/AUTHENTICATION.md): - Quick start instructions - Two integration options (middleware vs dependencies) - JWT token creation examples - API key management guide - Permission system explanation - Client authentication examples - Service integration patterns - Security best practices - Troubleshooting guide Middleware Usage: ```python from shared.auth.middleware import AuthenticationMiddleware app.add_middleware( AuthenticationMiddleware, jwt_handler=jwt_handler, api_key_handler=api_key_handler, exempt_paths=["/health", "/docs"], ) ``` Benefits: - Zero-touch authentication for all routes - Automatic 401 responses - User identity in request.state - Flexible exempt paths - Works with both JWT and API keys - Optional per-service Security: - Validates JWT signatures - Checks token expiration - Verifies API key hashes - Prevents unauthorized access - Logs authentication failures 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .env.auth.example | 53 +++++ docs/AUTHENTICATION.md | 359 ++++++++++++++++++++++++++++++++ scripts/generate_auth_config.py | 72 +++++++ shared/auth/middleware.py | 130 ++++++++++++ 4 files changed, 614 insertions(+) create mode 100644 .env.auth.example create mode 100644 docs/AUTHENTICATION.md create mode 100755 scripts/generate_auth_config.py create mode 100644 shared/auth/middleware.py diff --git a/.env.auth.example b/.env.auth.example new file mode 100644 index 0000000..613b4d4 --- /dev/null +++ b/.env.auth.example @@ -0,0 +1,53 @@ +# Authentication Configuration Example +# +# Copy this file to .env and configure for your environment +# These settings control JWT and API key authentication + +# ======================================== +# JWT Configuration +# ======================================== + +# REQUIRED: Secret key for signing JWT tokens +# Generate with: python -c "import secrets; print(secrets.token_urlsafe(32))" +AUTH_JWT_SECRET_KEY=your-super-secret-key-change-this-in-production + +# JWT algorithm (default: HS256) +AUTH_JWT_ALGORITHM=HS256 + +# JWT token expiration in minutes (default: 60) +AUTH_JWT_ACCESS_TOKEN_EXPIRE_MINUTES=60 + +# JWT issuer (default: contextiq) +AUTH_JWT_ISSUER=contextiq + +# ======================================== +# API Key Configuration +# ======================================== + +# Enable API key authentication (default: true) +AUTH_API_KEY_ENABLED=true + +# ======================================== +# Authentication Enforcement +# ======================================== + +# Require authentication for all endpoints (default: true) +# Set to false to disable authentication globally +AUTH_REQUIRE_AUTH=true + +# Comma-separated list of paths exempt from authentication +# These paths will be accessible without authentication +AUTH_REQUIRE_AUTH_EXCEPTIONS=/health,/health/live,/health/ready,/docs,/redoc,/openapi.json,/metrics + +# ======================================== +# Example API Keys +# ======================================== + +# In production, store API keys in a secure database +# This is just for development/testing + +# Example API key (generated): ck_xxxxx... +# To generate: python -c "from shared.auth.api_key import APIKeyHandler; print(APIKeyHandler().generate_api_key())" + +# Example user_id for API key: user_123 +# Example org_id for API key: org_456 diff --git a/docs/AUTHENTICATION.md b/docs/AUTHENTICATION.md new file mode 100644 index 0000000..c6407cd --- /dev/null +++ b/docs/AUTHENTICATION.md @@ -0,0 +1,359 @@ +# Authentication Integration Guide + +This guide explains how to integrate authentication into ContextIQ services. + +## Overview + +ContextIQ provides a flexible authentication system supporting: +- **JWT Bearer Tokens**: For user-based authentication +- **API Keys**: For service-to-service and programmatic access +- **Optional Enforcement**: Can be enabled/disabled per service + +## Quick Start + +### 1. Generate Configuration + +```bash +# Generate JWT secrets and API keys +python scripts/generate_auth_config.py + +# Copy example configuration +cp .env.auth.example .env +``` + +### 2. Configure Environment + +Edit `.env` and set your JWT secret: + +```bash +AUTH_JWT_SECRET_KEY=your-generated-secret-here +AUTH_REQUIRE_AUTH=true # or false to disable +``` + +### 3. Enable Authentication (Optional) + +Authentication can be enabled in two ways: + +#### Option A: Using Middleware (Recommended) + +Add authentication middleware to your FastAPI application: + +```python +from shared.auth.middleware import AuthenticationMiddleware +from shared.auth.jwt import JWTHandler +from shared.auth.api_key import APIKeyHandler +from shared.auth.config import AuthSettings + +# Load settings +auth_settings = AuthSettings() + +# Initialize handlers +jwt_handler = JWTHandler(secret_key=auth_settings.jwt_secret_key) +api_key_handler = APIKeyHandler() + +# Add middleware +app.add_middleware( + AuthenticationMiddleware, + jwt_handler=jwt_handler, + api_key_handler=api_key_handler, + exempt_paths=["/health", "/docs", "/metrics"], +) +``` + +#### Option B: Using Dependencies + +Require authentication per-endpoint: + +```python +from typing import Annotated +from fastapi import Depends +from shared.auth.dependencies import get_current_user, require_permissions +from shared.auth.models import UserIdentity, Permission + +# Override default dependencies +app.dependency_overrides[get_jwt_handler] = lambda: jwt_handler +app.dependency_overrides[get_api_key_handler] = lambda: api_key_handler + +# Require authentication +@app.get("/sessions") +async def list_sessions( + user: Annotated[UserIdentity, Depends(get_current_user)], +): + return {"user_id": user.user_id} + +# Require specific permissions +@app.post("/sessions") +async def create_session( + user: Annotated[UserIdentity, Depends(require_permissions(Permission.SESSION_CREATE))], +): + return {"created_by": user.user_id} +``` + +## Creating JWT Tokens + +### For Users + +```python +from shared.auth.jwt import JWTHandler +from shared.auth.models import Permission + +jwt_handler = JWTHandler(secret_key="your-secret") + +# Create token for user +token = jwt_handler.create_access_token( + user_id="user_123", + org_id="org_456", + email="user@example.com", + name="John Doe", + permissions=[ + Permission.SESSION_CREATE, + Permission.SESSION_READ, + Permission.MEMORY_READ, + ], +) + +print(f"Token: {token}") +# Use with: curl -H "Authorization: Bearer {token}" http://localhost:8000/... +``` + +### For Services + +```python +# Create service token (no expiration, full permissions) +from datetime import timedelta + +service_token = jwt_handler.create_access_token( + user_id="service_memory_worker", + org_id=None, + permissions=[ + Permission.MEMORY_CREATE, + Permission.MEMORY_READ, + Permission.SESSION_READ, + ], + expires_delta=timedelta(days=365), # Long-lived for services +) +``` + +## Managing API Keys + +### Generate Keys + +```python +from shared.auth.api_key import APIKeyHandler, APIKeyInfo +from shared.auth.models import Permission +from datetime import datetime, timedelta + +handler = APIKeyHandler() + +# Generate new API key +api_key = handler.generate_api_key() +print(f"API Key: {api_key}") # "ck_xxxxx..." + +# Register key with permissions +key_info = APIKeyInfo( + key_id="key_001", + user_id="user_123", + org_id="org_456", + permissions=[ + Permission.SESSION_READ, + Permission.MEMORY_READ, + ], + expires_at=datetime.utcnow() + timedelta(days=90), + rate_limit=1000, # requests per hour + is_active=True, +) + +handler.register_api_key(api_key, key_info) +``` + +### Store Keys Securely + +**Development**: Store in-memory or config file +**Production**: Store hashed keys in database + +```python +# Hash for storage +key_hash = handler.hash_api_key(api_key) + +# Store key_hash and key_info in database +# DO NOT store the raw API key +``` + +## Client Authentication + +### Using JWT + +```bash +# Get token from your auth system +TOKEN="eyJ0eXAiOiJKV1QiLCJhbGc..." + +# Use in requests +curl -H "Authorization: Bearer $TOKEN" \ + http://localhost:8000/api/v1/sessions +``` + +### Using API Key + +```bash +API_KEY="ck_xxxxx..." + +curl -H "X-API-Key: $API_KEY" \ + http://localhost:8000/api/v1/sessions +``` + +## Permission System + +### Available Permissions + +**Session Permissions**: +- `SESSION_CREATE` - Create new sessions +- `SESSION_READ` - Read session data +- `SESSION_UPDATE` - Update sessions +- `SESSION_DELETE` - Delete sessions +- `SESSION_LIST` - List sessions + +**Memory Permissions**: +- `MEMORY_CREATE` - Create memories +- `MEMORY_READ` - Read memories +- `MEMORY_UPDATE` - Update memories +- `MEMORY_DELETE` - Delete memories +- `MEMORY_SEARCH` - Search memories +- `MEMORY_LIST` - List memories + +**Admin Permissions**: +- `ADMIN_READ` - Read admin data +- `ADMIN_WRITE` - Write admin data + +### Permission Checking + +```python +from shared.auth.models import UserIdentity, Permission + +user = UserIdentity(...) + +# Check single permission +if user.has_permission(Permission.SESSION_CREATE): + # Create session + +# Check any permission +if user.has_any_permission([Permission.ADMIN_READ, Permission.ADMIN_WRITE]): + # Admin access + +# Check all permissions +if user.has_all_permissions([Permission.SESSION_READ, Permission.MEMORY_READ]): + # Read access +``` + +## Service Integration Examples + +### API Gateway + +```python +# services/gateway/app/main.py + +from shared.auth.middleware import AuthenticationMiddleware +from shared.auth.jwt import JWTHandler +from shared.auth.config import AuthSettings + +auth_settings = AuthSettings() + +if auth_settings.require_auth: + jwt_handler = JWTHandler(secret_key=auth_settings.jwt_secret_key) + + app.add_middleware( + AuthenticationMiddleware, + jwt_handler=jwt_handler, + exempt_paths=auth_settings.require_auth_exceptions, + ) +``` + +### Memory Service + +```python +# services/memory/app/main.py + +from shared.auth.dependencies import get_current_user, require_permissions + +# Protected endpoint +@app.post("/api/v1/memories") +async def create_memory( + request: CreateMemoryRequest, + user: Annotated[UserIdentity, Depends(require_permissions(Permission.MEMORY_CREATE))], +): + # User is authenticated and has MEMORY_CREATE permission + memory = await service.create_memory(...) + return MemoryResponse.model_validate(memory) +``` + +## Security Best Practices + +1. **Secrets Management** + - Never commit JWT secrets to git + - Use environment variables + - Rotate secrets periodically + +2. **Token Expiration** + - Set appropriate expiration times (default: 60 minutes) + - Implement token refresh for long-lived sessions + - Use short-lived tokens for high-security operations + +3. **API Keys** + - Hash keys before storage (SHA-256) + - Store only hashes in database + - Implement rate limiting + - Set expiration dates + - Allow key revocation + +4. **HTTPS** + - Always use HTTPS in production + - JWT and API keys in transit must be encrypted + +5. **Permissions** + - Grant minimal required permissions + - Use scoped permissions per service + - Audit permission usage regularly + +6. **Error Handling** + - Don't leak authentication details in errors + - Return generic 401/403 responses + - Log authentication failures + +## Troubleshooting + +### "JWT handler not configured" + +```python +# Make sure to override the dependency +from shared.auth.dependencies import get_jwt_handler + +app.dependency_overrides[get_jwt_handler] = lambda: jwt_handler +``` + +### "Token has expired" + +- Check token expiration time +- Implement token refresh +- Increase `AUTH_JWT_ACCESS_TOKEN_EXPIRE_MINUTES` + +### "Invalid or missing authentication credentials" + +- Verify Authorization header format: `Bearer ` +- Check API key header: `X-API-Key: ` +- Ensure handlers are configured correctly + +### Authentication disabled but still getting 401 + +- Check `AUTH_REQUIRE_AUTH=false` in .env +- Verify exempt_paths includes your endpoint +- Ensure middleware is configured correctly + +## Example: Complete Integration + +See `examples/auth_integration.py` for a complete working example. + +## References + +- [Authentication Package README](../shared/auth/README.md) +- [Permission Types](../shared/auth/models.py) +- [FastAPI Security](https://fastapi.tiangolo.com/tutorial/security/) +- [JWT Specification](https://jwt.io/) diff --git a/scripts/generate_auth_config.py b/scripts/generate_auth_config.py new file mode 100755 index 0000000..07dfbab --- /dev/null +++ b/scripts/generate_auth_config.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +""" +Generate authentication configuration. + +Helps developers generate JWT secrets and API keys for development. +""" + +import secrets +import sys +from pathlib import Path + +# Add parent directory to path for imports +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from shared.auth.api_key import APIKeyHandler + + +def generate_jwt_secret() -> str: + """Generate a secure JWT secret key.""" + return secrets.token_urlsafe(32) + + +def generate_api_key() -> str: + """Generate a secure API key.""" + handler = APIKeyHandler() + return handler.generate_api_key() + + +def main() -> None: + """Main function to generate auth configuration.""" + print("=" * 60) + print("ContextIQ Authentication Configuration Generator") + print("=" * 60) + print() + + # Generate JWT secret + jwt_secret = generate_jwt_secret() + print("JWT Secret Key:") + print(f" AUTH_JWT_SECRET_KEY={jwt_secret}") + print() + + # Generate API keys + print("Example API Keys (for development):") + print() + + for i in range(3): + api_key = generate_api_key() + print(f" API Key #{i + 1}:") + print(f" Key: {api_key}") + print(f" User ID: user_{i + 1}") + print(f" Org ID: org_123") + print() + + print("=" * 60) + print("Next Steps:") + print("=" * 60) + print() + print("1. Copy .env.auth.example to .env:") + print(" cp .env.auth.example .env") + print() + print("2. Update .env with the generated JWT secret") + print() + print("3. Store API keys securely (database or secrets manager)") + print() + print("4. Configure your application to use the auth handlers") + print() + print("For more information, see shared/auth/README.md") + print() + + +if __name__ == "__main__": + main() diff --git a/shared/auth/middleware.py b/shared/auth/middleware.py new file mode 100644 index 0000000..fc72b85 --- /dev/null +++ b/shared/auth/middleware.py @@ -0,0 +1,130 @@ +""" +Authentication middleware for FastAPI applications. + +Provides automatic authentication enforcement for all routes. +""" + +from collections.abc import Callable +from typing import Any + +from fastapi import Request, Response +from starlette.middleware.base import BaseHTTPMiddleware + +from shared.auth.api_key import APIKeyHandler +from shared.auth.jwt import JWTHandler +from shared.auth.models import UserIdentity + + +class AuthenticationMiddleware(BaseHTTPMiddleware): + """Middleware to enforce authentication on all routes.""" + + def __init__( + self, + app: Any, + jwt_handler: JWTHandler | None = None, + api_key_handler: APIKeyHandler | None = None, + exempt_paths: list[str] | None = None, + ): + """ + Initialize authentication middleware. + + Args: + app: FastAPI application + jwt_handler: JWT handler for token validation + api_key_handler: API key handler for key validation + exempt_paths: List of paths that don't require authentication + """ + super().__init__(app) + self.jwt_handler = jwt_handler + self.api_key_handler = api_key_handler + self.exempt_paths = exempt_paths or [ + "/health", + "/health/live", + "/health/ready", + "/health/detailed", + "/health/services", + "/docs", + "/redoc", + "/openapi.json", + "/metrics", + ] + + def _is_exempt(self, path: str) -> bool: + """ + Check if path is exempt from authentication. + + Args: + path: Request path + + Returns: + True if path is exempt + """ + return any(path.startswith(exempt) for exempt in self.exempt_paths) + + async def _authenticate(self, request: Request) -> UserIdentity | None: + """ + Authenticate request using JWT or API key. + + Args: + request: HTTP request + + Returns: + User identity if authenticated, None otherwise + """ + # Try JWT authentication + if self.jwt_handler: + auth_header = request.headers.get("Authorization", "") + if auth_header.startswith("Bearer "): + token = auth_header.replace("Bearer ", "") + identity = self.jwt_handler.verify_token(token) + if identity: + return identity + + # Try API key authentication + if self.api_key_handler: + api_key = request.headers.get("X-API-Key") + if api_key: + identity = self.api_key_handler.verify_api_key(api_key) + if identity: + return identity + + return None + + async def dispatch(self, request: Request, call_next: Callable) -> Response: + """ + Process request with authentication. + + Args: + request: HTTP request + call_next: Next middleware in chain + + Returns: + HTTP response + + Raises: + AuthenticationError: If authentication is required but fails + """ + # Skip authentication for exempt paths + if self._is_exempt(request.url.path): + return await call_next(request) + + # Authenticate request + identity = await self._authenticate(request) + + if identity is None: + # Return 401 Unauthorized + from fastapi.responses import JSONResponse + + return JSONResponse( + status_code=401, + content={ + "detail": "Authentication required", + "error": "Missing or invalid authentication credentials", + }, + ) + + # Attach user identity to request state + request.state.user = identity + + # Continue processing + return await call_next(request)