Aller au contenu principal

JWT ou session : révocation, refresh tokens et stockage du token

Deux modèles d'authentification, deux endroits où vit l'état de connexion. Cet article compare le JWT et la session serveur sur les points qui coincent en production : la révocation immédiate, le rôle exact des refresh tokens, les situations où le JWT complique le travail, et l'endroit où ranger le token côté navigateur selon l'OWASP et la RFC 10017.

Sécurité ·
Adel LATIBI
Adel LATIBI

Le Briefing Dev - les ressources et actus de la semaine, droit dans ta boîte chaque vendredi gratuitement.

En vous inscrivant, vous acceptez de recevoir notre newsletter. Désinscription possible à tout moment.

La plupart des tutoriels d'authentification se terminent de la même manière : le serveur signe un JWT, le navigateur le range dans localStorage, chaque requête part avec un en-tête Authorization. L'application fonctionne, l'écran de connexion renvoie bien vers le tableau de bord, et le sujet semble clos.

Entre ce montage et une authentification qui tient en production, il reste des décisions que le tutoriel ne prend pas. Que se passe-t-il au clic sur Déconnexion. Comment couper l'accès d'un compte compromis avant l'expiration du token. Ce que devient ce token quand un script tiers s'exécute dans la page.

Ces trois questions ont une origine commune : un JWT et un identifiant de session ne rangent pas l'état de connexion au même endroit. Une fois ce point posé, la révocation, le poids des requêtes et la surface d'attaque en découlent mécaniquement.

Voici la comparaison des deux modèles sur ces critères, appuyée sur les textes de référence : les cheat sheets de l'OWASP et la RFC 10017 publiée par l'IETF en août 2026. L'objectif est qu'une personne en reconversion, un développeur junior ou un curieux non-informaticien reparte avec un critère de choix, pas avec une préférence de tribu.

Le logout qui ne déconnecte personne

Une application émet des JWT valables 24 heures. À 10 h, un utilisateur clique sur Déconnexion. Le front supprime la clé dans localStorage, redirige vers la page de login, et l'interface se comporte exactement comme prévu. Sauf qu'une copie du token a été exfiltrée trois minutes plus tôt par un script injecté dans la page. Cette copie continue d'ouvrir toutes les routes protégées jusqu'au lendemain matin.

Deuxième situation, plus banale : à 14 h, un administrateur retire les droits d'administration d'un compte. Le token en circulation contient toujours role: admin. L'API fait confiance au token qu'elle vérifie, pas à la ligne en base qui vient de changer, donc l'ancien niveau de privilège survit jusqu'à l'expiration.

Dans les deux cas, le code ne contient aucun bug. Le comportement découle du modèle retenu, et c'est précisément pour ça qu'il faut le choisir en connaissance de cause plutôt que par habitude.

Deux façons de ranger l'état

Avec une session serveur, l'application crée un enregistrement de son côté et remet au navigateur un identifiant aléatoire. L'OWASP demande que cet identifiant soit dépourvu de sens, tiré d'un générateur cryptographique et porteur d'au moins 64 bits d'entropie. Le rôle, les droits, la date de dernière connexion, tout reste sur le serveur. L'identifiant fonctionne comme un numéro de vestiaire : il ne dit rien du manteau.

Avec un JWT, défini par la RFC 7519, l'information voyage avec le client. Trois parties encodées en base64url, séparées par des points : un en-tête qui annonce l'algorithme, une charge utile qui contient les claims, une signature qui protège les deux premières. Le serveur vérifie la signature avec sa clé et travaille sur ce qu'il lit, sans aller chercher quoi que ce soit en base.

Session : par référence JWT : par valeur Navigateur cookie : id=7f3a91c4... Navigateur eyJhbGci... sub 42, role admin, exp Serveur 7f3a91c4 -> user 42, role admin l'état vit ici Serveur verify(signature, cle) aucun état stocké Révocation : supprimer l'enregistrement Révocation : attendre l'expiration
L'emplacement de l'état de connexion décide de la stratégie de révocation.

Un point mérite d'être vérifié à la main, parce qu'il surprend souvent : signé ne veut pas dire chiffré. La signature garantit que personne n'a modifié le contenu, elle ne le cache pas. Les exemples de code de cet article tournent sur un navigateur à jour ou sur Node.js 22, où atob et TextDecoder sont disponibles sans dépendance.

// Console du navigateur, sur un JWT signe en HS256.
// Aucune cle n'est necessaire pour lire la charge utile.
const [, payload] = token.split(".");
 
