guide · auth

JWTs in practice: signing, verifying and the mistakes that leak accounts

A JSON Web Token looks like an impenetrable wall of characters, which fools people into trusting it. It shouldn't. The string is readable by anyone, and almost every JWT vulnerability comes down to a server trusting a token it never properly checked.

QuickDevKit
Built and maintained by an independent developer · Updated June 2026

JWTs are everywhere in modern auth, and they're powerful precisely because they're self-contained: a server can check one without looking anything up. That same property is where teams get burned, because "self-contained and readable" quietly becomes "trusted" in code that was written in a hurry. This guide is about the gap between reading a token and trusting it, and how to stay on the right side of it.

the shape

Three parts, two of them public

A JWT is three dot-separated Base64url segments — header.payload.signature. The first part names the signing algorithm, the payload carries the claims (who the user is, when the token expires), and the signature is a cryptographic stamp over the first two parts. The first two are merely encoded, not encrypted — paste any token into the JWT decoder and you'll read the payload instantly, no key required. So the mental model to start from is: the contents are a postcard, not a sealed letter. The signature doesn't hide anything; it only proves the postcard wasn't rewritten in transit.

verification

How a server checks a token

Verification depends on the algorithm, and the difference matters for your architecture. With a symmetric algorithm like HS256, one secret both signs and verifies. The server recomputes an HMAC over header.payload with that secret and checks it equals the signature on the token. It's simple and fast, but every party that can verify can also forge, so the secret can't be shared with clients.

With an asymmetric algorithm like RS256, a private key signs and a matching public key verifies. The auth server holds the private key; anything that needs to validate tokens — your API, a third-party service — gets only the public key. That's what makes RS256 the right choice once more than one service has to trust the same tokens, since you can hand out the public key freely without letting anyone mint tokens. The mechanics are the same idea as HMAC underneath, but the key being public or private changes who's allowed to do what.

The mistake that matters most: decoding is not verifying. Reading the payload tells you what a token claims; it tells you nothing about whether those claims are true. A user can edit the payload and re-encode it in seconds. Authorization decisions must come from a verified signature on the server, never from decoded claims — and the decoder here deliberately stops at reading, because checking a signature needs the key and belongs server-side.

two tokens

Why you usually issue an access token and a refresh token

Statelessness is a JWT's best feature and its sharpest edge. Because nothing is stored server-side, you can verify a token without a database hit — but you also can't easily revoke one. A leaked token stays valid until it expires, full stop. The standard answer is two tokens with very different lifetimes. A short-lived access token (minutes) rides along with every request and does the actual authorizing. A longer-lived refresh token (days or weeks) lives somewhere safer and is used only to obtain fresh access tokens. If an access token leaks, it dies quickly on its own; and when you need to cut someone off, revoking their refresh token ends the session the next time an access token would have been renewed.

Serious implementations also rotate refresh tokens: each use issues a new one and invalidates the old. If an old refresh token is ever presented again, that's a signal it was stolen, and you can kill the whole session. None of this is exotic, but it's the part people skip, which is how "log out everywhere" ends up being impossible to build later.

storage

Where you keep the token is a security decision

There's no storage option without a trade-off, so choose deliberately instead of defaulting to whatever's easiest. Keep a token in localStorage and any cross-site-scripting flaw anywhere on your page can read it and walk off with the session — XSS turns into full account takeover. Move it into an HttpOnly cookie and scripts can no longer reach it, which shuts the theft-via-XSS path but raises a cross-site request forgery question you then answer with the SameSite attribute and anti-CSRF tokens. A common middle path is an HttpOnly cookie for the refresh token and the short-lived access token held only in memory, so nothing long-lived is sitting where a script can grab it.

attacks

The classic ways verification gets broken

Most JWT vulnerabilities aren't clever cryptography — they're verifiers being too trusting of the token's own header. Two show up again and again.

The alg: none attack strips the signature and sets the algorithm to "none", betting the server will accept an unsigned token as valid. A correct verifier refuses outright; a naive one waves it through. The algorithm-confusion attack is subtler: a server that expects an RS256 token (verified with a public key) is tricked into treating the incoming token as HS256, so it verifies the signature using the public key as if that key were an HMAC secret. Since the public key isn't secret, the attacker can forge a valid-looking token. The defence for both is identical and worth tattooing on the back of your hand: pin the expected algorithm in your verifier rather than trusting whatever alg the token announces, and always validate exp (not expired) and aud (intended for you) instead of just confirming a signature parses.

Don't hand-roll any of this. Use a maintained JWT library and let it verify, and feed it a strong, random secret for HS256 — a short or guessable secret means an attacker can brute-force it offline and sign their own tokens. The library has already thought about the edge cases you haven't.

when not to

JWTs aren't always the answer

It's worth saying plainly: a stateless token is the wrong tool when you need instant revocation, fine-grained session control, or the ability to log everyone out now. A traditional server-side session — an opaque ID that points at state you control — gives you all of that, at the cost of a lookup per request. JWTs shine for short-lived authorization across services and APIs; sessions shine when control and revocation matter more than avoiding a database hit. Picking the wrong one isn't a security hole by itself, but pretending a JWT can do a session's job leads to the awkward workarounds (denylists, ultra-short expiry, constant refreshes) that signal you wanted a session all along.

Strip away the jargon and the whole topic reduces to one habit: never let "I can read this token" turn into "I trust this token". Read freely, verify ruthlessly, and let a library do the verifying.