JWT Decoder JavaScript — atob(), TextDecoder e jose
Usa il Decoder JWT gratuito direttamente nel tuo browser — nessuna installazione.
Prova Decoder JWT online →Ogni flusso di autenticazione che ho costruito raggiunge prima o poi lo stesso punto: hai un JWT in un cookie, un header o un URL di callback OAuth, e devi leggere cosa c'è dentro. Un decoder JWT in JavaScript non richiede alcun pacchetto npm. L'header e il payload del token sono semplicemente JSON codificato in Base64url, e sia il browser che Node.js includono tutto il necessario per decodificarli. Questa guida copre l'intero pipeline del text decoder JavaScript per i JWT: suddividere il token, normalizzare base64url verso il Base64 standard, atob() e TextDecoder per una corretta gestione UTF-8, il Buffer.from() di Node.js, la verifica della firma con jose, e gli errori comuni che mettono in difficoltà gli sviluppatori ogni giorno. Per un'ispezione rapida una tantum, prova il JWT Decoder online invece. Tutti gli esempi sono destinati a ES2020+ e Node.js 18+.
- ✓Dividi il JWT su "." — l'indice 0 è l'header, l'indice 1 è il payload, l'indice 2 è la firma.
- ✓atob() decodifica il Base64 ma restituisce Latin-1, non UTF-8. Usa TextDecoder o Buffer.from() per i claims non-ASCII.
- ✓Buffer.from(segment, "base64url") gestisce base64url nativamente in Node.js — nessuna sostituzione manuale di caratteri necessaria.
- ✓Decodifica NON è verifica. Non fidarti mai dei claims di un JWT decodificato senza controllare la firma lato server.
- ✓La libreria jose fa entrambe le cose: verifica le firme HS256/RS256/ES256 e restituisce il payload decodificato in una sola chiamata.
Cos'è la Decodifica JWT?
Un JSON Web Token è composto da tre segmenti codificati in Base64url separati da punti. Il primo segmento è l'header, il secondo è il payload (i claims che ti interessano davvero), e il terzo è la firma crittografica. L'header è un piccolo oggetto JSON che descrive il token stesso. Il suo campo più importante è alg — l'algoritmo di firma (es., HS256, RS256, ES256). Il campo typ è quasi sempre "JWT", e il campo opzionale kid identifica quale chiave è stata usata per firmare il token — fondamentale quando un identity provider ruota le chiavi e pubblica un endpoint JWKS con più chiavi pubbliche.
Il payload contiene i claims. RFC 7519 definisce sette nomi di claim registrati: sub (soggetto — di solito l'ID utente), iss (emittente — l'URL del server di autenticazione), aud (pubblico — l'API per cui il token è destinato), iat (timestamp di emissione), exp (timestamp di scadenza), nbf (timestamp non-prima-di), e jti (JWT ID — usato per prevenire gli attacchi replay). Tutti i timestamp sono in secondi epoch Unix, non in millisecondi. Il segmento della firma è binario grezzo — un digest HMAC con chiave o una firma digitale asimmetrica. È codificato in Base64url come gli altri segmenti, ma i suoi byte non sono JSON e non hanno struttura leggibile dall'uomo.
In pratica, decodifichi i JWT in JavaScript per tre motivi comuni. Primo, il debug: hai un token da un flusso OAuth o un ambiente di test e vuoi confermare che i claims corrispondano a ciò che il server di autenticazione avrebbe dovuto emettere. Secondo, leggere i claims utente per scopi di visualizzazione sul lato client — mostrare il nome dell'utente connesso, l'URL dell'avatar, o il badge del ruolo dal payload del token senza una chiamata API aggiuntiva. Terzo, controllare la scadenza prima di tentare un refresh: se exp è entro i prossimi 60 secondi, attiva un refresh silenzioso prima della prossima chiamata API anziché aspettare una risposta 401.
La decodifica non verifica se il token è valido o manomesso. Questa è un'operazione separata chiamata verifica, che richiede il segreto HMAC o la chiave pubblica RSA/ECDSA. Chiunque può decodificare un JWT. Solo il detentore della chiave corretta può verificarne uno. Questa distinzione mette in difficoltà molti sviluppatori, specialmente quando si costruiscono flussi di autenticazione lato client dove i claims decodificati vengono visualizzati ma non devono mai essere attendibili per decisioni di autorizzazione senza un controllo backend verificato.
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3JfOTIxZiIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTcxMTYxMDAwMH0.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
// Header
{ "alg": "HS256" }
// Payload
{
"sub": "usr_921f",
"role": "admin",
"iat": 1711610000
}atob() + TextDecoder — Decodifica JWT Nativa del Browser
Il pipeline nativo del browser per decodificare un JWT ha quattro passaggi. Primo, dividi la stringa del token su "." per ottenere i tre segmenti. Secondo, normalizza il segmento base64url sostituendo - con + e _ con /, poi aggiunge caratteri = fino a quando la lunghezza è un multiplo di 4. Terzo, chiama atob() per decodificare il Base64 in una stringa binaria. Quarto, converti la stringa binaria in UTF-8 corretto usando TextDecoder. Quest'ultimo passaggio è importante perché atob() restituisce Latin-1. I caratteri multibyte — emoji, testo CJK, caratteri accentati oltre il range Latin-1 — risultano corrotti senza il passaggio del text decoder 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 }Il passaggio del padding è facile da trascurare. I JWT rimuovono i caratteri = finali dai loro segmenti Base64url perché la specifica JWT (RFC 7515) definisce base64url senza padding. Ma atob() in alcuni motori browser lancia un InvalidCharacterError se la lunghezza dell'input non è divisibile per 4. Aggiungere padding in modo difensivo con padEnd() evita questo caso limite in tutti gli ambienti. Ecco una versione riutilizzabile che decodifica sia l'header che il payload in oggetti separati:
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("Algorithm:", header.alg); // "HS256"
console.log("Subject:", payload.sub); // "usr_921f"
console.log("Role:", payload.role); // "admin"Una volta che hai queste due funzioni, vale la pena inserirle in un modulo di utilità condiviso anziché copiare la logica in tutti i file. Un file src/lib/jwt.ts o utils/jwt-decode.ts con un tipo di ritorno tipizzato rende esplicita l'intenzione in tutto il codebase. In TypeScript, puoi tipizzare il ritorno come { header: JwtHeader; payload: JwtPayload } dove JwtHeader include alg, typ, e opzionalmente kid, e JwtPayload estende i claims registrati RFC 7519 con una firma di indice per i claims personalizzati. Centralizzare la logica di decodifica significa che quando in seguito vuoi aggiungere la gestione degli errori (catturare segmenti malformati) o la telemetria (registrare i fallimenti di decodifica), hai un unico posto da aggiornare.
TextDecoder è ciò che rende questo pipeline sicuro per i claims non-ASCII. Senza di esso, atob() restituisce una stringa Latin-1 dove le sequenze UTF-8 multibyte vengono suddivise tra i caratteri. Vedrai spazzatura invece di emoji o testo CJK. Passa sempre attraverso new TextDecoder("utf-8") dopo atob().Decodifica Claims JWT UTF-8 con Caratteri Multibyte
I payload JWT sono JSON UTF-8 codificati come base64url. La maggior parte dei payload contiene campi solo ASCII come ID utente e timestamp, quindi gli sviluppatori non si accorgono mai che atob() restituisce Latin-1 invece di UTF-8. Il problema emerge nel momento in cui un claim contiene emoji, caratteri giapponesi, cirillico o qualsiasi code point sopra U+00FF. Il pattern JavaScript decode UTF-8 richiede di convertire prima la stringa binaria in un array di byte, poi di passarla attraverso TextDecoder.
// Simulazione di un payload JWT con emoji e caratteri CJK
const payloadObj = {
sub: "usr_e821",
display_name: "田中太郎",
team: "Platform 🚀",
region: "ap-northeast-1"
};
// Codifica: oggetto → JSON → byte 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(/=+$/, "");
// Decodifica: base64url → base64 → stringa binaria → byte → stringa 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); // "田中太郎" — corretto
console.log(result.team); // "Platform 🚀" — correttoC'è un pattern di fallback legacy che vedrai nei codebase più vecchi che usa decodeURIComponent combinato con un trucco di percent-encoding. Questo approccio JavaScript decodeURIComponent funziona perché ri-codifica ogni byte come coppia percent-hex, poi decodeURIComponent riassembla le sequenze UTF-8 multibyte:
function decodeBase64UrlLegacy(segment) {
const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
const binary = atob(base64);
// Converti ogni char in %XX esadecimale, poi decodeURIComponent riassembla UTF-8
const utf8 = decodeURIComponent(
binary.split("").map(c =>
"%" + c.charCodeAt(0).toString(16).padStart(2, "0")
).join("")
);
return utf8;
}
// Funziona per i claims non-ASCII senza TextDecoder
const payload = decodeBase64UrlLegacy(token.split(".")[1]);
console.log(JSON.parse(payload));decodeURIComponent(escape(atob(segment))) in snippet di utilità JWT più vecchi. La funzione escape()è deprecata e non standard. Sostituiscila con l'approccio TextDecoder mostrato sopra. Il pattern JavaScript unescape decoder ha lo stesso problema: unescape() è deprecata. Entrambe le funzioni potrebbero essere rimosse dai futuri motori JavaScript.Pipeline di Decodifica JWT — Riferimento dei Passaggi
Ogni passaggio nel pipeline di decodifica JWT nativo del browser, con l'API JavaScript utilizzata e cosa produce:
L'equivalente Node.js comprime i passaggi da 2 a 4 in una singola chiamata: Buffer.from(segment, "base64url").toString("utf-8"). L'opzione di codifica "base64url" gestisce internamente la conversione dell'alfabeto e il padding.
Buffer.from() — Il Decoder di Stringhe Node.js per i JWT
Node.js ha un percorso molto più semplice. La classe Buffer accetta direttamente una codifica "base64url", quindi salti la sostituzione manuale dei caratteri e il padding. Questo è il percorso del decoder di stringhe JavaScript per il codice lato server. Una riga converte un segmento JWT in una stringa UTF-8, e gestisce correttamente i caratteri multibyte senza passaggi aggiuntivi.
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 }Questo è l'approccio che uso in ogni progetto Node.js. È più breve, più veloce, e gestisce già correttamente l'UTF-8. Nessun TextDecoder necessario, nessuna sostituzione di caratteri, nessun calcolo di padding. La classe Buffer è un decoder di stringhe JavaScript che gestisce nativamente l'alfabeto base64url, il che elimina un'intera categoria di bug relativi alla sostituzione dei caratteri. Se il tuo codice deve girare sia nel browser che in Node.js, controlla le FAQ in fondo per una funzione wrapper isomorfica che rileva l'ambiente a runtime.
Ecco un esempio più completo che mostra come estrarre i claims JWT comuni e convertire i timestamp in date leggibili, che è il pattern che userai più spesso nel middleware e nei gestori di route API:
function inspectToken(token) {
const segments = token.split(".");
if (segments.length !== 3) {
throw new Error("Not a valid JWT — expected 3 dot-separated segments");
}
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 || "(not set)",
audience: payload.aud || "(not set)",
issuedAt: payload.iat ? new Date(payload.iat * 1000).toISOString() : "(not set)",
expiresAt: payload.exp ? new Date(payload.exp * 1000).toISOString() : "(never)",
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"]
// }Nei servizi Node.js in produzione, il pattern di decodifica Buffer.from() appare in tre posti ricorrenti. Il primo è il middleware di logging delle richieste: decodifichi l'header Authorization in arrivo per allegare userId e org a ogni voce di log strutturata senza un round-trip di rete aggiuntivo verso il server di autenticazione. Il secondo è il debug: stampi i claims del token decodificato sulla console durante lo sviluppo per confermare che gli scope corretti siano stati emessi prima di scrivere le asserzioni di test. Il terzo è il refresh proattivo del token nei gateway API. Anziché inoltrare un token a monte e lasciare che il servizio downstream restituisca un 401 quando il token scade durante la richiesta, il gateway decodifica il token all'edge, legge il claim exp e attiva un refresh se la scadenza è entro i prossimi 30 secondi. Questo elimina una categoria di errori di autenticazione transitori difficili da riprodurre e frustranti da debuggare.
"base64url" è stata aggiunta in Node.js 15.7.0. Se sei bloccato su Node.js 14 o precedente, torna a Buffer.from(segment.replace(/-/g, "+").replace(/_/g, "/"), "base64") che funziona allo stesso modo ma richiede lo swap manuale dei caratteri.Decodifica JWT da File e Risposta API
Due scenari si presentano costantemente. Il primo è leggere un JWT da un file locale: un token salvato durante lo sviluppo, una fixture di test, o un file scaricato durante un incidente per l'analisi post-mortem. Il secondo è estrarre un JWT da una risposta HTTP, tipicamente il campo access_token nel corpo di una risposta del token OAuth o in un header Authorization. Entrambi necessitano di gestione degli errori perché token malformati, file troncati ed errori di rete sono realtà quotidiane. Un token valido la settimana scorsa potrebbe avere spazi bianchi o newline finali da copia-incolla. Un corpo di risposta potrebbe essere HTML invece di JSON se il server di autenticazione ha restituito una pagina di errore.
Leggi JWT da un File (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(`Invalid JWT: expected 3 segments, got ${segments.length}`);
}
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(`Failed to decode JWT from ${filePath}: ${err.message}`);
}
}
try {
const { header, payload } = decodeJwtFromFile("./test-fixtures/access-token.txt");
console.log("Algorithm:", header.alg);
console.log("Expires:", new Date(payload.exp * 1000).toISOString());
} catch (err) {
console.error(err.message);
}Estrai JWT da una Risposta 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(`Login failed: ${response.status} ${response.statusText}`);
}
const { access_token } = await response.json();
if (!access_token || access_token.split(".").length !== 3) {
throw new Error("Response does not contain a valid JWT");
}
const payload = access_token.split(".")[1];
const json = Buffer.from(payload, "base64url").toString("utf-8");
return JSON.parse(json);
}
// Utilizzo
try {
const claims = await fetchAndDecodeToken(
"https://auth.internal/oauth/token",
{ username: "deploy-bot", password: process.env.DEPLOY_TOKEN }
);
console.log("Token subject:", claims.sub);
console.log("Token scopes:", claims.scope);
console.log("Expires at:", new Date(claims.exp * 1000).toISOString());
} catch (err) {
console.error("Token decode error:", err.message);
}Decodifica JWT da Riga di Comando
A volte vuoi semplicemente dare un'occhiata a un token dal terminale senza scrivere uno script. Node.js è disponibile sulla maggior parte delle macchine degli sviluppatori, quindi un one-liner funziona bene. jq gestisce la formattazione.
# Decodifica payload JWT con one-liner 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'))))"
# Passa a jq per output formattato
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 .
# Decodifica sia header che payload
echo "$JWT_TOKEN" | node -e "
process.stdin.on('data', d => {
const parts = d.toString().trim().split('.');
console.log('Header:', JSON.parse(Buffer.from(parts[0], 'base64url').toString()));
console.log('Payload:', JSON.parse(Buffer.from(parts[1], 'base64url').toString()));
});
"Se preferisci bash puro senza Node.js, passa il segmento attraverso base64 -d dopo aver corretto i caratteri base64url con tr:
# Bash puro: decodifica payload JWT senza Node.js echo "$JWT_TOKEN" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null | jq . # Variante macOS (base64 -D invece di -d) echo "$JWT_TOKEN" | cut -d. -f2 | tr '_-' '/+' | base64 -D 2>/dev/null | jq .
Per un'ispezione visiva rapida senza alcun terminale, incolla il tuo token nel JWT Decoder di ToolDeck per una panoramica affiancata di tutti e tre i segmenti con etichette dei claims codificate a colori e stato di scadenza.
jose — Verifica e Decodifica in Una Sola Libreria
Per il middleware di autenticazione in produzione, hai bisogno della verifica della firma, non solo della decodifica. La libreria jose è la migliore opzione. Funziona sia in Node.js che nei browser (tramite la Web Crypto API), supporta HS256, RS256, ES256, EdDSA e JWE (token crittografati), e non ha dipendenze native. Installa con 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("Algorithm:", protectedHeader.alg); // "HS256"
console.log("Subject:", payload.sub); // "usr_921f"
console.log("Scope:", payload.scope); // "billing:read"
} catch (err) {
if (err.code === "ERR_JWT_EXPIRED") {
console.error("Token expired at:", err.payload.exp);
} else {
console.error("Verification failed:", err.message);
}
}import * as jose from "jose";
// Recupera il set di chiavi pubbliche dall'identity provider
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: "Missing token" });
}
try {
const { payload } = await jose.jwtVerify(token, jwks, {
issuer: "https://auth.internal",
audience: "billing-api",
});
// payload.sub, payload.scope, ecc. sono ora verificati
req.userId = payload.sub;
} catch (err) {
return res.status(401).json({ error: "Invalid token" });
}Quando si decide tra jose e il vecchio pacchetto jsonwebtoken, la differenza chiave è l'ambito del runtime. jsonwebtoken è solo Node.js — si basa sul built-in crypto e non può essere incluso in bundle per il browser. jose è completamente isomorfico: usa la Web Crypto API, disponibile in tutti i browser moderni, Node.js 16+, Deno, Bun e Cloudflare Workers. Se la tua logica di autenticazione si trova in un file middleware Next.js (che gira nell'Edge Runtime), o in un Cloudflare Worker, o in una utility condivisa importata sia dal codice server che da quello client, jose è la scelta corretta perché non ha dipendenze native e si installa senza un passaggio di build. jsonwebtoken rimane ragionevole per applicazioni server Node.js pure dove hai bisogno del suo più ampio ecosistema di helper per la firma e non stai pianificando di eseguire il codice in un ambiente edge. In un progetto greenfield nel 2026, scegli di default jose a meno che tu non abbia un motivo specifico per preferire la vecchia API.
Se hai bisogno solo della decodifica senza verifica, jose fornisce jose.decodeJwt(token) che restituisce il payload e jose.decodeProtectedHeader(token) per l'header. Queste sono funzioni di convenienza che eseguono internamente la decodifica Base64url. Ma il vero motivo per usare jose è che raramente dovresti decodificare senza verificare anche. Se sei sul lato client e hai solo bisogno di mostrare all'utente il proprio nome visualizzato o l'URL dell'avatar dai claims del token, la sola decodifica va bene. Sul lato server, verifica sempre. Ho visto sistemi in produzione che decodificavano i claims JWT per decisioni di controllo degli accessi senza controllare la firma, e questa è una porta aperta per qualsiasi attaccante che comprende il formato JWT.
import * as jose from "jose";
// Sola decodifica: nessun segreto necessario, nessuna verifica
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"
// Controlla la scadenza senza verifica (visualizzazione lato client)
if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
console.log("Token scaduto — reindirizza al login");
}Output del Terminale con Evidenziazione della Sintassi
Quando si esegue il debug di token JWT in uno strumento CLI Node.js o durante un incidente, l'output con codici colore fa davvero la differenza. La libreria chalk abbinata a JSON.stringify fa il lavoro. Installa con npm install chalk.
import chalk from "chalk";
function printJwt(token) {
const segments = token.split(".");
if (segments.length !== 3) {
console.error(chalk.red("Invalid JWT: expected 3 segments"));
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=== JWT Header ==="));
console.log(chalk.gray(JSON.stringify(header, null, 2)));
console.log(chalk.bold.green("\n=== JWT Payload ==="));
console.log(chalk.gray(JSON.stringify(payload, null, 2)));
// Evidenzia lo stato di scadenza
if (payload.exp) {
const expiresAt = new Date(payload.exp * 1000);
const isExpired = expiresAt < new Date();
console.log(
chalk.bold("\nScadenza:"),
isExpired
? chalk.red(`SCADUTO il ${expiresAt.toISOString()}`)
: chalk.green(`Valido fino a ${expiresAt.toISOString()}`)
);
}
console.log(chalk.dim("\nFirma: " + segments[2].substring(0, 20) + "..."));
}
printJwt(process.argv[2]);
// Esegui: node jwt-debug.mjs "eyJhbGci..."Elaborazione JWT da File di Log di Grandi Dimensioni
La moderna infrastruttura API emette log di accesso strutturati in formato NDJSON — un oggetto JSON per riga, con ogni riga contenente il percorso della richiesta, lo stato della risposta, la latenza, e l'header Authorization decodificato o grezzo. In un servizio trafficato questi file crescono rapidamente: un gateway che gestisce 10.000 richieste al minuto produce oltre 14 milioni di voci di log al giorno. I casi d'uso di sicurezza e conformità richiedono regolarmente la scansione di questi file a posteriori — identificare ogni richiesta effettuata da un account di servizio compromesso (analisi post-incidente), confermare che i token di un utente specifico siano scaduti prima di una finestra di accesso ai dati (audit di conformità), o estrarre l'insieme completo dei soggetti che hanno acceduto a un endpoint sensibile durante una finestra di manutenzione. Poiché un singolo file di log può superare diversi gigabyte, caricarlo in memoria con readFileSync non è praticabile. Gli stream readline di Node.js elaborano il file una riga alla volta con overhead di memoria costante, rendendo pratico la scansione di log arbitrariamente grandi su un normale laptop da sviluppatore.
Non incorrerai nel problema "file troppo grande per la memoria" con i singoli JWT, poiché un singolo token raramente supera qualche kilobyte. Lo scenario che si presenta è la scansione di un grande log di accesso o di un audit trail per i token JWT, la decodifica di ognuno e l'estrazione di claims specifici. Gli stream Node.js gestiscono questo senza caricare l'intero file.
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("Line " + lineCount + ": expired token for " + payload.sub + ", exp=" + expDate);
}
} catch {
// Salta le righe malformate
}
}
console.log(`\nScansionate ${lineCount} righe, trovati ${expiredCount} token scaduti`);
}
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 {
// Non è un JWT valido
}
}
}
console.log(`Trovati ${subjects.size} soggetti univoci:`);
for (const sub of subjects) console.log(` ${sub}`);
}
extractUniqueSubjects("./logs/gateway-2026-03.log");readFileSyncblocca la memoria e innesca pause GC. L'approccio con readline elabora una riga alla volta con utilizzo di memoria costante.Errori Comuni
Problema: atob() restituisce una stringa Latin-1. I caratteri UTF-8 multibyte (emoji, CJK, caratteri accentati) vengono suddivisi tra i caratteri e risultano corrotti.
Soluzione: Converti l'output di atob() in un Uint8Array, poi passalo attraverso new TextDecoder('utf-8').
// Si rompe con i claims non-ASCII nel payload
const payload = JSON.parse(atob(token.split(".")[1]));
// display_name appare come "ç°ä¸å¤ªé\x83\x8E" invece di "田中太郎"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 mostra correttamente "田中太郎"Problema: atob() lancia "InvalidCharacterError" perché base64url usa - e _ invece di + e /.
Soluzione: Sostituisci - con + e _ con / prima di chiamare atob(). Node.js Buffer.from() con 'base64url' gestisce questo automaticamente.
// Lancia: 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); // ora funzionaProblema: Chiunque può creare un JWT con qualsiasi payload. La decodifica legge solo i dati — non prova che il token sia stato emesso dal tuo server di autenticazione.
Soluzione: Lato server, verifica sempre la firma usando jose.jwtVerify() o jsonwebtoken.verify(). La sola decodifica è accettabile per la visualizzazione lato client dei claims utente.
// PERICOLOSO: decodificato ma non verificato
const claims = JSON.parse(atob(token.split(".")[1]));
if (claims.role === "admin") {
grantAdminAccess(); // un attaccante può falsificare questo
}import * as jose from "jose";
const { payload } = await jose.jwtVerify(token, secretKey);
if (payload.role === "admin") {
grantAdminAccess(); // sicuro — la firma è verificata
}Problema: Il JWT exp è in secondi dall'epoch, ma Date.now() restituisce millisecondi. Il confronto dirà sempre che il token è valido perché il timestamp in millisecondi è 1000 volte più grande.
Soluzione: Dividi Date.now() per 1000 e arrotonda il risultato prima di confrontarlo con exp.
// Bug: Date.now() è in millisecondi, exp è in secondi
if (payload.exp > Date.now()) {
console.log("Token is valid"); // sempre vero — sbagliato!
}const nowSeconds = Math.floor(Date.now() / 1000);
if (payload.exp > nowSeconds) {
console.log("Token is valid"); // confronto corretto
}Metodi di Decodifica JWT — Confronto Rapido
Usa atob() + TextDecoder per la decodifica lato browser quando devi solo mostrare i claims all'utente. Usa Buffer.from() negli script Node.js e negli strumenti CLI. Scegli jose nel momento in cui hai bisogno di verificare una firma, ovvero in qualsiasi middleware di autenticazione lato server. Il pacchetto jwt-decode è un'alternativa leggera se vuoi un'API a funzione singola per la sola decodifica nel browser. Per un'ispezione visiva rapida senza scrivere codice, incolla il tuo token nel strumento JWT Decoder.
Domande Frequenti
Come decodifico un token JWT in JavaScript senza una libreria?
Dividi il token su ".", prendi il secondo segmento (il payload), normalizza la codifica base64url sostituendo - con + e _ con /, aggiunge caratteri =, poi chiama atob() seguito da TextDecoder per ottenere la stringa JSON UTF-8. Passa il risultato attraverso JSON.parse() e ottieni l'oggetto claims. Nessun pacchetto npm necessario. Questo approccio funziona in tutti i browser moderni e in Node.js 18+. Se hai bisogno di leggere anche l'header, applica gli stessi passaggi di decodifica al primo segmento. Tieni presente che questo ti fornisce i dati grezzi senza alcuna verifica della firma — tratta il risultato come solo per la visualizzazione a meno che tu non verifichi la firma lato server.
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" }Qual è la differenza tra atob() e Buffer.from() per la decodifica JWT?
atob() è un'API del browser che decodifica il Base64 standard in una stringa binaria Latin-1. Non comprende direttamente la codifica base64url, quindi devi prima sostituire i caratteri - e _. Buffer.from(segment, "base64url") è un'API Node.js che gestisce nativamente l'alfabeto base64url e restituisce un Buffer su cui puoi chiamare .toString("utf-8"). Usa atob() nel browser, Buffer.from() in Node.js. Una terza opzione — più lenta ma storicamente comune — è il trucco percent-encoding con decodeURIComponent, ma tale schema si basa sulla funzione escape() deprecata in alcuni snippet più vecchi e dovrebbe essere evitato nel nuovo codice. Per il codice isomorfico che gira in entrambi gli ambienti, controlla se typeof Buffer !== "undefined" e diramati di conseguenza.
// Browser
const json = atob(payload.replace(/-/g, "+").replace(/_/g, "/"));
// Node.js
const json2 = Buffer.from(payload, "base64url").toString("utf-8");Perché atob() non funziona con i claims JWT non-ASCII?
atob() restituisce una stringa Latin-1 dove ogni carattere corrisponde a un singolo byte. Le sequenze UTF-8 multibyte (emoji, caratteri CJK, lettere accentate oltre Latin-1) vengono suddivise su più caratteri, producendo output corrotto. La soluzione è convertire prima la stringa binaria in un Uint8Array, poi passare quell'array a new TextDecoder("utf-8").decode(). L'API TextDecoder riassembla correttamente le sequenze multibyte. Questo problema è facile da non notare durante lo sviluppo perché la maggior parte dei payload JWT contiene solo ID utente ASCII, timestamp e nomi di ruolo — il bug emerge solo quando un claim contiene un nome visualizzato non-ASCII o una stringa localizzata. Usa sempre il percorso TextDecoder nel nuovo codice anche quando i tuoi payload attuali sono solo ASCII, poiché i claims potrebbero cambiare man mano che l'applicazione evolve.
// Errato: atob restituisce Latin-1, i caratteri multibyte sono corrotti
const broken = atob(base64); // "ð\x9F\x8E\x89" invece dell'emoji
// Corretto: converti in array di byte, poi TextDecoder
const bytes = Uint8Array.from(atob(base64), c => c.charCodeAt(0));
const fixed = new TextDecoder("utf-8").decode(bytes);Posso verificare una firma JWT in JavaScript?
Decodifica e verifica sono operazioni diverse. La decodifica legge solo il payload, che non è criptato. La verifica controlla la firma rispetto a un segreto (HMAC) o a una chiave pubblica (RSA/ECDSA). La libreria jose supporta entrambi nel browser tramite la Web Crypto API e in Node.js. Il pacchetto jsonwebtoken funziona solo in Node.js. Non fidarti mai dei claims decodificati senza verificare la firma lato server. Sul lato client è accettabile decodificare un JWT per leggere il nome visualizzato dell'utente o il tempo di scadenza, ma qualsiasi decisione di controllo degli accessi — verificare se un utente ha un determinato ruolo o permesso — deve avvenire nel codice lato server dopo la verifica. Un attaccante che comprende il formato JWT può creare un token con claims arbitrari e il tuo controllo lato client verrà superato.
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); // verified claimsCome verifico se un JWT è scaduto in JavaScript?
Decodifica il payload e leggi il claim exp, che è un timestamp Unix in secondi. Confrontalo con l'ora corrente usando Math.floor(Date.now() / 1000). Se l'ora corrente è maggiore di exp, il token è scaduto. Ricorda: il valore exp è in secondi dall'epoch, non in millisecondi, quindi è necessario dividere Date.now() per 1000. In pratica, includi un piccolo margine per la differenza di orologio — controlla se il token scade entro i prossimi 30 secondi anziché strettamente nel passato per prevenire casi limite in cui il token è ancora tecnicamente valido quando lo decodifichi ma scade nel momento in cui la prossima chiamata API downstream lo elabora. Gestisci anche il caso in cui exp sia del tutto assente, il che significa che il token non scade mai.
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 or falseCome scrivo codice JWT isomorfico che funziona sia in Node.js che nel browser?
Controlla l'esistenza di globalThis.Buffer. Se esiste, sei in Node.js e puoi usare Buffer.from(segment, "base64url").toString("utf-8"). Se non esiste, sei in un browser e dovresti usare atob() con l'approccio TextDecoder. Racchiudi questo controllo in una singola funzione decodeBase64Url e usala ovunque. Questo è particolarmente importante per i pacchetti di utilità, i componenti dei design system e qualsiasi codice condiviso che vive in un pacchetto monorepo importato sia da un server component Next.js che da un componente React del browser. Mantenere il rilevamento dell'ambiente in un unico posto significa che devi aggiornarlo solo in un punto se il runtime cambia — ad esempio, quando Deno aggiunge il supporto completo a Buffer o un nuovo edge runtime richiede un percorso di codice diverso.
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);
}Strumenti Correlati
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.