jwtsecurityauthentication

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

AlgorithmTypeUse Case
ES256Asymmetric (ECDSA)Best for new projects — small signatures, fast
RS256Asymmetric (RSA)Widely supported, good for interop
HS256Symmetric (HMAC)Only when secret is truly shared and never exposed
noneNoneNever use
RS512RSAUnnecessary 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

StorageXSS RiskCSRF RiskRecommendation
localStorageHighNoneNever for auth tokens
sessionStorageHighNoneNever for auth tokens
Memory (JS var)LowNoneGood for access tokens
HttpOnly cookieNoneMediumBest 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
});
ClaimMeaningAlways Validate
expExpiry timeYes
nbfNot beforeYes
issIssuerYes
audAudienceYes
subSubject (user ID)Yes, match to session
jtiJWT IDYes, 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, nbf on every request
  • Implement JTI-based revocation for logout/password change
  • Never log full JWTs — they are bearer credentials

More Tools from Cosyslabs