labs / src /auth /google.py
3v324v23's picture
deploy: unified router + dreamy website (2026-06-16T09:46:52Z)
c1a683f
Raw
History Blame Contribute Delete
4.68 kB
"""Google OAuth2 (authorization-code flow) — no third-party dep.
Flow:
1. GET /auth/google -> 302 redirect to Google's consent page
2. Google redirects back to /auth/google/callback?code=...
3. We exchange `code` for tokens (POST to oauth2.googleapis.com/token)
4. We fetch the user (openid userinfo endpoint)
5. On success: set session cookie, redirect to dashboard.html
Docs: https://developers.google.com/identity/openid-connect
"""
from __future__ import annotations # defer annotation evaluation (PEP 563)
import os # read GOOGLE_* env vars
import urllib.parse # build the authorize URL query string
import httpx # async HTTP for the token exchange + user fetch
GOOGLE_API = "https://oauth2.googleapis.com" # token endpoint host
AUTHORIZE_URL = "https://accounts.google.com/o/oauth2/v2/auth" # the user-facing consent page
TOKEN_URL = GOOGLE_API + "/token" # code -> tokens exchange
USERINFO_URL = "https://openidconnect.googleapis.com/v1/userinfo" # current user info (OpenID)
# Read the OAuth app config from env (set as HF Space secrets).
CLIENT_ID = os.environ.get("GOOGLE_CLIENT_ID", "") # public app id (from Google Cloud Console)
CLIENT_SECRET = os.environ.get("GOOGLE_CLIENT_SECRET", "") # app secret (never shipped to browser)
REDIRECT_URI = os.environ.get("GOOGLE_REDIRECT_URI", "") # our callback URL (must be in Authorized redirect URIs)
HOSTED_DOMAIN = os.environ.get("GOOGLE_HOSTED_DOMAIN", "").strip() # optional: restrict to a Google Workspace domain (hd param)
# Scopes: openid = id token; email = email address; profile = name/picture.
SCOPES = "openid email profile" # request identity, email, and basic profile
def is_configured() -> bool: # is Google login enabled on this deployment?
"""True only if all required env vars are set (used to show/hide the button)."""
return bool(CLIENT_ID and CLIENT_SECRET and REDIRECT_URI and os.environ.get("SESSION_SECRET")) # all four required
def authorize_url(state: str) -> str: # build the Google authorize redirect URL
"""Build the full authorize URL with query params for the OAuth redirect."""
params = { # query string parameters
"client_id": CLIENT_ID, # our app id
"redirect_uri": REDIRECT_URI, # where Google sends the user back
"response_type": "code", # authorization-code flow (not implicit)
"scope": SCOPES, # what user data we request
"state": state, # CSRF token echoed back by Google
"prompt": "consent", # always show the consent screen (allows account picker + re-login)
"access_type": "online", # we don't need refresh tokens (stateless sessions)
}
if HOSTED_DOMAIN: # restrict to a specific Google Workspace domain
params["hd"] = HOSTED_DOMAIN # Google enforces this server-side
return AUTHORIZE_URL + "?" + urllib.parse.urlencode(params) # encode + concatenate
async def exchange_code(code: str, client: httpx.AsyncClient) -> dict | None: # code -> tokens
"""Exchange an authorization code for tokens. Returns None on failure."""
data = { # token endpoint form params
"client_id": CLIENT_ID, # app id
"client_secret": CLIENT_SECRET, # app secret (server-side only)
"grant_type": "authorization_code", # the OAuth grant type
"code": code, # the code from the callback
"redirect_uri": REDIRECT_URI, # must match the authorize request exactly
}
headers = {"Content-Type": "application/x-www-form-urlencoded"} # Google wants form-encoded
try: # network call to Google
r = await client.post(TOKEN_URL, data=data, headers=headers, timeout=15.0) # POST exchange
except Exception: # network error / timeout
return None # caller treats as login failure
if r.status_code != 200: # Google rejected the code (expired, reused, mismatched redirect)
return None # caller shows a login error
return r.json() # {access_token, id_token, token_type, scope, ...}
async def fetch_user(access_token: str, client: httpx.AsyncClient) -> dict | None: # get the user profile
"""Fetch the OpenID userinfo with the access token. Returns None on failure."""
headers = {"Authorization": f"Bearer {access_token}"} # Bearer auth with the OAuth token
try: # network call
r = await client.get(USERINFO_URL, headers=headers, timeout=15.0) # GET userinfo
except Exception: # network error
return None # treat as failure
if r.status_code != 200: # token invalid / revoked
return None # reject
return r.json() # {sub, email, email_verified, name, picture, ...}