tokens.py 1.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
  1. from __future__ import annotations
  2. from datetime import datetime, timedelta, timezone
  3. import jwt
  4. from app.core.common.identifiers import new_governance_uid
  5. class TokenError(ValueError):
  6. pass
  7. def issue_access_token(
  8. *,
  9. user_id: str,
  10. roles: list[str],
  11. secret: str,
  12. now: datetime | None = None,
  13. lifetime: timedelta = timedelta(minutes=30),
  14. ) -> str:
  15. now = now or datetime.now(timezone.utc)
  16. claims = {
  17. "sub": str(user_id),
  18. "roles": sorted(set(roles)),
  19. "iat": int(now.timestamp()),
  20. "exp": int((now + lifetime).timestamp()),
  21. "jti": new_governance_uid(),
  22. }
  23. return jwt.encode(claims, secret, algorithm="HS256")
  24. def decode_access_token(
  25. token: str,
  26. *,
  27. secret: str,
  28. now: datetime | None = None,
  29. ) -> dict:
  30. try:
  31. claims = jwt.decode(
  32. token,
  33. secret,
  34. algorithms=["HS256"],
  35. options={"verify_exp": False, "require": ["sub", "roles", "iat", "exp", "jti"]},
  36. )
  37. except jwt.PyJWTError as exc:
  38. raise TokenError("invalid access token") from exc
  39. current = int((now or datetime.now(timezone.utc)).timestamp())
  40. if int(claims["exp"]) <= current:
  41. raise TokenError("access token expired")
  42. if int(claims["iat"]) > current + 30:
  43. raise TokenError("access token issued in the future")
  44. if not isinstance(claims.get("roles"), list):
  45. raise TokenError("invalid role claims")
  46. return {key: claims[key] for key in ("sub", "roles", "iat", "exp", "jti")}