Spaces:
Paused
Paused
Download server/oauth.py from Bit-Trading-Company/ASE-GLM: direct link, hf CLI and curl.
- Browser
- Download file 6.55 kB
-
https://huggingface.co/spaces/Bit-Trading-Company/ASE-GLM/resolve/main/server/oauth.py
- Command line
-
hf download hf://spaces/Bit-Trading-Company/ASE-GLM/server/oauth.py
-
curl -L -o oauth.py https://huggingface.co/spaces/Bit-Trading-Company/ASE-GLM/resolve/main/server/oauth.py
6.55 kB
| """Sign in with Hugging Face, so inference is billed to the viewer. | |
| A Space with ``hf_oauth: true`` in its README header is given | |
| ``OAUTH_CLIENT_ID``, ``OAUTH_CLIENT_SECRET``, ``OAUTH_SCOPES`` and | |
| ``OPENID_PROVIDER_URL`` in its environment. Gradio implements this flow for | |
| you; a Docker Space has to do it itself, which is this file. | |
| The scope that matters is ``inference-api``: the access token it yields can | |
| call the router, and the *viewer's* account is billed rather than the Space | |
| owner's. Without it a sign-in tells us who someone is and nothing else, which | |
| is the wrong trade for the amount of machinery involved. | |
| The token is held in a signed, http-only session cookie and never leaves the | |
| server. Nothing here is reachable off a Space, because no client id exists. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import secrets | |
| from urllib.parse import urlencode | |
| import httpx | |
| from fastapi import APIRouter, Request | |
| from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse | |
| router = APIRouter() | |
| # The paths a Space's OAuth app is allowed to redirect back to are fixed by the | |
| # platform. `/auth/callback` is the one this app registers. | |
| CALLBACK_PATH = "/auth/callback" | |
| def provider() -> str: | |
| return os.environ.get("OPENID_PROVIDER_URL", "https://huggingface.co").rstrip("/") | |
| def client_id() -> str | None: | |
| return os.environ.get("OAUTH_CLIENT_ID") | |
| def configured() -> bool: | |
| """True when the platform gave us an OAuth app to use.""" | |
| return bool(client_id() and os.environ.get("OAUTH_CLIENT_SECRET")) | |
| def scopes() -> str: | |
| # `openid profile` identifies the viewer; `inference-api` is what lets us | |
| # spend their quota instead of ours. | |
| return os.environ.get("OAUTH_SCOPES") or "openid profile inference-api" | |
| def _redirect_uri(request: Request) -> str: | |
| space = os.environ.get("SPACE_HOST") | |
| if space: | |
| return f"https://{space}{CALLBACK_PATH}" | |
| return str(request.base_url).rstrip("/") + CALLBACK_PATH | |
| async def login(request: Request): | |
| if not configured(): | |
| return JSONResponse( | |
| {"error": "Sign-in is only available on the deployed Space."}, | |
| status_code=503, | |
| ) | |
| # CSRF: the callback must present the state we generated for this session. | |
| state = secrets.token_urlsafe(24) | |
| request.session["oauth_state"] = state | |
| query = urlencode({ | |
| "client_id": client_id(), | |
| "redirect_uri": _redirect_uri(request), | |
| "response_type": "code", | |
| "scope": scopes(), | |
| "state": state, | |
| "prompt": "consent", | |
| }) | |
| return RedirectResponse(f"{provider()}/oauth/authorize?{query}", status_code=302) | |
| async def callback(request: Request, code: str | None = None, | |
| state: str | None = None, error: str | None = None): | |
| if error: | |
| return _close_window(f"Hugging Face returned: {error}") | |
| expected = request.session.pop("oauth_state", None) | |
| if not expected: | |
| # We issued a state and it did not come back, which is a cookie | |
| # problem rather than a forgery: the session is gone. Say which, | |
| # because the two have completely different fixes. | |
| return _close_window( | |
| "The sign-in session was not returned by your browser. This is " | |
| "usually third-party cookies being blocked for the embedded " | |
| "Space \u2014 open the Space in its own tab and try again." | |
| ) | |
| # Constant time, and both halves required: a callback whose state we did | |
| # not issue is not ours to trust. | |
| if not state or not secrets.compare_digest(state, expected): | |
| return _close_window("Sign-in state did not match. Please try again.") | |
| if not code: | |
| return _close_window("Hugging Face did not return an authorization code.") | |
| try: | |
| async with httpx.AsyncClient(timeout=20.0) as client: | |
| tok = await client.post( | |
| f"{provider()}/oauth/token", | |
| data={ | |
| "client_id": client_id(), | |
| "client_secret": os.environ.get("OAUTH_CLIENT_SECRET"), | |
| "grant_type": "authorization_code", | |
| "code": code, | |
| "redirect_uri": _redirect_uri(request), | |
| }, | |
| headers={"Accept": "application/json"}, | |
| ) | |
| if tok.status_code >= 400: | |
| return _close_window(f"Token exchange failed ({tok.status_code}).") | |
| payload = tok.json() | |
| access = payload.get("access_token") | |
| if not access: | |
| return _close_window("Hugging Face did not return an access token.") | |
| who = await client.get( | |
| f"{provider()}/oauth/userinfo", | |
| headers={"Authorization": f"Bearer {access}"}, | |
| ) | |
| info = who.json() if who.status_code < 400 else {} | |
| except httpx.HTTPError as e: | |
| return _close_window(f"Could not reach Hugging Face: {type(e).__name__}") | |
| request.session["oauth_token"] = access | |
| request.session["oauth_user"] = info.get("preferred_username") or info.get("name") | |
| request.session["oauth_profile"] = { | |
| "name": info.get("name"), | |
| "username": info.get("preferred_username"), | |
| "picture": info.get("picture"), | |
| # `isPro` and org membership come back on the userinfo for HF. | |
| "isPro": bool(info.get("isPro")), | |
| "orgs": [o.get("preferred_username") or o.get("name") | |
| for o in (info.get("orgs") or []) if isinstance(o, dict)], | |
| "grantedScopes": scopes(), | |
| } | |
| return _close_window(None) | |
| async def logout(request: Request): | |
| for key in ("oauth_token", "oauth_user", "oauth_profile", "oauth_state"): | |
| request.session.pop(key, None) | |
| return {"ok": True} | |
| def _close_window(error: str | None) -> HTMLResponse: | |
| """Land the browser back on the app. | |
| Sign-in is started in the same tab, so the callback simply navigates home; | |
| an error is carried in the fragment, which never reaches a server log the | |
| way a query string would. | |
| """ | |
| target = "/" if not error else "/#auth-error=" + error.replace('"', "") | |
| return HTMLResponse( | |
| f'<!doctype html><meta charset="utf-8">' | |
| f'<meta http-equiv="refresh" content="0;url={target}">' | |
| f'<body style="background:#1a1c1f;color:#e8e2d6;font:13px ui-monospace,monospace">' | |
| f'<p style="padding:2rem">{error or "Signed in. Returning…"}</p>' | |
| ) | |