Spaces:
Sleeping
Sleeping
File size: 3,869 Bytes
d37642c 714a774 d37642c 714a774 d37642c 714a774 d37642c 714a774 d37642c 714a774 d37642c 714a774 d37642c 714a774 d37642c 714a774 d37642c | 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 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | """
auth.py — Hugging Face token verification.
The frontend signs the user in with "Sign in with Hugging Face" and forwards the
resulting OAuth access token to this server on every scan. We verify that token
against Hugging Face's userinfo endpoint to establish *who* the caller is. The
stable identity is the `sub` field (an immutable user id); usernames can change.
This module is deliberately tiny and dependency-light (just `requests`).
"""
from __future__ import annotations
import requests
USERINFO_URL = "https://huggingface.co/oauth/userinfo"
WHOAMI_URL = "https://huggingface.co/api/whoami-v2"
_TIMEOUT_S = 10
def verify_hf_token(token: str | None) -> dict | None:
"""Return the user info dict for a valid token, else None.
A valid response contains at least: sub, preferred_username, name, picture.
Two token kinds are accepted:
- OAuth access tokens (production frontend "Sign in with HF") -> verified
via the OIDC /oauth/userinfo endpoint.
- Personal access tokens (local testing / single-user mode) -> /oauth/userinfo
rejects them, so we fall back to /api/whoami-v2 and map its response into
the same shape (sub<-id, preferred_username<-name, name<-fullname,
picture<-avatarUrl).
Never raises — all failures collapse to None so callers can treat an invalid
token identically to a missing one.
"""
if not token:
return None
info = _verify_oauth_userinfo(token)
if info is None:
info = _verify_whoami(token)
if not info or not info.get("sub"):
return None
return info
def _verify_oauth_userinfo(token: str) -> dict | None:
"""OIDC userinfo — works for OAuth access tokens."""
try:
resp = requests.get(
USERINFO_URL,
headers={"Authorization": f"Bearer {token}"},
timeout=_TIMEOUT_S,
)
except requests.RequestException as exc:
print(f"[auth] userinfo request failed: {exc}")
return None
if resp.status_code != 200:
# 401 for an invalid/expired/non-OAuth token is expected and quiet.
if resp.status_code != 401:
print(f"[auth] userinfo returned {resp.status_code}")
return None
try:
return resp.json()
except ValueError:
print("[auth] userinfo returned non-JSON body")
return None
def _verify_whoami(token: str) -> dict | None:
"""Fallback for personal access tokens (and also accepts OAuth tokens).
Maps the whoami-v2 response into the userinfo shape."""
try:
resp = requests.get(
WHOAMI_URL,
headers={"Authorization": f"Bearer {token}"},
timeout=_TIMEOUT_S,
)
except requests.RequestException as exc:
print(f"[auth] whoami request failed: {exc}")
return None
if resp.status_code != 200:
if resp.status_code != 401:
print(f"[auth] whoami returned {resp.status_code}")
return None
try:
data = resp.json()
except ValueError:
print("[auth] whoami returned non-JSON body")
return None
if data.get("type") != "user" or not data.get("id"):
return None
return {
"sub": data["id"],
"preferred_username": data.get("name", ""),
"name": data.get("fullname") or data.get("name", ""),
"picture": data.get("avatarUrl", ""),
}
def extract_bearer(auth_header: str | None) -> str | None:
"""Pull the token out of an 'Authorization: Bearer <token>' header.
Provided for any future REST-style callers; the Gradio endpoints receive the
token as a plain parameter and don't need this.
"""
if not auth_header:
return None
parts = auth_header.split(None, 1)
if len(parts) == 2 and parts[0].lower() == "bearer":
return parts[1].strip()
return None
|