const bytes = Uint8Array.from(
  atob(payload.replaceAll("-", "+").replaceAll("_", "/")),
  (c) => c.charCodeAt(0),
);
 
console.log(JSON.parse(new TextDecoder().decode(bytes)));
// { sub: '42', role: 'admin', exp: 1789000000 }

Toute personne qui tient le token lit son contenu, y compris n'importe quel script exécuté dans la page. Un identifiant de e-mail, un numéro de client interne ou un rôle placé dans les claims devient une donnée exposée. La cheat sheet OWASP sur les sessions insiste sur le point symétrique : la valeur d'un identifiant de session ne doit jamais contenir d'information sur l'utilisateur.

La révocation, là où les deux modèles divergent

Couper une session serveur revient à supprimer un enregistrement. La requête suivante présente un identifiant qui ne correspond plus à rien, l'utilisateur repasse par la case connexion. L'OWASP ajoute une règle que beaucoup de projets oublient : l'identifiant de session doit être régénéré à chaque changement de niveau de privilège, connexion comprise, sous peine de laisser la porte ouverte à la fixation de session.

Avec un JWT, il n'y a rien à supprimer. Trois approches existent, et chacune se paie.

  • Raccourcir la durée de vie. La fenêtre d'abus se réduit, elle ne disparaît pas, et les utilisateurs se reconnectent plus souvent.
  • Tenir une liste de tokens révoqués. La cheat sheet OWASP sur les JWT note l'effet de bord : dès qu'une liste existe, la session cesse d'être sans état, ce qui annule l'argument principal du JWT pour ce cas d'usage. Le texte suggère d'ailleurs d'envisager un système de sessions classique quand l'invalidation est nécessaire.
  • Utiliser une Token Status List, mécanisme en cours de normalisation à l'IETF : le token porte un claim status avec l'URI d'une liste compressée et son index dans cette liste, que le consommateur va interroger.

Ce que les refresh tokens règlent, et ce qu'ils laissent ouvert

Le montage habituel consiste à émettre un access token de courte durée et un refresh token plus long, échangeable contre un nouvel access token auprès du serveur d'autorisation. La RFC 10017 encadre strictement cette mécanique pour les applications qui tournent dans un navigateur, considérées comme des clients publics.

  • Le serveur d'autorisation doit soit faire tourner le refresh token à chaque usage, soit le lier au client qui le présente.
  • Il doit poser une durée de vie maximale, ou une expiration après une période sans usage.
  • Un token renouvelé ne doit pas prolonger la durée du token initial. L'exemple donné par la RFC : access token de 10 minutes, refresh token de 8 heures, puis 7 heures 50 après le premier renouvellement, jusqu'au retour obligatoire par un flux complet à la huitième heure.

La rotation sert de détecteur : si un token déjà consommé revient, le serveur en déduit qu'une copie circule et invalide la chaîne. Cette détection tombe face au scénario que la RFC appelle le vol persistant. Un attaquant qui a installé un mécanisme d'exfiltration continue récupère la dernière version du token, puis s'arrange pour que l'application légitime ne l'utilise jamais, en effaçant son stockage ou en attendant que l'utilisateur ferme l'onglet. Sans réutilisation, il n'y a rien à détecter.

Le texte va plus loin sur les applications où le navigateur porte lui-même le rôle de client OAuth. Du code malveillant exécuté dans l'origine de l'application peut ouvrir une iframe cachée, relancer un flux d'autorisation silencieux dans la session existante de l'utilisateur, et obtenir un jeu de tokens neuf et indépendant. La RFC est explicite : aucun mécanisme applicable côté frontend ne contre ce scénario, ni les durées courtes, ni la rotation, ni les tokens liés à une clé.

Où stocker le token dans le navigateur

La cheat sheet OWASP sur la gestion des sessions ouvre sa section Web Storage par un avertissement sans nuance : ne pas y placer de token d'authentification, d'identifiant de session, de JWT ni de refresh token, parce que ces API sont accessibles à tout JavaScript exécuté dans l'origine et qu'une seule faille XSS suffit à tout exposer. Elle oriente vers un cookie HttpOnly; Secure; SameSite=Strict ou vers un Backend For Frontend.

localStorage et sessionStorage

Le premier persiste d'une visite à l'autre et n'est pas chiffré au repos, les standards ne l'exigeant pas, donc son contenu peut se lire depuis le disque. Le second se limite à un onglet et disparaît à sa fermeture, ce qui réduit la fenêtre sans changer le fond : les deux se lisent en une ligne de JavaScript. Pour voir à quoi ressemble l'injection qui va chercher cette valeur, l'article sur CSRF, XSS et SQLi démontrés avec du code qui casse déroule l'attaque puis le correctif.

