Spaces:
Running
Running
| import os | |
| import urllib.parse | |
| import time | |
| from fastapi import Request, HTTPException, Security | |
| from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials | |
| import jwt | |
| import jwt.algorithms | |
| import httpx | |
| # In-memory cache for JWK | |
| JWKS_CACHE = None | |
| JWKS_LAST_FETCH = 0 | |
| JWKS_TTL = 3600 # Cache for 1 hour | |
| security = HTTPBearer(auto_error=False) | |
| def get_supabase_project_id() -> str: | |
| # 1. Try to get direct SUPABASE_URL or NEXT_PUBLIC_SUPABASE_URL | |
| supabase_url = os.getenv("SUPABASE_URL") or os.getenv("NEXT_PUBLIC_SUPABASE_URL") | |
| if supabase_url: | |
| parsed = urllib.parse.urlparse(supabase_url) | |
| parts = parsed.netloc.split(".") | |
| if parts: | |
| return parts[0] | |
| # 2. Fallback: Parse DATABASE_URL | |
| db_url = os.getenv("DATABASE_URL") | |
| if db_url: | |
| try: | |
| parsed = urllib.parse.urlparse(db_url) | |
| # Try to parse from username (e.g. postgres.sjqiojrqigyufftigvyp) | |
| if parsed.username and "." in parsed.username: | |
| user_parts = parsed.username.split(".") | |
| if len(user_parts) >= 2: | |
| return user_parts[1] | |
| # Try to parse from host (e.g. db.sjqiojrqigyufftigvyp.supabase.co) | |
| if parsed.hostname: | |
| host_parts = parsed.hostname.split(".") | |
| if len(host_parts) >= 2 and host_parts[0] == "db": | |
| return host_parts[1] | |
| except Exception: | |
| pass | |
| raise RuntimeError("Neither SUPABASE_URL nor DATABASE_URL was found or could be parsed to retrieve the Supabase project ID.") | |
| async def fetch_jwks(force_refresh: bool = False) -> dict: | |
| global JWKS_CACHE, JWKS_LAST_FETCH | |
| now = time.time() | |
| if not force_refresh and JWKS_CACHE and (now - JWKS_LAST_FETCH) < JWKS_TTL: | |
| return JWKS_CACHE | |
| project_id = get_supabase_project_id() | |
| jwks_url = f"https://{project_id}.supabase.co/auth/v1/.well-known/jwks.json" | |
| import urllib.request | |
| import json | |
| import asyncio | |
| def _fetch(): | |
| with urllib.request.urlopen(jwks_url, timeout=10.0) as response: | |
| return json.loads(response.read().decode()) | |
| JWKS_CACHE = await asyncio.to_thread(_fetch) | |
| JWKS_LAST_FETCH = now | |
| return JWKS_CACHE | |
| async def verify_jwt(credentials: HTTPAuthorizationCredentials = Security(security)) -> dict: | |
| """FastAPI security dependency to verify the JWT from Supabase. | |
| If no authorization credentials are provided, falls back to a guest session. | |
| Otherwise, cryptographically verifies the token signature against the JWKS. | |
| """ | |
| if credentials is None: | |
| print("[auth.py] verify_jwt: No credentials/header provided. Falling back to guest user session.") | |
| return {"sub": "guest"} | |
| token = credentials.credentials | |
| print(f"[auth.py] verify_jwt: Authenticating request. JWT token prefix: {token[:15]}...") | |
| try: | |
| jwks = await fetch_jwks() | |
| headers = jwt.get_unverified_header(token) | |
| print(f"[auth.py] verify_jwt: JWT Headers decoded: {headers}") | |
| kid = headers.get("kid") | |
| alg = headers.get("alg", "RS256") | |
| if not kid: | |
| print("[auth.py] verify_jwt: Validation error - Missing kid in token header") | |
| raise HTTPException(status_code=401, detail="Missing kid in JWT header") | |
| key_data = next((k for k in jwks["keys"] if k["kid"] == kid), None) | |
| if not key_data: | |
| print(f"[auth.py] verify_jwt: Key ID '{kid}' not found in cached JWKS. Forcing immediate refresh...") | |
| jwks = await fetch_jwks(force_refresh=True) | |
| key_data = next((k for k in jwks["keys"] if k["kid"] == kid), None) | |
| if not key_data: | |
| print(f"[auth.py] verify_jwt: Validation error - Key ID '{kid}' not found in JWKS") | |
| raise HTTPException(status_code=401, detail="Key ID not found in JWKS") | |
| # Select correct algorithm parsing based on the algorithm used by Supabase | |
| if alg == "RS256": | |
| public_key = jwt.algorithms.RSAAlgorithm.from_jwk(key_data) | |
| elif alg == "ES256": | |
| public_key = jwt.algorithms.ECAlgorithm.from_jwk(key_data) | |
| else: | |
| print(f"[auth.py] verify_jwt: Validation error - Unsupported signing algorithm '{alg}'") | |
| raise HTTPException(status_code=401, detail=f"Unsupported signing algorithm: {alg}") | |
| # Verify the token. Supabase default audience is 'authenticated'. | |
| payload = jwt.decode( | |
| token, | |
| public_key, | |
| algorithms=["RS256", "ES256"], | |
| audience="authenticated" | |
| ) | |
| print(f"[auth.py] verify_jwt: Cryptographic validation succeeded! sub: {payload.get('sub')}, email: {payload.get('email')}, role: {payload.get('role')}") | |
| return payload | |
| except jwt.ExpiredSignatureError as e: | |
| print("[auth.py] verify_jwt: Token signature has expired.") | |
| raise HTTPException(status_code=401, detail="Token has expired") | |
| except jwt.InvalidTokenError as e: | |
| print(f"[auth.py] verify_jwt: Invalid token error - {str(e)}") | |
| raise HTTPException(status_code=401, detail=f"Invalid token: {str(e)}") | |
| except Exception as e: | |
| print(f"[auth.py] verify_jwt: Authentication failed with unhandled exception - {str(e)}") | |
| raise HTTPException(status_code=401, detail=f"Authentication failed: {str(e)}") | |