An Interactive Explainer · v2

How JWT Actually Works

From the stateless web all the way to cryptographic fingerprints — step by step.

▼ scroll to begin

Act I

The Web Has No Memory

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:

Browser 🧑 ``` Server GET /dashboard 401 Who are you? request response

Press "Send request" to see what happens

Simulate:
```

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

The Naive Fix — Just Tell the Server Who You Are

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:

HTTP REQUEST GET /dashboard Host: api.example.com Accept: application/json ``` ··· + identity IDENTITY CLAIM userId: 42 role: user SERVER RESPONSE Welcome, user 42!
userId claim
role claim
✓ Server trusts it: "Hello user 42, you have user access."
Try:
```

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

Fingerprints: Proving a Message Is Untouched

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.

MESSAGE {"userId":42,"role":"user"} ``` SECRET KEY only-the-server-knows HMAC SHA-256 FINGERPRINT computing... ← changed!
Message (try editing)
Secret key
Full fingerprint (hex):
computing...
Try:
```
Try it Change just one character in the message. The entire fingerprint changes completely — there's no partial match, no way to predict what the new value will be. Now try changing the key — same effect. Without the key, you cannot compute the right fingerprint for any message.

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

The Tamper-Proof Token

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.

``` ① HEADER alg: "HS256" typ: "JWT" algorithm metadata ② CLAIMS userId: 42 role: "user" exp: 1700000000 your frozen identity data ③ FINGERPRINT (SIGNATURE) HMAC( ① + ② , secret_key ) SflKxwRJSMeKKF2QT4fwpMeJf 36POk6yJV_adQssw5c tamper-proof seal . .

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:

``` HTTP REQUEST GET /dashboard HTTP/1.1 Host: api.example.com Accept: application/json Authorization: Bearer eyJhbGci...V_adQssw5c [no body — it's a GET] ← changes every request ← same JWT on every request
```

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

How the Server Validates the Token

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:

``` INCOMING TOKEN eyJhbGci... . eyJ1c2VySWQ6NDIsInJvbGUiOiJ1c2VyIn0 . SflKxwRJ...w5c CLAIMS (from token) userId:42 role:user exp:1700000000 HMAC server re-signs header + claims SIGNATURE (from token) SflKxwRJ...w5c computed: SflKxwRJ… == ✓ VALID
✓ Valid — fingerprint matches. Trusting claims: user 42, role user.
Try tampering:
```
The server doesn't need to remember anything. It just runs the same math it ran at login, and checks whether the answer matches. No database. No state. Just a hash function that takes a microsecond.

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

One Thing JWT Doesn't Do: Hide Your Data

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.

BASE64URL ENCODED eyJ1c2VySWQiOjQyLCJyb2xl IjoidXNlciIsImV4cCI6MTcw MDAwMDAwfQ looks secret... ``` decode() no key needed PLAIN JSON { "userId": 42, "role": "user", "exp": 1700000000 }
Try decoding any Base64url string:
```

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.

JWT provides integrity — proof that data wasn't tampered with. HTTPS provides confidentiality — privacy in transit. These are different guarantees, and JWT only delivers one of them.

The Full Picture

Putting It All Together

``` 1 Login: send username + password Server checks credentials → creates claims → computes fingerprint → issues JWT 2 Client stores the JWT (memory or httpOnly cookie) On every subsequent request: attach it in the Authorization header Authorization: Bearer eyJhbGci...SflKxwRJ...w5c 3 Server receives request → splits JWT → re-runs HMAC Match → trusted. Read userId, role. Handle request. No match → 401. Done. No database consulted. 4 exp passes → token expired → re-authenticate Usually done silently via a refresh token. Short expiry = stolen token dies fast. Typical access token lifetime: 15 minutes.
```

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.