issue-core/issue_core/api/auth.py

67 lines
2 KiB
Python
Raw Normal View History

"""
API key authentication for the issue-core REST API.
A single shared key is read from the ISSUE_CORE_API_KEY environment variable.
Clients send it either as `Authorization: Bearer <key>` or as `X-API-Key: <key>`.
If ISSUE_CORE_API_KEY is unset, the server refuses to start — the workplan
explicitly forbids an unauthenticated ingestion surface.
"""
import os
import secrets
from typing import Optional
from fastapi import Header, HTTPException, status
API_KEY_ENV_VAR = "ISSUE_CORE_API_KEY"
class AuthConfigError(RuntimeError):
"""Raised at startup when no API key is configured."""
def get_configured_api_key() -> str:
key = os.environ.get(API_KEY_ENV_VAR, "").strip()
if not key:
raise AuthConfigError(
f"{API_KEY_ENV_VAR} is not set. The ingestion endpoint requires an API key. "
f"Generate one with: python -c 'import secrets; print(secrets.token_urlsafe(32))'"
)
return key
def _extract_token(
authorization: Optional[str],
x_api_key: Optional[str],
) -> Optional[str]:
if x_api_key:
return x_api_key.strip()
if authorization:
scheme, _, value = authorization.partition(" ")
if scheme.lower() == "bearer" and value:
return value.strip()
return None
async def require_api_key(
authorization: Optional[str] = Header(default=None),
x_api_key: Optional[str] = Header(default=None, alias="X-API-Key"),
) -> None:
"""FastAPI dependency enforcing the shared API key."""
try:
expected = get_configured_api_key()
except AuthConfigError as exc:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=str(exc),
)
presented = _extract_token(authorization, x_api_key)
if not presented or not secrets.compare_digest(presented, expected):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Missing or invalid API key.",
headers={"WWW-Authenticate": "Bearer"},
)