"""Bearer token auth for /api/. The lock that keeps a public API host closed.""" from __future__ import annotations import hmac from collections.abc import Callable from functools import wraps from django.conf import settings from django.http import HttpRequest, JsonResponse from django.views.decorators.csrf import csrf_exempt def parse_bearer(request: HttpRequest) -> str | None: header = request.META.get("HTTP_AUTHORIZATION") or "" if not header.startswith("Bearer "): return None token = header[7:].strip() return token or None def authenticate_token(raw_token: str | None) -> str | None: """Return the configured token name, or None if no match. Accepts either ``name:secret`` (as callers send) or the bare secret. Always compares against every configured token (constant-time). """ configured = list(getattr(settings, "SHORTENER_API_TOKENS", []) or []) if not configured or not raw_token: return None matched_name: str | None = None for name, secret in configured: full = f"{name}:{secret}" if hmac.compare_digest(raw_token, full) or hmac.compare_digest(raw_token, secret): matched_name = name # Keep looping so compare_digest runs for every token. return matched_name def token_name_for_request(request: HttpRequest) -> str | None: return authenticate_token(parse_bearer(request)) def require_bearer(view: Callable) -> Callable: """Decorator: 503 if no tokens configured, 401 if missing/wrong Bearer.""" @csrf_exempt @wraps(view) def wrapper(request, *args, **kwargs): configured = list(getattr(settings, "SHORTENER_API_TOKENS", []) or []) if not configured: return JsonResponse({"detail": "Service unavailable"}, status=503) name = token_name_for_request(request) if not name: response = JsonResponse({"detail": "Unauthorized"}, status=401) response["WWW-Authenticate"] = "Bearer" return response request.token_name = name return view(request, *args, **kwargs) return wrapper