What are the common JWT pitfalls — algorithm confusion (alg:none), no revocation, storing it insecurely?
A JWT is only as safe as the verification code that checks it — and the three most common ways to check it wrong all lead to full authentication bypass.
LEARN BY DOINGPut your intuition to work3 steps
What are the common JWT pitfalls — algorithm confusion (alg:none), no revocation, storing it insecurely?
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.#What a JWT actually is, and why that shape is dangerous
A JSON Web Token is three base64url segments joined by dots: header.payload.signature. The header declares metadata about the token itself — critically, which algorithm was used to sign it (alg). The payload carries claims (sub, role, exp, whatever the issuer put there). The signature is computed over the header and payload using a key the server controls. The dangerous part is structural: the header is part of the token, which means it's attacker-controlled input, exactly like the payload. Every JWT pitfall below comes from verification code that trusts something in the header or payload before the signature has actually been checked.
#Pitfall 1: algorithm confusion and alg: none
The JWT spec defines none as a legal algorithm value, meaning "this token isn't signed at all." It exists for narrow, non-production use cases, but if a verifier reads alg from the token and dispatches its verification logic based on that value, an attacker can simply rewrite the header to {"alg":"none"}, strip the signature entirely, and change any claim they like — including role: "admin".
A subtler variant hits systems that issue asymmetric (RS256) tokens signed with a private key and verified with a public key. If the verifier accepts either HS256 or RS256 and uses the same key material for both, an attacker can craft a token with alg: "HS256" and sign it using the RS256 public key as the HMAC secret. Public keys are, by definition, not secret — they're often published at a /.well-known/jwks.json endpoint — so the attacker now holds a validly "signed" token the server will accept.
// VULNERABLE — trusts the header instead of pinning the algorithmconst jwt = require('jsonwebtoken');function verifyToken(token) {const decoded = jwt.decode(token, { complete: true });if (decoded.header.alg === 'none') {// "just for local dev" checks like this have shipped to productionreturn decoded.payload;}// No `algorithms` allow-list: if this server also accepts HS256 tokens// elsewhere, an attacker can sign with the RS256 public key as an// HMAC secret and pass verification here.return jwt.verify(token, PUBLIC_KEY);}
// FIXED — pin the algorithm explicitly; never trust the header, never// call decode() when the result will be used for a trust decisionconst jwt = require('jsonwebtoken');function verifyToken(token) {return jwt.verify(token, PUBLIC_KEY, {algorithms: ['RS256'], // rejects "none" and HS256 alikeissuer: 'https://auth.example.com',audience: 'api.example.com',clockTolerance: 5,});// jwt.verify() throws on a bad signature, wrong algorithm, or expiry —// treat any throw as "unauthenticated," full stop.}
#Pitfall 2: no built-in revocation
A JWT's whole value proposition — no database lookup needed to verify it — is also its biggest operational weakness. Once issued, a valid JWT stays valid until exp, no matter what happens afterward: a password reset, a role downgrade, an account ban, a stolen device being reported. There is no DELETE for a signature.
The practical mitigations all reintroduce a small amount of state, on purpose:
- Keep access tokens short-lived (5–15 minutes) so the exposure window for anything that goes wrong is naturally small.
- Track a
jti(JWT ID) or atokenVersionper user in a fast store; increment it on password change or explicit logout, and check it on each verification for operations that truly need instant revocation. - Pair short-lived access tokens with a long-lived, server-tracked refresh token — the refresh token is what actually gets revoked; the access token is simply left to expire.
#Pitfall 3: storing the token insecurely on the client
Where the client keeps a JWT determines what class of attack can steal it:
localStorage/sessionStorage— readable by any JavaScript running on the page. One XSS bug anywhere in the app — a third-party script, an unescaped user comment, a vulnerable dependency — lets an attacker read every stored token and exfiltrate it.- A JS variable held only in memory — gone on refresh, but immune to being read by an unrelated script after the fact (though still readable while an XSS payload is actively executing).
- An
HttpOnlycookie — invisible to JavaScript entirely, so a standard XSS payload can't read it directly, but it now rides along automatically on every request, which reopens CSRF as a concern unless paired withSameSiteand/or a CSRF token.
| Pitfall | Root cause | Mitigation |
|---|---|---|
alg: none accepted | Verifier trusts the attacker-controlled header instead of pinning the algorithm | Always pass an explicit algorithms: [...] allow-list to verify(); never call decode() for a trust decision |
| Algorithm confusion (RS256 → HS256) | Same key material accepted under both an asymmetric and a symmetric algorithm | Pin exactly one algorithm family per key; never let one endpoint accept both HS* and RS*/ES* |
| No revocation | Signature verification never touches the database | Short-lived access tokens + a jti/token-version check where instant revocation genuinely matters |
| Insecure client storage | Token sits in localStorage, readable by any injected script | Keep access tokens in memory; put refresh tokens in an HttpOnly, Secure, SameSite cookie |
| Sensitive data in the payload | JWT payload is base64url-encoded, not encrypted | Never put secrets/PII in claims; use JWE if the payload itself must be confidential |
🧠 Check your understanding
0/3 · 0/3 answered1. A JWT verifier reads the algorithm to use from the token's own header field before checking any signature. What attack does this directly enable?
2. Why can't a compromised JWT be revoked the instant it's discovered, in a pure stateless design?
3. What is the main risk of storing a JWT access token in localStorage?