Meilleures pratiques de sécurité JWT : Algorithmes, expiration, rotation et prévention des attaques
Les JSON Web Tokens sont omniprésents — mais la plupart des implémentations présentent des failles de sécurité critiques. Ce guide couvre la sélection des algorithmes, l'expiration et la rotation des tokens, les patterns de refresh token, et comment se défendre contre les principales attaques JWT.
Qu'est-ce qu'un JWT et comment fonctionne-t-il ?
Un JWT est une chaîne encodée en Base64URL avec trois parties séparées par des points : en-tête (algorithme + type), payload (claims) et signature.
Sélection d'algorithmes : HS256 vs RS256 vs ES256
L'erreur de sécurité JWT la plus courante est d'utiliser HS256 dans un système distribué. RS256 et ES256 utilisent des clés asymétriques : seul le serveur d'authentification détient la clé de signature privée.
// Anatomy of a JWT — Header.Payload.Signature
// 1. Header (Base64URL decoded)
const header = {
alg: "RS256", // Algorithm: RS256 (asymmetric) preferred over HS256 (symmetric)
typ: "JWT"
};
// 2. Payload (Base64URL decoded) — the claims
const payload = {
iss: "https://auth.example.com", // Issuer
sub: "user_01HXYZ123", // Subject (user ID)
aud: "https://api.example.com", // Audience (target service)
exp: 1735689600, // Expiry: Unix timestamp (15 min from now)
iat: 1735688700, // Issued at
jti: "8f14e0f9-21af-4c1e-8f26-3b4a5c6d7e8f", // Unique token ID (for revocation)
roles: ["user"], // Custom claims
email: "alice@example.com"
};
// 3. Signature (server-side only)
// RS256: sign(base64url(header) + '.' + base64url(payload), PRIVATE_KEY)
// Verification: verify(token, PUBLIC_KEY) — other services only need the public key
// --- Generating a JWT with jsonwebtoken (Node.js) ---
import jwt from 'jsonwebtoken';
import { readFileSync } from 'fs';
import { randomUUID } from 'crypto';
const privateKey = readFileSync('./private.pem'); // RSA private key
const publicKey = readFileSync('./public.pem'); // RSA public key (shared with all services)
function issueAccessToken(userId: string, roles: string[]): string {
return jwt.sign(
{
sub: userId,
aud: 'https://api.example.com',
roles,
jti: randomUUID(),
},
privateKey,
{
algorithm: 'RS256',
issuer: 'https://auth.example.com',
expiresIn: '15m', // Short-lived access token
}
);
}
function verifyAccessToken(token: string) {
return jwt.verify(token, publicKey, {
algorithms: ['RS256'], // CRITICAL: whitelist algorithms — never allow 'none'
issuer: 'https://auth.example.com',
audience: 'https://api.example.com',
});
}Stratégie d'expiration et de rotation des tokens
Les tokens d'accès à courte durée de vie (15 minutes) associés à des tokens de rafraîchissement à longue durée de vie (7 à 30 jours) constituent la norme industrielle.
// Refresh Token Pattern — Stateless Access + Stateful Refresh
import jwt from 'jsonwebtoken';
import { randomBytes } from 'crypto';
// ---- Token issuance ----
async function issueTokenPair(userId: string) {
const accessToken = jwt.sign(
{ sub: userId, jti: randomUUID() },
process.env.JWT_PRIVATE_KEY!,
{ algorithm: 'RS256', expiresIn: '15m' }
);
// Refresh token: random bytes stored in DB — NOT a JWT
const refreshToken = randomBytes(64).toString('hex');
const refreshExpiry = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days
// Store hashed refresh token in DB (never store plaintext)
await db.refreshTokens.create({
tokenHash: sha256(refreshToken),
userId,
expiresAt: refreshExpiry,
createdAt: new Date(),
});
return { accessToken, refreshToken, refreshExpiry };
}
// ---- Token refresh endpoint ----
app.post('/auth/refresh', async (req, res) => {
const { refreshToken } = req.cookies; // httpOnly cookie
if (!refreshToken) return res.status(401).json({ error: 'No refresh token' });
const stored = await db.refreshTokens.findOne({
tokenHash: sha256(refreshToken),
expiresAt: { >: new Date() }, // Not expired
revokedAt: null, // Not revoked
});
if (!stored) return res.status(401).json({ error: 'Invalid or expired refresh token' });
// Rotate: invalidate old, issue new pair (refresh token rotation)
await db.refreshTokens.update(
{ id: stored.id },
{ revokedAt: new Date() } // Revoke old refresh token
);
const newPair = await issueTokenPair(stored.userId);
// Set new refresh token as httpOnly cookie
res.cookie('refreshToken', newPair.refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
expires: newPair.refreshExpiry,
path: '/auth/refresh', // Scope cookie to refresh endpoint only
});
return res.json({ accessToken: newPair.accessToken });
});
// ---- Logout: revoke refresh token ----
app.post('/auth/logout', async (req, res) => {
const { refreshToken } = req.cookies;
if (refreshToken) {
await db.refreshTokens.update(
{ tokenHash: sha256(refreshToken) },
{ revokedAt: new Date() }
);
}
res.clearCookie('refreshToken', { path: '/auth/refresh' });
return res.json({ success: true });
});Attaques JWT courantes et défenses
Les vulnérabilités JWT les plus dangereuses sont : la confusion d'algorithme, la confusion RS256→HS256, les secrets faibles et la validation de l'exp manquante.
// JWT Attack Defenses — Code Patterns
// ---- ATTACK 1: Algorithm confusion ("none" bypass) ----
// VULNERABLE: never use verify() without specifying allowed algorithms
const VULNERABLE = jwt.verify(token, secret); // attacker can set alg: "none"
// SAFE: always whitelist algorithms
const SAFE = jwt.verify(token, publicKey, {
algorithms: ['RS256'], // Never include 'none', 'HS256' if you're using RS256
});
// ---- ATTACK 2: RS256 > HS256 confusion ----
// Attacker changes alg from RS256 to HS256 and signs with the PUBLIC key as the secret
// Defense: always specify algorithm on verify — never infer from the token header
// VULNERABLE:
const header = JSON.parse(atob(token.split('.')[0]));
jwt.verify(token, publicKey, { algorithms: [header.alg] }); // attacker controls this!
// SAFE:
jwt.verify(token, publicKey, { algorithms: ['RS256'] }); // hardcoded
// ---- ATTACK 3: Weak HS256 secrets (brute-force) ----
// VULNERABLE: short or predictable secrets
const BAD_SECRET = 'secret';
const BAD_SECRET2 = 'mysupersecretkey';
// SAFE: 256-bit cryptographically random secret
import { randomBytes } from 'crypto';
const GOOD_SECRET = randomBytes(32).toString('hex'); // 256-bit
// Store in environment variable, rotate annually
// ---- ATTACK 4: Missing expiry validation ----
// Some older libraries skip exp check by default
// SAFE: always validate exp (jsonwebtoken does this by default)
try {
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'],
clockTolerance: 30, // Allow 30 seconds clock skew max
ignoreExpiration: false, // NEVER set this to true in production
});
} catch (err) {
if (err instanceof jwt.TokenExpiredError) {
return res.status(401).json({ error: 'Token expired', code: 'TOKEN_EXPIRED' });
}
return res.status(401).json({ error: 'Invalid token' });
}
// ---- Token blacklist (for early revocation via jti) ----
import { createClient } from 'redis';
const redis = createClient();
async function revokeToken(jti: string, exp: number) {
const ttl = exp - Math.floor(Date.now() / 1000); // seconds until natural expiry
if (ttl > 0) {
await redis.set(`jwt:revoked:${jti}`, '1', { EX: ttl });
}
}
async function isTokenRevoked(jti: string): Promise<boolean> {
return (await redis.get(`jwt:revoked:${jti}`)) !== null;
}
// Middleware: check blacklist
app.use(async (req, res, next) => {
const decoded = jwt.verify(req.headers.authorization?.split(' ')[1] ?? '', publicKey, {
algorithms: ['RS256'],
});
if (await isTokenRevoked((decoded as jwt.JwtPayload).jti ?? '')) {
return res.status(401).json({ error: 'Token revoked' });
}
next();
});Stockage sécurisé des tokens
Tokens d'accès : stockez en mémoire (variable JavaScript). Tokens de rafraîchissement : stockez dans des cookies httpOnly, Secure, SameSite=Strict.
Questions fréquentes
Devrais-je utiliser JWT ou des tokens de session opaques ?
Utilisez des tokens de session opaques pour les applications rendues côté serveur. Utilisez les JWT pour les microservices ou les API où une vérification sans état est nécessaire.
Comment révoquer un JWT avant son expiration ?
Solutions : expiration courte (15 min), liste noire de tokens dans Redis, ou version de token dans l'enregistrement utilisateur.
Quels claims doit contenir chaque JWT ?
Obligatoires : iss, sub, aud, exp, iat, jti. Validez toujours iss et aud sur le service destinataire.
Quelle longueur pour les secrets JWT et les clés privées ?
Secrets HS256 : minimum 256 bits. RS256 : clé RSA minimum 2048 bits. ES256 : clé de courbe elliptique 256 bits.