from __future__ import annotations
from dataclasses import dataclass
from datetime import UTC, datetime
from django.core.signing import BadSignature, SignatureExpired, TimestampSigner
from rest_framework import exceptions
from rest_framework.request import Request
@dataclass(frozen=True)
class ProofClaims:
address: str
scopes: list[str]
issued_at: datetime
expires_at: datetime
_SIGNER = TimestampSigner(salt="cyber_valley.siwe_proof")
def issue_proof_token(*, address: str, scopes: list[str]) -> str:
payload = {"address": address.lower(), "scopes": scopes}
return _SIGNER.sign_object(payload)
def require_proof_claims(
request: Request,
*,
required_scopes: list[str],
max_age_seconds: int,
) -> ProofClaims:
auth = request.headers.get("Authorization", "")
if not auth.startswith("Bearer "):
raise exceptions.NotAuthenticated("Missing Bearer proof token")
token = auth.removeprefix("Bearer ").strip()
if not token:
raise exceptions.NotAuthenticated("Missing Bearer proof token")
try:
data = _SIGNER.unsign_object(token, max_age=max_age_seconds)
except SignatureExpired as e:
raise exceptions.NotAuthenticated("Proof token expired") from e
except BadSignature as e:
raise exceptions.NotAuthenticated("Invalid proof token") from e
if not isinstance(data, dict):
raise exceptions.NotAuthenticated("Invalid proof token payload")
address = str(data.get("address", "")).lower()
scopes = data.get("scopes")
if (
not address
or not isinstance(scopes, list)
or not all(isinstance(s, str) for s in scopes)
):
raise exceptions.NotAuthenticated("Invalid proof token payload")
missing = [s for s in required_scopes if s not in scopes]
if missing:
raise exceptions.PermissionDenied(f"Missing required scope(s): {missing}")
now = datetime.now(tz=UTC)
return ProofClaims(
address=address,
scopes=scopes,
issued_at=now,
expires_at=now,
)