JWT Security Best Practices for 2026
· Cosyslabs
JWT security failures are consistently among the top API vulnerabilities. Use RS256 or ES256 algorithms, reject the none algorithm explicitly, set access token expiry under 15 minutes, implement refresh token rotation, and always verify signatures server-side before trusting any claim.
What Is a JWT?
A JSON Web Token is three Base64URL-encoded segments separated by dots:
header.payload.signature
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.signature
- Header: algorithm and token type
- Payload: claims (user data, expiry, etc.)
- Signature: cryptographic proof the token was not tampered with
The signature is what you must verify. A JWT without verified signature is just unverified JSON.
Critical Vulnerability: Algorithm Confusion
The none Algorithm Attack
Early JWT libraries accepted alg: "none" in the header, meaning no signature required. An attacker could forge any payload:
{
"alg": "none",
"typ": "JWT"
}
With no signature check, they could claim to be any user. Fix:
// Node.js — always explicitly specify allowed algorithms
jwt.verify(token, publicKey, { algorithms: ["RS256"] });
// Never allow "none"
// Never use algorithms: ["RS256", "none"] — this is a vulnerability
RS256 vs HS256 Confusion Attack
HS256 (HMAC) uses a shared secret — the same key signs and verifies. RS256 (RSA) uses a key pair — private key signs, public key verifies.
The attack: if a library sees alg: "HS256" and uses the RS256 public key as the HMAC secret, an attacker who obtained the public key (which is public!) can forge tokens.
Always pin the algorithm server-side. Never read alg from the token to decide how to verify it.
// Vulnerable — reads alg from token
function verify(token, key) {
const { alg } = decodeHeader(token);
return verifyWith(token, key, alg); // NEVER do this
}
// Secure — algorithm is hardcoded server-side
function verify(token) {
return jwt.verify(token, PUBLIC_KEY, { algorithms: ["RS256"] });
}
Algorithm Recommendations
| Algorithm | Type | Use Case |
|---|---|---|
| ES256 | Asymmetric (ECDSA) | Best for new projects — small signatures, fast |
| RS256 | Asymmetric (RSA) | Widely supported, good for interop |
| HS256 | Symmetric (HMAC) | Only when secret is truly shared and never exposed |
| None | Never use | |
| RSA | Unnecessary overhead vs RS256 |
For public-facing APIs where multiple services verify tokens, use asymmetric algorithms (ES256/RS256). The private key stays on the auth server; all other services only hold the public key.
Token Expiry and Refresh Strategy
Short-lived access tokens limit the damage from token theft. Use a two-token pattern:
- Access token: expires in 5–15 minutes, sent with every API request
- Refresh token: expires in 7–30 days, stored securely, used only to get new access tokens
// Issuing tokens
const accessToken = jwt.sign(
{ sub: user.id, role: user.role },
PRIVATE_KEY,
{ algorithm: "ES256", expiresIn: "15m" }
);
const refreshToken = jwt.sign(
{ sub: user.id, jti: crypto.randomUUID() },
REFRESH_SECRET,
{ expiresIn: "7d" }
);
Refresh Token Rotation
Every time a refresh token is used, invalidate it and issue a new one. If a stolen refresh token is detected being used twice, invalidate the entire session:
async function refreshTokens(oldRefreshToken) {
const payload = jwt.verify(oldRefreshToken, REFRESH_SECRET);
// Check token has not been used before (reuse detection)
const tokenRecord = await db.refreshTokens.findOne({ jti: payload.jti });
if (!tokenRecord || tokenRecord.used) {
// Token reuse detected — revoke entire family
await db.refreshTokens.revokeFamily(payload.sub);
throw new Error("Token reuse detected");
}
// Mark as used
await db.refreshTokens.markUsed(payload.jti);
// Issue new pair
return issueTokenPair(payload.sub);
}
Storing JWTs Securely
| Storage | XSS Risk | CSRF Risk | Recommendation |
|---|---|---|---|
localStorage | High | None | Never for auth tokens |
sessionStorage | High | None | Never for auth tokens |
| Memory (JS var) | Low | None | Good for access tokens |
HttpOnly cookie | None | Medium | Best for refresh tokens + CSRF tokens |
Store access tokens in JavaScript memory. Store refresh tokens in HttpOnly, Secure, SameSite=Strict cookies.
// Set refresh token as HttpOnly cookie
res.cookie("refresh_token", refreshToken, {
httpOnly: true,
secure: true,
sameSite: "strict",
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
path: "/auth/refresh", // only sent to refresh endpoint
});
Claims to Always Validate
Beyond verifying the signature, validate these standard claims:
// These should be checked by your JWT library automatically
// but confirm your configuration enforces them:
jwt.verify(token, PUBLIC_KEY, {
algorithms: ["ES256"],
issuer: "https://auth.yourdomain.com", // iss claim
audience: "https://api.yourdomain.com", // aud claim
// exp is checked automatically
// nbf is checked automatically
});
| Claim | Meaning | Always Validate |
|---|---|---|
exp | Expiry time | Yes |
nbf | Not before | Yes |
iss | Issuer | Yes |
aud | Audience | Yes |
sub | Subject (user ID) | Yes, match to session |
jti | JWT ID | Yes, for revocation |
JWT Revocation
JWTs are stateless — a valid token stays valid until expiry. For immediate revocation (logout, password change, account suspension), maintain a blocklist:
// On logout
await redis.setex(`revoked:${payload.jti}`, tokenTtlSeconds, "1");
// On every request
async function verifyToken(token) {
const payload = jwt.verify(token, PUBLIC_KEY, { algorithms: ["ES256"] });
const isRevoked = await redis.exists(`revoked:${payload.jti}`);
if (isRevoked) throw new Error("Token revoked");
return payload;
}
Short access token expiry reduces how long you need to maintain the blocklist.
Debugging JWTs
Use the JWT Decoder Tool to inspect headers and payloads without sending tokens to external services. All decoding happens in your browser.
Checklist
- Use ES256 or RS256 — never HS256 for public APIs
- Explicitly reject
alg: "none"in library config - Pin algorithm server-side — never read from token header
- Set access token expiry to 5–15 minutes
- Implement refresh token rotation with reuse detection
- Store refresh tokens in HttpOnly cookies, access tokens in memory
- Validate
iss,aud,exp,nbfon every request - Implement JTI-based revocation for logout/password change
- Never log full JWTs — they are bearer credentials
More Tools from Cosyslabs
- Rough Estimator — Estimate the development effort and cost for implementing JWT authentication, refresh token rotation, and secure API layers in your project.
- Routine Toolkit — Everyday utilities including a loan calculator, date calculator, and word counter — built by the same team.
- Cosyslabs — The studio behind Dev Tools !, PDF Convert All, Unit Convert All, Astrilio, CastFleet, and more.