Le cookie posé par le serveur

Set-Cookie: __Host-id=<valeur>; Secure; HttpOnly; SameSite=Strict; Path=/

Chaque attribut a une fonction précise. HttpOnly retire la valeur de document.cookie, Secure la réserve aux échanges chiffrés, le préfixe __Host- impose au navigateur un cookie sans attribut Domain et en Path=/, ce qui bloque les attaques venues d'un sous-domaine compromis. L'OWASP recommande aussi d'écrire SameSite explicitement plutôt que de compter sur la valeur par défaut, qui varie selon les navigateurs et les versions, et de renommer les identifiants de session par défaut des frameworks, du type PHPSESSID ou JSESSIONID, qui annoncent la technologie du serveur.

Le cookie a deux contreparties. Il part automatiquement avec les requêtes, donc la protection CSRF devient obligatoire. Et sa taille est contrainte : la RFC 6265 demande aux navigateurs de supporter au moins 4096 octets par cookie, nom et attributs compris, ce qui rend inconfortable un JWT gonflé de claims, envoyé à chaque requête vers le serveur.

La mémoire et le Web Worker

Garder le token dans une variable JavaScript le fait disparaître à chaque rechargement, ce qui gêne l'utilisateur sans arrêter un script malveillant qui lit la même variable. Le Web Worker offre mieux : selon l'OWASP, un secret conservé dans un worker bénéficie de la même garantie de confidentialité qu'un cookie HttpOnly, avec l'avantage qu'un code isolé peut s'en servir. Une attaque XSS reste capable de demander au worker d'effectuer l'opération à sa place, mais la valeur elle-même ne sort pas.

Le Backend For Frontend

Le BFF est un composant serveur qui devient le client OAuth de l'application front. Il détient les tokens, les rattache à une session par cookie, et relaie chaque appel vers l'API en y ajoutant l'access token côté serveur. Le navigateur ne voit jamais de token, donc il n'y a rien à voler dans la page. La RFC 10017 impose les attributs Secure et HttpOnly, recommande SameSite=Strict et un préfixe de nom, exige une défense CSRF et une liste blanche des destinations vers lesquelles le BFF accepte de relayer. Elle recommande fortement cette architecture pour les applications métier, sensibles ou manipulant des données personnelles, et déconseille pour ces mêmes cas le client OAuth entièrement embarqué dans le navigateur.

Emplacement Lisible par un script de la page Survit au rechargement
localStorage Oui Oui, entre les visites
sessionStorage Oui Oui, tant que l'onglet vit
Variable JavaScript Oui Non
Web Worker Non, usage possible à distance Non
Cookie HttpOnly Non Oui, selon la durée posée
Session côté BFF Aucun token dans le navigateur Oui, côté serveur

Les cas où le JWT complique le travail

La RFC 10017 consacre une section aux applications dont le front et l'API vivent sur le même domaine. Son constat mérite d'être lu deux fois : ces applications sont souvent rendues inutilement complexes par l'emploi d'OAuth en remplacement d'une simple gestion de session, alors qu'un cookie de session suffit à suivre l'état d'authentification. OAuth a été conçu pour l'accès délégué à des API tierces, pas pour remplacer une session dans une application qui parle à son propre backend.

Quatre situations rendent le JWT coûteux :

  • Une session utilisateur classique, front et API sur le même domaine.
  • Un besoin de couper un accès dans la seconde : bannissement, compte compromis, fin de contrat, sortie d'un prestataire.
  • Des droits qui bougent pendant la session, avec des rôles portés par le token.
  • Des claims volumineux ou sensibles, lisibles par qui détient le token et transportés à chaque appel.

À l'inverse, le format tient sa promesse quand plusieurs services doivent vérifier une autorisation sans partager de base commune, quand un serveur d'autorisation externe émet des access tokens de courte durée, pour les appels entre machines, ou pour l'ID Token d'OpenID Connect qui transporte une identité et non une session. Si le sujet t'intéresse côté serveur, la formation Créer une API REST avec Python et FastAPI traite ces choix sur du code, et l'article sur une première API REST avec FastAPI pose les bases avant d'y ajouter l'authentification.

