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.
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
statusavec 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,issetaudferment 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,jkuetx5usont des données fournies par l'appelant. La confiance doit venir d'ailleurs : clé épinglée oujwks_uripublié 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
HttpOnlypour 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.