JWT ou cookies de session : différences et cas d'usage

Un cookie de session est une clé vers un état conservé sur le serveur, alors qu'un JWT transporte un état signé jusqu'au serveur. Cette seule différence explique tous les compromis.

Essayez : Décodeur JWT

On présente souvent la question comme un duel entre JWT et cookies, mais ces deux notions ne sont pas de même nature. Un cookie est un moyen d'envoyer des données avec chaque requête. Un JWT est un format de données. Le vrai choix se fait entre une session côté serveur, où le navigateur détient un identifiant aléatoire et où le serveur cherche à qui il appartient, et un token autonome, où le client détient des claims signés que le serveur peut vérifier sans rien consulter. Une fois ce point clarifié, les questions de révocation, de stockage et de CSRF trouvent naturellement leur place.

Comment fonctionne une session côté serveur

Après une connexion réussie, le serveur crée un enregistrement de session (ID de l'utilisateur, rôles, expiration) en mémoire, dans une base de données ou dans un magasin comme Redis, puis envoie au navigateur un identifiant impossible à deviner dans un cookie. Le navigateur joint ce cookie à chaque requête suivante vers ce site, et le serveur charge l'enregistrement à chaque fois. L'identifiant ne signifie rien en lui-même : 32 octets aléatoires s'encodent en 43 caractères.

Set-Cookie: __Host-session=p-sLYX7J3ziMKxq-dadVMuQJNSVBHM5hrw7MJPihGQ0; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=86400

Avec le préfixe __Host-, les navigateurs refusent le cookie s'il n'est pas Secure, s'il n'a pas Path=/ ou s'il définit un Domain : un sous-domaine ne peut donc pas l'écraser.

Comment fonctionne un JWT

Un JSON Web Token (RFC 7519) se compose de trois parties encodées en Base64URL et séparées par des points : un en-tête qui nomme l'algorithme, une charge utile (payload) de claims comme sub et exp, et une signature calculée sur les deux premières parties. Le serveur vérifie la signature et l'expiration à l'aide d'une clé, puis fait confiance aux claims sans interroger de base de données. Le token est en général envoyé dans un en-tête Authorization: Bearer.

La charge utile est seulement encodée, pas chiffrée. Quiconque détient le token peut la lire, ce que vous pouvez constater en en collant un dans le décodeur JWT. Ne mettez jamais dans un JWT de secrets ni de données personnelles que vous ne montreriez pas à l'utilisateur. Des tokens chiffrés existent (JWE, RFC 7516), mais la plupart des JWT en circulation sont seulement signés. Pour comprendre pourquoi les parties utilisent - et _ au lieu de + et /, consultez Base64 vs Base64URL.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzg0MTIiLCJpYXQiOjE3NjAwMDAwMDAsImV4cCI6MTc2MDAwMDkwMH0.08BFm85KQIp34VSetcibqwBVLpHKR3IZhI_IPNeTif4

// Reading the payload needs no key at all (browser):
const part = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
const bytes = Uint8Array.from(atob(part), c => c.charCodeAt(0));
JSON.parse(new TextDecoder().decode(bytes));
// { sub: 'user_8412', iat: 1760000000, exp: 1760000900 }

Les compromis côte à côte

Session côté serveurJWT
Le client détientUn identifiant aléatoire, sans significationDes claims signés et lisibles
Le serveur a besoin deUn magasin de sessions, partagé par tous les serveursUniquement la clé qui vérifie les signatures
Déconnecter un utilisateur partoutSupprimer la session : effet immédiatImpossible avant exp sans état supplémentaire
Changement de rôle ou de permissionS'applique dès la requête suivanteS'applique à l'émission d'un nouveau token
Transport habituelCookie, envoyé automatiquementEn-tête Authorization, ou un cookie
Taille par requêteID de 43 caractères dans notre exempleDe 152 à 540 caractères dans nos exemples

Révocation, expiration et refresh tokens

L'absence d'état côté serveur (stateless) est le principal argument en faveur des JWT, et aussi leur principale faiblesse. Un token valide le reste jusqu'à son expiration, même après que l'utilisateur s'est déconnecté, a changé son mot de passe ou a été banni. La réponse habituelle consiste à garder des access tokens à courte durée de vie, souvent quelques minutes plutôt que quelques heures, et à les associer à un refresh token (jeton de rafraîchissement) valable plus longtemps, que le client échange contre de nouveaux access tokens. Le refresh token est vérifié par rapport à un état côté serveur : le révoquer empêche donc l'émission de nouveaux access tokens.

L'OAuth 2.0 Security Best Current Practice (RFC 9700) décrit la rotation des refresh tokens : chaque rafraîchissement renvoie un nouveau refresh token et invalide l'ancien, ce qui permet de détecter la réutilisation d'un token volé. Si vous avez aussi besoin de révoquer instantanément les access tokens, vous finissez par tenir une liste de refus d'identifiants de tokens (jti) ou une version de token par utilisateur, c'est-à-dire de nouveau un état côté serveur. À ce stade, demandez-vous si une simple session ne serait pas plus simple.

Où stocker le token : cookies, localStorage et XSS

Tout ce qui se trouve dans localStorage ou sessionStorage peut être lu par n'importe quel script exécuté sur votre page. Une seule faille de cross-site scripting (XSS) ou un seul script tiers compromis suffit pour copier le token et l'utiliser depuis n'importe où jusqu'à son expiration.

Un cookie marqué HttpOnly est totalement illisible pour JavaScript : une faille XSS ne peut donc pas le voler. Elle peut en revanche envoyer des requêtes depuis la page ouverte par la victime, et le navigateur y joindra le cookie : HttpOnly limite les dégâts sans les empêcher. Prévenir les failles XSS reste indispensable dans tous les cas ; les cookies HttpOnly rendent simplement une attaque réussie moins utile à l'attaquant.

CSRF et SameSite

La commodité du cookie fait aussi son risque : le navigateur le joint automatiquement, y compris aux requêtes déclenchées par un autre site web. C'est ce qu'on appelle la falsification de requête intersite (CSRF). Les tokens envoyés dans un en-tête Authorization ne sont pas exposés au CSRF classique, car le navigateur n'ajoute pas de lui-même un bearer token : c'est votre propre code qui doit le faire.

  • SameSite=Strict : le cookie n'est jamais envoyé avec les requêtes parties d'un autre site, y compris quand on suit un lien vers le vôtre.
  • SameSite=Lax : le cookie est envoyé quand l'utilisateur arrive sur votre site par un lien normal (un GET de premier niveau), mais pas avec les envois de formulaires intersites, les requêtes fetch ou les ressources intégrées. Chrome applique Lax aux cookies qui ne définissent pas l'attribut ; définissez-le explicitement plutôt que de compter là-dessus.
  • SameSite=None : le cookie est envoyé partout, et cette valeur doit être combinée avec Secure (sinon Chrome la rejette). Elle ne sert que pour un vrai usage intersite, comme des widgets intégrés.

« Même site » (same site) désigne le même domaine enregistrable : app.example.com et forum.example.com comptent donc comme le même site. SameSite ne vous protège pas d'un sous-domaine compromis. Pour les requêtes qui modifient des données, conservez aussi un token CSRF ou vérifiez l'en-tête Origin.

La taille des tokens

Un identifiant de session reste minuscule, quelle que soit la quantité d'informations dont vous disposez sur l'utilisateur. Un JWT grossit avec chaque claim et selon l'algorithme de signature. Les tokens que nous avons générés pour ce guide faisaient 152 caractères en HS256 avec trois claims, 241 caractères après l'ajout d'un nom, d'une adresse e-mail et d'un rôle, et 540 caractères pour les mêmes claims signés en RS256 avec une clé de 2048 bits. La RFC 6265 impose seulement aux navigateurs d'accepter des cookies d'au moins 4 096 octets, nom et attributs compris : un JWT rempli de permissions peut donc dépasser la taille d'un cookie. Et ce poids est envoyé avec chaque requête.

Les erreurs qui ruinent la sécurité d'un JWT

  • Faire confiance à l'en-tête alg. Fixez les algorithmes que votre serveur accepte. Accepter none, ou laisser un token passer de RS256 à HS256 pour que la clé publique serve de secret HMAC, permet à un attaquant de forger des tokens.
  • Décoder au lieu de vérifier. Lire la charge utile ne prouve rien : vérifiez d'abord la signature avec une bibliothèque maintenue.
  • Négliger la vérification des claims. Validez exp, nbf, iss et aud, pour qu'un token émis pour un service ne soit pas accepté par un autre.

La RFC 8725, JWT Best Current Practices, traite de ces erreurs et de bien d'autres.

Quand choisir l'un ou l'autre

SituationLe choix le plus adapté en général
Application web rendue côté serveur sur un seul domaineCookie de session
Single-page app servie depuis le même site que son APICookie de session, ou un backend-for-frontend qui garde les tokens sur le serveur
Application mobile nativeAccess token à courte durée de vie et refresh token dans le stockage sécurisé de la plateforme
Microservices qui vérifient les requêtes sans appeler un service d'authentificationAccess tokens JWT signés avec une clé asymétrique, par exemple RS256 ou ES256
Accès de tiers à votre APIAccess tokens OAuth 2.0, qui sont souvent des JWT

La solution hybride : un JWT dans un cookie HttpOnly

Beaucoup d'applications placent le JWT lui-même dans un cookie HttpOnly; Secure; SameSite. Les scripts ne peuvent pas le lire, et les serveurs peuvent toujours le vérifier sans consulter de session. Les compromis suivent eux aussi : le cookie est envoyé automatiquement, il faut donc de nouveau des défenses contre le CSRF ; la limite de 4 Ko des cookies s'applique ; et la révocation reste aussi difficile qu'avec n'importe quel autre JWT. C'est une architecture solide pour une application web adossée à des API stateless, à condition de la traiter comme un cookie pour le CSRF et comme un JWT pour la révocation.

Questions Fréquentes

Un JWT est-il plus sûr qu'un cookie de session ?

Non, aucun des deux n'est plus sûr par nature. Les sessions sont plus faciles à révoquer et ne révèlent rien au client. Les JWT évitent de consulter une session, mais sont difficiles à révoquer avant leur expiration. L'endroit où l'identifiant est stocké et la manière de gérer le CSRF et le XSS comptent davantage que le format.

Faut-il stocker un JWT dans localStorage ?

C'est simple, mais le token est alors exposé à tous les scripts de la page : une seule faille XSS peut le faire fuiter. Un cookie HttpOnly et Secure, avec un attribut SameSite explicite, le met hors de portée de JavaScript, au prix d'une protection CSRF nécessaire.

Peut-on invalider un JWT ?

Pas à lui seul. Un token signé reste valide jusqu'à son heure exp. Pour couper l'accès plus tôt, gardez des access tokens à courte durée de vie, révoquez le refresh token, ou comparez les identifiants de tokens à une liste de refus, ce qui ajoute un état côté serveur.

Les données d'un JWT sont-elles chiffrées ?

Normalement non. L'en-tête et la charge utile sont du JSON encodé en Base64URL que tout le monde peut décoder ; seule la signature empêche les modifications. Utilisez JWE si le contenu doit rester confidentiel, ou gardez les données sensibles hors du token.
Essayez : Décodeur JWT