Oppo-TIA / core /security.py
DJ-Goanna-Coding's picture
Upload folder using huggingface_hub
67be617 verified
Raw History Blame Contribute Delete
4.42 kB
"""JWT Bearer authentication with lightweight ABAC checks.
The API uses symmetric HS256 JWTs. Tokens carry standard claims plus an
``attrs`` object used for attribute-based access control, e.g.:
{"sub": "alice", "exp": 1700000000,
"attrs": {"role": "admin", "scopes": ["harvest:read", "harvest:write"]}}
Configuration via environment:
JWT_SECRET required HMAC secret for signing / verifying tokens
JWT_ALGORITHM optional, defaults to HS256
JWT_ISSUER optional issuer claim to validate
JWT_AUDIENCE optional audience claim to validate
"""
from __future__ import annotations
import os
from dataclasses import dataclass
import jwt
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
_bearer_scheme = HTTPBearer(auto_error=False)
@dataclass(frozen=True)
class Principal:
"""Authenticated caller and their attributes."""
subject: str
attrs: dict
def has_scope(self, scope: str) -> bool:
scopes = self.attrs.get("scopes") or []
return scope in scopes
def has_role(self, role: str) -> bool:
return self.attrs.get("role") == role
def _get_secret() -> str:
secret = os.environ.get("JWT_SECRET")
if not secret:
# Fail closed — never accept tokens without a configured secret.
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="JWT_SECRET is not configured",
)
return secret
def _decode(token: str) -> dict:
algorithm = os.environ.get("JWT_ALGORITHM", "HS256")
options = {"require": ["exp"]}
kwargs: dict = {"algorithms": [algorithm], "options": options}
audience = os.environ.get("JWT_AUDIENCE")
if audience:
kwargs["audience"] = audience
issuer = os.environ.get("JWT_ISSUER")
if issuer:
kwargs["issuer"] = issuer
try:
return jwt.decode(token, _get_secret(), **kwargs)
except jwt.ExpiredSignatureError as exc:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token has expired",
headers={"WWW-Authenticate": "Bearer"},
) from exc
except jwt.InvalidTokenError as exc:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication token",
headers={"WWW-Authenticate": "Bearer"},
) from exc
def get_current_principal(
credentials: HTTPAuthorizationCredentials | None = Depends(_bearer_scheme),
) -> Principal:
"""FastAPI dependency that resolves the caller from a Bearer token."""
if credentials is None or credentials.scheme.lower() != "bearer":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Missing Bearer credentials",
headers={"WWW-Authenticate": "Bearer"},
)
payload = _decode(credentials.credentials)
subject = payload.get("sub") or payload.get("subject")
if not subject:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token is missing 'sub' claim",
headers={"WWW-Authenticate": "Bearer"},
)
attrs = payload.get("attrs") or {}
if not isinstance(attrs, dict):
attrs = {}
# Fall back to top-level scope/role if not nested under attrs.
if "scopes" not in attrs and "scope" in payload:
scope = payload["scope"]
attrs["scopes"] = scope.split() if isinstance(scope, str) else list(scope)
if "role" not in attrs and "role" in payload:
attrs["role"] = payload["role"]
return Principal(subject=str(subject), attrs=attrs)
def require_scopes(*required: str):
"""Return a dependency enforcing that the caller has all given scopes."""
required_set = tuple(required)
def _checker(principal: Principal = Depends(get_current_principal)) -> Principal:
missing: list[str] = [s for s in required_set if not principal.has_scope(s)]
if missing:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Missing required scopes: {', '.join(required_set)}",
)
return principal
return _checker