ayanshuDS
Deploy to HF without binaries
685cc60
Raw
History Blame Contribute Delete
5.5 kB
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)}")