Charger un fichier
  1. base64hub
  2. Apprendre Base64
  3. Base64 dans les JWT
Explication

Comment les JWT utilisent Base64URL : décoder un jeton à la main

Un JWT, ce sont trois chaînes Base64URL reliées par des points. N’importe qui peut décoder les deux premières, et seule la signature les protège. Voici comment chaque partie est construite et lue.

Anatomie d’un jeton

Voici un jeton HS256 complet, signé avec le secret de démonstration demo-secret-do-not-use. Les sauts de ligne ont été ajoutés pour la lisibilité ; un vrai jeton tient sur une seule ligne :

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiJ1c2VyLTQyIiwibmFtZSI6IkFkYSIsImFkbWluIjpmYWxzZSwiaWF0IjoxNzYwMDAwMDAwLCJleHAiOjE3NjAwMDM2MDB9
.nKTVXOM_r1JbE92WsFd2SSun0a1TByUV8ZpqrRUnIhk

Les trois parties, séparées par . :

PartieLongueur en Base64URLSe décode en
En-tête36{"alg":"HS256","typ":"JWT"}
Payload104{"sub":"user-42","name":"Ada","admin":false,"iat":1760000000,"exp":1760003600}
Signature4332 octets bruts de HMAC-SHA256

L’en-tête et le payload sont du JSON encodé en UTF-8, puis en Base64URL. La signature est binaire : elle n’a de sens qu’en tant qu’octets. iat et exp sont des horodatages Unix en secondes : ce jeton a été émis le 2025-10-09 à 08:53:20 UTC et a expiré une heure plus tard.

Pourquoi Base64URL sans remplissage

Un jeton circule là où +, / et = posent problème : chaînes de requête (+ devient une espace), chemins d’URL (/ est un séparateur), cookies et en-têtes HTTP. JWS, le format de signature sur lequel repose JWT, défini dans la RFC 7515, utilise donc l’alphabet URL-safe de la RFC 4648 et supprime le remplissage :

Encodage Base64 utilisant le jeu de caractères sûr pour les URL et les noms de fichiers défini à la section 5 de la RFC 4648, avec suppression de tous les caractères « = » finaux (comme le permet la section 3.2) et sans aucun saut de ligne, espace ou autre caractère supplémentaire.

Il en découle deux différences avec le Base64 standard :

  • + devient - et / devient _. La signature ci-dessus contient un _, que les décodeurs standard rejettent.
  • Pas de = à la fin. La longueur d’un segment n’est jamais 4n+1 : un décodeur peut donc toujours déduire combien de remplissage a été retiré. Un reste de 2 dans la division par 4 signifie ==, un reste de 3 signifie =.

Plus de détails sur la différence d’alphabet dans Base64 ou Base64URL.

Décoder les parties à la main

Découpez sur les points, rétablissez l’alphabet standard et le remplissage, décodez, puis analysez le JSON. Dans le navigateur :

function decodePart(part) {
  const b64 = part.replace(/-/g, "+").replace(/_/g, "/");
  const padded = b64 + "=".repeat((4 - (b64.length % 4)) % 4);
  const bytes = Uint8Array.from(atob(padded), (c) => c.charCodeAt(0));
  return JSON.parse(new TextDecoder().decode(bytes));
}

const [header, payload] = token.split(".").slice(0, 2).map(decodePart);

Sans l’étape TextDecoder, le texte non ASCII est abîmé. Un payload contenant {"name":"Zoë 日本"} s’encode en eyJuYW1lIjoiWm_DqyDml6XmnKwifQ. Passez la chaîne convertie dans atob seul et le nom ressort en mojibake commençant par Zoë, car atob renvoie un caractère par octet. Dans les environnements qui la proposent, Uint8Array.fromBase64(part, { alphabet: "base64url" }) remplace les trois premières lignes (voir Uint8Array.toBase64). Dans Node.js, Buffer.from(part, "base64url") fait la même chose.

En Python, urlsafe_b64decode gère l’alphabet, mais veut toujours le remplissage :

import base64, json

def b64url_decode(s: str) -> bytes:
    return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))

header, payload, signature = token.split(".")
print(json.loads(b64url_decode(header)))   # {'alg': 'HS256', 'typ': 'JWT'}
print(json.loads(b64url_decode(payload)))  # {'sub': 'user-42', 'name': 'Ada', ...}

Dans un shell, convertissez l’alphabet avec tr et complétez le remplissage avec une boucle :

