Back

Cookies vs localStorage pour l'authentification JWT

Cookies vs localStorage pour l'authentification JWT

Vous avez mis en place votre flux d’authentification, les JWT fonctionnent, et vous vous retrouvez face à la même question que tout développeur frontend finit par se poser : où stocker concrètement ce token ? La réponse est plus importante que la plupart des tutoriels ne le laissent entendre, et le conseil habituel — « utilisez simplement les cookies HttpOnly » — passe sous silence des compromis réels que vous devez comprendre.

Voici une analyse claire des deux options, de ce contre quoi chacune protège réellement, et de la manière dont les applications modernes gèrent cela en pratique.

Points clés

  • localStorage est entièrement accessible à tout JavaScript s’exécutant sur votre page, ce qui le rend vulnérable au vol de token par XSS.
  • Les cookies HttpOnly bloquent totalement l’accès JavaScript, mais introduisent un risque CSRF, que les attributs SameSite et Secure permettent d’atténuer.
  • Le pattern moderne consiste à stocker les access tokens de courte durée en mémoire et les refresh tokens dans des cookies HttpOnly, Secure, SameSite.
  • L’OWASP et les recommandations OAuth pour les applications web déconseillent de placer des tokens de longue durée dans localStorage.
  • Le bon choix dépend de votre modèle de menace, de votre maîtrise du backend et de la nécessité pour votre API d’utiliser des en-têtes Authorization.

Ce qui est réellement en jeu avec le stockage des JWT

L’endroit où vous stockez un JWT détermine quels vecteurs d’attaque s’appliquent à votre application. Les deux menaces principales sont :

  • XSS (Cross-Site Scripting) : du JavaScript malveillant s’exécutant dans le contexte de votre application.
  • CSRF (Cross-Site Request Forgery) : le fait d’inciter le navigateur d’un utilisateur à effectuer des requêtes authentifiées non intentionnelles.

Aucune option de stockage n’élimine simultanément les deux risques. L’objectif est de comprendre quel risque vous acceptez et comment l’atténuer.

localStorage : pratique, mais accessible en JavaScript

Stocker un JWT dans localStorage est simple. Vous l’écrivez, vous le lisez et vous l’attachez manuellement aux en-têtes Authorization: Bearer. Cela fonctionne parfaitement avec les API qui attendent ce format d’en-tête.

Le problème est que localStorage est entièrement accessible à tout JavaScript s’exécutant sur votre page. Si un attaquant parvient à injecter un script — via une vulnérabilité dans une dépendance, un CDN compromis ou une faille XSS dans votre propre code — il peut lire le token directement et l’exfiltrer. L’OWASP déconseille explicitement de stocker des identifiants de session dans localStorage pour cette raison.

Ce n’est pas un risque théorique. Les applications web modernes intègrent des dizaines de scripts tiers, et chacun représente une surface d’attaque potentielle.

Cookies HttpOnly : meilleure résistance aux XSS, nouvelles considérations

Un cookie HttpOnly ne peut pas être lu par JavaScript. Même si un attaquant exécute du code sur votre page, il ne peut pas extraire la valeur du token. C’est une amélioration significative.

Mais les cookies exposent à un risque CSRF. Les navigateurs attachent automatiquement les cookies aux requêtes correspondantes, y compris celles déclenchées par des sites tiers malveillants.

Trois attributs de cookie fonctionnent conjointement pour combler cette lacune :

  • HttpOnly — bloque totalement l’accès JavaScript.
  • Secure — transmet le cookie uniquement via HTTPS.
  • SameSite — contrôle dans quels cas les cookies sont envoyés en contexte cross-site.

Pour SameSite, les navigateurs modernes utilisent par défaut la valeur Lax lorsque l’attribut n’est pas défini, ce qui bloque les cookies sur les sous-requêtes cross-site (comme les POST provenant d’une autre origine) mais les autorise lors des navigations de premier niveau. Strict est plus conservateur et empêche l’envoi du cookie pour toute requête cross-site, y compris les navigations de premier niveau. Définissez toujours cet attribut explicitement plutôt que de vous fier aux valeurs par défaut du navigateur. La prise en charge de SameSite est excellente dans les navigateurs modernes et peut être vérifiée sur Can I Use.

Avec SameSite=Strict ou Lax correctement configuré, le risque CSRF est considérablement réduit pour la plupart des configurations d’authentification same-site. Pour les endpoints sensibles impliquant des modifications d’état, associez cela à un token anti-CSRF pour une défense en profondeur.

