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