Configuration¶
ConfigDict is Pydantic's ConfigDict with four
extra keys. Because it subclasses the original, every standard Pydantic setting
keeps working next to the JWT ones:
from pydantic_jwt import ConfigDict, JWTModel
class AccessToken(JWTModel):
model_config = ConfigDict(
# JWT keys
algorithm="HS256",
encoding_key=SECRET,
decoding_key=SECRET,
require_keys=True,
# ordinary Pydantic keys
extra="forbid",
frozen=True,
populate_by_name=True,
)
sub: str
The keys¶
| Key | Type | Default | Meaning |
|---|---|---|---|
algorithm |
str |
— | Algorithm used to sign and verify, e.g. "HS256". Passed straight to PyJWT. |
encoding_key |
str \| None |
— | Key used by generate() and str(). |
decoding_key |
str \| None |
— | Key used to verify incoming tokens. |
require_keys |
bool |
True |
Whether a missing key is an error. False accepts tokens without verifying the signature. |
ConfigDict is total=False, so any subset is valid — including none of them,
for a model that only ever receives keys per call.
All four are plain TypedDict keys, which means a typo like algorythm= is
caught by mypy rather than silently ignored at runtime.
Symmetric keys¶
For the HMAC family (HS256, HS384, HS512) the same secret signs and
verifies:
import os
SECRET = os.environ["JWT_SECRET"]
class AccessToken(JWTModel):
model_config = ConfigDict(algorithm="HS256", encoding_key=SECRET, decoding_key=SECRET)
sub: str
Use at least 32 bytes of entropy — secrets.token_hex(32) — or PyJWT warns with
InsecureKeyLengthWarning.
Asymmetric algorithms¶
RS*, ES*, PS* and EdDSA need PyJWT's cryptography backend
(pip install "pyjwt[crypto]"). The keys are PEM strings: the private key
signs, the public key verifies.
The issuing service holds both, or just the private key:
class AccessToken(JWTModel):
model_config = ConfigDict(
algorithm="RS256",
encoding_key=PRIVATE_KEY_PEM,
decoding_key=PUBLIC_KEY_PEM,
)
sub: str
exp: Exp = after(minutes=15)
A consuming service that only ever reads tokens configures the public key and
nothing else. It cannot mint tokens, and generate() on it raises
jwt_missing_key — which is exactly the desired failure:
class IncomingToken(JWTModel):
model_config = ConfigDict(algorithm="RS256", decoding_key=PUBLIC_KEY_PEM)
sub: str
exp: Exp
Per-call keys¶
Keys do not have to live in model_config. Both directions accept them per
call, which covers key rotation and multi-tenant setups where the key depends on
the request.
Signing — generate():
Verifying — from_token():
Verifying through model_validate() — the same values, passed in the
validation context:
token = AccessToken.model_validate(
raw,
context={"decoding_key": current_key, "algorithm": "HS256", "require_keys": True},
)
The context form is the one to use when the token is a field of a larger
model, since there is no from_token() call to pass arguments to. Only str
values for decoding_key/algorithm and a bool for require_keys are read;
anything else in the context is ignored by the signature check (but still
reaches your own validators, and validate_claims is read by the
claim validators).
Rotating a signing key¶
Sign with the newest key, accept any key still in the rotation window:
from pydantic import ValidationError
from pydantic_core import PydanticCustomError
KEYS = {"2024-06": OLD_SECRET, "2024-09": NEW_SECRET}
CURRENT = "2024-09"
def issue(sub: str) -> str:
return AccessToken(sub=sub).generate(encoding_key=KEYS[CURRENT], algorithm="HS256")
def read(raw: str) -> AccessToken:
for key in KEYS.values():
try:
return AccessToken.from_token(raw, decoding_key=key, algorithm="HS256")
except (ValidationError, PydanticCustomError):
continue
raise ValueError("no configured key verifies this token")
Once every token signed with OLD_SECRET has expired, drop it from KEYS.
Retry only on signature failures
The loop above retries on any error, so an expired token is tried against
every key before failing. To keep the diagnostics sharp, catch
jwt_invalid_signature specifically — see
Validation and errors.
require_keys¶
When a model has no decoding_key/algorithm and none is supplied per call,
require_keys decides what happens:
True(the default) — verification is impossible, so reading the token fails withjwt_missing_key.False— the signature is not checked. The claims are still validated, a warning is logged to thepydantic_jwt.baselogger, and the model is returned.
class UnverifiedToken(JWTModel):
model_config = ConfigDict(require_keys=False)
sub: str
exp: Exp
UnverifiedToken.from_token(anything_at_all)
# WARNING pydantic_jwt.base: JWT token approved without signature verification
This is for tests, local development and one-off inspection scripts. In an application it turns your auth layer into a formality — anybody can forge a token. See Security notes.
Note that require_keys only matters when a key is missing. With a key
configured, the signature is always checked, whatever require_keys says.
Reading configuration at runtime¶
model_config is a plain dict, so the effective values are readable — handy in
tests and health checks:
AccessToken.model_config["algorithm"] #> 'HS256'
AccessToken.model_config.get("require_keys", True) #> True
API reference¶
Full definition: ConfigDict.