Ce qu'il y a dans un JWT — et ce qu'il ne protège pas
Publié le 06/07/2026 · 17 min de lecture · Outils pour développeurs
Daniel Okonkwo — Développeur front-end et rédacteur Tech chez Allin
Performance web · Formats de fichiers
Vérifié à partir de 6 sources
Un JSON Web Token, ce sont trois chaînes encodées en base64url et jointes par des points : en-tête, charge utile, signature. La signature prouve que les deux premières parties n'ont pas été modifiées par quelqu'un sans la clé. Elle ne les cache pas. Prends ce vrai jeton HS256 — eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFkYSBMb3ZlbGFjZSIsInJvbGUiOiJlZGl0b3IiLCJpYXQiOjE3ODY1NzkyMDAsImV4cCI6MTc4NjU4MjgwMCwiaXNzIjoiaHR0cHM6Ly9hbGxpbi5leGFtcGxlIiwianRpIjoiYTFiMmMzIn0.bu5qB6d_D3--WUj4b77tnh-wdH6FhKhLcbh2nvNmyl4 — et décode la section du milieu sans aucune clé. Il en sort {"sub":"1234567890","name":"Ada Lovelace","role":"editor","iat":1786579200,"exp":1786582800,"iss":"https://allin.example","jti":"a1b2c3"}. Le base64url est un encodage, pas un chiffrement. La première partie nomme l'algorithme, la troisième est une étiquette de 43 caractères sur les deux premières, et le jeton entier fait 264 caractères qui voyagent à chaque requête. Tout ce que tu mets dans une charge utile JWT — adresses e-mail, identifiants internes, drapeaux de permission — est lisible par le navigateur, par tout proxy qui journalise l'en-tête, et par quiconque lit le jeton sur un écran. Deux conséquences. Ne mets rien de confidentiel dans une charge utile ; utilise JWE si le contenu doit vraiment être caché, ce qui est une spécification différente. Et rappelle-toi que la signature ne révoque rien : un jeton volé reste valide jusqu'à son exp, d'où des jetons d'accès à durée courte associés à un jeton de rafraîchissement révocable.
Un JWT est signé, pas chiffré. Quiconque détient le jeton peut décoder la charge utile et lire chaque revendication. Voici un vrai jeton, décodé sans aucune clé, plus les trois attaques que la signature est censée arrêter et le seul problème qu'elle ne peut pas résoudre.
Trois parties, deux points
Tout JWT a le même squelette : en-tête, point, charge utile, point, signature. L'en-tête est un minuscule objet JSON nommant l'algorithme — ici {"alg":"HS256","typ":"JWT"}, qui s'encode en base64url en eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. La charge utile est un autre objet JSON contenant les revendications. La signature est calculée sur les deux premières parties encodées jointes par leur point : c'est pourquoi tu ne peux jamais les réordonner ni les reformater, car ce sont ces octets exacts qui ont été signés.
La RFC 7519 réserve sept noms de revendications, et les connaître évite bien des réinventions. iss est l'émetteur, sub le sujet, aud le public visé, exp l'expiration, nbf l'heure avant laquelle le jeton est invalide, iat l'heure d'émission et jti un identifiant unique de jeton. Les trois revendications temporelles sont des NumericDate : de simples secondes depuis l'époque Unix, pas des millisecondes — une source fiable de bugs d'un facteur 1000 en JavaScript. Dans le jeton ci-dessus, iat vaut 1786579200 et exp 1786582800 : un écart de 3 600, donc une durée de vie d'une heure.
La taille grimpe plus vite qu'on ne l'imagine. Cette charge utile fait 137 octets de JSON et devient 183 caractères une fois encodée, car le base64 coûte toujours quatre caractères de sortie pour trois octets d'entrée — une surcharge de 33 %. Le jeton entier fait 264 caractères, et il voyage à chaque requête. Passe à RS256 avec une clé de 2048 bits et la seule signature passe de 32 octets bruts à 256, soit 342 caractères base64url : le jeton double à peu près. C'est en fourrant un tableau de permissions dans la charge utile que les équipes finissent par heurter les limites d'en-tête des proxys.
Décode-le toi-même : la charge utile est en clair
Copie la section du milieu du jeton — la suite de caractères entre les deux points — et décode-la en base64url. Aucune clé, aucune bibliothèque, aucune permission. Ce qui revient est {"sub":"1234567890","name":"Ada Lovelace","role":"editor","iat":1786579200,"exp":1786582800,"iss":"https://allin.example","jti":"a1b2c3"}. Chaque revendication, en clair, y compris le rôle. Si c'avait été une adresse e-mail, un numéro client interne, un niveau d'abonnement ou un drapeau de fonctionnalité, ce serait tout aussi visible.
La confusion est compréhensible, car le jeton ressemble à du chiffré. Il ne l'est pas. Le base64url existe pour faire passer des octets quelconques dans des canaux qui ne tolèrent qu'un jeu de caractères restreint — URL, en-têtes, noms de fichiers. Il n'y a pas de clé, donc rien à garder secret et rien à casser. La signature protège l'intégrité : si quelqu'un modifie la charge utile, la troisième partie ne se vérifiera plus et le serveur rejettera le jeton. L'intégrité n'est pas la confidentialité, et un JWT ne fournit que la première.
La règle pratique en découle directement. Traite une charge utile JWT comme un panneau d'affichage public qui se trouve être infalsifiable. Mets-y des identifiants, pas des secrets. Mets un identifiant d'utilisateur plutôt qu'un e-mail ; un nom de rôle plutôt que le raisonnement derrière ; rien que tu n'imprimerais à l'extérieur d'une enveloppe. Et stocke-le en conséquence : un cookie HttpOnly, Secure, SameSite le met hors de portée des scripts de page, alors que localStorage le remet à tout script qui parvient à s'exécuter sur ton origine.
Pourquoi base64url et pas base64
Le base64 standard utilise soixante-quatre caractères se terminant par + et /, et complète la sortie avec = jusqu'à un multiple de quatre. Ces trois-là sont hostiles aux URL et aux en-têtes : + signifie une espace dans les données de formulaire, / est un séparateur de chemin, et = un séparateur clé-valeur dans une chaîne de requête. La RFC 4648 définit donc un alphabet compatible URL qui remplace + par -, / par _, et supprime entièrement le remplissage. C'est le base64url, et c'est ce qu'utilisent toutes les parties d'un JWT.
La signature du jeton ci-dessus le montre à découvert : bu5qB6d_D3--WUj4b77tnh-wdH6FhKhLcbh2nvNmyl4 contient un tiret bas et trois traits d'union. Reconvertis en base64 standard, ces mêmes 32 octets se lisent bu5qB6d/D3++WUj4b77tnh+wdH6FhKhLcbh2nvNmyl4=. Si tu décodes un jour une partie de JWT avec une fonction base64 ordinaire et obtiens du charabia, c'est pour cela : il faut d'abord retransformer - en + et _ en /, et remettre le remplissage si le décodeur l'exige.
Ce que la signature arrête, et ce qui la met en échec
Modifie un caractère de la charge utile et la signature ne correspond plus, car HMAC-SHA-256 sur la chaîne en-tête.charge modifiée produit tout autre chose. Resigner la charge falsifiée exige le secret, que le client n'a pas. Jusque-là, tout va bien. Les échecs historiques ne sont pas des attaques contre la cryptographie ; ce sont des attaques contre le vérificateur, et les deux classiques viennent d'une confiance accordée à l'en-tête.
La première est alg: none. La RFC 7515 définit un JWS non sécurisé dont l'en-tête vaut {"alg":"none"} et dont la partie signature est simplement vide — le jeton se termine par un point nu. Les bibliothèques qui lisaient alg dans l'en-tête et aiguillaient en conséquence acceptaient un tel jeton et traitaient ses revendications comme vérifiées. Un attaquant réécrit la charge à sa guise, met alg à none, supprime la signature et entre. Le correctif n'est pas de rejeter la chaîne none ; c'est de cesser de demander au jeton quel algorithme employer. Le vérificateur sait déjà quel algorithme et quelle clé il attend, et tout le reste est rejeté avant même de poursuivre l'analyse.
La seconde est la confusion HS256 contre RS256, plus subtile. Avec RS256, le serveur détient une clé privée pour signer et publie la clé publique correspondante pour vérifier. Si un vérificateur prend l'algorithme dans l'en-tête, un attaquant peut mettre alg à HS256 et signer le jeton avec la clé publique comme si c'était un secret HMAC. Le vérificateur exécute alors consciencieusement HMAC avec cette même clé publique — qu'il possède, puisqu'elle est publique — et l'étiquette correspond. Cela fonctionne : donner à HMAC-SHA-256 le texte PEM d'une clé publique comme clé produit un jeton qu'un vérificateur naïf accepte. Épingler l'algorithme attendu ferme les deux attaques d'un coup, d'où la recommandation phare de la RFC 8725.
Une troisième erreur ne demande aucune cryptographie : vérifier la signature puis oublier de contrôler les revendications. Un jeton structurellement valide dont l'exp est passé la semaine dernière reste structurellement valide. De même pour un jeton émis par un autre locataire, ou destiné à un autre public. Contrôle toujours exp contre l'heure courante avec une petite tolérance de dérive d'horloge, iss contre l'émetteur attendu, et aud contre ton propre identifiant — la signature dit qui a écrit les revendications, pas si elles s'appliquent encore à toi.
Le problème qu'aucune signature ne résout : la révocation
Tout l'intérêt d'un JWT est que le serveur n'a rien à consulter. Le jeton porte ses propres revendications et sa propre preuve : tout nœud disposant de la clé le vérifie en microsecondes sans toucher à une base de données. C'est aussi sa faiblesse structurelle, et les deux sont inséparables : un serveur qui ne consulte aucun état ne peut pas savoir que tu as licencié l'utilisateur il y a cinq minutes. Le jeton reste valide jusqu'à exp, et rien dans la spécification n'offre de moyen d'abréger ce délai.
La réponse standard est un partage en deux jetons. Le jeton d'accès est un JWT à exp délibérément court — cinq à quinze minutes est la fourchette usuelle — vérifié sans état à chaque requête. Le jeton de rafraîchissement est de longue durée, opaque, stocké côté serveur, et échangé contre un nouveau jeton d'accès quand le court expire. La révocation porte sur le jeton de rafraîchissement, avec état et donc annulable. La fenêtre d'exposition se réduit à la durée restante du jeton d'accès, précisément ce que exp a été choisi pour borner.
S'il te faut une révocation plus rapide, tu dois réintroduire de l'état, et le faire délibérément. Une liste de refus indexée sur la revendication jti permet d'annuler des jetons individuels ; les entrées peuvent disparaître dès que l'exp correspondant est passé, la liste reste donc petite. Faire tourner la clé de signature invalide tous les jetons d'un coup : c'est l'instrument brutal en cas de suspicion de compromission de clé. Les deux coûtent une consultation, et il est alors légitime de se demander si un identifiant de session opaque dans un cookie n'aurait pas été plus simple dès le départ.
JWE est l'autre spécification, et ce n'est pas un réglage de JWT
Quand les revendications doivent réellement être cachées, la réponse est JSON Web Encryption, définie dans la RFC 7516. Un JWE est une sérialisation différente à cinq parties plutôt que trois — en-tête protégé, clé chiffrée, vecteur d'initialisation, texte chiffré, étiquette d'authentification — et il fournit ensemble confidentialité et intégrité, car il utilise du chiffrement authentifié. Tu peux imbriquer les deux, en signant un JWT puis en chiffrant le résultat : c'est ce que la RFC 7519 appelle un JWT imbriqué.
En pratique, la plupart des équipes n'ont pas besoin de JWE et ne devraient pas s'y précipiter. Si la charge utile contient quelque chose que tu préférerais que personne ne lise, la bonne réponse habituelle est de l'en retirer. Remplace la valeur sensible par un identifiant opaque que le serveur de ressources sait résoudre, et le problème de confidentialité disparaît avec la gestion de clés supplémentaire, la surface de bibliothèque supplémentaire et les modes de défaillance supplémentaires. Recours à JWE quand un jeton doit traverser une partie qui doit le relayer sans le lire — c'est le cas pour lequel il a été conçu.
Une liste de contrôle pour le vérificateur
Épingle l'algorithme avant toute analyse, et rejette un jeton dont l'en-tête diverge. Vérifie la signature avec une comparaison à temps constant. Contrôle ensuite exp, et nbf s'il est présent, contre l'heure courante avec une tolérance de dérive d'au plus une minute. Contrôle iss contre l'émetteur exact auquel tu fais confiance, et aud contre ton propre identifiant. Ce n'est qu'après tout cela que tu dois lire les revendications applicatives — et même là, traite role ou scope comme une assertion de l'émetteur, pas comme le dernier mot : le serveur de ressources reste maître de ses décisions d'autorisation.
| Partie | Contenu | Taille dans ce jeton | Lisible sans clé ? | Couverte par la signature ? |
|---|---|---|---|---|
| En-tête | alg et typ — quel algorithme l'a signé | 36 caractères | Oui, entièrement | Oui — mais le vérificateur ne doit pas croire alg aveuglément |
| Charge utile | Les revendications : sub, iss, exp, iat, jti et ce que tu ajoutes | 183 caractères pour 137 octets de JSON | Oui — c'est ce que tout le monde rate | Oui — impossible à modifier sans la clé |
| Signature | HMAC-SHA-256 sur en-tête.charge, ou une signature RSA/ECDSA | 43 caractères pour 32 octets bruts | Oui, mais elle ne veut rien dire seule | C'est la signature |
| Ce qui manque | La confidentialité, et tout moyen d'annuler le jeton par avance | Zéro octet n'y est consacré | Sans objet | Utilise JWE pour la première, un jeton de rafraîchissement ou une liste de refus pour le second |
Questions fréquentes
- Un JWT est-il chiffré ?
- Non. Un JWT standard est signé, ce qui est une garantie différente. L'en-tête et la charge utile sont encodés en base64url, un encodage sans clé ni secret : quiconque détient le jeton peut les lire. Décoder la charge utile du jeton d'exemple de cet article sans aucune clé renvoie {"sub":"1234567890","name":"Ada Lovelace","role":"editor","iat":1786579200,"exp":1786582800,"iss":"https://allin.example","jti":"a1b2c3"} — chaque revendication en clair. Ce que la signature t'achète, c'est la détection de falsification : modifie un caractère et la troisième partie ne se vérifie plus, le serveur rejette. S'il te faut vraiment cacher le contenu, JSON Web Encryption (RFC 7516) est la spécification adaptée, et c'est un format distinct à cinq parties, pas un drapeau à activer sur un JWT. Dans la plupart des conceptions, mieux vaut simplement garder les valeurs confidentielles hors de la charge utile et transporter un identifiant opaque.
- Peut-on modifier la charge utile d'un JWT sans le secret ?
- On peut changer les caractères, mais le résultat ne se vérifiera pas — à condition que ton vérificateur soit correctement écrit. La signature est calculée sur la chaîne exacte en-tête.charge : toute modification provoque une discordance et le jeton est rejeté. Deux bugs de vérificateur annulent cette protection. Le premier est la confiance accordée au champ alg de l'en-tête : un jeton déclarant {"alg":"none"} avec une partie signature vide était historiquement accepté par les bibliothèques qui aiguillaient sur l'en-tête, permettant à un attaquant de réécrire la charge à volonté. Le second est la confusion HS256/RS256, où l'attaquant bascule un jeton asymétrique vers HMAC et le signe avec la clé publique du serveur, que le vérificateur emploie alors comme secret HMAC ; cela produit réellement une étiquette concordante. Les deux se ferment par la même mesure : décide dans ton propre code l'algorithme et la clé attendus avant l'analyse, et rejette tout ce qui diverge. La RFC 8725 en fait une recommandation de premier plan.
- Comment déconnecter un utilisateur si un JWT n'est pas révocable ?
- Supprimer le jeton côté client met fin à la session pour ce navigateur, et pour bien des produits cela suffit réellement. Cela ne suffit plus si le jeton a pu être copié, car un jeton signé reste valide jusqu'à son exp quoi que fasse le client. La structure standard est un JWT d'accès à courte durée — cinq à quinze minutes — associé à un jeton de rafraîchissement opaque de longue durée stocké côté serveur. La déconnexion supprime le jeton de rafraîchissement : la session ne peut plus être renouvelée et meurt dans la durée restante du jeton d'accès. S'il te faut plus rapide, ajoute de l'état délibérément : une liste de refus indexée sur jti annule des jetons individuels, et les entrées sont purgées dès que l'exp correspondant passe, si bien qu'elle ne croît jamais sans limite. Faire tourner la clé de signature tue d'un coup tous les jetons en circulation : c'est la bonne réponse à une suspicion de compromission. Chacune de ces options réintroduit une consultation, prix de la révocation que tu voulais.
- Où stocker un JWT dans un navigateur ?
- Dans un cookie marqué HttpOnly, Secure et SameSite, dans la quasi-totalité des cas. HttpOnly met le jeton hors de portée de JavaScript : une faille de script intersite n'importe où sur ton origine ne peut ni le lire ni l'exfiltrer ; Secure l'empêche de circuler en clair ; SameSite bloque la falsification de requête intersite à laquelle les cookies t'exposeraient sinon. localStorage est l'alternative courante, et la plus faible, car tout script exécuté sur ta page — y compris un script tiré par une dépendance compromise — peut lire chacune de ses clés. L'argument pour localStorage est généralement que le jeton doit être attaché à la main aux appels d'API multi-origines, ce que les cookies rendent malcommode ; c'est une vraie contrainte, mais mieux vaut la résoudre par un proxy de même origine que par un jeton lisible par script. Quel que soit ton choix, garde la charge utile exempte de tout élément sensible : le stockage décide qui peut voler le jeton, pas qui peut le lire une fois volé.
- Combien de temps un JWT doit-il durer ?
- Assez court pour que sa durée inannulable soit une exposition acceptable. Comme un jeton signé reste valide jusqu'à exp quoi qu'il arrive de ton côté, exp est toute la largeur de la fenêtre pendant laquelle un jeton volé fonctionne encore. Cinq à quinze minutes est la fourchette courante pour les jetons d'accès, et le jeton d'exemple de cet article utilise une heure — iat 1786579200, exp 1786582800, un écart d'exactement 3 600 secondes. Toute durée qui se compte en jours est en pratique un identifiant permanent muni d'une signature. C'est le jeton de rafraîchissement qui porte la longue durée, et il le peut précisément parce qu'il est opaque, stocké côté serveur et révocable. Deux détails d'implémentation comptent. Les valeurs NumericDate sont en secondes, pas en millisecondes : comparer exp à Date.now() en JavaScript sans diviser par 1000 est le bug classique. Et prévois une petite tolérance de dérive d'horloge, de l'ordre de trente à soixante secondes, sans quoi des jetons seront parfois rejetés par un serveur légèrement en avance sur l'émetteur.
- Faut-il utiliser un JWT ou un simple cookie de session ?
- Pour une application unique avec une seule base de données, un simple identifiant de session dans un cookie est généralement le choix le plus simple et le plus solide. C'est une chaîne opaque aléatoire, elle ne révèle rien, et la déconnexion est une suppression de ligne qui prend effet immédiatement. Le JWT justifie sa complexité quand la vérification doit avoir lieu là où le magasin de sessions n'est pas — plusieurs services derrière une passerelle, une API tierce acceptant tes assertions d'identité, une fonction en périphérie qui ne peut pas se permettre un aller-retour vers la base. C'est un vrai avantage architectural et c'est la raison d'être des JWT. N'en adopte pas un pour un monolithe qui a déjà une table de sessions, et sois lucide sur l'échange : l'absence d'état t'achète une vérification distribuée rapide et te coûte la révocation immédiate ; tu ne peux pas garder les deux. Les équipes qui ajoutent une consultation de liste de refus à chaque requête ont payé la complexité du JWT et rendu son seul bénéfice structurel.
Articles qui pourraient t'intéresser
Tous les guides →Outils similaires
Sources
Tu as repéré une erreur dans cet article ?