From bd6f7669808d6338f677145a2a089667a9a1bd9a Mon Sep 17 00:00:00 2001 From: Russell Bryant Date: Wed, 17 Dec 2025 16:43:47 -0500 Subject: [PATCH] feat: add optional bearer token authentication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement API token authentication using FastAPI's built-in security utilities. When the API_TOKENS environment variable is set, all API endpoints require a valid bearer token. Implementation details: - New src/auth.py module handles token validation - Uses FastAPI's HTTPBearer security scheme for OpenAPI integration - API_TOKENS env var accepts comma-separated list of valid tokens - Uses secrets.compare_digest for constant-time token comparison to prevent timing attacks - Returns 401 with WWW-Authenticate header on auth failure - When API_TOKENS is not set or empty, authentication is disabled (backwards compatible) - Applied as global dependency, protecting all API routes - /docs endpoint remains accessible for API documentation Usage: # Without auth (development) uvicorn src.api:app --reload # With auth (production) API_TOKENS="token1,token2" uvicorn src.api:app # Making authenticated requests curl -H "Authorization: Bearer token1" http://localhost:8000/top_players/ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/api.py | 8 ++++++-- src/auth.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 src/auth.py diff --git a/src/api.py b/src/api.py index f6f0945..bd029b9 100644 --- a/src/api.py +++ b/src/api.py @@ -1,12 +1,16 @@ import requests -from fastapi import FastAPI +from fastapi import Depends, FastAPI from fastapi.responses import ORJSONResponse, RedirectResponse from fastapi.middleware.cors import CORSMiddleware +from src.auth import verify_token from src.scraper import fide_scraper -app = FastAPI(default_response_class=ORJSONResponse) +app = FastAPI( + default_response_class=ORJSONResponse, + dependencies=[Depends(verify_token)], +) app.add_middleware( CORSMiddleware, diff --git a/src/auth.py b/src/auth.py new file mode 100644 index 0000000..a7d36d5 --- /dev/null +++ b/src/auth.py @@ -0,0 +1,34 @@ +import os +import secrets + +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer + +API_TOKENS: set[str] = set() + +_tokens_env = os.environ.get("API_TOKENS", "") +if _tokens_env: + API_TOKENS = {token.strip() for token in _tokens_env.split(",") if token.strip()} + +security = HTTPBearer(auto_error=False) + + +def verify_token(credentials: HTTPAuthorizationCredentials | None = Depends(security)) -> None: + if not API_TOKENS: + return + + if credentials is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Missing authentication token", + headers={"WWW-Authenticate": "Bearer"}, + ) + + token = credentials.credentials + + if not any(secrets.compare_digest(token, valid_token) for valid_token in API_TOKENS): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid authentication token", + headers={"WWW-Authenticate": "Bearer"}, + )