Aniket2006's picture
fix: fallback to virtual guest session when Supabase anonymous sign-in is disabled
1c0c90f
Raw
History Blame Contribute Delete
3.68 kB
"""
Supabase JWT verification β€” FastAPI dependency.
When SUPABASE_URL + SUPABASE_ANON_KEY are configured, verifies the Bearer
token from Authorization header using Supabase's JWKS endpoint.
When not configured (local dev / SQLite mode), returns an anonymous user so
all routes work without auth.
"""
from __future__ import annotations
import httpx
from typing import Optional
from fastapi import Header, HTTPException
from jose import jwt, JWTError
from .config import get_settings, is_supabase_configured
# In-memory JWKS cache (refreshed on startup)
_jwks_cache: dict | None = None
ANON_USER = {'id': 'anonymous', 'name': 'Investigator', 'email': ''}
async def _fetch_jwks() -> dict:
global _jwks_cache
if _jwks_cache is not None:
return _jwks_cache
s = get_settings()
url = f"{s['supabase_url']}/auth/v1/.well-known/jwks.json"
try:
async with httpx.AsyncClient(timeout=5.0) as client:
resp = await client.get(url)
resp.raise_for_status()
_jwks_cache = resp.json()
return _jwks_cache
except Exception as exc:
print(f"[auth] JWKS fetch failed: {exc}")
return {}
def _decode_token(token: str, jwks: dict) -> dict:
"""Decode a Supabase JWT and return user claims."""
keys = jwks.get('keys', [])
if keys:
# jose accepts the full JWKS dict as the key argument
return jwt.decode(
token, jwks,
algorithms=['RS256', 'ES256'],
options={'verify_aud': False}, # Supabase audience varies by project
)
jwt_secret = get_settings().get('jwt_secret', '')
if jwt_secret:
return jwt.decode(
token,
jwt_secret,
algorithms=['HS256'],
options={'verify_aud': False},
)
raise JWTError("No JWKS keys or SUPABASE_JWT_SECRET available")
async def get_current_user(
authorization: Optional[str] = Header(default=None),
) -> dict:
"""
FastAPI dependency β€” returns current user dict:
{ id, name, email }
If Supabase is not configured or no token provided, returns ANON_USER.
Routes can use this for audit attribution without requiring auth.
"""
if not is_supabase_configured():
return ANON_USER
if not authorization or not authorization.startswith('Bearer '):
return ANON_USER
token = authorization[7:].strip()
if token.startswith("guest:"):
parts = token.split(":")
guest_id = parts[1] if len(parts) > 1 else "guest_unknown"
name_slice = guest_id[6:12] if len(guest_id) > 12 else guest_id
return {
'id': guest_id,
'name': f"Guest ({name_slice})",
'email': '',
}
try:
jwks = await _fetch_jwks()
payload = _decode_token(token, jwks)
meta = payload.get('user_metadata', {}) or {}
return {
'id': payload.get('sub', 'unknown'),
'name': meta.get('full_name') or meta.get('name') or payload.get('email', 'Investigator'),
'email': payload.get('email', ''),
}
except JWTError as exc:
# Expired / tampered token β€” reject with 401 when auth is configured
raise HTTPException(status_code=401, detail=f"Invalid token: {exc}")
async def get_current_user_optional(
authorization: Optional[str] = Header(default=None),
) -> dict:
"""
Soft auth β€” never raises 401. Used on endpoints that are readable
without auth but write attribution when authenticated.
"""
try:
return await get_current_user(authorization)
except HTTPException:
return ANON_USER