Defaults¶
Three helpers produce Pydantic field defaults for the values a token needs
generated at issue time. All of them return a Field(default_factory=...), so
the value is computed per instance โ every token gets its own timestamp and
its own id.
from pydantic_jwt import Exp, Iat, JWTModel, after, at, uuid
class AccessToken(JWTModel):
sub: str
exp: Exp = after(minutes=15)
iat: Iat = after()
jti: str = uuid()
after()¶
after() returns "now plus a duration", as an integer
epoch second.
All arguments are keyword-only, accept floats, and add up:
exp: Exp = after(hours=1, minutes=30) # 5400 seconds from now
exp: Exp = after(days=30) # a refresh-token lifetime
iat: Iat = after() # no arguments: now
The duration is fixed when the class is defined; the base is read fresh on every instantiation:
class SessionToken(JWTModel):
exp: Exp = after(minutes=5)
first = SessionToken() # exp = t0 + 300
# ... an hour passes ...
second = SessionToken() # exp = t0 + 3600 + 300
The result is truncated to a whole second with int(), as JWT NumericDate
requires.
Short access tokens, long refresh tokens
A conventional split is after(minutes=15) for an access token and
after(days=30) for a refresh token. Since a JWT cannot be revoked before it
expires, the access token's lifetime is your worst-case exposure window.
at()¶
at() pins a claim to a fixed moment rather than an offset:
from datetime import datetime, timezone
from pydantic_jwt import Nbf, at
class ScheduledToken(JWTModel):
sub: str
nbf: Nbf = at(datetime(2030, 1, 1, tzinfo=timezone.utc))
Every instance gets the same timestamp โ useful for a token that must not become valid until a launch date, or for deterministic tests.
Always pass an aware datetime
at() uses datetime.timestamp(), and a naive datetime is interpreted in
the server's local timezone. The same code then produces different tokens
on a developer laptop and a UTC container. Pass tzinfo=timezone.utc (or
any explicit zone).
uuid()¶
uuid() returns a fresh UUID4 string per instance โ
normally for the jti claim, which
RFC 7519 ยง4.1.7
defines as a unique identifier for the token.
Pass hex_uuid=True for the 32-character form without hyphens:
A jti is what makes selective revocation possible: store the ids of tokens you
want to reject in a denylist keyed until their exp, and check it after
validation.
token = AccessToken.from_token(raw)
if await revoked.exists(token.jti):
raise PermissionError("token revoked")
Return types¶
All three are annotated -> Any, deliberately: they return a FieldInfo, but
annotating them as such would make exp: Exp = after(minutes=15) a type error in
every checker. Any lets the assignment type-check against the field's real
annotation.
You can therefore combine them with other Field() settings only by dropping to
Field() directly:
from pydantic import Field
exp: Exp = Field(default_factory=lambda: int(time.time() + 900), description="Expiry")
Anything else Pydantic can do¶
These helpers are conveniences, not a closed set. Any default_factory works:
from pydantic import Field
class AccessToken(JWTModel):
sub: str
iss: str = "https://auth.example.com" # a constant default
scope: list[str] = Field(default_factory=list)
kid: str = Field(default_factory=current_key_id)
API reference¶
Full signatures: Claims and defaults.