from typing import Annotated import pydantic from fansifter_common.utils.functional import lazy_proxy from fastapi import HTTPException, Request, Security from fastapi.security import APIKeyCookie, HTTPAuthorizationCredentials, HTTPBearer from jwtauth import JWTAuth from jwtauth.exceptions import JWTAuthError from jwtauth.utils import get_default_audience, get_default_issuer, get_default_jwks_url from pydantic import BaseModel from resonance_engine.config import settings class Identity(BaseModel): # both optional — M2M (client-credentials) tokens carry no email/name email: str | None = None name: str | None = None cookie_auth = APIKeyCookie( name=settings.session_cookie_name, auto_error=False, ) bearer_auth = HTTPBearer( scheme_name="JWTAuthorization", auto_error=False, ) jwt_auth = lazy_proxy( lambda: JWTAuth( jwks_url=get_default_jwks_url(settings.environment), audience=get_default_audience(settings.environment), issuer=get_default_issuer(settings.environment), options={ "require": ["iss", "sub", "exp", "iat"], "verify_aud": False, "verify_exp": True, "verify_iat": True, "verify_iss": True, "verify_nbf": True, }, ) ) _DEV_IDENTITY = Identity(email="dev@localhost", name="Dev") def is_authenticated(request: Request) -> bool: if not settings.auth_enabled: return True return bool(request.cookies.get(settings.session_cookie_name)) def authenticate( bearer: Annotated[HTTPAuthorizationCredentials | None, Security(bearer_auth)], cookie: Annotated[str | None, Security(cookie_auth)], ) -> Identity: if not settings.auth_enabled: return _DEV_IDENTITY token_string = (bearer.credentials if bearer else None) or cookie if not token_string: raise HTTPException(status_code=401, detail="Not authenticated") try: token = jwt_auth.get_token(token_string) except JWTAuthError as exc: raise HTTPException(status_code=401, detail=exc.message) from exc try: return Identity.model_validate(token) except pydantic.ValidationError as exc: raise HTTPException(status_code=400, detail="Invalid jwt payload.") from exc