personabot-api / app /security /jwt_auth.py
GitHub Actions
Deploy 5a96418
bbe01fe
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from jose import JWTError, jwt
import time
from app.core.config import get_settings
# auto_error=False so we can raise 401 (Unauthorized) ourselves when no token
# is provided. Without this, FastAPI's HTTPBearer raises 403 (Forbidden) for
# missing credentials, which is semantically incorrect.
security = HTTPBearer(auto_error=False)
def verify_jwt(
credentials: HTTPAuthorizationCredentials | None = Depends(security),
) -> dict:
"""
Decodes the Bearer token provided by the PersonaBot API / SSE client.
Must perfectly match the JWT_SECRET and algorithm configured in .env.
Typically, this is generated by the user's external Vercel/NextJS routing layer.
"""
if credentials is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authorization header required.",
headers={"WWW-Authenticate": "Bearer"},
)
settings = get_settings()
token = credentials.credentials
# If we are in local testing and no secret is set, we might bypass,
# but for production parity, we strictly enforce it unless explicitly configured.
if not settings.JWT_SECRET:
# In a true 0$ prod deployment, failing to set a secret drops all traffic.
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="JWT_SECRET is not configured on the server."
)
try:
# Decode the token securely
payload = jwt.decode(
token,
settings.JWT_SECRET,
algorithms=[settings.JWT_ALGORITHM]
)
# Verify expiration
exp = payload.get("exp")
if not exp or time.time() > exp:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token has expired.",
headers={"WWW-Authenticate": "Bearer"},
)
# Optional: You can add `aud` (audience) or `iss` (issuer) validations here.
# But this basic structurally sound signature & expiry check stops scraping.
return payload
except JWTError as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Could not validate credentials: {str(e)}",
headers={"WWW-Authenticate": "Bearer"},
)