Source code for loom.core.identity.issuer
"""Minting a credential for a verified :class:`Identity`.
The port lives beside the identity it consumes, not beside the transport that
implements it: the caller is a login use case, so putting the abstraction in
``loom.rest`` would make the application layer import infrastructure. What is
domain here is *"this service needs to mint a credential for this identity"*;
that a JWT comes out is the detail, and it belongs to the implementation.
"""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import Protocol
from loom.core.identity.identity import Identity
[docs]
@dataclass(frozen=True, slots=True)
class IssuedToken:
"""A minted credential together with what the issuer already knew about it.
Returned instead of a bare string so a login endpoint never has to decode
the token it just signed: the expiry the HTTP response advertises and the
identifier the audit trail records are both produced by the signing step.
Attributes:
token: The encoded credential, ready to travel as a bearer token.
expires_at: Instant the credential stops being valid, timezone-aware.
jti: Unique identifier of this minting, for correlation and audit.
"""
token: str
expires_at: datetime
jti: str
[docs]
class TokenIssuer(Protocol):
"""Mints a credential that an :class:`Authenticator` can later verify.
Implementations guarantee the round trip: whatever the identity carries —
subject, roles and attributes — a matching authenticator recovers intact.
Everything the credential says comes from the identity, so no caller can
smuggle a claim past it.
Example::
issued = issuer.issue(identity)
response = {"access_token": issued.token, "expires_at": issued.expires_at}
"""
[docs]
def issue(self, identity: Identity, *, ttl: timedelta | None = None) -> IssuedToken:
"""Mint a credential for *identity*.
Args:
identity: Verified caller the credential speaks for.
ttl: Lifetime override. ``None`` uses the configured one, which is
also the ceiling: a longer lifetime is refused.
Returns:
The minted credential and its metadata.
Raises:
ValueError: If the identity cannot be represented — anonymous, or
carrying an attribute that would be unreadable once encoded —
or if *ttl* exceeds the configured lifetime.
RuntimeError: If the credential cannot be produced. Implementations
keep the cause out of the traceback: it can carry key material.
"""
...