import time from uuid import UUID from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer from jose import JWTError, jwt from app.core.config import settings oauth2_optional = OAuth2PasswordBearer( tokenUrl="keycloak", auto_error=False ) # logger = logging.getLogger(__name__) def _decode_keycloak_token(token: str) -> dict: if not settings.KEYCLOAK_PUBLIC_KEY: raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Keycloak public key is not configured", ) key = settings.KEYCLOAK_PUBLIC_KEY.replace("\\n", "\n") payload = jwt.decode( token, key, algorithms=[settings.KEYCLOAK_ALGORITHM], audience=settings.KEYCLOAK_AUDIENCE or None, ) if settings.KEYCLOAK_ACCESS_TOKEN_MAX_AGE_SECONDS <= 0: return payload issued_at = payload.get("iat") if not isinstance(issued_at, (int, float)) or ( time.time() >= issued_at + settings.KEYCLOAK_ACCESS_TOKEN_MAX_AGE_SECONDS ): raise JWTError("Keycloak access token is older than the allowed maximum age") return payload async def get_current_keycloak_payload( token: str | None = Depends(oauth2_optional), ) -> dict: if not token: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated", headers={"WWW-Authenticate": "Bearer"}, ) try: return _decode_keycloak_token(token) except JWTError as exc: # logger.warning("Keycloak token validation failed: %s", exc) raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token", headers={"WWW-Authenticate": "Bearer"}, ) from exc async def get_current_keycloak_sub( payload: dict = Depends(get_current_keycloak_payload), ) -> UUID: sub = payload.get("sub") if not sub: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing subject claim", headers={"WWW-Authenticate": "Bearer"}, ) try: return UUID(sub) except ValueError as exc: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid subject claim", headers={"WWW-Authenticate": "Bearer"}, ) from exc def get_keycloak_preferred_username(payload: dict) -> str: username = payload.get("preferred_username") if not username: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing preferred_username claim", headers={"WWW-Authenticate": "Bearer"}, ) return str(username) async def get_current_keycloak_username( payload: dict = Depends(get_current_keycloak_payload), ) -> str: return get_keycloak_preferred_username(payload)