JWT Decoder JavaScript — atob(), TextDecoder & jose
Utilisez le Décodeur JWT gratuit directement dans votre navigateur — sans installation.
Essayer Décodeur JWT en ligne →Chaque flux d'authentification que j'ai construit finit par atteindre le même point : vous avez un JWT dans un cookie, un en-tête ou une URL de callback OAuth, et vous devez lire ce qu'il contient. Un décodeur JWT en JavaScript ne nécessite aucun paquet npm. L'en-tête et la charge utile du token sont simplement du JSON encodé en Base64url, et le navigateur comme Node.js embarquent tout ce qu'il faut pour les décoder. Ce guide couvre l'ensemble du pipeline de décodage de texte JavaScript pour les JWT : découpage du token, normalisation du base64url vers le Base64 standard, atob() et TextDecoder pour une gestion correcte de l'UTF-8, Node.js Buffer.from(), vérification de signature avec jose, et les erreurs courantes qui font trébucher les développeurs au quotidien. Pour une inspection rapide, essayez le décodeur JWT en ligne à la place. Tous les exemples ciblent ES2020+ et Node.js 18+.
- ✓Découpez le JWT sur « . » — l'index 0 est l'en-tête, l'index 1 est la charge utile, l'index 2 est la signature.
- ✓atob() décode le Base64 mais renvoie du Latin-1, pas de l'UTF-8. Utilisez TextDecoder ou Buffer.from() pour les revendications non-ASCII.
- ✓Buffer.from(segment, "base64url") gère nativement le base64url dans Node.js — aucun remplacement manuel de caractères nécessaire.
- ✓Le décodage N'EST PAS une vérification. Ne faites jamais confiance aux revendications d'un JWT décodé sans vérifier la signature côté serveur.
- ✓La bibliothèque jose fait les deux : elle vérifie les signatures HS256/RS256/ES256 et renvoie la charge utile décodée en un seul appel.
Qu'est-ce que le décodage JWT ?
Un JSON Web Token est constitué de trois segments encodés en Base64url séparés par des points. Le premier segment est l'en-tête, le deuxième est la charge utile (les revendications qui vous intéressent vraiment), et le troisième est la signature cryptographique. L'en-tête est un petit objet JSON qui décrit le token lui-même. Son champ le plus important est alg — l'algorithme de signature (p. ex. HS256, RS256, ES256). Le champ typ est presque toujours "JWT", et le champ optionnel kid identifie quelle clé a été utilisée pour signer le token — essentiel quand un fournisseur d'identité fait tourner ses clés et publie un endpoint JWKS avec plusieurs clés publiques.
La charge utile contient les revendications. La RFC 7519 définit sept noms de revendications enregistrés : sub (sujet — généralement l'ID utilisateur), iss (émetteur — l'URL du serveur d'authentification), aud (audience — l'API à laquelle le token est destiné), iat (horodatage d'émission), exp (horodatage d'expiration), nbf (horodatage not-before), et jti (ID JWT — utilisé pour prévenir les attaques par rejeu). Tous les horodatages sont en secondes d'époque Unix, pas en millisecondes. Le segment de signature est du binaire brut — un condensé HMAC avec clé ou une signature numérique asymétrique. Il est encodé en Base64url comme les autres segments, mais ses octets ne sont pas du JSON et n'ont pas de structure lisible par l'homme.
En pratique, vous décodez des JWT en JavaScript pour trois raisons courantes. Premièrement, le débogage : vous avez un token provenant d'un flux OAuth ou d'un environnement de test et vous voulez confirmer que les revendications correspondent à ce que le serveur d'authentification aurait dû émettre. Deuxièmement, lire les revendications utilisateur à des fins d'affichage côté client — montrer le nom de l'utilisateur connecté, l'URL de son avatar, ou son badge de rôle depuis la charge utile du token sans appel API supplémentaire. Troisièmement, vérifier l'expiration avant de tenter un rafraîchissement : si exp est dans les 60 secondes à venir, déclenchez un rafraîchissement silencieux avant le prochain appel API plutôt qu'attendre une réponse 401.
Le décodage ne vérifie pas si le token est valide ou altéré. C'est une opération distincte appelée vérification, qui nécessite le secret HMAC ou la clé publique RSA/ECDSA. N'importe qui peut décoder un JWT. Seul le détenteur de la clé correcte peut en vérifier un. Cette distinction piège de nombreux développeurs, notamment lors de la construction de flux d'authentification côté client où les revendications décodées sont affichées mais ne doivent jamais être fiables pour des décisions d'autorisation sans vérification côté serveur.
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3JfOTIxZiIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTcxMTYxMDAwMH0.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
// En-tête
{ "alg": "HS256" }
// Charge utile
{
"sub": "usr_921f",
"role": "admin",
"iat": 1711610000
}atob() + TextDecoder — Décodage JWT natif du navigateur
Le pipeline natif du navigateur pour décoder un JWT comporte quatre étapes. Premièrement, découpez la chaîne du token sur "." pour obtenir les trois segments. Deuxièmement, normalisez le segment base64url en remplaçant - par + et _ par /, puis en complétant avec des caractères = jusqu'à ce que la longueur soit un multiple de 4. Troisièmement, appelez atob() pour décoder le Base64 en chaîne binaire. Quatrièmement, convertissez la chaîne binaire en UTF-8 correct en utilisant TextDecoder. Cette dernière étape est importante car atob() renvoie du Latin-1. Les caractères multi-octets — emoji, texte CJK, caractères accentués au-delà de la plage Latin-1 — ressortent illisibles sans l'étape de décodeur de texte JavaScript.
const token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3JfOTIxZiIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTcxMTYxMDAwMH0.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
function decodeJwtPayload(jwt) {
const base64Url = jwt.split(".")[1];
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
const padded = base64.padEnd(base64.length + (4 - (base64.length % 4)) % 4, "=");
const binary = atob(padded);
const bytes = Uint8Array.from(binary, ch => ch.charCodeAt(0));
const json = new TextDecoder("utf-8").decode(bytes);
return JSON.parse(json);
}
console.log(decodeJwtPayload(token));
// { sub: "usr_921f", role: "admin", iat: 1711610000 }L'étape de remplissage est facile à négliger. Les JWT suppriment les caractères = de fin de leurs segments Base64url car la spécification JWT (RFC 7515) définit le base64url sans remplissage. Mais atob() dans certains moteurs de navigateur lève une erreur InvalidCharacterError si la longueur de l'entrée n'est pas divisible par 4. Compléter défensivement avec padEnd() évite ce cas limite dans tous les environnements. Voici une version réutilisable qui décode à la fois l'en-tête et la charge utile en objets séparés :
function decodeBase64Url(segment) {
const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
const padded = base64.padEnd(base64.length + (4 - (base64.length % 4)) % 4, "=");
const binary = atob(padded);
const bytes = Uint8Array.from(binary, ch => ch.charCodeAt(0));
return new TextDecoder("utf-8").decode(bytes);
}
function decodeJwt(token) {
const [headerB64, payloadB64] = token.split(".");
return {
header: JSON.parse(decodeBase64Url(headerB64)),
payload: JSON.parse(decodeBase64Url(payloadB64)),
};
}
const { header, payload } = decodeJwt(token);
console.log("Algorithme:", header.alg); // "HS256"
console.log("Sujet:", payload.sub); // "usr_921f"
console.log("Rôle:", payload.role); // "admin"Une fois que vous avez ces deux fonctions, il vaut la peine de les placer dans un module utilitaire partagé plutôt que de copier-coller la logique dans différents fichiers. Un fichier src/lib/jwt.ts ou utils/jwt-decode.ts avec une forme de retour typée rend l'intention explicite dans toute la base de code. En TypeScript, vous pouvez typer le retour comme { header: JwtHeader; payload: JwtPayload } où JwtHeader inclut alg, typ, et l'optionnel kid, et JwtPayload étend les revendications enregistrées de la RFC 7519 avec une signature d'index pour les revendications personnalisées. Centraliser la logique de décodage signifie que lorsque vous souhaitez ultérieurement ajouter la gestion d'erreurs (capture des segments malformés) ou de la télémétrie (journalisation des échecs de décodage), vous n'avez qu'un seul endroit à mettre à jour.
TextDecoder est ce qui rend ce pipeline sûr pour les revendications non-ASCII. Sans elle, atob() renvoie une chaîne Latin-1 où les séquences UTF-8 multi-octets sont réparties sur plusieurs caractères. Vous verrez des caractères illisibles au lieu des emoji ou du texte CJK. Passez toujours par new TextDecoder("utf-8") après atob().Décoder les revendications JWT UTF-8 avec des caractères multi-octets
Les charges utiles JWT sont du JSON UTF-8 encodé en base64url. La plupart des charges utiles ne contiennent que des champs ASCII comme les identifiants utilisateurs et les horodatages, donc les développeurs ne remarquent jamais que atob() renvoie du Latin-1 au lieu de l'UTF-8. Le problème apparaît dès qu'une revendication contient des emoji, des caractères japonais, du cyrillique, ou tout point de code au-dessus de U+00FF. Le modèle de décodage UTF-8 JavaScriptnécessite de convertir d'abord la chaîne binaire en tableau d'octets, puis de la passer dans TextDecoder.
// Simulation d'une charge utile JWT avec emoji et caractères CJK
const payloadObj = {
sub: "usr_e821",
display_name: "田中太郎",
team: "Platform 🚀",
region: "ap-northeast-1"
};
// Encodage : objet → JSON → octets UTF-8 → base64url
const jsonStr = JSON.stringify(payloadObj);
const utf8Bytes = new TextEncoder().encode(jsonStr);
const base64 = btoa(String.fromCharCode(...utf8Bytes))
.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
// Décodage : base64url → base64 → chaîne binaire → octets → chaîne UTF-8
const base64Std = base64.replace(/-/g, "+").replace(/_/g, "/");
const binary = atob(base64Std);
const bytes = Uint8Array.from(binary, c => c.charCodeAt(0));
const decoded = new TextDecoder("utf-8").decode(bytes);
const result = JSON.parse(decoded);
console.log(result.display_name); // "田中太郎" — correct
console.log(result.team); // "Platform 🚀" — correctIl existe un modèle de repli hérité que vous verrez dans les bases de code plus anciennes utilisant decodeURIComponent combiné avec une astuce de percent-encoding. Cette approche JavaScript decodeURIComponent fonctionne car elle ré-encode chaque octet comme une paire pourcent-hexadécimal, puis decodeURIComponent réassemble les séquences UTF-8 multi-octets :
function decodeBase64UrlLegacy(segment) {
const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
const binary = atob(base64);
// Convertir chaque char en %XX hex, puis decodeURIComponent réassemble l'UTF-8
const utf8 = decodeURIComponent(
binary.split("").map(c =>
"%" + c.charCodeAt(0).toString(16).padStart(2, "0")
).join("")
);
return utf8;
}
// Fonctionne pour les revendications non-ASCII sans TextDecoder
const payload = decodeBase64UrlLegacy(token.split(".")[1]);
console.log(JSON.parse(payload));decodeURIComponent(escape(atob(segment)))dans d'anciens extraits d'utilitaires JWT. La fonction escape()est dépréciée et non standard. Remplacez-la par l'approche TextDecoder présentée ci-dessus. Le modèle JavaScript unescape decoder présente le même problème : unescape() est dépréciée. Les deux fonctions pourraient être supprimées des futurs moteurs JavaScript.Pipeline de décodage JWT — Référence des étapes
Chaque étape du pipeline de décodage JWT natif du navigateur, avec l'API JavaScript utilisée et ce qu'elle produit :
L'équivalent Node.js condense les étapes 2 à 4 en un seul appel : Buffer.from(segment, "base64url").toString("utf-8"). L'option d'encodage "base64url" gère la conversion d'alphabet et le remplissage en interne.
Buffer.from() — Le décodeur de chaînes Node.js pour les JWT
Node.js propose un chemin bien plus simple. La classe Buffer accepte directement un encodage "base64url", donc vous évitez le remplacement manuel de caractères et le remplissage. C'est le chemin du décodeur de chaînes JavaScript pour le code côté serveur. Une seule ligne transforme un segment JWT en chaîne UTF-8, et gère correctement les caractères multi-octets sans étapes supplémentaires.
const token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3JfOTIxZiIsIm9yZyI6ImFjbWUtY29ycCIsInJvbGUiOiJiaWxsaW5nIiwiaWF0IjoxNzExNjEwMDAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
function decodeJwt(jwt) {
const segments = jwt.split(".");
return {
header: JSON.parse(Buffer.from(segments[0], "base64url").toString("utf-8")),
payload: JSON.parse(Buffer.from(segments[1], "base64url").toString("utf-8")),
};
}
const { header, payload } = decodeJwt(token);
console.log(header);
// { alg: "HS256" }
console.log(payload);
// { sub: "usr_921f", org: "acme-corp", role: "billing", iat: 1711610000 }C'est l'approche que j'utilise dans chaque projet Node.js. Elle est plus courte, plus rapide, et gère déjà correctement l'UTF-8. Pas besoin de TextDecoder, ni de remplacement de caractères, ni de calcul de remplissage. La classe Buffer est un décodeur de chaînes JavaScript qui gère nativement l'alphabet base64url, ce qui élimine toute une catégorie de bugs liés à la substitution de caractères. Si votre code doit s'exécuter à la fois dans le navigateur et dans Node.js, consultez la FAQ en bas pour une fonction wrapper isomorphe qui détecte l'environnement à l'exécution.
Voici un exemple plus complet montrant comment extraire les revendications JWT courantes et convertir les horodatages en dates lisibles, ce qui est le modèle que vous utiliserez le plus souvent dans les middleware et les gestionnaires de routes API :
function inspectToken(token) {
const segments = token.split(".");
if (segments.length !== 3) {
throw new Error("JWT invalide — 3 segments séparés par des points attendus");
}
const header = JSON.parse(Buffer.from(segments[0], "base64url").toString("utf-8"));
const payload = JSON.parse(Buffer.from(segments[1], "base64url").toString("utf-8"));
const inspection = {
algorithm: header.alg,
tokenType: header.typ || "JWT",
subject: payload.sub,
issuer: payload.iss || "(non défini)",
audience: payload.aud || "(non défini)",
issuedAt: payload.iat ? new Date(payload.iat * 1000).toISOString() : "(non défini)",
expiresAt: payload.exp ? new Date(payload.exp * 1000).toISOString() : "(jamais)",
isExpired: payload.exp ? payload.exp < Math.floor(Date.now() / 1000) : false,
customClaims: Object.keys(payload).filter(
k => !["sub", "iss", "aud", "iat", "exp", "nbf", "jti"].includes(k)
),
};
return inspection;
}
console.log(inspectToken(process.env.ACCESS_TOKEN));
// {
// algorithm: "RS256",
// tokenType: "JWT",
// subject: "usr_921f",
// issuer: "https://auth.internal",
// audience: "billing-api",
// issuedAt: "2026-03-10T14:00:00.000Z",
// expiresAt: "2026-03-10T15:00:00.000Z",
// isExpired: true,
// customClaims: ["role", "scope", "org"]
// }Dans les services Node.js en production, le modèle de décodage Buffer.from() apparaît dans trois endroits récurrents. Le premier est le middleware de journalisation des requêtes : vous décodez l'en-tête Authorization entrant pour attacher userId et org à chaque entrée de journal structurée sans aller-retour réseau supplémentaire vers le serveur d'authentification. Le second est le débogage : vous affichez les revendications de token décodées dans la console pendant le développement pour confirmer que les scopes corrects ont été émis avant d'écrire des assertions de test. Le troisième est le rafraîchissement proactif de token dans les passerelles API. Plutôt que de transmettre un token en amont et laisser le service en aval renvoyer un 401 quand le token expire en cours de requête, la passerelle décode le token à la périphérie, lit la revendication exp, et déclenche un rafraîchissement si l'expiration est dans les 30 secondes à venir. Cela élimine une catégorie d'échecs d'authentification transitoires difficiles à reproduire et frustrants à déboguer.
"base64url" a été ajouté dans Node.js 15.7.0. Si vous êtes bloqué sur Node.js 14 ou antérieur, utilisez le repli Buffer.from(segment.replace(/-/g, "+").replace(/_/g, "/"), "base64") qui fonctionne de la même manière mais nécessite le remplacement manuel des caractères.Décoder un JWT depuis un fichier et une réponse API
Deux scénarios reviennent constamment. Le premier est la lecture d'un JWT depuis un fichier local : un token sauvegardé pendant le développement, une fixture de test, ou un fichier extrait lors d'un incident pour une analyse post-mortem. Le second est l'extraction d'un JWT depuis une réponse HTTP, typiquement le champ access_token dans le corps d'une réponse de token OAuth ou un en-tête Authorization. Les deux nécessitent une gestion d'erreurs car les tokens malformés, les fichiers tronqués et les erreurs réseau sont des réalités quotidiennes. Un token valide la semaine dernière peut avoir des espaces ou des sauts de ligne de fin suite à un copier-coller. Un corps de réponse peut être du HTML au lieu de JSON si le serveur d'authentification a renvoyé une page d'erreur.
Lire un JWT depuis un fichier (Node.js)
import { readFileSync } from "node:fs";
function decodeJwtFromFile(filePath) {
const raw = readFileSync(filePath, "utf-8").trim();
const segments = raw.split(".");
if (segments.length !== 3) {
throw new Error(`JWT invalide : 3 segments attendus, ${segments.length} trouvés`);
}
try {
return {
header: JSON.parse(Buffer.from(segments[0], "base64url").toString("utf-8")),
payload: JSON.parse(Buffer.from(segments[1], "base64url").toString("utf-8")),
};
} catch (err) {
throw new Error(`Échec du décodage JWT depuis ${filePath} : ${err.message}`);
}
}
try {
const { header, payload } = decodeJwtFromFile("./test-fixtures/access-token.txt");
console.log("Algorithme:", header.alg);
console.log("Expire le:", new Date(payload.exp * 1000).toISOString());
} catch (err) {
console.error(err.message);
}Extraire un JWT depuis une réponse API (fetch)
async function fetchAndDecodeToken(loginUrl, credentials) {
const response = await fetch(loginUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(credentials),
});
if (!response.ok) {
throw new Error(`Connexion échouée : ${response.status} ${response.statusText}`);
}
const { access_token } = await response.json();
if (!access_token || access_token.split(".").length !== 3) {
throw new Error("La réponse ne contient pas de JWT valide");
}
const payload = access_token.split(".")[1];
const json = Buffer.from(payload, "base64url").toString("utf-8");
return JSON.parse(json);
}
// Utilisation
try {
const claims = await fetchAndDecodeToken(
"https://auth.internal/oauth/token",
{ username: "deploy-bot", password: process.env.DEPLOY_TOKEN }
);
console.log("Sujet du token:", claims.sub);
console.log("Scopes du token:", claims.scope);
console.log("Expire le:", new Date(claims.exp * 1000).toISOString());
} catch (err) {
console.error("Erreur de décodage du token:", err.message);
}Décodage JWT en ligne de commande
Parfois vous voulez juste jeter un coup d'œil à un token depuis le terminal sans écrire un script. Node.js est disponible sur la plupart des machines de développeur, donc une ligne de commande fonctionne bien. jq gère la mise en forme.
# Décoder la charge utile JWT avec une ligne Node.js
echo "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3JfOTIxZiIsInJvbGUiOiJhZG1pbiJ9.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" \
| cut -d. -f2 \
| node -e "process.stdin.on('data', d => console.log(JSON.parse(Buffer.from(d.toString().trim(), 'base64url').toString('utf-8'))))"
# Passer dans jq pour une sortie formatée
echo "$JWT_TOKEN" | cut -d. -f2 \
| node -e "process.stdin.on('data', d => process.stdout.write(Buffer.from(d.toString().trim(), 'base64url').toString('utf-8')))" \
| jq .
# Décoder à la fois l'en-tête et la charge utile
echo "$JWT_TOKEN" | node -e "
process.stdin.on('data', d => {
const parts = d.toString().trim().split('.');
console.log('En-tête:', JSON.parse(Buffer.from(parts[0], 'base64url').toString()));
console.log('Charge utile:', JSON.parse(Buffer.from(parts[1], 'base64url').toString()));
});
"Si vous préférez du bash pur sans Node.js, passez le segment dans base64 -d après avoir corrigé les caractères base64url avec tr :
# Bash pur : décoder la charge utile JWT sans Node.js echo "$JWT_TOKEN" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null | jq . # Variante macOS (base64 -D au lieu de -d) echo "$JWT_TOKEN" | cut -d. -f2 | tr '_-' '/+' | base64 -D 2>/dev/null | jq .
Pour une inspection visuelle rapide sans terminal du tout, collez votre token dans le décodeur JWT ToolDeck pour un aperçu côte à côte des trois segments avec des étiquettes de revendications colorées et le statut d'expiration.
jose — Vérification et décodage en une seule bibliothèque
Pour le middleware d'authentification en production, vous avez besoin de la vérification de signature, pas seulement du décodage. La bibliothèque jose est la meilleure option. Elle fonctionne à la fois dans Node.js et les navigateurs (via l'API Web Crypto), prend en charge HS256, RS256, ES256, EdDSA et JWE (tokens chiffrés), et n'a aucune dépendance native. Installez avec npm install jose.
import * as jose from "jose";
const secret = new TextEncoder().encode("k8s-webhook-signing-secret-2026");
const token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3JfOTIxZiIsInNjb3BlIjoiYmlsbGluZzpyZWFkIiwiaWF0IjoxNzExNjEwMDAwLCJleHAiOjE3MTE2MTM2MDB9.abc123";
try {
const { payload, protectedHeader } = await jose.jwtVerify(token, secret);
console.log("Algorithme:", protectedHeader.alg); // "HS256"
console.log("Sujet:", payload.sub); // "usr_921f"
console.log("Scope:", payload.scope); // "billing:read"
} catch (err) {
if (err.code === "ERR_JWT_EXPIRED") {
console.error("Token expiré le:", err.payload.exp);
} else {
console.error("Vérification échouée:", err.message);
}
}import * as jose from "jose";
// Récupérer le jeu de clés publiques depuis le fournisseur d'identité
const jwks = jose.createRemoteJWKSet(
new URL("https://auth.internal/.well-known/jwks.json")
);
const token = req.headers.authorization?.split(" ")[1];
if (!token) {
return res.status(401).json({ error: "Token manquant" });
}
try {
const { payload } = await jose.jwtVerify(token, jwks, {
issuer: "https://auth.internal",
audience: "billing-api",
});
// payload.sub, payload.scope, etc. sont maintenant vérifiés
req.userId = payload.sub;
} catch (err) {
return res.status(401).json({ error: "Token invalide" });
}Pour décider entre jose et l'ancien paquet jsonwebtoken, la différence clé est la portée du runtime. jsonwebtoken est uniquement Node.js — il repose sur le module intégré crypto et ne pourra pas être bundlé pour le navigateur. jose est entièrement isomorphe : il utilise l'API Web Crypto, disponible dans tous les navigateurs modernes, Node.js 16+, Deno, Bun et Cloudflare Workers. Si votre logique d'authentification réside dans un fichier middleware Next.js (qui s'exécute dans l'Edge Runtime), ou dans un Cloudflare Worker, ou dans un utilitaire partagé importé à la fois par du code serveur et client, jose est le bon choix car il n'a aucune dépendance native et s'installe sans étape de compilation. jsonwebtoken reste raisonnable pour les applications serveur Node.js pures où vous avez besoin de son écosystème plus large d'utilitaires de signature et que vous ne prévoyez pas d'exécuter le code dans un environnement edge. Dans un projet vert en 2026, choisissez jose par défaut, sauf si vous avez une raison spécifique de préférer l'ancienne API.
Si vous n'avez besoin que du décodage sans vérification, jose fournit jose.decodeJwt(token) qui renvoie la charge utile et jose.decodeProtectedHeader(token) pour l'en-tête. Ce sont des fonctions de commodité qui effectuent le décodage Base64url en interne. Mais la raison principale d'utiliser jose est que vous ne devriez rarement décoder sans aussi vérifier. Si vous êtes côté client et avez juste besoin d'afficher le nom d'affichage de l'utilisateur ou l'URL de son avatar depuis les revendications du token, le décodage seul convient. Côté serveur, vérifiez toujours. J'ai vu des systèmes en production qui décodaient les revendications JWT pour des décisions de contrôle d'accès sans vérifier la signature, et c'est une porte ouverte pour tout attaquant qui comprend le format JWT.
import * as jose from "jose";
// Décodage seul : pas de secret nécessaire, pas de vérification
const payload = jose.decodeJwt(token);
console.log(payload.sub); // "usr_921f"
console.log(payload.scope); // "billing:read"
const header = jose.decodeProtectedHeader(token);
console.log(header.alg); // "HS256"
console.log(header.typ); // "JWT"
// Vérifier l'expiration sans vérification (affichage côté client)
if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
console.log("Token expiré — redirection vers la connexion");
}Sortie terminal avec mise en surbrillance syntaxique
Lors du débogage de tokens JWT dans un outil CLI Node.js ou pendant un incident, une sortie colorée fait une vraie différence. La bibliothèque chalk combinée avec JSON.stringify fait le travail. Installez avec npm install chalk.
import chalk from "chalk";
function printJwt(token) {
const segments = token.split(".");
if (segments.length !== 3) {
console.error(chalk.red("JWT invalide : 3 segments attendus"));
return;
}
const header = JSON.parse(Buffer.from(segments[0], "base64url").toString("utf-8"));
const payload = JSON.parse(Buffer.from(segments[1], "base64url").toString("utf-8"));
console.log(chalk.bold.cyan("\n=== En-tête JWT ==="));
console.log(chalk.gray(JSON.stringify(header, null, 2)));
console.log(chalk.bold.green("\n=== Charge utile JWT ==="));
console.log(chalk.gray(JSON.stringify(payload, null, 2)));
// Mettre en évidence le statut d'expiration
if (payload.exp) {
const expiresAt = new Date(payload.exp * 1000);
const isExpired = expiresAt < new Date();
console.log(
chalk.bold("\nExpire :"),
isExpired
? chalk.red(`EXPIRÉ le ${expiresAt.toISOString()}`)
: chalk.green(`Valide jusqu'au ${expiresAt.toISOString()}`)
);
}
console.log(chalk.dim("\nSignature : " + segments[2].substring(0, 20) + "..."));
}
printJwt(process.argv[2]);
// Exécuter : node jwt-debug.mjs "eyJhbGci..."Traitement des JWT depuis de grands fichiers journaux
Les infrastructures API modernes émettent des journaux d'accès structurés au format NDJSON — un objet JSON par ligne, chaque ligne contenant le chemin de la requête, le statut de la réponse, la latence, et l'en-tête Authorization décodé ou brut. Dans un service actif, ces fichiers grandissent rapidement : une passerelle traitant 10 000 requêtes par minute produit plus de 14 millions d'entrées de journal par jour. Les cas d'usage de sécurité et de conformité nécessitent régulièrement de scanner ces fichiers après coup — identifier chaque requête effectuée par un compte de service compromis (analyse post-incident), confirmer que les tokens d'un utilisateur spécifique ont expiré avant une fenêtre d'accès aux données (audit de conformité), ou extraire l'ensemble complet des sujets qui ont accédé à un endpoint sensible pendant une fenêtre de maintenance. Comme un seul fichier journal peut dépasser plusieurs gigaoctets, le charger en mémoire avec readFileSync n'est pas viable. Les flux readline Node.js traitent le fichier ligne par ligne avec une utilisation mémoire constante, rendant pratique le scan de journaux arbitrairement grands sur un ordinateur portable de développeur standard.
Vous ne rencontrerez pas le problème « fichier trop grand pour la mémoire » avec des JWT individuels, puisqu'un seul token dépasse rarement quelques kilo-octets. Le scénario qui survient vraiment est le scan d'un grand journal d'accès ou d'une piste d'audit pour les tokens JWT, le décodage de chacun, et l'extraction de revendications spécifiques. Les flux Node.js gèrent cela sans charger l'intégralité du fichier.
import { createReadStream } from "node:fs";
import { createInterface } from "node:readline";
async function scanLogsForExpiredTokens(logPath) {
const fileStream = createReadStream(logPath, { encoding: "utf-8" });
const rl = createInterface({ input: fileStream, crlfDelay: Infinity });
let lineCount = 0;
let expiredCount = 0;
const nowSeconds = Math.floor(Date.now() / 1000);
for await (const line of rl) {
lineCount++;
try {
const entry = JSON.parse(line);
if (!entry.authorization_token) continue;
const segments = entry.authorization_token.split(".");
if (segments.length !== 3) continue;
const payload = JSON.parse(
Buffer.from(segments[1], "base64url").toString("utf-8")
);
if (payload.exp && payload.exp < nowSeconds) {
expiredCount++;
const expDate = new Date(payload.exp * 1000).toISOString();
console.log("Ligne " + lineCount + " : token expiré pour " + payload.sub + ", exp=" + expDate);
}
} catch {
// Ignorer les lignes malformées
}
}
console.log(`\nAnalysé ${lineCount} lignes, trouvé ${expiredCount} tokens expirés`);
}
scanLogsForExpiredTokens("./logs/api-access-2026-03.ndjson");import { createReadStream } from "node:fs";
import { createInterface } from "node:readline";
async function extractUniqueSubjects(logPath) {
const rl = createInterface({
input: createReadStream(logPath, { encoding: "utf-8" }),
crlfDelay: Infinity,
});
const subjects = new Set();
const jwtRegex = /eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g;
for await (const line of rl) {
const matches = line.match(jwtRegex);
if (!matches) continue;
for (const token of matches) {
try {
const payload = JSON.parse(
Buffer.from(token.split(".")[1], "base64url").toString("utf-8")
);
if (payload.sub) subjects.add(payload.sub);
} catch {
// Pas un JWT valide
}
}
}
console.log(`Trouvé ${subjects.size} sujets uniques :`);
for (const sub of subjects) console.log(` ${sub}`);
}
extractUniqueSubjects("./logs/gateway-2026-03.log");readFileSyncsaturera la mémoire et déclenchera des pauses GC. L'approche readline traite une ligne à la fois avec une utilisation mémoire constante.Erreurs courantes
Problème : atob() renvoie une chaîne Latin-1. Les caractères UTF-8 multi-octets (emoji, CJK, caractères accentués) sont répartis sur plusieurs caractères et ressortent illisibles.
Correction : Convertissez la sortie de atob() en Uint8Array, puis passez-la dans new TextDecoder('utf-8').
// Échoue sur les revendications de charge utile non-ASCII
const payload = JSON.parse(atob(token.split(".")[1]));
// display_name apparaît comme "ç°ä¸å¤ªé\x83\x8E" au lieu de "田中太郎"const binary = atob(token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/"));
const bytes = Uint8Array.from(binary, c => c.charCodeAt(0));
const payload = JSON.parse(new TextDecoder("utf-8").decode(bytes));
// display_name affiche correctement "田中太郎"Problème : atob() lève une "InvalidCharacterError" car base64url utilise - et _ au lieu de + et /.
Correction : Remplacez - par + et _ par / avant d'appeler atob(). Node.js Buffer.from() avec 'base64url' gère cela automatiquement.
// Lève : InvalidCharacterError: String contains an invalid character
const payload = atob(token.split(".")[1]);const segment = token.split(".")[1];
const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
const payload = atob(base64); // fonctionne maintenantProblème : N'importe qui peut créer un JWT avec n'importe quelle charge utile. Le décodage lit seulement les données — il ne prouve pas que le token a été émis par votre serveur d'authentification.
Correction : Côté serveur, vérifiez toujours la signature en utilisant jose.jwtVerify() ou jsonwebtoken.verify(). Le décodage seul est acceptable pour l'affichage côté client des revendications utilisateur.
// DANGEREUX : décodé mais non vérifié
const claims = JSON.parse(atob(token.split(".")[1]));
if (claims.role === "admin") {
grantAdminAccess(); // un attaquant peut falsifier cela
}import * as jose from "jose";
const { payload } = await jose.jwtVerify(token, secretKey);
if (payload.role === "admin") {
grantAdminAccess(); // sûr — la signature est vérifiée
}Problème : JWT exp est en secondes depuis l'époque, mais Date.now() renvoie des millisecondes. La comparaison dira toujours que le token est valide car l'horodatage en millisecondes est 1000 fois plus grand.
Correction : Divisez Date.now() par 1000 et arrondissez vers le bas le résultat avant de le comparer à exp.
// Bug : Date.now() est en millisecondes, exp est en secondes
if (payload.exp > Date.now()) {
console.log("Token valide"); // toujours vrai — incorrect !
}const nowSeconds = Math.floor(Date.now() / 1000);
if (payload.exp > nowSeconds) {
console.log("Token valide"); // comparaison correcte
}Méthodes de décodage JWT — Comparaison rapide
Utilisez atob() + TextDecoder pour le décodage côté navigateur quand vous avez juste besoin d'afficher les revendications à l'utilisateur. Utilisez Buffer.from() dans les scripts Node.js et les outils CLI. Utilisez jose dès que vous avez besoin de vérifier une signature, c'est-à-dire dans tout middleware d'authentification côté serveur. Le paquet jwt-decode est une alternative légère si vous souhaitez une API à une seule fonction pour le décodage seul dans le navigateur. Pour une inspection visuelle rapide sans écrire de code, collez votre token dans l' outil de décodage JWT.
Questions fréquemment posées
Comment décoder un token JWT en JavaScript sans bibliothèque ?
Découpez le token sur « . », prenez le deuxième segment (la charge utile), normalisez l'encodage base64url en remplaçant - par + et _ par /, complétez avec des caractères =, puis appelez atob() suivi de TextDecoder pour obtenir la chaîne JSON en UTF-8. Passez le résultat dans JSON.parse() et vous obtenez l'objet des revendications. Aucun paquet npm requis. Cette approche fonctionne dans tous les navigateurs modernes et dans Node.js 18+. Si vous avez également besoin de lire l'en-tête, appliquez les mêmes étapes de décodage au premier segment. Gardez à l'esprit que cela vous donne les données brutes sans vérification de signature — traitez le résultat comme affichage uniquement, à moins de vérifier la signature côté serveur.
const token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3JfOTIxZiIsInJvbGUiOiJhZG1pbiJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
const payload = token.split(".")[1];
const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
const json = atob(base64);
const claims = JSON.parse(json);
console.log(claims);
// { sub: "usr_921f", role: "admin" }Quelle est la différence entre atob() et Buffer.from() pour décoder un JWT ?
atob() est une API navigateur qui décode du Base64 standard en chaîne binaire Latin-1. Elle ne comprend pas directement l'encodage base64url, donc vous devez d'abord remplacer les caractères - et _. Buffer.from(segment, "base64url") est une API Node.js qui gère nativement l'alphabet base64url et renvoie un Buffer sur lequel vous pouvez appeler .toString("utf-8"). Utilisez atob() dans le navigateur, Buffer.from() dans Node.js. Une troisième option — plus lente mais historiquement courante — est l'astuce percent-encoding avec decodeURIComponent, mais ce modèle repose sur la fonction dépréciée escape() dans certains anciens extraits de code et doit être évitée dans le nouveau code. Pour du code isomorphe fonctionnant dans les deux environnements, vérifiez typeof Buffer !== "undefined" et branchez en conséquence.
// Navigateur
const json = atob(payload.replace(/-/g, "+").replace(/_/g, "/"));
// Node.js
const json2 = Buffer.from(payload, "base64url").toString("utf-8");Pourquoi atob() échoue-t-il sur les revendications JWT non-ASCII ?
atob() renvoie une chaîne Latin-1 où chaque caractère correspond à un seul octet. Les séquences UTF-8 multi-octets (emoji, caractères CJK, lettres accentuées au-delà du Latin-1) sont réparties sur plusieurs caractères, produisant une sortie illisible. La correction consiste à convertir d'abord la chaîne binaire en Uint8Array, puis à passer ce tableau à new TextDecoder("utf-8").decode(). L'API TextDecoder réassemble correctement les séquences multi-octets. Ce problème est facile à rater en développement car la plupart des charges utiles JWT ne contiennent que des identifiants utilisateurs ASCII, des horodatages et des noms de rôles — le bug n'apparaît que lorsqu'une revendication contient un nom d'affichage non-ASCII ou une chaîne localisée. Utilisez toujours le chemin TextDecoder dans le nouveau code, même si vos charges utiles actuelles ne sont qu'en ASCII, car les revendications peuvent changer à mesure que l'application évolue.
// Cassé : atob renvoie Latin-1, les caractères multi-octets sont illisibles
const broken = atob(base64); // "ð\x9F\x8E\x89" au lieu de l'emoji
// Corrigé : convertir en tableau d'octets, puis TextDecoder
const bytes = Uint8Array.from(atob(base64), c => c.charCodeAt(0));
const fixed = new TextDecoder("utf-8").decode(bytes);Puis-je vérifier une signature JWT en JavaScript ?
Le décodage et la vérification sont des opérations différentes. Le décodage lit simplement la charge utile, qui n'est pas chiffrée. La vérification contrôle la signature par rapport à un secret (HMAC) ou une clé publique (RSA/ECDSA). La bibliothèque jose prend en charge les deux dans le navigateur via l'API Web Crypto et dans Node.js. Le paquet jsonwebtoken fonctionne uniquement dans Node.js. Ne faites jamais confiance aux revendications décodées sans vérifier la signature côté serveur. Côté client, il est acceptable de décoder un JWT pour lire le nom d'affichage ou l'heure d'expiration de l'utilisateur, mais toute décision de contrôle d'accès — vérifier si un utilisateur possède un rôle ou une permission particulière — doit se faire dans du code côté serveur après vérification. Un attaquant qui comprend le format JWT peut créer un token avec des revendications arbitraires et votre vérification côté client passera.
import * as jose from "jose";
const secret = new TextEncoder().encode("your-256-bit-secret");
const { payload } = await jose.jwtVerify(token, secret);
console.log(payload.sub); // revendications vérifiéesComment vérifier si un JWT est expiré en JavaScript ?
Décodez la charge utile et lisez la revendication exp, qui est un horodatage Unix en secondes. Comparez-le à l'heure actuelle en utilisant Math.floor(Date.now() / 1000). Si l'heure actuelle est supérieure à exp, le token est expiré. Rappel : la valeur exp est en secondes depuis l'époque, pas en millisecondes, donc diviser Date.now() par 1000 est indispensable. En pratique, prévoyez une petite marge pour le décalage d'horloge — vérifier si le token expire dans les 30 secondes à venir plutôt que strictement dans le passé évite les cas limites où le token est encore techniquement valide au moment du décodage mais expire avant que le prochain appel API en aval le traite. Gérez également le cas où exp est absent, ce qui signifie que le token n'expire jamais.
function isTokenExpired(token) {
const payload = JSON.parse(
atob(token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/"))
);
const nowSeconds = Math.floor(Date.now() / 1000);
return payload.exp < nowSeconds;
}
console.log(isTokenExpired(myToken)); // true ou falseComment écrire du code de décodage JWT isomorphe fonctionnant à la fois dans Node.js et le navigateur ?
Vérifiez l'existence de globalThis.Buffer. S'il existe, vous êtes dans Node.js et pouvez utiliser Buffer.from(segment, "base64url").toString("utf-8"). S'il n'existe pas, vous êtes dans un navigateur et devez utiliser atob() avec l'approche TextDecoder. Encapsulez cette vérification dans une fonction decodeBase64Url unique et utilisez-la partout. Cela importe surtout pour les packages utilitaires, les composants de système de design, et tout code partagé vivant dans un package de monorepo importé à la fois par un composant serveur Next.js et un composant React navigateur. Garder la détection d'environnement en un seul endroit signifie que vous n'avez besoin de la mettre à jour qu'en un seul endroit si le runtime change — par exemple, quand Deno ajoute un support complet de Buffer ou qu'un nouveau runtime edge nécessite un chemin de code différent.
function decodeBase64Url(segment) {
if (typeof Buffer !== "undefined") {
return Buffer.from(segment, "base64url").toString("utf-8");
}
const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
const bytes = Uint8Array.from(atob(base64), c => c.charCodeAt(0));
return new TextDecoder("utf-8").decode(bytes);
}Outils associés
Marcus specialises in JavaScript performance, build tooling, and the inner workings of the V8 engine. He has spent years profiling and optimising React applications, working on bundler configurations, and squeezing every millisecond out of critical rendering paths. He writes about Core Web Vitals, JavaScript memory management, and the tools developers reach for when performance really matters.
Sophie is a full-stack developer focused on TypeScript across the entire stack — from React frontends to Express and Fastify backends. She has a particular interest in type-safe API design, runtime validation, and the patterns that make large JavaScript codebases stay manageable. She writes about TypeScript idioms, Node.js internals, and the ever-evolving JavaScript module ecosystem.