File size: 4,618 Bytes
c14ceee | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | """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
|