""" Real Firebase ID token verification — decoupled from LOCAL_MODE (which only governs storage) and from REAL_SEND_ENABLED (mail transport), same independent-flag pattern as backend/mailer_client.py. A request either carries a real Firebase ID token (REAL_AUTH_ENABLED=true) or, for local/dev/test convenience, an X-Dev-Uid header (REAL_AUTH_ENABLED=false — the default in tests, see tests/conftest.py). """ from __future__ import annotations import os import firebase_admin from fastapi import Header, HTTPException from firebase_admin import auth as firebase_auth from backend.firebase_app import get_app def real_auth_enabled() -> bool: return os.getenv("REAL_AUTH_ENABLED", "false").lower() == "true" class AuthError(Exception): pass def verify_id_token(token: str) -> dict: get_app() try: return firebase_auth.verify_id_token(token) except firebase_admin.exceptions.FirebaseError as exc: raise AuthError(str(exc)) from exc def current_uid( authorization: str | None = Header(default=None), x_dev_uid: str = Header(default="dev-user-1", alias="X-Dev-Uid"), ) -> str: """FastAPI dependency — every route that needs "who is calling" uses this instead of reading X-Dev-Uid directly, so the real/local branch lives in exactly one place.""" if not real_auth_enabled(): return x_dev_uid if not authorization or not authorization.startswith("Bearer "): raise HTTPException(status_code=401, detail="Missing bearer token") token = authorization[len("Bearer "):] try: claims = verify_id_token(token) except AuthError as exc: raise HTTPException(status_code=401, detail=f"Invalid token: {exc}") from exc # Password sign-ups must prove they own the address before touching anything real. if not claims.get("email_verified", False): raise HTTPException(status_code=403, detail="Email not verified") return claims["uid"] def current_email( authorization: str | None = Header(default=None), x_dev_email: str = Header(default="", alias="X-Dev-Email"), ) -> str: """The signed-in address (checked to be verified by current_uid). Used where an action depends on who the person is, such as accepting an invite.""" if not real_auth_enabled(): return x_dev_email if not authorization or not authorization.startswith("Bearer "): raise HTTPException(status_code=401, detail="Missing bearer token") try: claims = verify_id_token(authorization[len("Bearer "):]) except AuthError as exc: raise HTTPException(status_code=401, detail=f"Invalid token: {exc}") from exc return claims.get("email", "") def lookup_user(uid: str) -> dict: """{email, name} for a uid, best effort; empty when unknown or running locally.""" if not real_auth_enabled(): return {} try: get_app() record = firebase_auth.get_user(uid) except Exception: # noqa: BLE001 — a missing name must not break the team list return {} return {"email": record.email or "", "name": record.display_name or ""}