"""Sign in with Google — TEMPLATE ONLY (wave-7 W10, 2026-07-28). Nothing here runs in production yet: every flow function raises until the wiring checklist in docs/GOOGLE_LOGIN_TODO.md is completed and GOOGLE_LOGIN_ENABLED=1 is set. DESIGN (recorded now so the full build has a contract to meet): - OAuth 2.0 authorization-code flow with PKCE against Google Identity (https://accounts.google.com/.well-known/openid-configuration). No SDK dependency — three HTTPS calls (authorize redirect, token exchange, JWKS fetch) keep the surface auditable and the requirements.txt unchanged until we commit. - Google is an IDENTITY, not an ACCOUNT SOURCE. A Google sign-in maps to an EXISTING core/users.py account via its `email` attribute (set_access(..., email=...)); an unknown email FAILS CLOSED with "no account for this Google identity" — Google login never creates users, so the per-BU/per-module grant model stays the only door. - The password gate stays byte-for-byte intact beside it. APP_PASSWORD remains the bootstrap master; removing password auth is a separate, owner-approved step. ENV (placeholders — see .env.example additions in docs/GOOGLE_LOGIN_TODO.md): GOOGLE_OAUTH_CLIENT_ID OAuth client id from Google Cloud console GOOGLE_OAUTH_CLIENT_SECRET its secret (Space secret / .env — never committed) GOOGLE_OAUTH_REDIRECT_URI e.g. https://royal-imports-cfo-os.hf.space/ (must be registered VERBATIM in the console; localhost:8501 for dev) GOOGLE_LOGIN_ENABLED '1' arms the flow; anything else keeps this a template """ from __future__ import annotations import os AUTH_ENDPOINT = "https://accounts.google.com/o/oauth2/v2/auth" TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token" JWKS_URI = "https://www.googleapis.com/oauth2/v3/certs" SCOPES = "openid email profile" def configured() -> bool: """True when the console credentials exist — the LOGIN BUTTON renders iff this is True. (Separate from enabled(): a configured-but-disabled deploy shows nothing either, so a half-finished setup never paints a dead button.)""" return bool(os.environ.get("GOOGLE_OAUTH_CLIENT_ID")) and enabled() def enabled() -> bool: return os.environ.get("GOOGLE_LOGIN_ENABLED", "") == "1" def auth_url(state: str, code_challenge: str) -> str: """The Google consent-screen URL for the authorization-code + PKCE flow. `state` must be an unguessable per-session token the callback VERIFIES (CSRF); `code_challenge` is BASE64URL(SHA256(code_verifier)) with the verifier held in the session. Template guard: raises until the TODO checklist lands. """ raise NotImplementedError( "Google login is a template — complete docs/GOOGLE_LOGIN_TODO.md, then implement " "auth_url() (urlencode client_id, redirect_uri, response_type=code, scope, state, " "code_challenge, code_challenge_method=S256 onto AUTH_ENDPOINT)." ) def exchange_code(code: str, code_verifier: str) -> dict: """POST the authorization code to TOKEN_ENDPOINT → {'id_token', 'access_token', ...}. Template guard: raises until implemented (requests.post with client_id/secret, redirect_uri, grant_type=authorization_code, code, code_verifier).""" raise NotImplementedError( "Google login is a template — implement exchange_code() per docs/GOOGLE_LOGIN_TODO.md." ) def verify_id_token(id_token: str) -> dict: """Validate the JWT against Google's JWKS (signature, iss, aud=client_id, exp) and return its claims ({'email', 'email_verified', 'name', ...}). MUST reject email_verified=False. Template guard: raises until implemented.""" raise NotImplementedError( "Google login is a template — implement verify_id_token() per docs/GOOGLE_LOGIN_TODO.md." ) def account_for_claims(claims: dict): """Map verified Google claims → the core/users.py account, FAIL-CLOSED. The one function already real: the mapping rule is the design's heart and testable today. Returns the users.registry() record whose `email` equals the verified claim (case-insensitive), only if the account is active; None otherwise — never a new user. """ email = str(claims.get("email") or "").strip().lower() if not email or claims.get("email_verified") is not True: return None from core import users for username, u in (users.registry() or {}).items(): if str(u.get("email") or "").strip().lower() == email and u.get("active", True): return users._public(username, u) return None