"""ForwardAuth verification endpoint for Traefik. Traefik calls GET /api/v1/auth/verify on every request to a protected service. This endpoint validates the JWT from the Authorization header and returns identity headers that Traefik injects into downstream requests. Downstream services NEVER validate JWTs themselves — they trust the X-User-Id, X-User-Email, X-User-Tier headers injected by Traefik. """ from __future__ import annotations from fastapi import APIRouter, Request, Response from fastapi import status as http_status from jose import JWTError, jwt from sqlalchemy import select from shared.config import settings from shared.db import async_session from shared.models import Subscription from app.config import auth_settings router = APIRouter(tags=["auth"]) @router.get("/auth/verify") async def verify(request: Request) -> Response: """Validate JWT and return identity headers for Traefik ForwardAuth. Returns 200 with X-User-* headers on success, 401 on failure. Traefik copies response headers to the downstream request. """ auth_header = request.headers.get("Authorization", "") if not auth_header.startswith("Bearer "): return Response(status_code=http_status.HTTP_401_UNAUTHORIZED) token = auth_header[7:] # strip "Bearer " try: payload = jwt.decode( token, auth_settings.JWT_PUBLIC_KEY, algorithms=["RS256"] ) user_id: str | None = payload.get("sub") email: str | None = payload.get("email") if not user_id or not email: return Response(status_code=http_status.HTTP_401_UNAUTHORIZED) except JWTError: return Response(status_code=http_status.HTTP_401_UNAUTHORIZED) # Live tier lookup from subscriptions table async with async_session() as db: result = await db.execute( select(Subscription.tier).where(Subscription.user_id == user_id) ) default_tier = "power" if settings.ENV == "dev" else "free" tier: str = result.scalar_one_or_none() or default_tier return Response( status_code=http_status.HTTP_200_OK, headers={ "X-User-Id": user_id, "X-User-Email": email, "X-User-Tier": tier, }, )