rtrm's picture
rtrm HF Staff
feat: HF OAuth sign-in, probe endpoints restricted to Hugging Face org members
88e4a42 unverified
Raw
History Blame Contribute Delete
4.71 kB
"""Sign-in with Hugging Face (OAuth) and probe-rights gating.
The Space metadata sets `hf_oauth_authorized_org: huggingface`, so only org
members can complete the sign-in. The session is a signed cookie; probe
endpoints require it. Without OAUTH_CLIENT_ID (local dev), gating is disabled.
"""
import base64
import hashlib
import hmac
import json
import logging
import os
import secrets
import time
import urllib.parse
import urllib.request
from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import RedirectResponse
log = logging.getLogger(__name__)
router = APIRouter()
CLIENT_ID = os.environ.get("OAUTH_CLIENT_ID", "")
CLIENT_SECRET = os.environ.get("OAUTH_CLIENT_SECRET", "")
PROVIDER = os.environ.get("OPENID_PROVIDER_URL", "https://huggingface.co")
SPACE_HOST = os.environ.get("SPACE_HOST", "")
SESSION_TTL = 8 * 3600
HF_ORG = "huggingface"
ENABLED = bool(CLIENT_ID)
_secret = (CLIENT_SECRET or secrets.token_hex(16)).encode()
def _sign(raw: str) -> str:
return hmac.new(_secret, raw.encode(), hashlib.sha256).hexdigest()
def _make_cookie(payload: dict) -> str:
raw = base64.urlsafe_b64encode(json.dumps(payload).encode()).decode()
return raw + "." + _sign(raw)
def _read_cookie(value: str) -> dict | None:
try:
raw, sig = value.rsplit(".", 1)
if not hmac.compare_digest(sig, _sign(raw)):
return None
payload = json.loads(base64.urlsafe_b64decode(raw))
if payload.get("exp", 0) < time.time():
return None
return payload
except Exception:
return None
def current_user(request: Request) -> dict | None:
value = request.cookies.get("session")
return _read_cookie(value) if value else None
def require_probe_rights(request: Request):
if not ENABLED:
return
user = current_user(request)
if user is None:
raise HTTPException(401, "sign in with Hugging Face to probe")
if not user.get("hf"):
raise HTTPException(403, "probing is restricted to Hugging Face members")
def _redirect_uri(request: Request) -> str:
if SPACE_HOST:
return f"https://{SPACE_HOST}/auth/callback"
return str(request.base_url) + "auth/callback"
@router.get("/login/huggingface")
def login(request: Request):
state = secrets.token_urlsafe(16)
query = urllib.parse.urlencode({
"redirect_uri": _redirect_uri(request),
"scope": "openid profile email",
"client_id": CLIENT_ID,
"state": state,
"response_type": "code",
})
resp = RedirectResponse(f"{PROVIDER}/oauth/authorize?{query}")
resp.set_cookie("oauth_state", state, max_age=600,
httponly=True, secure=True, samesite="lax")
return resp
@router.get("/auth/callback")
def callback(request: Request, code: str, state: str):
if state != request.cookies.get("oauth_state"):
raise HTTPException(400, "state mismatch")
basic = base64.b64encode(f"{CLIENT_ID}:{CLIENT_SECRET}".encode()).decode()
data = urllib.parse.urlencode({
"client_id": CLIENT_ID,
"code": code,
"grant_type": "authorization_code",
"redirect_uri": _redirect_uri(request),
}).encode()
req = urllib.request.Request(
f"{PROVIDER}/oauth/token", data=data,
headers={"Authorization": f"Basic {basic}",
"Content-Type": "application/x-www-form-urlencoded"},
)
token = json.load(urllib.request.urlopen(req, timeout=30))
req = urllib.request.Request(
f"{PROVIDER}/oauth/userinfo",
headers={"Authorization": f"Bearer {token['access_token']}"},
)
info = json.load(urllib.request.urlopen(req, timeout=30))
orgs = info.get("orgs") or info.get("organizations") or []
org_names = {o.get("preferred_username") or o.get("name")
for o in orgs if isinstance(o, dict)}
email = info.get("email") or ""
# hf_oauth_authorized_org already gates the sign-in to org members; the
# org/email checks are a second factor in case that metadata is removed.
is_hf = (HF_ORG in org_names
or bool(info.get("email_verified") and email.endswith("@huggingface.co")))
username = info.get("preferred_username", "?")
log.info("sign-in: %s (hf member: %s)", username, is_hf)
resp = RedirectResponse("/")
resp.set_cookie(
"session",
_make_cookie({"u": username, "hf": is_hf, "exp": time.time() + SESSION_TTL}),
max_age=SESSION_TTL, httponly=True, secure=True, samesite="lax",
)
resp.delete_cookie("oauth_state")
return resp
@router.get("/logout")
def logout():
resp = RedirectResponse("/")
resp.delete_cookie("session")
return resp