Les JSON Web Tokens (JWT) sont devenus la norme de facto pour sécuriser les API et gérer l'authentification des utilisateurs dans les applications web modernes. Bien que les JWT offrent l'attrait d'une architecture stateless (sans état) — où le serveur n'a pas besoin de stocker de données de session — cette commodité introduit des défis de sécurité significatifs. Plus précisément, si un JWT est volé, il reste valide jusqu'à son expiration. Cet article explore des stratégies robustes pour la rotation et la révocation des JWT, en comparant le fonctionnement de ces mécanismes dans des environnements stateful (avec état) et stateless (sans état).
Le problème fondamental : Vol de jeton et expiration
Dans une implémentation basique de JWT, une fois qu'un jeton est émis, le serveur fait confiance à sa signature jusqu'à ce que la revendication exp (expiration) soit atteinte. Si un attaquant intercepte ce jeton, il dispose d'un accès non autorisé jusqu'à la fermeture de cette fenêtre. Pour atténuer ce risque, nous devons mettre en œuvre une rotation des jetons (émission de nouveaux jetons) et une révocation (invalidation précoce des jetons).
Stratégie 1 : L'approche stateless avec des jetons d'accès à courte durée de vie
Dans une architecture purement stateless, le serveur ne peut pas vérifier dans une base de données centrale si un jeton a été révoqué. Par conséquent, la principale défense consiste à réduire la fenêtre d'opportunité pour les attaquants. Nous y parvenons en utilisant des jetons d'accès à courte durée de vie associés à des jetons de rafraîchissement à longue durée de vie.
Comment cela fonctionne :
- Le client reçoit un jeton d'accès à courte durée de vie (par exemple, 15 minutes).
- Lorsque le jeton d'accès expire, le client utilise le jeton de rafraîchissement pour obtenir un nouveau jeton d'accès.
- Le jeton de rafraîchissement est stocké en toute sécurité (par exemple, dans un cookie HttpOnly) et a une durée de vie plus longue (par exemple, 7 jours).
Même dans une configuration stateless, nous pouvons mettre en œuvre une révocation minimale en vérifiant une "liste noire de jetons" ou un horodatage de "dernière activité" stocké dans un magasin clé-valeur rapide comme Redis. Bien que cela introduise une légère dépendance à l'état, elle est minime par rapport à la gestion de session traditionnelle.
// Pseudo-code pour vérifier un JWT avec une vérification rapide de Redis
async function verifyToken(token, redisClient) {
const decoded = jwt.verify(token, SECRET_KEY);
// Vérifier si l'ID du jeton existe dans une liste noire à court terme ou a été mis à jour
const tokenRecord = await redisClient.get(`token:${decoded.jti}`);
if (tokenRecord === 'revoked') {
throw new Error('Token révoqué');
}
// Optionnel : Vérifier si le "dernier changement de mot de passe" de l'utilisateur est plus récent que l'émission du jeton
const user = await getUser(decoded.sub);
if (decoded.iat < user.lastPasswordChange) {
throw new Error('Jeton émis avant le changement de mot de passe');
}
return decoded;
}
Stratégie 2 : L'approche stateful avec des sessions côté serveur
Dans une architecture stateful, le serveur maintient un enregistrement des sessions actives. Cela offre un contrôle granulaire sur la validité des jetons, mais nécessite plus d'infrastructure. Ici, les JWT peuvent toujours être utilisés comme support des revendications, mais le serveur les valide par rapport à une base de données de sessions actives.
Mécanismes de révocation :
- Déconnexion : Lorsqu'un utilisateur se déconnecte, le serveur supprime l'enregistrement de session de la base de données. Toute demande future avec ce JWT échouera à la validation car le serveur ne trouvera pas la session associée.
- Déconnexion forcée : Le serveur peut supprimer toutes les sessions pour un ID utilisateur spécifique, invalidant immédiatement tous les jetons actifs sur tous les appareils.
Implémentation : Liste noire de jetons dans Node.js
Que l'architecture soit stateful ou stateless (avec un léger état), la mise en œuvre d'une liste noire est cruciale pour une révocation immédiate. Voici un exemple de la manière de mettre un JWT sur liste noire en utilisant Node.js et Redis. Nous stockons la revendication jti (ID du JWT) comme clé et définissons un temps d'expiration égal à la validité restante du jeton.
const jwt = require('jsonwebtoken');
const redis = require('redis');
const redisClient = redis.createClient();
// Mettre un jeton sur liste noire lors de la déconnexion
async function blacklistToken(req, res) {
const token = req.headers.authorization.split(' ')[1];
const decoded = jwt.decode(token); // Décoder sans vérification pour obtenir jti
if (!decoded || !decoded.jti) {
return res.status(400).send('Structure de jeton invalide');
}
// Calculer le TTL en fonction de l'expiration du jeton
const now = Math.floor(Date.now() / 1000);
const ttl = decoded.exp - now;
// Stocker le jti dans Redis avec un temps d'expiration
await redisClient.setex(`blacklist:${decoded.jti}`, ttl, 'revoked');
res.status(200).send('Déconnexion réussie');
}
// Middleware pour vérifier la liste noire lors de la validation de la requête
async function checkBlacklist(req, res, next) {
const token = req.headers.authorization.split(' ')[1];
const decoded = jwt.decode(token);
if (decoded && decoded.jti) {
const isBlacklisted = await redisClient.get(`blacklist:${decoded.jti}`);
if (isBlacklisted) {
return res.status(401).send('Le jeton a été révoqué');
}
}
next();
}
Bonnes pratiques pour une rotation sécurisée
- Utiliser des identifiants uniques : Incluez toujours une revendication
jtiunique dans vos JWT. Cela permet une identification précise des jetons aux fins de révocation. - Rotation des jetons de rafraîchissement : Chaque fois qu'un jeton d'accès est rafraîchi, émettez un nouveau jeton de rafraîchissement et invalidez l'ancien. Cela empêche les attaques par rejeu si un jeton de rafraîchissement est volé après utilisation.
- Stockage sécurisé : Ne stockez jamais les JWT dans
localStoragesi votre application est vulnérable aux XSS. Utilisez plutôt des cookiesHttpOnlyetSecurepour atténuer les risques de script intersite. - Surveillance des anomalies : Mettez en œuvre la journalisation et la surveillance pour détecter des schémas d'utilisation de jetons inhabituels, tels que plusieurs connexions simultanées depuis différentes localisations géographiques.
Conclusion
Mettre en œuvre une rotation et une révocation sécurisées des JWT nécessite de trouver un équilibre entre sécurité et performance. Les architectures stateless s'appuient fortement sur des durées de vie de jetons courtes et des couches de mise en cache efficaces (comme Redis) pour gérer la révocation sans stocker de sessions complètes. Les architectures stateful offrent une révocation plus facile au prix d'une augmentation de la mémoire serveur et de la charge de la base de données. Pour la plupart des applications modernes, une approche hybride — utilisant des jetons d'accès à courte durée de vie, la rotation des jetons de rafraîchissement et l'emploi d'un magasin clé-valeur rapide pour la liste noire des jetons — offre le meilleur équilibre entre sécurité, évolutivité et expérience utilisateur.