JWT Security: Common Mistakes and Fixes

JWT Security: Common Mistakes and Fixes

JWT security is where a lot of otherwise-careful teams slip, because JSON Web Tokens are easy to issue and surprisingly easy to misuse. This guide covers what a JWT actually is, the mistakes that show up again and again—alg=none, weak secrets, tokens that never expire—and the practical fix for each. It is for developers using tokens for authentication who want to sidestep the well-known traps.

Quick answer: A JWT is a signed, base64url-encoded token whose claims anyone can read but only the holder of the key can validly sign. Good JWT security means verifying the signature with a fixed algorithm, using a strong secret or key, keeping tokens short-lived, and validating every standard claim—never trusting the token’s own header about how to check it.

What a JWT actually is

A JSON Web Token (JWT) is a compact, self-contained token that carries a set of claims—such as a user ID and an expiry time—along with a signature. The server signs it at login, hands it to the client, and the client returns it on each request; the server trusts it because the signature checks out. That signature is the whole basis of JWT security: it proves the token has not been tampered with.

The critical thing to internalise is that the payload is not secret. It is encoded, not encrypted, so anyone can read the claims. What a JWT gives you is integrity—confidence the contents were not changed—not confidentiality. Never put anything in a token that you would not be willing to hand to the user.

The three parts of a token

A JWT has three parts separated by dots: header, payload, and signature.

  • Header: metadata, including the signing algorithm (alg)—and, importantly, this part is attacker-supplied.
  • Payload: the claims, such as sub (subject), exp (expiry), and iat (issued-at).
  • Signature: the header and payload signed with your secret or private key.

The first two parts are just base64url-encoded JSON, readable by anyone with a decoder. Only the signature depends on a secret. That split explains most JWT mistakes: people trust the readable parts, or trust the header’s claim about which algorithm to use.

What JWTs are good (and bad) at

JWTs shine when you need stateless, cross-service authentication: an API gateway or several microservices can each verify a token with a shared public key, no central session store required. They also suit short-lived, single-purpose tokens such as a password-reset link.

They are a worse fit when you need instant revocation. Because a valid signature is enough, a JWT stays good until it expires—you cannot easily log someone out server-side without extra machinery. For a classic single web app, a plain server-side session is often simpler and safer than reaching for JWTs by default.

Mistake: trusting alg=none

The most infamous JWT flaw is the none algorithm. The spec allows an alg value of none, meaning “unsigned.” If your library accepts it, an attacker can strip the signature, set alg to none, and forge any claims they like—including another user’s ID.

A related bug is algorithm confusion, where an attacker changes an RS256 token (public/private key) to HS256 and tricks the server into verifying it with the public key as though it were the shared secret. The fix for both is the same: never let the token’s header choose the algorithm. Pin the expected algorithm in your verification code and reject anything else.

Mistake: weak or shared secrets

With HMAC-based tokens (HS256), the whole system’s security rests on one secret. A short, guessable, or committed-to-git secret can be brute-forced offline from a single captured token, after which an attacker can mint valid tokens for any user.

Use a long, random secret, treat it like any other credential, and keep it in a secrets manager rather than in source. If you sign across services, prefer asymmetric keys (RS256 or EdDSA) so only the issuer holds the private key and everyone else verifies with the public one. Rotate keys on a schedule, and support more than one active key so rotation does not break live tokens.

Expiry and revocation

Short lifetimes are your main safety valve. Because you cannot easily revoke a JWT, a leaked token is valid until exp—so keep access tokens short, often minutes, and use a separate longer-lived refresh token to obtain new ones.

Validate the standard claims on every request: check exp (and nbf if present), and verify iss and aud so a token minted for another service is rejected. When you need real revocation—a logout or a compromised account—keep a small server-side denylist of token IDs, or track a token version per user that you bump to invalidate everything at once.

Where to store tokens on the client

Where the browser keeps a token decides which attack you are exposed to. The two common choices each trade one risk for another:

StorageStrengthWatch out for
HttpOnly cookieJavaScript cannot read it, so XSS cannot steal itNeeds CSRF protection (SameSite, tokens)
localStorageSimple to use; not sent automatically, so no CSRFReadable by any XSS on the page

For most web apps, a secure HttpOnly, SameSite cookie is the safer default, paired with CSRF defenses. Whatever you choose, remember that a cross-site scripting bug undermines client-side token storage—one more reason to fix XSS at the source.

Frequently asked questions

Is a JWT encrypted?

No, not by default. A standard JWT is signed, not encrypted, so its claims are only base64url-encoded and anyone can read them. If you need the contents hidden, use an encrypted token (JWE) or simply keep sensitive data out of the token.

Are JWTs more secure than sessions?

Not inherently—they solve a different problem. JWTs enable stateless, cross-service auth, while server sessions are simpler and revoke instantly. For a single web app, sessions are often the safer default; reach for JWTs when you genuinely need statelessness across services.

How long should a JWT last?

Keep access tokens short—minutes rather than days—because you cannot easily revoke them before they expire. Pair a short access token with a longer-lived refresh token you can revoke server-side, so a leaked access token has only a small window of use.

Where should I store a JWT in the browser?

A secure, HttpOnly, SameSite cookie is the safer default for most web apps because JavaScript cannot read it, which blocks theft via XSS; add CSRF protection to cover the trade-off. localStorage is simpler, but any XSS on the page can read it.

JWTs are a sharp tool: excellent for stateless auth, unforgiving when misused. Pin your algorithm, use a strong secret or key, keep tokens short-lived, validate every claim, and store them where XSS cannot reach—and most of the well-known failures simply cannot happen. For the big picture, start with our cornerstone guide.

Last updated: July 6, 2026

Comments

Popular posts from this blog

The OWASP Top 10 Explained for Developers