JWT Security: How JSON Web Tokens Work and How They Get Abused
· by Andergrove Software
A JSON Web Token (JWT) is signed, not encrypted. The header and payload are just base64url-encoded JSON, so anyone holding the token can read every claim inside it. The signature does not hide the contents; it proves the token has not been altered since the server issued it. Confusing those two ideas, secrecy versus integrity, is behind most JWT mistakes.
Here is how a JWT is actually put together, the attacks that target each part, and a checklist for issuing and validating them safely. You can pull any token apart in the JWT decoder as you read. It runs entirely in your browser, so it is safe to inspect a real token without sending it anywhere.
Anatomy: three base64 parts
A JWT is three base64url strings joined by dots: header.payload.signature.
- Header. JSON naming the signing algorithm, e.g.
{"alg":"HS256","typ":"JWT"}. - Payload. JSON "claims": who the token is for, when it expires, and whatever else you put in.
- Signature. The header and payload signed with a key.
Decode the first two parts and you get plain JSON. Paste a token into the decoder and you will see the payload in clear text. That is the single most important thing to understand: the payload is encoded, not encrypted. Base64 is reversible by anyone, so never put a secret or sensitive personal data in it.
How the signature works
The signature is what makes a JWT trustworthy. Two families are common:
- HS256 (HMAC + SHA-256). A symmetric secret: the same key signs and verifies. Simple, but anyone who can verify can also forge, so the secret must stay on your servers.
- RS256 (RSA signature). An asymmetric key pair: a private key signs, the matching public key verifies. Useful when third parties need to verify tokens without being able to mint them.
Either way, the server recomputes the signature over the received header and payload and checks that it matches. Change a single character of the payload and the signature no longer validates. That is the integrity guarantee, and it says nothing about secrecy. The hash underneath HS256 is the same one explained in SHA-256 explained.
The classic attacks
alg: none. The spec allows an "unsecured" token with no signature. A naive library that honours the header'salgwill accept a token withalg:noneand no signature at all, letting an attacker forge any claims. Always pin the expected algorithm server-side; never let the token's own header choose it.- Algorithm confusion (RS256 to HS256). If your code verifies "whatever alg the header says," an attacker can take your public RSA key (public by design), switch the header to HS256, and sign a forged token using that public key as the HMAC secret. A server expecting RS256 then verifies HS256 with the public key, and it matches. The fix is the same: pin the algorithm and key type.
- Weak HMAC secrets. HS256 is only as strong as its secret. A short or dictionary secret can be brute-forced offline from a single captured token. Use a long, random secret.
- Missing expiry or claim checks. A token with no
exp, or a server that never checks it, is valid forever. Always set and verifyexp, and validateaud(audience) andiss(issuer) so a token minted for one service cannot be replayed against another.
What to put in a token, and what not to
Put in: a subject (sub), an expiry (exp), an issued-at
(iat), the audience (aud) and issuer (iss), and minimal
authorization data such as roles or scopes. Keep it small, because tokens travel on every
request.
Keep out: passwords, API keys, secrets, and any personal data you would not want readable. The payload is visible to anyone holding the token.
A safe validation checklist
- Pin the algorithm server-side and reject anything else, including
none. - Verify the signature with the right key before trusting any claim.
- Check
exp(andnbfif used) on every request. - Check
audandissmatch your service. - Use a strong, rotated secret (HS*) or protect your private key (RS*).
- Keep payloads minimal and non-sensitive.
Inspect a token safely
The fastest way to build intuition is to take a token apart. The Andergrove JWT Decoder splits a token into header, payload and signature and shows the decoded JSON, entirely in your browser, so you can inspect production tokens without pasting them into someone else's server. Decode one, then change a character in the payload and note that the signature no longer matches. That mismatch is the whole point of a signed token.