← All posts
Try it

Read this JWT: four tokens, which one would you trust?

Four JSON Web Tokens, three with a weakness you can see from the header and payload alone. Spot each flaw before the answers, then paste any token into the JWT inspector to confirm — and read the one-line server fix for each.

A JSON Web Token carries its own instructions for how it should be checked, and that is the whole problem: a careless verifier does what the token's header tells it. You do not need to attack anything to learn this. You need to read a token the way the server reads it, and notice when the server is being told to lower its own guard.

So this is a reading drill. Below are four tokens. For each, answer one question: would you accept this on an endpoint that returns someone's account? The first two parts of a JWT are only base64url-encoded, not encrypted, so you can read them by eye once you know the shape. Decide on all four before the answers.

To confirm, paste any token into the JWT inspector. It decodes the header and payload, turns the timestamps into real dates, and names the weaknesses, and nothing you paste is uploaded. Every flaw below is paired with the one line of server code that removes it, because reading the weakness is only half the point.

The shape

A token is three base64url strings joined by dots: header.payload.signature. The header says which algorithm signs it. The payload holds the claims, and the ones that matter for trust are exp (when it expires), alg in the header, and whatever your app reads to make decisions, usually something like role. The signature is what proves the first two parts were not changed — if the server actually checks it.

Token 1

eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMDI0Iiwi
bmFtZSI6IlByaXlhIE5haXIiLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE3
NjAwMDAwMDAsImV4cCI6MTc5MDAwMDAwMH0.

Token 2

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1NTMxIiwi
bmFtZSI6IlNhbSBPa2Fmb3IiLCJyb2xlIjoidXNlciIsImlhdCI6MTc2
MDAwMDAwMCwiZXhwIjoxNzkwMDAwMDAwfQ.EqvDQ81v_WaEVN3UxZO9
Raz-ktvMIBCbTwX2Fb3ZZ8Q

Token 3

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI4ODkwIiwi
bmFtZSI6IkxlbmEgRmlzY2hlciIsInJvbGUiOiJ1c2VyIiwiaWF0Ijox
NzYwMDAwMDAwfQ.U9Tf8T_v_0EUHbvLKUwVKNAC-LCanbToqdFLrOwfDuI

Token 4

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjIwMjYtcm90
LTMifQ.eyJpc3MiOiJodHRwczovL2F1dGguYWNtZS5jb20iLCJhdWQi
OiJhY21lLWFwaSIsInN1YiI6Ijc3NDIiLCJyb2xlIjoidXNlciIsImlh
dCI6MTc2MDAwMDAwMCwiZXhwIjoxNzYwMDAzNjAwfQ.<rsa-signature>

Decode the header and payload of each in your head, or in the inspector, and commit to four answers.

Answers

Token 1: do not trust it. The header says alg: none. Decode the first segment and it reads {"alg":"none","typ":"JWT"}, and the token ends in a dot with nothing after it — there is no signature at all. A verifier that honours the header's choice of algorithm will accept this token because it was told no signature is required, and the payload claims role: admin. The header is attacker-controlled input, so letting it pick the algorithm is the mistake. The fix: pin the algorithm on the server instead of reading it from the token — verify(token, key, { algorithms: ['RS256'] }) — and reject none outright.

Token 2: do not trust it. It is HS256 signed with a guessable secret. The header is honest and the signature is real, so a plain decoder calls this valid. The weakness is not visible in the token; it is in the key. With HS256 the same secret both signs and verifies, so a secret that is a dictionary word or a framework default can be recovered offline from any token the server ever issued, and then anyone can mint their own. Paste this one into the inspector and its secret test flags it. The fix: use a long random secret, at least 32 bytes from a real random source, or move to an asymmetric algorithm like RS256 so the signing key never sits on the verifying service at all.

Token 3: do not trust it. There is no exp claim. The payload decodes to sub, name, iat and role, and no expiry. The signature is fine and the secret is strong, so this token is cryptographically valid — forever. A token that never expires cannot be aged out, which is the hard half of why a JWT cannot be logged out: revocation depends on the token lapsing on its own. The fix: always set a short exp (minutes, not months) and have the server reject a token that is missing it, rather than treating absent-expiry as never-expires.

Token 4: this is the one to trust, as far as a token alone goes. It is RS256, so the API verifies with a public key and never holds a secret that could sign. It names a kid, so the key can be rotated. And the payload carries iss, aud and a one-hour exp — the claims that let the server confirm the token was minted by the right issuer, for the right audience, and recently. There is still work the server must do: check iss and aud, verify the signature against the key named by kid, and enforce exp. But nothing in the token is telling the server to weaken itself.

What the score means

Four out of four, and alg: none jumped out of token 1's header: you have read tokens before, or been handed one in an incident. Two or three: the usual miss is token 3, because a missing claim is harder to notice than a wrong one — you have to see what is not there. One or zero: you were trusting that a token which decodes cleanly is a token that was checked properly, and those are different things.

The habit that survives this drill: a JWT tells you what it claims, never that anyone verified it. Read the header's algorithm, confirm the server pins it rather than obeys it; read the payload for an exp and the claims your app trusts; and remember that a valid signature proves the token was not altered, not that the person holding it should have it.

Your JWT cannot be logged out is the long version of the session problem behind token 3, and the JWT inspector will read any token you hand it and name these weaknesses for you.

Try itAuthenticationWeb security
It looks strict. It permits everything. Which is which?Try it

Read this CSP: four policies, which one actually stops an XSS?

Four Content-Security-Policy headers that all look strict. Predict what each one really permits before you read the answers, then paste any of them into the CSP builder to confirm.

A signed token stays valid until it expires, whatever you doWeb security

Your JWT cannot be logged out

The forgery bugs get all the attention and they are the easy half. The hard half is that a signed token stays valid until it expires, whatever you do afterwards — so a logout button, a password reset and a termination all quietly fail to end a session.

Your fingers type lodahs. The install script runs anyway.Try it

Spot the typosquat: eight package names, which one is fake?

Eight install commands, each with one impostor package name hidden among the real ones. Train the instinct that reads a dependency name twice, because the install script runs before any of your code does.