Spaces:
Sleeping
Sleeping
| """Google OAuth 2.0 — connexion self-hosted (sans Emergent).""" | |
| from __future__ import annotations | |
| import ssl_fix # noqa: F401 | |
| import base64 | |
| import json | |
| import os | |
| from urllib.parse import urlencode | |
| import httpx | |
| GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth" | |
| GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token" | |
| GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v3/userinfo" | |
| def has_client_id() -> bool: | |
| return bool(os.environ.get("GOOGLE_CLIENT_ID", "").strip()) | |
| def is_configured() -> bool: | |
| return has_client_id() and bool(os.environ.get("GOOGLE_CLIENT_SECRET", "").strip()) | |
| def redirect_uri() -> str: | |
| explicit = os.environ.get("GOOGLE_REDIRECT_URI", "").strip() | |
| if explicit: | |
| return explicit | |
| base = os.environ.get("EMO_PUBLIC_BACKEND_URL", "https://xroxx-emo-online-api.hf.space").rstrip("/") | |
| return f"{base}/api/auth/google/callback" | |
| def normalize_frontend_callback(url: str) -> str: | |
| url = (url or "").strip().rstrip("/") | |
| if not url: | |
| return "http://127.0.0.1:3000/auth/google/callback" | |
| if url.endswith("/auth/google/callback"): | |
| return url | |
| if url.endswith("/chat"): | |
| return url[: -len("/chat")] + "/auth/google/callback" | |
| if url.endswith("/login"): | |
| return url[: -len("/login")] + "/auth/google/callback" | |
| return f"{url}/auth/google/callback" | |
| def build_login_url(frontend_redirect: str, desktop: bool = False) -> str: | |
| state = base64.urlsafe_b64encode( | |
| json.dumps({"redirect": frontend_redirect}).encode() | |
| ).decode().rstrip("=") | |
| params = { | |
| "client_id": os.environ["GOOGLE_CLIENT_ID"], | |
| "redirect_uri": redirect_uri(), | |
| "response_type": "code", | |
| "scope": "openid email profile", | |
| "access_type": "online", | |
| "state": state, | |
| } | |
| if desktop: | |
| pass | |
| else: | |
| params["prompt"] = "select_account" | |
| return f"{GOOGLE_AUTH_URL}?{urlencode(params)}" | |
| def parse_state(state: str) -> str: | |
| try: | |
| pad = "=" * (-len(state) % 4) | |
| data = json.loads(base64.urlsafe_b64decode(state + pad).decode()) | |
| return normalize_frontend_callback(data.get("redirect") or "") | |
| except Exception: | |
| return "http://127.0.0.1:3000/auth/google/callback" | |
| def map_oauth_error(error: str, detail: str = "") -> str: | |
| if error == "access_denied": | |
| return "access_denied" | |
| if "redirect_uri_mismatch" in detail: | |
| return "redirect_uri_mismatch" | |
| if "invalid_client" in detail: | |
| return "invalid_client" | |
| return error or "google_auth_failed" | |
| async def exchange_code(code: str) -> dict: | |
| async with httpx.AsyncClient(timeout=20) as client: | |
| token_resp = await client.post( | |
| GOOGLE_TOKEN_URL, | |
| data={ | |
| "code": code, | |
| "client_id": os.environ["GOOGLE_CLIENT_ID"], | |
| "client_secret": os.environ["GOOGLE_CLIENT_SECRET"], | |
| "redirect_uri": redirect_uri(), | |
| "grant_type": "authorization_code", | |
| }, | |
| ) | |
| if token_resp.status_code != 200: | |
| detail = token_resp.text[:300] | |
| if "invalid_client" in detail: | |
| raise ValueError("invalid_client") | |
| if "redirect_uri_mismatch" in detail: | |
| raise ValueError("redirect_uri_mismatch") | |
| raise ValueError(f"Token Google invalide: {detail}") | |
| access_token = token_resp.json().get("access_token") | |
| if not access_token: | |
| raise ValueError("Pas de access_token Google") | |
| user_resp = await client.get( | |
| GOOGLE_USERINFO_URL, | |
| headers={"Authorization": f"Bearer {access_token}"}, | |
| ) | |
| if user_resp.status_code != 200: | |
| raise ValueError("Impossible de lire le profil Google") | |
| return user_resp.json() | |
| async def verify_id_token(id_token: str) -> dict: | |
| client_id = os.environ.get("GOOGLE_CLIENT_ID", "").strip() | |
| if not client_id: | |
| raise ValueError("missing_client_id") | |
| async with httpx.AsyncClient(timeout=20) as client: | |
| resp = await client.get( | |
| "https://oauth2.googleapis.com/tokeninfo", | |
| params={"id_token": id_token}, | |
| ) | |
| if resp.status_code != 200: | |
| raise ValueError("invalid_id_token") | |
| data = resp.json() | |
| aud = data.get("aud") or data.get("azp") | |
| if aud != client_id: | |
| raise ValueError("wrong_audience") | |
| if not data.get("email"): | |
| raise ValueError("no_email") | |
| if str(data.get("email_verified", "true")).lower() == "false": | |
| raise ValueError("email_not_verified") | |
| return data | |