Step 1 of 5 · Reading · ~3 min
Read
JWT Anatomy
Header & Payload
Two JSON objects, encoded and joined by a dot. The header says how the token is protected, the payload what is asserted. Keep that split straight and most JWT confusion goes away.
What belongs in the header?
{"alg":"HS256","typ":"JWT"}
| Parameter | What it means |
|---|---|
alg | Required. The algorithm that produced the signature: HS256, RS256, ES256, none. |
typ | Optional. Media type of the whole token. RFC 7519 §5.1 recommends "JWT" and says JWT implementations ignore it — it exists so an app handed several kinds of object can tell them apart. |
cty | Optional, and only for a nested JWT (one carried as the payload of another), where it MUST be "JWT" (§5.2). |
kid | Optional. Which key signed this, so a verifier holding several picks the right one. |
There is no "typ":"JWE" — an encrypted JWT is still typed "JWT"; its
five-segment structure makes it a JWE. And typ is not a security control; the spec
tells implementations to ignore it.
The header is not protected until you check the signature — which is computed using the algorithm the header names. That circularity is the root of nearly every JWT vulnerability.
What belongs in the payload?
The payload is the JWT Claims Set: a flat JSON object of name/value pairs, each a statement about the subject. RFC 7519 sorts claim names into three classes, and the class says whether you may invent one.
| Class | Rule | Example |
|---|---|---|
| Registered (§4.1) | The seven IANA-registry names. The RFC defines their meaning; all are OPTIONAL. | iss sub aud exp nbf iat jti |
| Public (§4.2) | IANA-registered, or collision-resistant — in practice a URI you control. | https://api.parcelforge.com/tenant |
| Private (§4.3) | A bare name you and your consumer agreed on. | role, plan |
The seven registered claims
| Claim | Name | Meaning |
|---|---|---|
iss | issuer | who minted it |
sub | subject | who it is about |
aud | audience | who may accept it |
exp | expiration time | reject at or after this instant |
nbf | not before | reject before this instant |
iat | issued at | when it was minted, so you can judge its age |
jti | JWT ID | unique id, so a replay can be spotted |
exp, nbf and iat are NumericDate values: a JSON number of seconds
since the Unix epoch. Not a string, not milliseconds. Emitting
"exp":"2027-01-01T00:00:00Z" is the most common claims bug — compliant verifiers
reject it, sloppy ones coerce it to 0 and decide every token expired in 1970.
Two rules the spec states that almost nobody implements
Claim names must be unique (§4) — a parser must reject a duplicate or keep only
the lexically last. If one component keeps the first sub and another the last, one
token authenticates as two different people.
Claims you do not understand must be ignored (§4) — and never assume one is present just because your issuer usually sends it.
Encode the bytes you were given
Same JSON value, two different segments — json.dumps adds a space after every
: and ,. RFC 7519 §7.1 allows whitespace here and requires no
canonicalization, so the exact bytes get signed. Re-serialize and you have built a
different token.
✓ encode the bytes you received or were handed
✗ json.loads(...) then json.dumps(...) and encode that
Your exercise
You will read <header_json>|<payload_json> and print
<base64url(header)>.<base64url(payload)>.
The hidden case feeds you {"typ":"JWT","alg":"RS256"} — typ first, which no
JSON library emits on its own. Parse and re-dump it and you produce
eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJSUzI1NiJ9 rather than
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9, failing on three extra spaces. Split on
| and encode each half's bytes untouched.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…