Le pattern utilisé par la plupart des applications modernes

De nombreuses applications en production séparent le problème en deux :

  1. Les access tokens de courte durée stockés en mémoire JavaScript (une variable au niveau du module ou un état React).
  2. Les refresh tokens stockés dans un cookie HttpOnly, Secure, SameSite.

L’access token disparaît à la fermeture de l’onglet ou au rechargement de la page, mais un appel silencieux à votre endpoint /refresh en récupère un nouveau en utilisant le cookie. L’access token ne touche jamais le stockage persistant, et le refresh token n’est jamais lisible par JavaScript.

Cette approche est conforme aux recommandations actuelles pour les applications web utilisant OAuth 2.0 avec PKCE (Authorization Code Flow with PKCE), comme le préconise le guide OAuth 2.0 for Browser-Based Apps. Si vous travaillez avec OpenID Connect (OIDC), le même pattern s’applique — gardez les ID tokens et les refresh tokens hors de localStorage.

Liste de contrôle pour l’audit de sécurité

Avant de mettre en production, vérifiez que :

  • Le flag HttpOnly est défini sur tout cookie contenant des tokens.
  • Le flag Secure est activé (HTTPS imposé).
  • SameSite est explicitement défini sur Strict ou Lax.
  • Les access tokens ont une durée de vie courte, généralement de l’ordre de quelques minutes plutôt que de quelques heures.
  • Les en-têtes Content Security Policy sont configurés.
  • Aucun JWT de longue durée ne réside dans localStorage.

Choisir la bonne approche pour votre application

Il n’existe pas de réponse universelle. Si vous maîtrisez votre backend et servez votre application depuis le même domaine, les cookies HttpOnly avec une configuration SameSite appropriée constituent le choix par défaut le plus solide. Si vous intégrez une API tierce qui exige des en-têtes Authorization et que vous ne pouvez pas définir de cookies côté serveur, le stockage en mémoire avec une expiration courte est une solution de repli raisonnable — ne persistez simplement jamais des tokens de longue durée dans localStorage.

Conclusion

Les JWT de longue durée dans localStorage sont ce que les recommandations de sécurité actuelles déconseillent systématiquement. Les cookies HttpOnly avec les attributs Secure et SameSite offrent le meilleur choix par défaut pour la plupart des configurations same-domain, tandis que le stockage en mémoire associé à un cookie de refresh token couvre les cas plus complexes. Une fois que vous comprenez le modèle de menace — XSS d’un côté, CSRF de l’autre — le bon choix pour votre application devient un compromis que vous pouvez raisonner clairement, plutôt qu’une simple supposition.

FAQ

sessionStorage partage la même faiblesse que localStorage : tout JavaScript s'exécutant sur la page peut le lire. La seule différence est que sessionStorage est effacé à la fermeture de l'onglet. Cela réduit la fenêtre d'exposition, mais ne protège pas contre les XSS. Pour le stockage de tokens, traitez sessionStorage avec la même prudence que localStorage et évitez d'y placer des tokens de longue durée.

SameSite=Strict empêche l'envoi des cookies lors de requêtes cross-site, ce qui bloque la plupart des schémas d'attaque CSRF. Cependant, pour les endpoints sensibles impliquant des modifications d'état à fort enjeu, l'ajout d'un token anti-CSRF vous offre une défense en profondeur. SameSite est appliqué par le navigateur, donc les anciens clients ou certains cas limites inhabituels pourraient ne pas le respecter. Le pattern double-submit token reste une mesure de protection judicieuse.

Une plage courante est de 5 à 15 minutes. Suffisamment courte pour qu'un token volé ait une valeur limitée, mais suffisamment longue pour éviter de surcharger votre endpoint de rafraîchissement. Associez cela à un refresh token de plus longue durée (de quelques heures à quelques jours) dans un cookie HttpOnly. Si votre application gère des opérations sensibles comme des paiements, optez pour une expiration plus courte et exigez une ré-authentification pour les actions critiques.

Stockez l'access token en mémoire JavaScript — une variable de module, un état React ou une closure — plutôt que dans localStorage. Gardez-le de courte durée et rafraîchissez-le via un endpoint backend dans la mesure du possible. Si vous devez absolument persister quelque chose entre les rechargements, faites transiter le flux de rafraîchissement par votre propre backend qui conserve les credentials de longue durée côté serveur, et n'exposez jamais des tokens de longue durée au stockage côté client.

Gain control over your UX

See how users are using your site as if you were sitting next to them, learn and iterate faster with OpenReplay. — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay