from __future__ import annotations
from typing import Any, List, Optional
[docs]
class JwtUser:
"""
Represents the authenticated user extracted from a decoded JWT Bearer token.
All standard claims from the Equisoft/Centralize token are exposed as typed
properties. Any extra or custom claim is also accessible via ``get()`` or
the subscript operator, so the class stays useful even when the token schema
evolves.
Typical usage inside a script (``self`` is any ``BaseRule`` subclass)::
user = self.jwt_user
if user is None:
# Request was made without a Bearer token
...
# Typed shorthand properties
print(user.application_user_id) # int – applicationUserId claim
print(user.user_id) # int – userId claim
print(user.email) # str – email claim
print(user.name) # str – name claim
print(user.org) # str – org claim
print(user.roles) # list – roles claim
print(user.divisions) # list – divisions claim
print(user.language_id) # int – languageId claim
print(user.application_uuid) # str – applicationUUID claim
print(user.subject) # str – sub claim
print(user.is_carbon) # bool – isCarbon claim
# Access any claim by its original JWT key name
value = user.get("someCustomClaim", default="fallback")
# Dict-style access
value = user["someCustomClaim"]
# Membership test
if "someCustomClaim" in user:
...
# Full payload as a plain dict
payload = user.as_dict()
"""
[docs]
def __init__(self, payload: dict):
"""
:param payload: The decoded JWT payload dict (as returned by ``jwt.decode()``).
"""
self._payload: dict = payload if isinstance(payload, dict) else {}
# ------------------------------------------------------------------
# Equisoft / Centralize application claims
# ------------------------------------------------------------------
@property
def application_user_id(self) -> Optional[int]:
"""``applicationUserId`` claim – primary user identifier used by ACE scripts."""
return self._payload.get("applicationUserId")
@property
def user_id(self) -> Optional[int]:
"""``userId`` claim."""
return self._payload.get("userId")
@property
def name(self) -> Optional[str]:
"""``name`` claim – display name of the authenticated user."""
return self._payload.get("name")
@property
def email(self) -> Optional[str]:
"""``email`` claim."""
return self._payload.get("email")
@property
def org(self) -> Optional[str]:
"""``org`` claim – organisation name."""
return self._payload.get("org")
@property
def roles(self) -> List[Any]:
"""``roles`` claim – list of role identifiers assigned to the user."""
return self._payload.get("roles", [])
@property
def divisions(self) -> List[str]:
"""``divisions`` claim – list of division names the user belongs to."""
return self._payload.get("divisions", [])
@property
def language_id(self) -> Optional[int]:
"""``languageId`` claim."""
return self._payload.get("languageId")
@property
def application_uuid(self) -> Optional[str]:
"""``applicationUUID`` claim."""
return self._payload.get("applicationUUID")
@property
def is_carbon(self) -> bool:
"""``isCarbon`` claim."""
return bool(self._payload.get("isCarbon", False))
@property
def is_commission(self) -> bool:
"""``isCommission`` claim."""
return bool(self._payload.get("isCommission", False))
@property
def audience(self) -> list:
"""``aud`` claim – list of intended audiences for this token."""
aud = self._payload.get("aud", [])
return aud if isinstance(aud, list) else [aud]
# ------------------------------------------------------------------
# Standard JWT registered claims (RFC 7519)
# ------------------------------------------------------------------
@property
def subject(self) -> Optional[str]:
"""``sub`` claim – UUID that uniquely identifies the user in the IdP."""
return self._payload.get("sub")
@property
def issuer(self) -> Optional[str]:
"""``iss`` claim – token issuer URL."""
return self._payload.get("iss")
@property
def issued_at(self) -> Optional[int]:
"""``iat`` claim – Unix timestamp when the token was issued."""
return self._payload.get("iat")
@property
def expires_at(self) -> Optional[int]:
"""``exp`` claim – Unix timestamp when the token expires."""
return self._payload.get("exp")
@property
def not_before(self) -> Optional[int]:
"""``nbf`` claim – Unix timestamp before which the token is not valid."""
return self._payload.get("nbf")
@property
def jwt_id(self) -> Optional[str]:
"""``jti`` claim – unique identifier for this specific token."""
return self._payload.get("jti")
# ------------------------------------------------------------------
# Generic / escape-hatch accessors
# ------------------------------------------------------------------
[docs]
def get(self, claim: str, default: Any = None) -> Any:
"""
Return the value of *any* claim by its original JWT key name.
:param claim: The raw claim key as it appears in the JWT payload
(e.g. ``"applicationUserId"``, ``"someCustomClaim"``).
:param default: Value to return when the claim is absent.
"""
return self._payload.get(claim, default)
[docs]
def __getitem__(self, claim: str) -> Any:
"""Allow dict-style access: ``user["applicationUserId"]``."""
return self._payload[claim]
[docs]
def __contains__(self, claim: str) -> bool:
"""Support ``"claim" in user`` membership checks."""
return claim in self._payload
[docs]
def as_dict(self) -> dict:
"""Return a copy of the full decoded JWT payload."""
return dict(self._payload)
def __repr__(self) -> str:
return (
f"JwtUser(name={self.name!r}, email={self.email!r}, "
f"application_user_id={self.application_user_id!r}, org={self.org!r})"
)
# ------------------------------------------------------------------
# Factory helpers
# ------------------------------------------------------------------
[docs]
@classmethod
def from_token(cls, token: str) -> Optional["JwtUser"]:
"""
Decode a raw JWT string (without signature verification) and return
a ``JwtUser`` instance, or ``None`` if decoding fails.
:param token: The raw Bearer token string (without the ``Bearer `` prefix).
"""
try:
import jwt # PyJWT
payload = jwt.decode(
token,
options={
"verify_signature": False,
"verify_aud": False,
"verify_exp": False,
"verify_nbf": False,
},
)
return cls(payload)
except Exception:
return None
[docs]
@classmethod
def from_payload(cls, payload: dict) -> "JwtUser":
"""Build a ``JwtUser`` directly from an already-decoded payload dict."""
return cls(payload)