Skip to content

JWT Security Best Practices | alg none Attack / Expiration / Signature Verification

Category: Authentication / Security

JWT (JSON Web Token) is the de facto standard for authentication in modern Web APIs, but implementing it while misunderstanding the specification can easily introduce vulnerabilities. This article explains common JWT misuse patterns and best practices to avoid them.

JWT Structure Review

JWT is a format that concatenates three Base64URL-encoded strings with dots.

eyJhbGci...    .   eyJzdWIi...     .    SflKxwRJSM...
   header              payload                signature
  • header: Signature algorithm (alg) and token type (typ)
  • payload: JSON object of claims (sub / iss / aud / exp / iat / ...)
  • signature: HMAC / RSA / ECDSA signature over header and payload

You can verify the contents of a JWT using DevLab's JWT Decoder. Remember that Base64URL is encoding, not encryption, so the payload contents can be read by anyone without server signature verification.

Threat 1: alg=none attack

JWT specification includes "alg": "none", a "no signature" algorithm designation. If an attacker exploits this to modify the payload with a {"alg":"none"} header, and the library accepts it as-is, it becomes a critical vulnerability allowing impersonation of any user.

Countermeasure:

  • Explicitly whitelist allowed algorithms during verification
  • Pass it as an array like jwt.verify(token, secret, { algorithms: ['HS256'] })
  • Libraries around 2015 were vulnerable, but current major libraries have implemented fixes. However, always use the latest version unless you're building from scratch.
// ✗ 悪い例 (アルゴリズム未指定 = ライブラリが alg ヘッダを信頼)
jwt.verify(token, secret);

// ✓ 良い例 (アルゴリズムを固定)
jwt.verify(token, secret, { algorithms: ['HS256'] });

Threat 2: Key confusion attack

An attacker with an RSA public key can change the algorithm from RS256 to HS256 to force the public key to be treated as a "secret key". If the JWT library trusts the alg field when selecting the verification function, forged tokens signed with HMAC using the public key will be accepted.

Countermeasure: Always fix the algorithm on the verification side (same countermeasure as threat 1). Additionally, explicitly distinguish the key type:

  • For HS256, pass as Buffer
  • For RS256, pass the public key in PEM format

Threat 3: Unverified expiration / indefinite tokens

The exp claim in JWT indicates the expiration time in UNIX seconds, but it is often ignored during validation. If a token issued once can be used indefinitely, the damage from a breach cannot be contained.

Countermeasure:

  • Set exp to a short duration when issuing (15 minutes to 1 hour is recommended for access tokens)
  • Always check exp during verification (major libraries do this automatically)
  • For long-lived sessions, use the refresh token pattern (short-lived access token + long-lived refresh token + server-side revocation list)

Threat 4: Storing sensitive information in JWT

The JWT payload is just Base64-encoded, and anyone can decode it. Despite this, implementations that put passwords or credit card numbers in the payload continue to appear.

Countermeasure:

  • Include only minimal identifying information in the payload stating 「this user is…」 (sub / user_id / role)
  • Store sensitive information in a server-side database and retrieve it from JWT using user_id
  • If you absolutely must send sensitive information in a JWT, use JWE (encrypted JWT)

Threat 5: Unable to revoke (logout)

When JWT is issued stateless, it cannot be revoked. Even if a user logs out, since the server has no token information, the token "can be used until expiration". Even after a password change, issued JWTs remain valid.

Countermeasure:

  • Minimize damage window with short exp (15 minutes or less)
  • On logout, register the jti (JWT ID) in the server-side denylist and verify against it during validation
  • For critical events (password change, permission change), store token_version in the user record and verify a match during validation

Threat 6: Weak Secrets

If the HS256 secret is too short, it can be cracked by brute force. Strings like "secret" and "password123" are particularly vulnerable.

Countermeasure:

  • HS256 should use at least 256 bits (32 bytes) of random values
  • Generate with openssl rand -base64 32 or the password generation tool
  • Never commit secrets to Git. Manage them with environment variables or Secrets Manager

Summary of best practices

  1. Fix the algorithm on the verification side (algorithms: ['HS256'])
  2. HS256 secret should be 256 bits or more, RS256 should use RSA with 2048 bits or more
  3. Access token exp is 15 minutes to 1 hour
  4. Implementing Long-term Sessions with Refresh Token Pattern
  5. Do not include sensitive information in the payload (Base64 is not encryption)
  6. On logout, register jti in the denylist
  7. Manage secrets with environment variables or Secrets Manager; never commit them
  8. Store JWT in HttpOnly Cookie, not localStorage, on the client side (XSS countermeasure)

Tools useful for debugging

  • JWT Decoder: visualizes header / payload / signature with explanations of the meaning of each claim
  • JWT Generation & Signing Tool: Sign using Web Crypto API in your browser with HS256 / HS384 / HS512. Convenient for generating test tokens
  • Password Generator: useful for generating secrets

Summary

JWT is a convenient authentication token if used correctly, but without knowing the specification pitfalls and attack patterns, you risk introducing vulnerabilities into production systems. Understand the 6 threats and countermeasures presented in this article and use the latest version of JWT libraries as a foundation. We recommend regularly reviewing your implementation and monitoring security advisories.

❓ Frequently Asked Questions

Where should a JWT be stored in the browser?
Avoid localStorage — it is readable the moment XSS lands. The default answer is a cookie with HttpOnly, Secure and SameSite set. Cookies bring CSRF into play, but SameSite=Lax plus a token check on state-changing requests closes that. You do not have to trade one risk for the other.
Is the alg:none attack still a realistic threat?
The major libraries now reject it by default, but it returns the moment you write code that reads alg from the header and picks a verifier from it. The fix is simple: pin the algorithm the server expects at verification time and never trust the alg the token declares. The same rule prevents the confusion where an RS256 public key is handed to a place expecting HS256.
Short expiry times log users out too often.
The standard shape is two tokens: a short-lived access token of a few to a dozen minutes, plus a long-lived refresh token. The access token cannot be revoked, so it expires naturally instead; the refresh token is stored server-side and can be cancelled at any moment. That separation is what lets a logout or a permission change take effect immediately.