loopable / platform /core /auth_google.py
fsanyoto's picture
Deploy AIOS web (React glide grid + FastAPI slice)
c14ceee verified
Raw
History Blame Contribute Delete
4.62 kB
"""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