A PR review shows a new JWT-based auth flow. The token gets slapped in an Authorization header and everyone moves on. Then a report comes in: users are getting logged out randomly. Someone checks the JWT, sees a string of Base64, and has no idea what to look for.
JWTs are not magic. They're three Base64-encoded strings separated by dots. You can decode the first two parts with any Base64 decoder. No secret key needed. The header and payload are readable by design. The signature is what proves they haven't been tampered with.
The Three Parts of a JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIn0.
abc123signature
Each segment separated by a dot decodes to something specific:
| Part | Encoded (what you see) | Decoded (what it means) |
|---|---|---|
| Header | Base64 | {"alg": "HS256", "typ": "JWT"} |
| Payload | Base64 | {"sub": "1234567890", "name": "Alice"} |
| Signature | Hash | Binary — not meant to be read directly |
The header tells you the algorithm. The payload contains the claims (who, when, expiration, custom data). The signature is a hash computed from the first two segments plus a secret key.
Why Debugging JWTs Is Annoying
The most common issue: someone copies a JWT from a network tab or cookie, tries to read it manually, and misses that the token was truncated, or misreads a claim because they're looking at raw Base64.
A JWT decoder pipes the token through a proper Base64url decoder and shows the JSON in a readable format. It highlights the three things that usually break:
- Expired tokens: the
expclaim is a Unix timestamp. Compare it to the current time. If it's in the past, the token is dead. - Wrong issuer: the
issclaim should match your auth server. If you're using Auth0 and the token says a different domain, something's off. - Missing claims: some APIs expect specific custom claims. A decoder shows you exactly what's there and what's not.
What Most Tutorials Leave Out
JWTs can get surprisingly large. Storing user permissions in the payload pushes a single JWT past 4KB easily. That's a problem for cookies (most browsers cap at 4KB per cookie) and sometimes for headers. Nginx rejects requests with oversized headers by default, and the error message gives no hint that the token size is the problem.
The signature does NOT verify the data is correct. It only proves the data hasn't changed since it was signed. If someone signs a token with "role: admin" and the server signs it, the signature is valid, even if the user shouldn't be an admin. Some teams assume JWT verification replaces server-side permission checks. That's how privilege escalation happens.
RS256 and HS256 are fundamentally different in a way that matters for microservices. HS256 uses a single shared secret, which means any service that can verify a token can also mint a new one. RS256 uses a public/private key pair. Services verify with the public key, and only the issuer holds the private key. For microservices, RS256 is usually the better choice because it limits which services can issue tokens.
Client-Side Is Fine
There's a persistent myth that you should never decode JWTs on the client side because "it's not secure." The header and payload are already readable by anyone who has the token. Decoding them on the client is fine. You need to read claims like exp to decide whether to redirect to login, or to get the user ID for API calls.
What you shouldn't do is trust the decoded data for authorization decisions. That's what the server is for. A lot of JWT libraries don't help here — they make verification look so simple that developers assume it covers everything.
Another thing that doesn't come up in tutorials: the alg: none attack. Some JWT libraries accept tokens with the algorithm set to "none" and skip signature verification entirely. If your server doesn't explicitly reject "none" tokens, an attacker can forge a token with arbitrary claims and no signature. Most modern libraries protect against this by default, but not all of them.
If you're debugging a JWT issue, paste the token into the JWT Decoder and look at the raw claims. The problem is usually obvious once you see what's actually in there.
For the full JWT specification, the IETF RFC 7519 defines the standard claims and format.