Working with raw tokens¶
JWTStr is a str subclass that validates the
structure of a JWT and exposes its three parts. It is what
JWTModel uses internally to split an incoming token, and it is
usable on its own whenever you need to look at a token without turning it into a
model.
from pydantic_jwt import JWTStr
token = JWTStr("eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dGVzdA")
token.header #> {'alg': 'HS256'}
token.algorithm #> 'HS256'
token.payload #> {'sub': '1234567890'}
token.signature #> b'test'
Since it is a str, it can go anywhere a string goes:
JWTStr verifies nothing
Constructing a JWTStr checks the shape of the token, not its signature
and not its claims. token.payload["sub"] is attacker-controlled data:
anyone can craft a structurally valid JWT with any payload they like. Use it
for routing and diagnostics, never for authorisation. Verification happens in
JWTModel.from_token().
What is checked¶
Construction raises PydanticCustomError unless all of the following hold:
- the value is a
str— otherwisejwt_type; - it splits on
.into exactly three parts — otherwisejwt_format("Value must include header, payload, and signature separated by dots"); - all three parts decode as urlsafe base64 (padding is added automatically, and
standard-alphabet characters like
+and/are rejected) — otherwisejwt_format; - header and payload decode as UTF-8 and parse as JSON — otherwise
jwt_format; - both are JSON objects, not arrays or scalars — otherwise
jwt_type.
validate()¶
validate() is the non-raising form — a
predicate for filtering:
JWTStr.validate("eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.dGVzdA") #> True
JWTStr.validate("not-a-token") #> False
JWTStr.validate(12345) #> False
As a Pydantic field¶
JWTStr carries its own core schema, so it works as a field type on any model,
and describes itself to JSON Schema as a string with format: jwt:
from pydantic import BaseModel
from pydantic_jwt import JWTStr
class LoginResponse(BaseModel):
access_token: JWTStr
token_type: str = "bearer"
LoginResponse.model_json_schema()["properties"]["access_token"]
#> {
#> 'type': 'string',
#> 'format': 'jwt',
#> 'title': 'Access Token',
#> 'examples': ['eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dGVzdA'],
#> }
That makes it a good type for a response model or a request body that carries a
token this service does not itself verify — a refresh token forwarded to another
service, say. Validation failures come back as a normal ValidationError.
Practical uses¶
Routing before verification¶
A gateway that trusts several issuers can read the header to pick the key, then verify properly:
raw = JWTStr(authorization.removeprefix("Bearer "))
key = KEYS_BY_KID.get(raw.header.get("kid"))
if key is None:
raise HTTPException(401, "Unknown key id")
token = AccessToken.from_token(raw, decoding_key=key, algorithm="RS256")
The alg from the header is deliberately not reused —
from_token() always takes the algorithm
from your configuration, which is what defeats algorithm confusion. Read
raw.algorithm for logging, not for deciding.
Diagnostics¶
import logging
logger = logging.getLogger(__name__)
def explain(raw: str) -> None:
if not JWTStr.validate(raw):
logger.warning("not a JWT at all")
return
token = JWTStr(raw)
logger.info("alg=%s claims=%s", token.algorithm, sorted(token.payload))
Log claim names, not values — a payload can hold personal data, and the token itself is a credential.
From a model¶
JWTModel.jwt_str hands you the signed token
already wrapped:
API reference¶
Full signatures: JWTStr.