| """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 |
|
|