jwt_part() {
  local s
  s=$(printf '%s' "$1" | tr '_-' '/+')
  while [ $(( ${#s} % 4 )) -ne 0 ]; do s="$s="; done
  printf '%s' "$s" | base64 -d
}
jwt_part "$(echo "$TOKEN" | cut -d. -f2)"

Si vous voulez simplement regarder l’intérieur d’un jeton, collez-le dans le décodeur JWT. Il tourne dans votre navigateur : un jeton de production ne quitte pas votre machine.

Pourquoi le payload est lisible

L’encodage n’est pas du chiffrement. Base64URL ne change que la façon d’écrire les octets, et le décodeur JWT l’inverse sans aucune clé. Un JWT signé garantit que le payload n’a pas été modifié depuis sa signature. Il ne cache pas le payload.

L’encodage coûte aussi de la place. Chaque claim grossit d’un tiers une fois encodé en Base64URL, et le jeton entier accompagne chaque requête dans un en-tête ou un cookie : gardez donc un payload léger, un identifiant et quelques claims courts plutôt qu’un profil utilisateur complet.

Considérez chaque claim comme public. Identifiants d’utilisateur, rôles et dates d’expiration ne posent pas de problème. Mots de passe, clés d’API et données personnelles que le détenteur du jeton ne doit pas voir, si. Si le contenu doit rester confidentiel, utilisez un JWT chiffré (JWE). Sa forme compacte compte cinq parties Base64URL : en-tête, clé chiffrée, IV, texte chiffré et tag d’authentification. L’article encodage ou chiffrement explique la différence de manière générale.

Comment la signature est calculée

La signature porte sur le texte ASCII exact header.payload, les deux chaînes Base64URL avec le point entre elles. Pour HS256 :

import hmac, hashlib

signing_input = f"{header}.{payload}".encode("ascii")
mac = hmac.new(b"demo-secret-do-not-use", signing_input, hashlib.sha256).digest()
hmac.compare_digest(mac, b64url_decode(signature))   # True

Les octets bruts de la signature sont ensuite encodés en Base64URL sans remplissage. Leur longueur dépend de l’algorithme :

AlgorithmeOctets de signatureCaractères Base64URL
HS2563243
ES2566486
RS256 (clé de 2048 bits)256342

Dans JWS, les signatures ES256 sont les valeurs brutes R || S, de 32 octets chacune. OpenSSL et de nombreuses bibliothèques cryptographiques produisent plutôt une signature ECDSA encodée en DER, qui commence par 30 et a une longueur variable. La conversion entre les deux est une source fréquente d’erreurs « invalid signature ».

Pour construire et signer un jeton avec des clés HS, RS, ES ou EdDSA, utilisez l’encodeur JWT. Pour un HMAC brut en Base64, voir l’outil HMAC.

Erreurs courantes

  • Utiliser directement atob ou un décodeur Base64 standard. Cela marche sur certains jetons et lève une erreur sur d’autres, selon qu’un - ou un _ y apparaît par hasard. Le payload de notre exemple se décode avec atob, la signature non.
  • Ajouter du remplissage en créant des jetons. Certaines bibliothèques rejettent les segments qui se terminent par =. Supprimez-le.
  • Réencoder l’en-tête ou le payload avant de vérifier. La signature porte sur le texte Base64URL d’origine. Analyser le JSON puis le resérialiser peut changer l’ordre des clés ou les espaces, et la signature ne correspond plus. Vérifiez toujours sur les segments tels que reçus.
  • Faire confiance à un payload décodé. Décoder n’est pas vérifier. Contrôlez la signature avec l’algorithme attendu, et n’acceptez jamais "alg":"none" (en-tête eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0) sauf si c’est voulu.
  • Utiliser la chaîne du jeton comme clé unique. 32 octets de signature demandent 256 bits, et 43 caractères en portent 258. Les 2 derniers bits sont inutilisés : dans notre exemple, le k final peut être remplacé par l, m ou n et les décodeurs tolérants renvoient toujours les mêmes octets. La signature reste valide. C’est sans conséquence pour la sécurité, mais deux chaînes différentes peuvent représenter le même jeton.
  • Lire les horodatages comme des millisecondes. exp et iat sont en secondes. Multipliez-les par 1000 avant de les passer au Date de JavaScript.

FAQ

Le payload d'un JWT est-il chiffré ?

Non. Un JWT signé classique (JWS) se contente d’encoder le payload en Base64URL : quiconque détient le jeton peut le lire. Les jetons chiffrés utilisent JWE, qui compte cinq parties au lieu de trois.

Pourquoi un JWT ne se termine-t-il pas par des signes = ?

La RFC 7515 définit l’encodage comme du Base64URL dont tous les = finaux sont supprimés, pour que les jetons puissent aller dans des URL et des en-têtes sans échappement. Les décodeurs doivent rajouter le remplissage ou accepter une entrée sans lui.

Peut-on décoder les trois parties sans la clé secrète ?

Oui. L’en-tête et le payload sont du simple Base64URL. La clé n’est nécessaire que pour vérifier la signature ou en créer une nouvelle. Le décodeur JWT affiche les deux parties et vérifie la signature si vous fournissez la clé.

Pourquoi tous les JWT commencent-ils par eyJ ?

L’en-tête est un objet JSON qui commence par {", et ces deux octets s’encodent toujours en eyJ. D’autres préfixes de ce genre sont listés dans les préfixes Base64 courants.

Continuer la lecture