voice-cloning-api / security.py
rahulseerand's picture
Add Lingo World child safety and reporting controls
0dddf11 verified
Raw
History Blame Contribute Delete
3.31 kB
"""Supabase JWT authentication for the Lingo World API."""
from __future__ import annotations
import os
from dataclasses import dataclass
from functools import lru_cache
import jwt
from fastapi import HTTPException, Security, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
_bearer = HTTPBearer(auto_error=False)
@dataclass(frozen=True)
class AuthenticatedUser:
id: str
email: str | None = None
class SupabaseJWTVerifier:
def __init__(self) -> None:
self.supabase_url = os.environ.get("SUPABASE_URL", "").rstrip("/")
self.jwt_secret = os.environ.get("SUPABASE_JWT_SECRET", "")
self.audience = os.environ.get("SUPABASE_JWT_AUDIENCE", "authenticated")
self.issuer = (
os.environ.get("SUPABASE_JWT_ISSUER")
or (f"{self.supabase_url}/auth/v1" if self.supabase_url else "")
)
self._jwks_client = (
jwt.PyJWKClient(f"{self.supabase_url}/auth/v1/.well-known/jwks.json")
if self.supabase_url and not self.jwt_secret
else None
)
@property
def configured(self) -> bool:
return bool(self.supabase_url and (self.jwt_secret or self._jwks_client))
def verify(self, token: str) -> AuthenticatedUser:
if not self.configured:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Authentication is not configured",
)
try:
if self.jwt_secret:
payload = jwt.decode(
token,
self.jwt_secret,
algorithms=["HS256"],
audience=self.audience,
issuer=self.issuer or None,
)
else:
signing_key = self._jwks_client.get_signing_key_from_jwt(token)
payload = jwt.decode(
token,
signing_key.key,
algorithms=["RS256", "ES256"],
audience=self.audience,
issuer=self.issuer or None,
)
except jwt.PyJWTError as exc:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired session",
headers={"WWW-Authenticate": "Bearer"},
) from exc
user_id = str(payload.get("sub") or "").strip()
if not user_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Session is missing a user identifier",
headers={"WWW-Authenticate": "Bearer"},
)
return AuthenticatedUser(id=user_id, email=payload.get("email"))
@lru_cache(maxsize=1)
def get_verifier() -> SupabaseJWTVerifier:
return SupabaseJWTVerifier()
def require_user(
credentials: HTTPAuthorizationCredentials | None = Security(_bearer),
) -> AuthenticatedUser:
if credentials is None or credentials.scheme.lower() != "bearer":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authentication required",
headers={"WWW-Authenticate": "Bearer"},
)
return get_verifier().verify(credentials.credentials)