"""Authentication module for Dangerous Pi API.""" import secrets from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import HTTPBasic, HTTPBasicCredentials from .. import config from .token_store import create_token security = HTTPBasic() router = APIRouter() def verify_credentials(credentials: HTTPBasicCredentials = Depends(security)) -> str: """Verify HTTP Basic Auth credentials. Args: credentials: HTTP Basic credentials from request Returns: Username if authentication successful Raises: HTTPException: If authentication fails """ if not config.AUTH_ENABLED: return "anonymous" if not config.AUTH_PASSWORD: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="AUTH_ENABLED is true but AUTH_PASSWORD is not set", ) # Use constant-time comparison to prevent timing attacks is_correct_username = secrets.compare_digest( credentials.username.encode("utf-8"), config.AUTH_USERNAME.encode("utf-8") ) is_correct_password = secrets.compare_digest( credentials.password.encode("utf-8"), config.AUTH_PASSWORD.encode("utf-8") ) if not (is_correct_username and is_correct_password): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials", headers={"WWW-Authenticate": "Basic"}, ) return credentials.username def get_optional_auth(credentials: HTTPBasicCredentials = Depends(security)) -> str | None: """Optional authentication - returns username or None. Use this for endpoints where auth is optional based on config. """ if not config.AUTH_ENABLED: return None try: return verify_credentials(credentials) except HTTPException: return None @router.get("/status") async def auth_status(): """Check whether authentication is enabled. This endpoint is always public so the frontend can determine whether to prompt for credentials before connecting the WebSocket. """ return {"auth_enabled": config.AUTH_ENABLED} @router.post("/token") async def get_auth_token(username: str = Depends(verify_credentials)): """Exchange Basic Auth credentials for a WebSocket auth token. The returned token should be passed as a query parameter when connecting to the WebSocket endpoint: ws://host/ws/events?token=... Tokens expire after 24 hours. """ token = create_token(username) return {"token": token}