An Interactive Explainer · v2
From the stateless web all the way to cryptographic fingerprints — step by step.
Act I
When your browser talks to a server, each request is completely independent. There's no persistent connection, no memory of what came before. The server receives a request, sends back a response, and immediately forgets everything.
This is a feature, not a bug — it's why the web scales to billions of people. But it creates an obvious problem. You log in once, and then on the very next request, the server has already forgotten who you are.
Watch what happens when you try to visit a page after logging in:
Press "Send request" to see what happens
So we need a way to carry identity from request to request. The classic approach is sessions: after login, the server saves a record in a database and gives you an ID in a cookie. Every request, it looks up that ID. It works — but now every server in your fleet needs to share that database, and that becomes its own headache.
What if the server could verify your identity without storing anything at all?
Act II
Here's the simplest possible idea. After login, the server hands you a small card: "You are user 42, role: user." Every subsequent request, you just... attach that card. No database needed — the identity travels with the request.
Try it. Edit the card and see what the server believes:
If the server just accepts whatever the request claims — anyone can say anything. There's nothing stopping you from claiming you're the admin.
We need the identity card to be tamper-proof. If you change anything on it, the server needs to instantly know.
Act III
Here's the key idea. There's a mathematical function — called HMAC — that takes two inputs: a message and a secret key. It produces a fixed-length string that looks like random noise. Cryptographers call this a fingerprint — or more formally, a signature.
What makes it useful is two properties:
1. It's deterministic. Same message + same key = always the exact same output. Always.
2. It avalanches. Change even one character of the message, and the entire output changes completely — unpredictably.
And crucially: without the secret key, you can't produce the correct fingerprint for any message. Not even close.
The fingerprint is what cryptographers call a signature. Now that we know what it is, we can use it to make our identity card tamper-proof.
Act IV
Here's how we fix the naive identity card. When the server issues your card at login, it doesn't just write your data — it also computes a fingerprint of that data using its secret key, and appends it to the card.
Now if you try to change anything on the card — your userId, your role, your expiry date — the fingerprint won't match anymore. And since you don't have the key, you can't compute the new correct fingerprint either.
Three parts joined by dots — that's the entire JWT format
Each part is encoded as Base64url — a way of turning bytes into plain ASCII text so it can travel safely in HTTP headers and URLs. The three encoded strings are joined with dots. That final dot-separated string is your JWT.
Notice the Claims (part ②) — that's what many docs unhelpfully call the "payload". It's the frozen identity data the server assigned you at login. This is not the same as the HTTP request body. The JWT token lives in the Authorization header, completely separate from whatever the request is actually doing:
The HTTP request answers "what do you want?" — it changes with every call. The JWT token answers "who are you?" — it stays exactly the same for the lifetime of your session. Two separate things, riding together in the same request.
Act V
When a JWT arrives, the server does something beautifully simple. It splits the token on the dots, takes parts ① and ②, runs HMAC with its own secret key, and compares the result against part ③.
If they match — the claims are genuine. The server didn't have to look anything up. Just math.
Try breaking the token and watch the validation fail:
This is the key insight: the fingerprint in part ③ is a cryptographic commitment to the exact contents of parts ① and ②. Change one byte anywhere — including the expiry date — and you've broken that commitment. And you can't fix it, because fixing it requires the server's secret key.
Act VI
Base64url encoding looks like gibberish, but it's not secret. It's just a way of turning bytes into URL-safe text characters. Anyone who intercepts a JWT can decode the claims and read them in plain text — instantly.
So the rules are simple: never put sensitive data in a JWT. Passwords, credit card numbers, personal details — none of that goes in the claims. Anyone can read it.
In practice this is fine, because JWTs travel over HTTPS, which encrypts the whole connection. The token is private in transit. But if you need the stored token itself to be secret (say, you're logging audit trails or storing tokens at rest), you'd use JWE — JSON Web Encryption — which actually encrypts the claims. Most applications don't need that.
The Full Picture
And that's it. The server holds a secret. At login it uses that secret to compute a fingerprint of your identity data. It hands you both, bundled together. On every future request, you hand that bundle back. The server re-runs the math — if the numbers agree, it trusts you. No session database. No server memory. Just arithmetic.
One small limitation to be aware of: because the server stores nothing, it can't easily revoke a token before it expires. If a token is stolen, it stays valid until the expiry. Most production systems work around this with short expiry times (15 minutes) paired with longer-lived refresh tokens, sometimes plus a small revocation list for emergencies.