Download auth.py from lvwerra/tracebin: direct link, hf CLI and curl.
- Browser
- Download file 6.66 kB
-
https://huggingface.co/spaces/lvwerra/tracebin/resolve/main/auth.py
- Command line
-
hf download hf://spaces/lvwerra/tracebin/auth.py
-
curl -L -o auth.py https://huggingface.co/spaces/lvwerra/tracebin/resolve/main/auth.py
6.66 kB
| """Sign-in with Hugging Face, and the signed session cookie behind it. | |
| Enabled by ``hf_oauth: true`` in the Space's README metadata, which injects | |
| ``OAUTH_CLIENT_ID`` / ``OAUTH_CLIENT_SECRET`` / ``OPENID_PROVIDER_URL``. Only | |
| ``openid profile`` is requested — the app needs a username and nothing else. It | |
| never asks for repo scopes, so signing in grants it no access to your account. | |
| Sessions are a signed cookie rather than server state: a Space can restart at | |
| any time and there is nowhere durable to keep sessions that isn't the traces | |
| bucket. | |
| """ | |
| from __future__ import annotations | |
| import base64 | |
| import hashlib | |
| import hmac | |
| import json | |
| import logging | |
| import os | |
| import secrets | |
| import time | |
| import urllib.parse | |
| import urllib.request | |
| log = logging.getLogger("tracebin.auth") | |
| SESSION_COOKIE = "tb_session" | |
| STATE_COOKIE = "tb_oauth" | |
| SESSION_TTL = int(os.environ.get("SESSION_TTL_SECONDS", 8 * 3600)) | |
| # Rotating this on restart just means signing in again — one redirect, usually | |
| # invisible. Set SESSION_SECRET to keep sessions across restarts. | |
| SESSION_SECRET = os.environ.get("SESSION_SECRET") or secrets.token_hex(32) | |
| _provider_cache: dict[str, str] = {} | |
| def oauth_enabled() -> bool: | |
| return bool(os.environ.get("OAUTH_CLIENT_ID") and os.environ.get("OAUTH_CLIENT_SECRET")) | |
| # --------------------------------------------------------------------------- | |
| # signed cookies | |
| # --------------------------------------------------------------------------- | |
| def _b64e(raw: bytes) -> str: | |
| return base64.urlsafe_b64encode(raw).decode().rstrip("=") | |
| def _b64d(text: str) -> bytes: | |
| return base64.urlsafe_b64decode(text + "=" * (-len(text) % 4)) | |
| def sign(payload: dict) -> str: | |
| body = _b64e(json.dumps(payload, separators=(",", ":")).encode()) | |
| mac = hmac.new(SESSION_SECRET.encode(), body.encode(), hashlib.sha256).digest() | |
| return f"{body}.{_b64e(mac)}" | |
| def unsign(value: str | None) -> dict | None: | |
| """Return the payload if the signature and expiry both hold, else None.""" | |
| if not value or "." not in value: | |
| return None | |
| body, _, mac = value.partition(".") | |
| expected = hmac.new(SESSION_SECRET.encode(), body.encode(), hashlib.sha256).digest() | |
| try: | |
| if not hmac.compare_digest(_b64d(mac), expected): | |
| return None | |
| payload = json.loads(_b64d(body)) | |
| except (ValueError, TypeError): | |
| return None | |
| if not isinstance(payload, dict): | |
| return None | |
| if payload.get("exp", 0) < time.time(): | |
| return None | |
| return payload | |
| def session_username(request) -> str | None: | |
| """The signed-in HF username, or None.""" | |
| payload = unsign(request.cookies.get(SESSION_COOKIE)) | |
| if not payload: | |
| return None | |
| username = payload.get("u") | |
| return username if isinstance(username, str) and username else None | |
| def make_session(username: str) -> str: | |
| return sign({"u": username, "exp": int(time.time()) + SESSION_TTL}) | |
| # --------------------------------------------------------------------------- | |
| # OIDC | |
| # --------------------------------------------------------------------------- | |
| def _provider() -> dict[str, str]: | |
| """Discover the provider's endpoints once, then reuse them.""" | |
| if _provider_cache: | |
| return _provider_cache | |
| base = os.environ.get("OPENID_PROVIDER_URL", "https://huggingface.co").rstrip("/") | |
| url = f"{base}/.well-known/openid-configuration" | |
| with urllib.request.urlopen(url, timeout=15) as response: | |
| config = json.loads(response.read()) | |
| _provider_cache.update( | |
| { | |
| "authorization_endpoint": config["authorization_endpoint"], | |
| "token_endpoint": config["token_endpoint"], | |
| "userinfo_endpoint": config["userinfo_endpoint"], | |
| } | |
| ) | |
| return _provider_cache | |
| def authorize_url(redirect_uri: str, state: str) -> str: | |
| query = urllib.parse.urlencode( | |
| { | |
| "client_id": os.environ["OAUTH_CLIENT_ID"], | |
| "redirect_uri": redirect_uri, | |
| "response_type": "code", | |
| "scope": "openid profile", | |
| "state": state, | |
| } | |
| ) | |
| return f"{_provider()['authorization_endpoint']}?{query}" | |
| def exchange_code(code: str, redirect_uri: str) -> str: | |
| """Authorization code -> access token.""" | |
| client_id = os.environ["OAUTH_CLIENT_ID"] | |
| client_secret = os.environ["OAUTH_CLIENT_SECRET"] | |
| data = urllib.parse.urlencode( | |
| { | |
| "client_id": client_id, | |
| "code": code, | |
| "grant_type": "authorization_code", | |
| "redirect_uri": redirect_uri, | |
| } | |
| ).encode() | |
| basic = base64.b64encode(f"{client_id}:{client_secret}".encode()).decode() | |
| request = urllib.request.Request( | |
| _provider()["token_endpoint"], | |
| data=data, | |
| headers={ | |
| "Authorization": f"Basic {basic}", | |
| "Content-Type": "application/x-www-form-urlencoded", | |
| }, | |
| ) | |
| with urllib.request.urlopen(request, timeout=15) as response: | |
| return json.loads(response.read())["access_token"] | |
| def fetch_username(access_token: str) -> str: | |
| request = urllib.request.Request( | |
| _provider()["userinfo_endpoint"], | |
| headers={"Authorization": f"Bearer {access_token}"}, | |
| ) | |
| with urllib.request.urlopen(request, timeout=15) as response: | |
| info = json.loads(response.read()) | |
| username = info.get("preferred_username") or info.get("name") | |
| if not username: | |
| raise ValueError("userinfo response carried no username") | |
| return username | |
| def new_state() -> str: | |
| return secrets.token_urlsafe(16) | |
| # --------------------------------------------------------------------------- | |
| # allow lists | |
| # --------------------------------------------------------------------------- | |
| def normalize_users(raw) -> list[str]: | |
| """Accept a list or a comma/whitespace-separated string of HF usernames.""" | |
| if raw is None: | |
| return [] | |
| if isinstance(raw, str): | |
| parts = raw.replace(",", " ").split() | |
| elif isinstance(raw, (list, tuple)): | |
| parts = [str(p) for p in raw] | |
| else: | |
| return [] | |
| seen, out = set(), [] | |
| for part in parts: | |
| name = part.strip().lstrip("@").lower() | |
| # HF usernames are alphanumeric with dashes and underscores. | |
| if name and name not in seen and all(c.isalnum() or c in "-_." for c in name): | |
| seen.add(name) | |
| out.append(name) | |
| return out[:100] | |
| def is_allowed(username: str | None, allowed: list[str] | None) -> bool: | |
| if not allowed: | |
| return True # unrestricted: the link is the credential | |
| return bool(username) and username.lower() in {a.lower() for a in allowed} | |