JWT Security Best Practices | alg none Attack / Expiration / Signature Verification
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
expto a short duration when issuing (15 minutes to 1 hour is recommended for access tokens) - Always check
expduring 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_versionin 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 32or the password generation tool - Never commit secrets to Git. Manage them with environment variables or Secrets Manager
Summary of best practices
- Fix the algorithm on the verification side (
algorithms: ['HS256']) - HS256 secret should be 256 bits or more, RS256 should use RSA with 2048 bits or more
- Access token
expis 15 minutes to 1 hour - Implementing Long-term Sessions with Refresh Token Pattern
- Do not include sensitive information in the payload (Base64 is not encryption)
- On logout, register
jtiin the denylist - Manage secrets with environment variables or Secrets Manager; never commit them
- 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.