La mise en œuvre de JSON Web Tokens (JWT) dans les architectures sans état offre des avantages de mise à l'échelle significatifs, mais introduit une vulnérabilité de sécurité critique : le vol de jetons. Contrairement aux systèmes basés sur des sessions, où le serveur peut invalider instantanément une session côté client, les JWT sont autonomes. Une fois émis, ils restent valides jusqu'à leur expiration, même si le serveur décide que l'utilisateur doit être déconnecté. Cet article explore des stratégies robustes pour atténuer ce risque grâce à la rotation des jetons et à la révocation sélective.
Le dilemme de l'absence d'état
Dans un environnement traditionnel sans état, le serveur ne stocke pas l'état de l'utilisateur. Le JWT agit comme la source de vérité, contenant toutes les revendications nécessaires. Cependant, si un attaquant vole un jeton d'accès via XSS ou interception réseau, il peut l'utiliser jusqu'à son expiration à brève échéance. Bien que limiter la durée de vie du jeton d'accès (par exemple, 15 minutes) aide, cela crée une friction pour l'expérience utilisateur. C'est ici que le modèle Jeton d'accès / Jeton d'actualisation devient essentiel.
En émettant des jetons d'accès à courte durée de vie et des jetons d'actualisation à longue durée de vie, nous limitons la fenêtre d'opportunité pour les attaquants. Le jeton d'accès gère les requêtes API, tandis que le jeton d'actualisation est utilisé uniquement pour obtenir de nouveaux jetons d'accès. Crucialement, le jeton d'actualisation doit être stocké de manière sécurisée, de préférence dans des cookies HTTP-only, pour empêcher l'accès par JavaScript.
Mise en œuvre de la rotation des jetons
La rotation des jetons est la pratique consistant à émettre un nouveau jeton d'actualisation chaque fois qu'un jeton d'actualisation est utilisé. Cette stratégie minimise la valeur d'un jeton d'actualisation volé. Si un attaquant vole un jeton d'actualisation, il ne peut effectuer qu'une seule requête avant que la prochaine tentative d'actualisation de l'utilisateur légitime n'annule le jeton volé.
Voici un exemple simplifié en Node.js illustrant la logique de rotation :
async function handleRefreshToken(req, res) {
const { refreshToken } = req.body;
// 1. Vérifier la signature et l'expiration du jeton
const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
// 2. Vérifier si le jeton existe dans notre stockage (Redis)
const storedToken = await redis.get(`refresh:${decoded.jti}`);
if (!storedToken) {
return res.status(401).send('Refresh token revoked or invalid');
}
// 3. Émettre de NOUVEAUX jetons d'accès et d'actualisation
const newAccessToken = generateAccessToken(decoded.userId);
const newRefreshToken = generateRefreshToken(decoded.userId);
// 4. Stocker le nouveau jeton d'actualisation et supprimer l'ancien
await redis.setEx(`refresh:${newJti}`, EXPIRY, newRefreshToken);
await redis.del(`refresh:${decoded.jti}`);
res.json({ accessToken: newAccessToken, refreshToken: newRefreshToken });
}
Stratégies de révocation
Malgré la rotation, il existe des scénarios où une révocation immédiate est nécessaire, tels que les changements de mot de passe ou les signalements de compromission de compte. Comme les JWT sont sans état, nous ne pouvons pas simplement les "supprimer". Nous avons besoin d'un moyen de vérifier leur validité avant d'accorder l'accès.
1. Jetons d'accès à courte durée de vie
La mitigation la plus efficace consiste à maintenir les jetons d'accès très courts (5 à 15 minutes). Cela garantit que même si un jeton est volé, l'attaquant dispose d'une fenêtre très limitée pour l'exploiter. Combinez cela avec la rotation pour rendre cette fenêtre pratiquement inexistante.
2. Liste noire de jetons (Denylist)
Pour les scénarios nécessitant une déconnexion immédiate, nous pouvons maintenir une liste noire des identifiants de jeton révoqués (JTI). Lorsqu'un utilisateur se déconnecte, le JTI est ajouté à un ensemble Redis avec une durée de vie (TTL) correspondant à la durée de vie restante du jeton. Lors de chaque requête, le serveur vérifie si le JTI existe dans la liste noire.
function isValidToken(jti, token) {
// Vérifier la liste noire en premier
if (redis.sIsMember('token_denylist', jti)) {
return false;
}
return true;
}
Cette approche ajoute un coût de latence minime mais garantit la sécurité. Pour les systèmes à fort trafic, assurez-vous que les opérations de liste noire sont optimisées en utilisant Redis, qui gère ces vérifications avec une complexité temporelle O(1).
Conclusion
Sécuriser les architectures sans état nécessite de dépasser la simple émission de JWT. En combinant des jetons d'accès à courte durée de vie, une rotation agressive des jetons d'actualisation et des mécanismes de révocation ciblés, les développeurs peuvent réduire considérablement l'impact du vol de jetons. Rappelez-vous, la sécurité est un équilibre entre l'utilisabilité et le risque. La mise en œuvre de ces stratégies garantit que votre application reste résiliente face aux menaces modernes sans sacrifier les avantages de mise à l'échelle des JWT.