Chapitre 4.1 — Sessions vs tokens
⏱️ TL;DR — Deux façons de « se souvenir » qu’un utilisateur est connecté. Session serveur : le serveur garde l’état (qui est connecté), le client ne détient qu’un identifiant opaque dans un cookie
HttpOnly. Token auto-porté (typiquement un JWT) : l’état est dans le jeton lui-même, côté client, et le serveur le vérifie par signature sans rien stocker. Le token brille pour le stateless et le multi-services ; mais il est difficile à révoquer et souvent mal stocké (localStorage → vol par XSS). Pour une appli web classique, la session serveur avec cookieHttpOnlyreste le défaut le plus sûr et le plus simple à révoquer. Choisis selon le besoin, pas selon la mode.
🎯 Objectifs
- Décrire le fonctionnement d’une session serveur et d’un token auto-porté.
- Comparer les deux sur la révocation, le stockage, le passage à l’échelle et l’exposition au XSS/CSRF.
- Savoir pourquoi la session serveur + cookie
HttpOnlyest souvent le meilleur défaut. - Choisir l’approche adaptée à un contexte (monolithe, SPA, microservices, mobile).
Session serveur : l’état reste côté serveur
Dans le modèle session, à la connexion, le serveur crée une session (un enregistrement : utilisateur, droits, date…) identifiée par un identifiant aléatoire (CSPRNG). Cet identifiant opaque est envoyé au client dans un cookie ; à chaque requête, le navigateur le renvoie, le serveur retrouve la session et sait qui parle.
Connexion -> serveur cree la session #a1b2..., la stocke (mémoire/Redis/DB)
-> Set-Cookie: __Host-session=a1b2...; HttpOnly; Secure; SameSite=Lax
Requetes -> Cookie: session=a1b2... -> serveur lit l'etat de la session
Deconnexion-> serveur SUPPRIME la session #a1b2... -> immediatement invalideLe cookie ne contient aucune donnée sensible, juste une clé. Tout l’état vit côté serveur, donc sous contrôle : on peut à tout moment invalider une session (déconnexion, changement de mot de passe, compromission détectée) en la supprimant côté serveur.
Token auto-porté : l’état voyage avec le client
Dans le modèle token (souvent un JWT), il n’y a pas d’état côté serveur. À la connexion, le serveur émet un jeton signé contenant les claims (userId, role, exp…). Le client le renvoie à chaque requête ; le serveur vérifie la signature et fait confiance au contenu, sans rien stocker.
Connexion -> serveur signe un JWT { userId, role, exp } et le renvoie
Requetes -> Authorization: Bearer eyJ... -> serveur VERIFIE la signature (stateless)
Deconnexion-> ... le jeton reste techniquement valide jusqu'a exp (probleme !)L’avantage est le stateless : n’importe quel serveur (ou service) qui connaît la clé de vérification peut valider le jeton, sans base de sessions partagée. Pratique pour du multi-services, des API distribuées, ou quand on veut éviter un store de sessions.
La comparaison qui compte
| Critère | Session serveur | Token auto-porté (JWT) |
|---|---|---|
| Où est l’état | Côté serveur (store) | Dans le jeton, côté client |
| Révocation | Immédiate (supprimer la session) | Difficile : valide jusqu’à exp, sauf liste de révocation (qui recrée de l’état…) |
| Stockage client | Cookie HttpOnly (hors JS) | Souvent localStorage (volable par XSS) ou cookie |
| Exposition XSS | Cookie HttpOnly non lisible par JS | En localStorage : lu et exfiltré trivialement |
| Exposition CSRF | Oui, gérée par SameSite + jeton CSRF | Si en header Authorization : pas d’envoi auto (moins de CSRF) |
| Passage à l’échelle | Store partagé (Redis) à prévoir | Stateless, simple à répliquer |
| Taille par requête | Petite (un id) | Plus grosse (tout le payload à chaque requête) |
| Contenu lisible | Rien (opaque) | Payload lisible (Base64) — pas de secret dedans |
Le point décisif pour la plupart des applis : la révocation. Une session serveur se coupe instantanément ; un JWT auto-porté reste valable jusqu’à son expiration, ce qui est un vrai problème en cas de compromission ou de déconnexion « qui doit vraiment déconnecter ». Les contournements (blacklist de jetons) réintroduisent de l’état côté serveur — on perd l’argument stateless.
⚠️ Piège — Le combo le plus dangereux, hélas très répandu dans les tutoriels SPA : JWT stocké en
localStorage. On cumule les défauts — un XSS vole le jeton en une ligne (localStorage.getItem), et le jeton volé reste valable et irrévocable jusqu’àexp. L’attaquant se connecte alors depuis chez lui, tranquillement. Si tu utilises des JWT, garde-les hors de portée du JS (cookieHttpOnly), et prévois des durées courtes + refresh.
Le bon défaut : session serveur + cookie HttpOnly
Pour une appli web classique (monolithe, SSR, SPA parlant à sa propre API sur le même domaine), le défaut le plus sûr et le plus simple reste :
- session côté serveur (état sous contrôle, révocable),
- identifiant opaque dans un cookie
__Host-+HttpOnly+Secure+SameSite=Lax(non volable par XSS, protégé du CSRF), - rotation de l’identifiant de session à la connexion (contre la session fixation, voir chapitre 4.4).
C’est ce que fournissent nativement les frameworks : les sessions PHP/Symfony, express-session, l’auth par session de la plupart des stacks. On ne prend un JWT que quand un vrai besoin l’exige (services distribués sans store partagé, auth machine-à-machine, mobile).
💡 Réflexe — Ne choisis pas « JWT » par réflexe parce que c’est moderne. Pose-toi : « ai-je vraiment besoin de stateless multi-services ? » Si non (cas le plus courant), la session serveur est plus simple, plus sûre (révocation immédiate, cookie
HttpOnly) et suffisante. La bonne question n’est pas « session ou token ? » mais « ai-je un besoin qui justifie de renoncer à la révocation immédiate ? ».
🎯 Côté attaquant — Face à une session opaque en cookie
HttpOnly, l’attaquant ne peut ni lire le cookie (XSS bloqué parHttpOnly) ni deviner l’identifiant (CSPRNG). Face à un JWT enlocalStorage, il lui suffit d’un XSS pour tout rafler — et le jeton volé marche partout, longtemps. Le choix d’architecture change radicalement ce qu’un attaquant obtient d’une même faille. C’est un choix de sécurité, pas juste de confort.
🧭 Sur FormaCampus — Le front
app.formacampus.fret l’APIapi.formacampus.frpartagent le même domaine parent : FormaCampus utilise donc une session serveur (store Redis), identifiant opaque dans un cookie__Host-sessionHttpOnly/Secure/SameSite=Lax, rotation à la connexion. Résultat : déconnexion et « déconnecter tous les appareils » sont instantanés, un XSS ne peut pas voler la session, et le CSRF est couvert (SameSite+ jeton). Les JWT sont réservés à un usage précis : l’authentification inter-services (API ↔ workers), stateless et à durée courte — jamais exposés au navigateur.
✏️ Exercices
Exercice 1 — Révocation. Un utilisateur signale que son compte est compromis ; tu veux le déconnecter immédiatement de partout. Compare la faisabilité selon qu’on utilise une session serveur ou un JWT auto-porté.
✅ Solution
Avec une session serveur : trivial et immédiat — on supprime (ou invalide) toutes les sessions de cet utilisateur dans le store ; la prochaine requête échoue. Avec un JWT auto-porté : impossible directement — le jeton reste valide jusqu’à exp, car le serveur ne stocke rien. Il faut soit attendre l’expiration (dangereux si longue), soit maintenir une liste de révocation côté serveur (ce qui réintroduit de l’état, annulant l’avantage « stateless »). C’est l’argument majeur en faveur des sessions pour une appli classique.
Exercice 2 — Critique le choix. Une SPA stocke un JWT en localStorage et l’envoie en Authorization: Bearer. Le lead dit « pas de risque CSRF, donc c’est plus sûr qu’un cookie ». Réponds de façon nuancée.
✅ Solution
C’est vrai sur un point : un jeton en header Authorization (pas de cookie) n’est pas envoyé automatiquement cross-site, donc peu exposé au CSRF. Mais le raisonnement ignore le risque dominant : en localStorage, le jeton est lisible par tout JS, donc volé et exfiltré dès qu’un XSS existe — et un JWT volé est irrévocable jusqu’à exp. On a échangé un risque bien maîtrisé (CSRF, réglé par SameSite + jeton) contre un risque grave et fréquent (vol par XSS). Un cookie HttpOnly est globalement plus sûr : invisible au JS, et la session serveur est révocable. Le CSRF n’est pas une raison suffisante pour choisir localStorage.
🧠 Quiz de révision
1. Où vit l’état dans une session serveur vs un token auto-porté ?
Session serveur : l’état (qui est connecté, droits) vit côté serveur ; le client n’a qu’un identifiant opaque. Token auto-porté (JWT) : l’état est dans le jeton, côté client ; le serveur le vérifie par signature sans rien stocker.
2. Pourquoi la révocation est-elle plus facile avec une session serveur ?
Parce que l’état est côté serveur : supprimer/invalider la session la rend inutilisable immédiatement. Un JWT auto-porté reste valable jusqu’à exp (le serveur ne stocke rien) ; le révoquer exige une liste de révocation qui réintroduit de l’état.
3. Quel est le combo dangereux souvent vu dans les SPA ?
JWT en localStorage : volé en une ligne par un XSS (localStorage.getItem) et irrévocable jusqu’à expiration. On cumule vol facile et absence de révocation. Préférer un cookie HttpOnly.
4. Quel est le défaut recommandé pour une appli web classique ?
Une session serveur avec identifiant opaque dans un cookie __Host- + HttpOnly + Secure + SameSite=Lax, avec rotation de l’id à la connexion. Simple, révocable, non volable par XSS.
5. Quand un token/JWT est-il justifié ?
Quand un vrai besoin de stateless multi-services existe : API distribuées sans store partagé, auth machine-à-machine, certains cas mobiles. Pas « par défaut parce que c’est moderne » — et jamais exposé au JS via localStorage.
Chapitre suivant : Mots de passe & connexion — construire un flux de login qui résiste au bruteforce, au credential stuffing et à l’énumération de comptes.