"""HF OAuth flow — anonymous-first, recognition-only. When OAUTH_CLIENT_ID is not set (local / unconfigured), all routes degrade gracefully: /login returns a friendly unavailable response, /me returns signed_in:false, /logout still clears the session and redirects. Privacy contract: ONLY hash_uid(sub) is stored in the session. Never the username, preferred_username, name, or raw sub. """ from __future__ import annotations import os from authlib.integrations.starlette_client import OAuth from fastapi import APIRouter, Request from fastapi.responses import JSONResponse, RedirectResponse from app.identity import hash_uid def make_oauth() -> OAuth | None: cid = os.environ.get("OAUTH_CLIENT_ID") if not cid: return None oauth = OAuth() oauth.register( name="hf", client_id=cid, client_secret=os.environ.get("OAUTH_CLIENT_SECRET"), server_metadata_url=( f"{os.environ.get('OPENID_PROVIDER_URL', 'https://huggingface.co')}" "/.well-known/openid-configuration" ), client_kwargs={"scope": "openid profile"}, ) return oauth # Module-level oauth instance; tests may monkeypatch this. _oauth: OAuth | None = make_oauth() def make_auth_router() -> APIRouter: router = APIRouter() @router.get("/login") async def login(request: Request): if _oauth is None: return JSONResponse( {"error": "login_unavailable", "detail": "OAuth not configured"}, status_code=503, ) redirect_uri = request.url_for("callback") return await _oauth.hf.authorize_redirect(request, redirect_uri) @router.get("/auth/callback", name="callback") async def callback(request: Request): if _oauth is None: return RedirectResponse("/") token = await _oauth.hf.authorize_access_token(request) # Prefer userinfo claim embedded in token; fall back to id_token sub. userinfo = token.get("userinfo") or {} sub = userinfo.get("sub") or token.get("sub", "") if sub: request.session["uid"] = hash_uid(sub) request.session["auth"] = True # real OAuth sign-in (vs anonymous cookie uid) return RedirectResponse("/") @router.get("/logout") async def logout(request: Request): request.session.clear() return RedirectResponse("/") @router.get("/me") async def me(request: Request): # anonymous cookie uid is NOT "signed in"; only a real OAuth login sets auth return {"signed_in": bool(request.session.get("auth"))} return router