Step 1 of 5 · Reading · ~4 min
Read
JWT Anatomy
JWT Structure: header.payload.signature
A JWT is one long string with two dots in it. Everything a server needs to answer "who is this, and what may they do?" rides in those three segments — no session table, no lookup, no call back to the login service.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiJ9.XaybqYCpG9G33sm03ZZRA3x1axtm8cRBBlsVnp8L6FI
└─────────── header ───────────────┘ └── payload ──┘ └──────────── signature ────────────────┘
Who computes what, and when?
Notice what the API server never does: it never asks the auth server about this user. It recomputes the signature locally. Verify locally, trust nothing else — that is the entire reason JWTs exist.
What is actually inside the three parts?
The first two segments are ordinary JSON, base64url-encoded. The third is the raw output of a signing algorithm, encoded the same way.
Why base64url instead of plain base64?
Standard base64 emits +, / and =. All three are hostile where tokens live:
in a URL + means a space, / is a path separator, = is a key/value delimiter.
RFC 4648 §5 defines a URL-safe alphabet, and JWT uses it throughout.
| standard base64 | base64url | |
|---|---|---|
| value 62 | + | - |
| value 63 | / | _ |
| padding | = up to a multiple of 4 | stripped entirely |
The same 32 signature bytes written both ways:
base64 vLynhsWfwoXvF1pzEl98zLzkpJ+m/hDYMVqvmHH4Bvs=
base64url vLynhsWfwoXvF1pzEl98zLzkpJ-m_hDYMVqvmHH4Bvs
All three differences at once: + became -, / became _, = is gone.
How much padding was dropped?
| input | base64 | base64url | dropped |
|---|---|---|---|
f | Zg== | Zg | 2 |
fo | Zm8= | Zm8 | 1 |
foo | Zm9v | Zm9v | 0 |
foob | Zm9vYg== | Zm9vYg | 2 |
fooba | Zm9vYmE= | Zm9vYmE | 1 |
To decode you put the padding back, and the string's length tells you how much:
Zm9vYg has and , so two = go back on. A remainder of
1 is impossible — no whole number of bytes encodes to a length one more than a
multiple of four. Compute a remainder of 1 and the segment is corrupt, not short.
The idiom and the trap
Signed is not encrypted
A signature proves the payload was not altered and came from someone holding the key. It hides nothing — as the code above shows, anyone with the token reads every claim in it. Never put a password, a card number, or anything you would not print in an access log inside a JWT. If the payload must be secret you need JWE, a different structure with five segments.
Your exercise
You will write b64u_encode and b64u_decode.
The hidden case ENCODE foob expects exactly Zm9vYg. Return
base64.urlsafe_b64encode(...) without .rstrip("=") and you produce
Zm9vYg== — two characters too long, and that is the entire test. Going the
other way, DECODE Zm9vYg raises a padding error unless you re-add those two
= first. The decode cases are deliberately chosen so one needs 0 padding
characters, one needs 1, and one needs 2: a solution that hardcodes any single
amount passes exactly one of them.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…