""" Public API Authentication API Key authentication for the public Evaluation-as-a-Service API. """ import hashlib import secrets import uuid from datetime import datetime from typing import Optional, Any from fastapi import Depends, HTTPException, status from fastapi.security import APIKeyHeader from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from backend.api.dependencies import get_db from backend.db.models import APIKey as APIKeyModel from backend.logging.logger import get_logger # Initialize logger logger = get_logger("public_api_auth", component="api") # ============================================================================= # API Key Header # ============================================================================= API_KEY_HEADER = APIKeyHeader( name="Authorization", auto_error=False, description="API Key in format: Bearer " ) # ============================================================================= # Helper Functions # ============================================================================= def hash_api_key(key: str) -> str: """ Hash an API key using SHA-256. Args: key: The raw API key to hash Returns: The hexadecimal hash of the key """ return hashlib.sha256(key.encode()).hexdigest() def generate_api_key() -> str: """ Generate a new random API key. Returns: A random 32-byte API key encoded as a hex string """ return secrets.token_hex(32) async def verify_api_key( key: str, db: AsyncSession ) -> Optional[APIKeyModel]: """ Verify an API key against the database. Args: key: The raw API key to verify db: Database session Returns: The APIKeyModel if valid, None otherwise """ key_hash = hash_api_key(key) result = await db.execute( select(APIKeyModel).where( APIKeyModel.key_hash == key_hash, APIKeyModel.active == True ) ) api_key = result.scalar_one_or_none() if api_key: # Update last used timestamp api_key.last_used = datetime.utcnow() await db.commit() logger.info( "API key verified", metadata={ "api_key_id": str(api_key.id), "owner": api_key.owner } ) return api_key async def get_current_api_key( api_key_header: Optional[str] = Depends(API_KEY_HEADER), db: AsyncSession = Depends(get_db) ) -> Any: """ Dependency to get the current authenticated API key. Args: api_key_header: The Authorization header value db: Database session Returns: The authenticated APIKeyModel Raises: HTTPException: If the API key is invalid or missing """ if not api_key_header: logger.warning("Missing API key header") raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail={ "error": "authentication_failed", "message": "API key is required. Include it in the Authorization header: Bearer " } ) # Parse the header - expect "Bearer " parts = api_key_header.split(" ") if len(parts) != 2 or parts[0].lower() != "bearer": logger.warning("Invalid Authorization header format") raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail={ "error": "authentication_failed", "message": "Invalid Authorization header format. Use: Bearer " } ) api_key = parts[1] # Verify the key verified_key = await verify_api_key(api_key, db) if not verified_key: logger.warning("Invalid API key attempted", metadata={"key_prefix": api_key[:8] + "..."}) raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail={ "error": "authentication_failed", "message": "Invalid or inactive API key" } ) return verified_key # ============================================================================= # API Key Management Functions # ============================================================================= async def create_api_key( owner: str, rate_limit: int = 100, evaluation_mode_restriction: Optional[str] = None, mutation_enabled: bool = True, monitoring_only: bool = False, db: Optional[AsyncSession] = None ) -> tuple[str, APIKeyModel]: """ Create a new API key. Args: owner: Owner identifier rate_limit: Requests per minute limit evaluation_mode_restriction: Optional evaluation mode restriction mutation_enabled: Whether mutation is enabled monitoring_only: Whether monitoring-only mode db: Database session (optional, for testing) Returns: Tuple of (raw_api_key, APIKeyModel) """ # Generate the raw key (this is shown only once) raw_key = generate_api_key() key_hash = hash_api_key(raw_key) # Create the model api_key = APIKeyModel( id=uuid.uuid4(), key_hash=key_hash, owner=owner, rate_limit=rate_limit, created_at=datetime.utcnow(), active=True, evaluation_mode_restriction=evaluation_mode_restriction, mutation_enabled=mutation_enabled, monitoring_only=monitoring_only ) if db: db.add(api_key) await db.commit() await db.refresh(api_key) logger.info( "API key created", metadata={ "api_key_id": str(api_key.id), "owner": owner, "rate_limit": rate_limit } ) return raw_key, api_key async def revoke_api_key( api_key_id: uuid.UUID, db: AsyncSession ) -> bool: """ Revoke an API key. Args: api_key_id: ID of the API key to revoke db: Database session Returns: True if revoked, False if not found """ result = await db.execute( select(APIKeyModel).where(APIKeyModel.id == api_key_id) ) api_key = result.scalar_one_or_none() if not api_key: return False api_key.active = False await db.commit() logger.info( "API key revoked", metadata={ "api_key_id": str(api_key_id), "owner": api_key.owner } ) return True async def get_api_key_info( api_key_id: uuid.UUID, db: AsyncSession ) -> Optional[APIKeyModel]: """ Get information about an API key (without exposing the hash). Args: api_key_id: ID of the API key db: Database session Returns: APIKeyModel if found """ result = await db.execute( select(APIKeyModel).where(APIKeyModel.id == api_key_id) ) return result.scalar_one_or_none()