Pièges fréquents

  • Accepter n'importe quel algorithme. Certaines bibliothèques acceptaient autrefois alg: none, ce qui laissait forger des tokens. Le risque actuel est la confusion de type de clé : une clé publique interprétée comme un secret HMAC. L'OWASP conseille de figer la liste des algorithmes acceptés et de ne pas mélanger signature asymétrique et MAC.
  • Un secret HMAC choisi à la main. Le secret doit venir d'un générateur cryptographique et faire au moins la taille de la sortie de la fonction de hachage, soit 256 bits pour HS256. Une phrase de passe ne remplit pas cette condition.
  • Ne pas valider les claims. Vérifier la signature ne suffit pas. exp, iss et aud ferment la porte à un token valide émis pour un autre destinataire.
  • Prendre la clé de vérification dans l'en-tête du token. Les paramètres jwk, jku et x5u sont des données fournies par l'appelant. La confiance doit venir d'ailleurs : clé épinglée ou jwks_uri publié dans les métadonnées de l'émetteur.
  • Ranger un refresh token dans le navigateur le temps du prototype. Sa durée de vie en fait la cible la plus intéressante, et le prototype finit souvent en production.
  • Prendre HttpOnly pour une protection contre le XSS. L'attribut protège la confidentialité de la valeur. Un script injecté dans la page continue d'envoyer des requêtes authentifiées depuis le navigateur de la victime, scénario que la RFC 10017 décrit sous le nom de relais via le navigateur et qu'aucune mesure applicative n'arrête.

Un dernier repère pour trancher : commence par écrire ce qui doit se passer quand un compte est compromis à 15 h 12. La réponse à cette phrase désigne le modèle, avant même la première ligne de code.

Questions fréquentes

Un JWT est-il chiffré ?

Un JWT signé est encodé, pas chiffré. N'importe qui peut décoder sa charge utile en base64url et lire les claims, comme le montre l'extrait de code plus haut. Une variante chiffrée existe, le JWE défini par la RFC 7516, mais elle n'est pas ce que produisent les bibliothèques par défaut.

Comment déconnecter un utilisateur quand on utilise des JWT ?

Trois voies : réduire la durée de vie pour raccourcir la fenêtre, tenir une liste de révocation côté serveur indexée sur les claims jti et iss, ou publier une Token Status List interrogée par les consommateurs du token. L'OWASP rappelle que la première liste de révocation venue fait perdre le caractère sans état, et suggère d'envisager une session classique quand l'invalidation immédiate est une exigence.

localStorage ou cookie pour stocker un JWT ?

La cheat sheet OWASP sur la gestion des sessions déconseille explicitement localStorage et sessionStorage pour tout token d'authentification, identifiant de session ou refresh token, et oriente vers un cookie HttpOnly, Secure, SameSite=Strict, ou vers un Backend For Frontend. Le cookie impose en échange une défense CSRF.

Les refresh tokens protègent-ils du vol de token ?

Ils réduisent la durée pendant laquelle une copie volée reste utilisable, et la rotation permet de détecter une réutilisation. La RFC 10017 décrit le cas où cette détection échoue : un attaquant qui exfiltre en continu et empêche l'application d'utiliser le dernier token ne produit aucune réutilisation détectable.

Faut-il un BFF pour une petite application ?

La RFC 10017 recommande fortement cette architecture pour les applications métier, sensibles ou traitant des données personnelles, et reconnaît qu'elle est nettement plus lourde qu'une application uniquement navigateur. Pour un front et une API sur le même domaine, elle signale qu'une session par cookie suffit souvent, sans OAuth du tout.

Les sessions serveur posent-elles un problème de montée en charge ?

Elles demandent un stockage partagé entre instances, avec des solutions de type sessions collantes ou réplication. La RFC 10017 signale cette contrainte et réserve les sessions côté serveur associées à un BFF aux déploiements de taille modeste. En contrepartie, elles offrent un contrôle total sur les sessions actives et la possibilité de les révoquer à volonté.

Un cookie HttpOnly suffit-il contre le XSS ?

Il empêche un script de lire la valeur du cookie, ce qui évite l'exfiltration et la réutilisation hors du navigateur de la victime. Il n'empêche pas ce script d'envoyer des requêtes authentifiées depuis la page, puisque le navigateur joint le cookie automatiquement. La protection contre le XSS reste l'encodage en sortie, la validation des entrées et une politique de sécurité de contenu.

Vous êtes expert ?

Partagez votre expertise sur notre blog

Tutoriel, retour d'expérience, analyse - publiez un article invité et gagnez en visibilité.

Écrire pour nous