Ce que contient réellement un JSON Web Token

L'intérieur d'un JWT, avec l'en-tête, la charge utile et la signature expliqués, les revendications standard, et pourquoi décoder un jeton en Base64url n'équivaut pas à le vérifier.

Un JSON Web Token (JWT) est fait de trois blocs de texte réunis par des points. Chaque bloc est encodé en Base64url, et les deux premiers ne sont que du JSON. Quiconque détient le jeton peut lire ce qu'il contient. Rien, dans un JWT ordinaire, n'est chiffré.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9      <- header
eyJzdWIiOiIxMjM0NSIsIm5hbWUiOiJBbGV4In0   <- payload
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFW     <- signature

Réunis par des points, cela donne le jeton que vous collez dans une requête. Décodez les deux premières parties et vous obtenez du JSON ordinaire :

{ "alg": "HS256", "typ": "JWT" }
{ "sub": "12345", "name": "Alex" }

Une charge utile réelle porte en général aussi des horodatages et une audience :

{
  "iss": "https://auth.example.com",
  "sub": "12345",
  "aud": "api.example.com",
  "iat": 1756000000,
  "exp": 1756003600,
  "roles": ["editor"]
}

L'en-tête

L'en-tête décrit la façon dont le jeton est signé. Deux champs comptent avant tout :

  • alg nomme l'algorithme de signature, par exemple HS256 (HMAC avec SHA-256, à l'aide d'un secret partagé) ou RS256 (signature RSA avec SHA-256, apposée avec une clé privée et contrôlée avec une clé publique).
  • kid, l'identifiant de clé, est facultatif et indique au destinataire laquelle de plusieurs clés a été employée, ce qui permet de faire tourner les clés sans casser les jetons existants.

typ vaut en général simplement JWT et n'a aucune portée en matière de sécurité.

La charge utile et ses revendications

La charge utile est un objet JSON de revendications, ce qui est simplement le mot employé par la spécification pour des affirmations à propos du sujet du jeton. Certains noms de revendications sont enregistrés par la norme, et ils sont tous courts :

RevendicationNomCe qu'elle contient
issÉmetteurQui a créé le jeton, en général une URL ou un nom de service
subSujetSur qui porte le jeton, en général un identifiant d'utilisateur stable
audAudienceÀ qui le jeton est destiné, pour qu'un service puisse rejeter les jetons destinés à un autre
expDate d'expirationL'instant après lequel le jeton doit être rejeté
nbfPas avantL'instant avant lequel le jeton doit être rejeté
iatÉmis àLe moment où le jeton a été créé
jtiIdentifiant du JWTUn identifiant unique, utile pour suivre ou bloquer des jetons individuels

exp, nbf et iat sont toutes des valeurs NumericDate, c'est-à-dire des secondes écoulées depuis le 1er janvier 1970 UTC. Ce sont des secondes, pas des millisecondes. Si un horodatage se décode en une date de l'an 57 000, c'est que vous lisez des millisecondes et qu'il faut diviser par mille.

Tout le reste de la charge utile est ce que l'émetteur y a mis : rôles, permissions, adresse e-mail, identifiant de locataire. Comme un nom de revendication privé pourrait un jour entrer en collision avec un nom enregistré, les émetteurs préfixent souvent les leurs par quelque chose qui ressemble à une URL, en guise d'espace de noms.

La signature

La signature est calculée sur le texte exact des deux premières parties réunies par un point, à l'aide de l'algorithme indiqué dans l'en-tête et d'une clé. Changez un seul caractère de l'en-tête ou de la charge utile et la signature ne correspond plus.

Avec HS256, le même secret sert à signer et à vérifier : quiconque peut contrôler un jeton peut donc aussi en fabriquer un. Avec RS256 ou ES256, une clé privée signe et une clé publique vérifie, ce qui permet à un fournisseur d'identité de distribuer des jetons que de nombreux services distincts peuvent valider sans détenir le moindre secret.

Base64url n'est pas du chiffrement

Base64url repose sur la même idée que Base64, avec deux différences : il emploie - et _ à la place de + et / afin que le résultat passe sans risque dans une URL, et le remplissage final par = est en général retiré. C'est un encodage, une façon réversible d'écrire des octets sous forme de texte. Il n'apporte absolument aucune confidentialité.

Ne placez donc jamais rien de sensible dans une charge utile de JWT : ni mots de passe, ni numéros de carte, ni notes internes sur l'utilisateur. Partez du principe que la personne qui détient le jeton, et quiconque le lit dans un fichier journal ou dans le stockage du navigateur, voit chacun des champs.

Il existe un format distinct, JWE, qui chiffre bel et bien la charge utile, et il compte cinq parties plutôt que trois. Si votre jeton a trois parties, il est signé et lisible.

Décoder n'est pas vérifier

C'est le point qui compte. Un décodeur vous montre ce qu'un jeton dit. Il ne vous dit pas si le jeton est authentique. La vérification est une étape distincte et exige la clé.

Pour faire réellement confiance à un jeton, un serveur doit :

  1. Vérifier la signature face à la bonne clé.
  2. Exiger l'algorithme qu'il attend, au lieu de se fier au champ alg du jeton. Deux attaques classiques naissent de l'oubli de ce point : mettre alg à none et retirer la signature, et prendre la clé publique RSA d'un service pour s'en servir comme secret HMAC, de sorte qu'un vérificateur RS256 soit amené à exécuter HS256.
  3. Vérifier exp et, s'il est présent, nbf, en tolérant une légère dérive d'horloge.
  4. Vérifier que iss et aud correspondent à ce que ce service attend.

Ce n'est qu'alors que les revendications signifient quelque chose. Avant cela, la charge utile n'est qu'une chaîne non vérifiée arrivée par le réseau.

Remarques pratiques

  • Les jetons sont en général envoyés dans un en-tête HTTP sous la forme Authorization: Bearer <token>, alors gardez la charge utile petite. Chaque revendication que vous ajoutez est envoyée à chaque requête.
  • Un jeton signé reste valide jusqu'à son expiration. Il n'existe pas de moyen intégré d'en révoquer un, et c'est pourquoi les jetons d'accès ont tendance à avoir une durée de vie courte, souvent de quelques minutes, avec un jeton de rafraîchissement distinct servant à en obtenir de nouveaux.
  • Si un jeton semble mal formé, commencez par compter les points. Deux points et trois parties, c'est un JWT signé normal. Un jeton tronqué collé depuis un terminal est une cause très fréquente de « signature invalide ».
  • Décoder un jeton pour lire son expiration, son sujet ou ses rôles pendant un débogage est parfaitement sans danger et ne demande aucun secret. C'est exactement à cela que sert la charge utile.