"""FastAPI dependencies for authenticating a request. One verification path for two transports, per docs/PLAN.md "Auth": a bearer token in the Authorization header takes priority (that's how a non-browser client, or a future native app, would authenticate), falling back to the session cookie the PWA uses. The cookie is HttpOnly — JavaScript never touches it — so it's read here purely server-side; the web client gets no capability a bearer-authenticated client wouldn't also have. CSRF: a cookie-authenticated request that MUTATES state must have an Origin header matching the configured public URL. A bearer-authenticated request skips this check, because a cross-origin attacker's page cannot set an Authorization header on a request it tricks the browser into sending — that's the whole CSRF attack surface, and it doesn't exist for bearer auth. """ from fastapi import Cookie, Header, HTTPException, Request, status from velodrome.auth.service import AuthenticatedSession, SessionInvalid, validate_session from velodrome.config import get_settings _UNAUTHORIZED = HTTPException(status.HTTP_401_UNAUTHORIZED, detail="not authenticated") def _extract_bearer(authorization: str | None) -> str | None: if authorization is None: return None scheme, _, token = authorization.partition(" ") if scheme.lower() != "bearer" or not token: return None return token async def get_current_user( authorization: str | None = Header(default=None), session_cookie: str | None = Cookie(default=None, alias="vd_session"), ) -> AuthenticatedSession: raw_token = _extract_bearer(authorization) or session_cookie if raw_token is None: raise _UNAUTHORIZED try: return await validate_session(raw_token) except SessionInvalid as exc: raise _UNAUTHORIZED from exc async def require_same_origin_for_cookie_auth( request: Request, authorization: str | None = Header(default=None), ) -> None: """Apply to every mutating route. No-ops for bearer auth; enforces Origin for cookie auth.""" if _extract_bearer(authorization) is not None: return # bearer-authenticated; CSRF doesn't apply, see module docstring. origin = request.headers.get("origin") expected = get_settings().public_url.rstrip("/") if origin is None or origin.rstrip("/") != expected: raise HTTPException(status.HTTP_403_FORBIDDEN, detail="cross-origin request rejected")