clanker / app /auth.py
deucebucket's picture
Upload folder using huggingface_hub
50cd42e verified
Raw
History Blame Contribute Delete
2.65 kB
"""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