Troja
All posts
JWTMar 22, 2026·15 min read

JWT Security: 7 Common Mistakes That Let Attackers In

JWTs are easy to use and easy to misuse. Here are the seven mistakes — from the alg:none bypass to storing tokens in localStorage — that turn your auth into an open door.

By Security Desk

A token is only as strong as its verification

A JSON Web Token is just base64url-encoded JSON with a signature. The security lives entirely in how you verify it. Skip or weaken that step and the token is meaningless. Here are the seven mistakes that do exactly that.

1. Accepting alg: none

The infamous one. The JWT header declares its own algorithm, and a naive verifier trusts it:

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

If your library honors none, an attacker forges any payload with no signature at all. Always pin the expected algorithm:

import jwt from "jsonwebtoken";

const payload = jwt.verify(token, secret, { algorithms: ["HS256"] });

Never pass the token's own header value as the trusted algorithm.

2. The RS256 → HS256 confusion attack

If your code uses your public RSA key as the verification key but allows HS256, an attacker signs a token with HMAC using your public key as the secret — which they know — and it verifies.

Fix: pin the algorithm to RS256 (or your asymmetric choice) so an HS256 token is rejected outright.

3. Weak or hardcoded signing secrets

An HS256 token is only as strong as its secret. "secret", "jwt_secret", or a short string is brute-forceable offline.

# Generate a strong secret
openssl rand -base64 48

Use a long, random secret from an env var — never commit it, and rotate it if it ever leaks.

4. Not verifying exp (and other claims)

Decoding is not verifying. jwt.decode() reads the payload without checking the signature or expiry. Always verify(), and check the claims that matter:

const payload = jwt.verify(token, secret, {
  algorithms: ["RS256"],
  issuer: "https://auth.example",
  audience: "https://api.example",
  // exp and nbf are checked automatically by verify()
});

Validating iss and aud stops a valid token from one service being replayed against another.

5. Putting secrets in the payload

JWT payloads are not encrypted — anyone can base64url-decode them. Paste a token into jwt.io and you'll see the contents in cleartext. Never put passwords, full PII, or API keys in the claims. Keep only identifiers and coarse authorization data.

6. Storing tokens in localStorage

localStorage is readable by any JavaScript on the page, so a single XSS exfiltrates the token and the attacker has full session access. Prefer an HttpOnly, Secure, SameSite cookie:

Set-Cookie: token=...; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=900

It's invisible to JavaScript, which neutralizes XSS token theft. (Then mind CSRF — see your cookie/SameSite setup.)

7. No revocation and tokens that live forever

JWTs are stateless, so a token is valid until it expires — you can't "log someone out" of a token you can't see. Mitigate:

  • Short-lived access tokens (5–15 minutes).
  • Refresh tokens stored server-side that you can revoke.
  • A denylist (by jti) for emergency revocation.
  • Rotate refresh tokens on use and detect reuse.
// Short access token, longer refresh token
const access = jwt.sign({ sub: userId }, secret, { algorithm: "RS256", expiresIn: "10m" });
const refresh = jwt.sign({ sub: userId, jti }, secret, { algorithm: "RS256", expiresIn: "7d" });

A correct verification, end to end

function authenticate(token: string) {
  try {
    return jwt.verify(token, publicKey, {
      algorithms: ["RS256"],           // pin it
      issuer: "https://auth.example",  // check iss
      audience: "https://api.example", // check aud
      clockTolerance: 5,               // seconds of leeway
    });
  } catch {
    return null; // never trust an unverifiable token
  }
}

Checklist

  • Algorithm pinned; none rejected
  • No RS256/HS256 confusion (asymmetric pinned)
  • Strong, env-stored signing secret
  • verify() (not decode()), with iss/aud/exp checks
  • No secrets/PII in the payload
  • Tokens in HttpOnly cookies, not localStorage
  • Short access tokens + revocable refresh tokens

Scan it with Troja

Troja inspects how your app issues and accepts tokens — flagging alg weaknesses, missing expiry checks, tokens in localStorage, and long-lived sessions — and gives you the exact fix. Scan your auth before someone else tests it for you.

Run the scan this post is about.

Free, no signup. See what's hiding inside your walls in ~30 seconds.

Free scan · no signup · results in ~30 seconds
JWT Security: 7 Common Mistakes That Let Attackers In — Troja