"""Token claims dataclass for validated JWT tokens.
Provides a structured representation of validated token claims
with helper methods for scope checking.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime
@dataclass
class TokenClaims:
"""Validated token claims from a JWT.
Represents the extracted and validated claims from an OAuth 2.1
access token after signature and standard validations have passed.
Attributes:
sub: Subject (user identifier).
iss: Issuer URL.
aud: Audience (may be string or list).
exp: Expiration time.
iat: Issued at time.
scopes: List of granted scopes.
email: User email (optional).
name: User display name (optional).
raw_claims: Original token payload for additional claims.
"""
sub: str
iss: str
aud: str | list[str]
exp: datetime
iat: datetime
scopes: list[str] = field(default_factory=list)
email: str | None = None
name: str | None = None
raw_claims: dict = field(default_factory=dict)
def has_scope(self, scope: str) -> bool:
"""Check if the token has a specific scope.
Args:
scope: The scope to check for.
Returns:
True if the token has the specified scope.
"""
return scope in self.scopes
def has_any_scope(self, scopes: list[str]) -> bool:
"""Check if the token has any of the specified scopes.
Args:
scopes: List of scopes to check.
Returns:
True if the token has at least one of the specified scopes.
"""
return any(scope in self.scopes for scope in scopes)
def has_all_scopes(self, scopes: list[str]) -> bool:
"""Check if the token has all of the specified scopes.
Args:
scopes: List of scopes to check.
Returns:
True if the token has all of the specified scopes.
"""
return all(scope in self.scopes for scope in scopes)
@classmethod
def from_jwt_payload(cls, payload: dict) -> TokenClaims:
"""Create TokenClaims from a validated JWT payload.
Args:
payload: The decoded JWT payload dictionary.
Returns:
TokenClaims instance with extracted values.
"""
# Parse scopes from 'scp' (Azure AD) or 'scope' (standard) claim
scopes_raw = payload.get("scp") or payload.get("scope") or ""
if isinstance(scopes_raw, str):
scopes = scopes_raw.split() if scopes_raw else []
elif isinstance(scopes_raw, list):
scopes = scopes_raw
else:
scopes = []
# Parse timestamps
exp_ts = payload.get("exp", 0)
iat_ts = payload.get("iat", 0)
return cls(
sub=payload.get("sub", ""),
iss=payload.get("iss", ""),
aud=payload.get("aud", ""),
exp=datetime.fromtimestamp(exp_ts),
iat=datetime.fromtimestamp(iat_ts),
scopes=scopes,
email=payload.get("email") or payload.get("preferred_username"),
name=payload.get("name"),
raw_claims=payload,
)