HashTools
← All posts

How JWT Actually Works

May 18, 2026·7 min read

A JWT is three base64url-encoded strings joined by dots. First is the header. Second is the payload. Third is the signature. That structure explains most of the common mistakes people make with them.

The three parts

Here's a real JWT (truncated):

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTcxNjIzOTAyMn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Decode the first part and you get the header:

{"alg": "HS256", "typ": "JWT"}

Decode the second and you get the payload:

{"sub": "user_123", "role": "admin", "exp": 1716239022}

The third part is the signature — an HMAC-SHA256 of the header and payload, computed with a secret key the server holds.

The part most people miss

The payload is not encrypted. It is base64url-encoded, which is reversible without any key. Anyone who holds a JWT can read the header and payload: the user themselves, anyone who intercepts it in transit, anyone who gets into your browser storage.

What the signature protects is integrity, not secrecy. It proves the token hasn't been tampered with since it was issued. If someone changes "role": "user" to "role": "admin" in the payload, the signature won't match and a properly implemented server will reject the token.

So: the signature stops tampering. It doesn't hide the data. Both things are true.

Common mistakes

Storing sensitive data in the payload. Passwords, credit card numbers, or anything you wouldn't want public doesn't belong in a JWT. It's visible in browser storage, in network logs, and to the user directly. Only put in the payload what you'd be fine with the user reading.

Not checking the exp claim. The expiry timestamp is just a number in the payload — your server has to check it. Some libraries do this automatically; some require you to opt in. If you forget, tokens are valid indefinitely.

Accepting the alg: none claim. Some older JWT libraries would accept a token with no signature if the header declared the algorithm as "none." This is a historical vulnerability, but it still shows up in the wild. Always enforce the algorithm on the server side — don't trust what the token says its algorithm is.

Skipping audience validation. If you have multiple services and a token is issued for service A, a user could try presenting it to service B. The aud claim exists for this. Each service should check that the token was intended for it.

JWT vs sessions

Sessions store state on the server. The client gets an opaque ID. JWTs put the state inside the token and rely on the secret key for verification — the server stores nothing per-user.

JWTs are useful when you can't share session storage between services. In a microservices setup where service A needs to trust a token issued by service B without a shared database, JWTs handle that cleanly. They're also convenient for mobile clients and APIs where cookie-based sessions are awkward.

The tradeoff that catches people: you can't invalidate a JWT without extra infrastructure. If you issue a 24-hour token and the user changes their password or gets their account suspended, that token stays valid until it expires. Sessions don't have this problem — you delete the session and the user is logged out immediately. There are solutions (token blocklists, short expiry times, refresh token rotation), but they add complexity that erodes the statelessness benefit.

Paste any JWT into the JWT decoder to see the header and payload decoded.