Anatomie d'un JWT : en-tête, charge utile, signature, base64url et pourquoi décoder n'est pas vérifier
Trois points : la structure d'un JSON Web Token
Un JSON Web Token est une chaîne composée de trois valeurs encodées en base64url séparées par des points : base64url(en-tête).base64url(charge utile).base64url(signature). La RFC 7519, publiée en mai 2015, définit ce format. Qu'un JWT arrive dans un en-tête Authorization, un cookie ou un paramètre de requête URL, il suit toujours cette structure. Les trois parties peuvent être décodées indépendamment, et la signature est ce qui les lie avec des garanties cryptographiques.
Ce format a été conçu pour être compact et sûr dans les URLs. Avant JWT, l'authentification d'API reposait souvent sur des identifiants de session opaques stockés côté serveur, nécessitant une requête en base de données à chaque appel. Les JWT intègrent les claims directement dans le token, permettant à un serveur sans état de les vérifier sans requête. Cette commodité implique des responsabilités : contrairement à un token de session révocable instantanément, un JWT signé reste valide jusqu'à l'expiration de son claim exp, sauf si le serveur maintient une liste de révocation.
Encodage base64url : la variante URL-safe de base64
Le base64 standard encode des données binaires avec 64 caractères : A–Z, a–z, 0–9, + et /, avec un rembourrage =. Deux de ces caractères (+ et /) ont une signification particulière dans les URLs et les chaînes de requête HTTP. Base64url, défini dans la RFC 4648, Section 5, remplace + par - et / par _, et supprime complètement le rembourrage =. Le résultat peut être utilisé directement dans les URLs, les en-têtes HTTP et les cookies sans encodage supplémentaire.
Le taux d'expansion est identique au base64 standard : chaque 3 octets d'entrée deviennent 4 caractères base64url — un facteur de 4/3, soit environ 33% d'expansion. Comme l'encodage est réversible sans aucune clé, quiconque obtient un JWT peut lire son en-tête et sa charge utile par simple décodage — base64url est un encodage, pas un chiffrement.
L'en-tête : sélection d'algorithme et type de token
L'en-tête est un objet JSON qui indique au destinataire comment traiter le token. Un en-tête minimal contient deux champs : alg (l'algorithme de signature) et typ (toujours "JWT" pour les tokens standards). La RFC 7518 définit les identifiants d'algorithme. Les plus courants sont : HS256 — HMAC-SHA256, un algorithme symétrique utilisant le même secret pour signer et vérifier ; RS256 — RSA-PKCS1v1.5 avec SHA-256, asymétrique, où une clé privée signe et la clé publique correspondante vérifie ; ES256 — ECDSA avec P-256 et SHA-256, aussi asymétrique mais produisant des signatures plus courtes que RSA.
Le choix entre algorithmes symétriques et asymétriques a de vraies conséquences architecturales. Avec HS256, chaque service vérifiant des tokens doit posséder le secret partagé. Avec RS256 ou ES256, l'émetteur garde la clé privée secrète et publie seulement la clé publique (souvent via un endpoint JWKS), de sorte que les services vérificateurs ne manipulent jamais le matériel de signature. Pour les intégrations multi-services ou tierces, les algorithmes asymétriques sont préférables.
La charge utile : claims enregistrés et leur signification
La charge utile est un objet JSON contenant des claims — des affirmations sur une entité, généralement l'utilisateur authentifié, plus des métadonnées sur le token lui-même. La Section 4.1 de la RFC 7519 définit sept noms de claims enregistrés à sémantique standardisée : iss (émetteur), sub (sujet, souvent un identifiant utilisateur), aud (audience, les destinataires prévus), exp (délai d'expiration, une NumericDate après laquelle le token ne doit pas être accepté), nbf (pas avant, une NumericDate avant laquelle le token ne doit pas être accepté), iat (émis à) et jti (identifiant JWT, unique, utile contre les attaques par rejeu).
NumericDate est le nombre de secondes depuis 1970-01-01T00:00:00Z UTC (l'epoch Unix), pas des millisecondes. C'est une source d'erreurs fréquente pour les développeurs habitués à Date.now() de JavaScript. La charge utile d'un JWT standard est signée mais pas chiffrée — toute partie interceptant le token peut lire tous les claims. Si la charge contient des données sensibles, le token doit être chiffré avec JWE (RFC 7516) ou transmis uniquement via TLS.
La signature : ce qu'elle protège — et le piège alg:none
La signature est calculée sur l'entrée de signature, qui est base64url(en-tête) + "." + base64url(charge utile). Pour HS256, c'est HMAC-SHA256(secret, entree_signature) encodé en base64url. Comme l'entrée inclut à la fois l'en-tête et la charge utile, toute modification dans l'une ou l'autre partie — même un seul caractère — invalide la signature. Un serveur qui vérifie correctement la signature peut être certain que le token n'a pas été modifié.
En 2015, une classe de vulnérabilités a été découverte dans plusieurs bibliothèques JWT : l'attaque alg:none. La RFC 7519 autorise techniquement "alg":"none" pour indiquer un JWT non sécurisé sans signature. La faille : certaines bibliothèques lisaient le champ alg de l'en-tête (non vérifié) pour choisir le chemin de vérification — si un attaquant changeait l'en-tête en "alg":"none" et supprimait la signature, la bibliothèque déclarait le token valide sans vérifier aucune signature. CVE-2015-9235 documente cela dans la populaire bibliothèque node-jsonwebtoken. Le correctif : la partie vérificatrice doit spécifier explicitement l'algorithme attendu et rejeter tout token dont l'en-tête indique un algorithme différent.
Décoder vs vérifier : pourquoi la distinction est cruciale
Décoder un JWT signifie appliquer base64url-decode aux trois parties pour récupérer le JSON de l'en-tête, la charge utile et les octets bruts de la signature. Aucune clé n'est requise. Quiconque possède une chaîne JWT peut la décoder et lire les claims. C'est voulu — les JWT ne sont pas des conteneurs opaques. Un développeur déboguant un problème d'authentification, un ingénieur lisant un journal d'accès, un chercheur en sécurité auditant un système : tous peuvent décoder tout JWT en leur possession.
Vérifier un JWT signifie contrôler que la signature a été produite par la bonne clé sur les octets exacts de l'en-tête et de la charge utile. Une vérification réussie prouve deux choses : le token a été créé par le détenteur de la clé de signature (authenticité), et ni l'en-tête ni la charge utile n'ont été modifiés depuis la signature (intégrité). La vérification nécessite la clé — le secret partagé pour HMAC, la clé publique pour RSA/ECDSA.
L'erreur critique est de traiter un JWT décodé comme digne de confiance sans vérification. Un client qui lit des claims depuis un JWT en localStorage et agit dessus sans vérification de signature côté serveur fait confiance à des données que n'importe quel utilisateur peut librement fabriquer. Un attaquant peut créer {"sub":"admin","role":"superuser"}, l'encoder en base64url avec n'importe quel en-tête et y joindre une fausse signature ou aucune — cela se décodera parfaitement. Les décisions d'autorisation doivent toujours être prises après vérification cryptographique côté serveur. Un décodeur JWT est l'outil approprié pour l'inspection et le débogage ; il ne remplace jamais la vérification côté serveur dans une application en production.