JustAppSec
Back to guides

JWT Security Best Practices

JSON Web Tokens are used everywhere for authentication and authorization. They are also misused everywhere. This guide covers the mistakes that matter and the patterns that work.

Always Validate the Signature

A JWT without signature validation is just a base64-encoded JSON blob that anyone can forge.

import jwt from "jsonwebtoken";

const SECRET = process.env.JWT_SECRET!;

function verifyToken(token: string) {
  try {
    return jwt.verify(token, SECRET, {
      algorithms: ["HS256"], // explicitly specify allowed algorithms
    });
  } catch {
    throw new Error("Invalid token");
  }
}

Never decode without verifying. jwt.decode() does not check signatures.

Reference: RFC 7519 — JSON Web Token

Specify Allowed Algorithms

The alg field in the JWT header tells the library which algorithm to use. If you do not restrict this, attackers can:

  • Set alg: "none" to bypass signature entirely.
  • Set alg: "HS256" on a system expecting RS256, using the public key as the HMAC secret.

Always pass the algorithms option:

// GOOD
jwt.verify(token, key, { algorithms: ["RS256"] });

// BAD — accepts whatever the token says
jwt.verify(token, key);

Reference: Auth0 — Critical JWT Vulnerability

Use Asymmetric Keys for Distributed Systems

If multiple services verify tokens, use RS256 or ES256 (asymmetric) instead of HS256 (symmetric). With HMAC, every service that verifies also has the secret to forge tokens.

  • HS256: One shared secret. OK for a single service.
  • RS256/ES256: Private key signs, public key verifies. Services only need the public key.

Reference: RFC 7518 — JSON Web Algorithms

Set Short Expiry (exp)

Tokens should expire. The shorter the better, balanced against usability:

  • Access tokens: 5–15 minutes.
  • Refresh tokens: hours to days, stored securely.
const token = jwt.sign(
  { sub: userId, role: "user" },
  SECRET,
  { algorithm: "HS256", expiresIn: "15m" }
);

Always validate exp on the server. Most libraries do this by default, but confirm.

Use iss, aud, and sub Claims

Standard claims prevent token misuse across services:

  • iss (issuer): who created the token.
  • aud (audience): who the token is intended for.
  • sub (subject): who the token represents.
const token = jwt.sign(
  { sub: userId, role: "user" },
  SECRET,
  {
    algorithm: "HS256",
    expiresIn: "15m",
    issuer: "auth.example.com",
    audience: "api.example.com",
  }
);

// Verify
jwt.verify(token, SECRET, {
  algorithms: ["HS256"],
  issuer: "auth.example.com",
  audience: "api.example.com",
});

Reference: RFC 7519 — Registered Claim Names

Do Not Store Sensitive Data in the Payload

JWTs are base64-encoded, not encrypted. Anyone with the token can read the payload. Never put passwords, credit card numbers, PII, or internal secrets in the claims.

If you need encrypted tokens, use JWE (JSON Web Encryption): RFC 7516 — JWE

Store Tokens Securely

StorageXSS SafeCSRF SafeRecommendation
HttpOnly cookieYesNeed SameSite/tokenBest for web apps
localStorageNoYesAvoid for auth tokens
sessionStorageNoYesAvoid for auth tokens
Memory (variable)YesYesOK but lost on refresh

For web applications, use HttpOnly, Secure, SameSite=Lax cookies.

Reference: OWASP — Session Management Cheat Sheet

Implement Token Refresh

Short-lived access tokens with longer-lived refresh tokens:

  1. Client authenticates → receives access token (15 min) + refresh token (7 days).
  2. Access token expires → client sends refresh token to get a new access token.
  3. Refresh token is single-use — issue a new one with each refresh (rotation).
  4. Store refresh tokens server-side so you can revoke them.

Token Revocation

JWTs are stateless by design, which means you cannot "invalidate" one without server-side state. Options:

  • Short expiry + refresh tokens: the access token naturally expires; revoke the refresh token server-side.
  • Token blocklist: maintain a list of revoked token IDs (jti claim) checked on each request. Use Redis with TTL matching the token expiry.
  • Token versioning: store a version counter per user; bump it on logout/password change; reject tokens with old versions.

Key Rotation

Signing keys should be rotated periodically:

  1. Generate a new key pair.
  2. Start signing new tokens with the new key.
  3. Continue accepting tokens signed with the old key until they expire.
  4. Remove the old key after the longest token TTL has passed.

For asymmetric keys, publish the public keys via a JWKS endpoint:

Reference: RFC 7517 — JSON Web Key

Common Mistakes

  • Trusting jwt.decode() for authentication (no signature check).
  • Not restricting algorithms in verification.
  • Storing tokens in localStorage (vulnerable to XSS).
  • Setting long expiry on access tokens (hours or days).
  • Putting PII in claims without encryption.
  • Not validating iss and aud claims.

Related Guides


Content is AI-assisted and reviewed by our team, but issues may be missed and best practices evolve rapidly, send corrections to [email protected]. Always consult official documentation and validate key implementation decisions before making design or security choices.

Need help?Get in touch.