JWT Decoder JavaScript — atob(), TextDecoder & jose
Använd det kostnadsfria JWT Decoder direkt i webbläsaren — ingen installation krävs.
Prova JWT Decoder online →Varje autentiseringsflöde jag byggt når till slut samma punkt: du har en JWT i en cookie, en header eller en OAuth-callback-URL och behöver läsa vad som finns inuti. En JWT-avkodare i JavaScript kräver inget npm-paket. Tokens header och payload är bara Base64url-kodad JSON, och både webbläsaren och Node.js levereras med allt som behövs för att avkoda dem. Den här guiden täcker hela JavaScript text decoder-flödet för JWT:er: uppdelning av token, normalisering av base64url till standard Base64, atob() och TextDecoder för korrekt UTF-8-hantering, Node.js Buffer.from(), signaturverifiering med jose, och de vanliga misstagen som snubblar upp utvecklare varje dag. För en snabb engångsgranskning, prova online-JWT-avkodaren istället. Alla exempel riktar sig mot ES2020+ och Node.js 18+.
- ✓Dela upp JWT:en på "." — index 0 är headern, index 1 är payload, index 2 är signaturen.
- ✓atob() avkodar Base64 men returnerar Latin-1, inte UTF-8. Använd TextDecoder eller Buffer.from() för icke-ASCII-claims.
- ✓Buffer.from(segment, "base64url") hanterar base64url inbyggt i Node.js — ingen manuell teckenbyte behövs.
- ✓Avkodning är INTE verifiering. Lita aldrig på claims från en avkodad JWT utan att kontrollera signaturen på serversidan.
- ✓Biblioteket jose gör båda: det verifierar HS256/RS256/ES256-signaturer och returnerar den avkodade payload i ett anrop.
Vad är JWT-avkodning?
En JSON Web Token består av tre Base64url-kodade segment separerade med punkter. Det första segmentet är headern, det andra är payload (de claims du faktiskt bryr dig om), och det tredje är den kryptografiska signaturen. Headern är ett litet JSON-objekt som beskriver token i sig. Dess viktigaste fält är alg — signeringsalgoritmen (t.ex. HS256, RS256, ES256). Fältet typ är nästan alltid "JWT", och det valfria kid fältet identifierar vilken nyckel som användes för att signera token — kritiskt när en identitetsleverantör roterar nycklar och publicerar en JWKS-slutpunkt med flera offentliga nycklar.
Payload innehåller claims. RFC 7519 definierar sju registrerade claimnamn: sub (subject — vanligtvis användar-ID), iss (issuer — auth-serverns URL), aud (audience — det API som token är avsedd för), iat (utfärdad-vid-tidsstämpel), exp (utgångstidsstämpel), nbf (inte-före-tidsstämpel), och jti (JWT ID — används för att förhindra replay-attacker). Alla tidsstämplar är Unix-epoktid i sekunder, inte millisekunder. Signatursegmentet är råbinärt — en HMAC-digest med nyckel eller en asymmetrisk digital signatur. Det är Base64url-kodat precis som de andra segmenten, men dess bytes är inte JSON och har ingen mänskligt läsbar struktur.
I praktiken avkodar du JWT:er i JavaScript av tre vanliga skäl. Först, felsökning: du har en token från ett OAuth-flöde eller en testmiljö och vill bekräfta att claims stämmer med vad auth-servern borde ha utfärdat. Andra, läsning av användar-claims för visningsändamål på klientsidan — att visa den inloggade användarens namn, avatar-URL, eller rollmärke från token-payload utan ett extra API-anrop. Tredje, kontroll av utgångstid innan ett försök till uppdatering: om exp är inom de närmaste 60 sekunderna, utlös en tyst uppdatering innan nästa API-anrop snarare än att vänta på ett 401-svar.
Avkodning kontrollerar inte om token är giltig eller manipulerad. Det är en separat operation kallad verifiering, som kräver HMAC-hemligheten eller RSA/ECDSA offentlig nyckel. Vem som helst kan avkoda en JWT. Endast innehavaren av rätt nyckel kan verifiera en. Denna distinktion snubblar upp många utvecklare, särskilt när man bygger klientsides-auth-flöden där avkodade claims visas men aldrig får litas på för auktoriseringsbeslut utan en verifierad backend-kontroll.
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3JfOTIxZiIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTcxMTYxMDAwMH0.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
// Header
{ "alg": "HS256" }
// Payload
{
"sub": "usr_921f",
"role": "admin",
"iat": 1711610000
}atob() + TextDecoder — Webbläsarens inbyggda JWT-avkodning
Det webbläsarinbyggda flödet för att avkoda en JWT har fyra steg. Först, dela upp token-strängen på "." för att få de tre segmenten. Andra, normalisera base64url-segmentet genom att ersätta - med + och _ med /, sedan fylla ut med = tecken tills längden är en multipel av 4. Tredje, anropa atob() för att avkoda Base64 till en binär sträng. Fjärde, konvertera den binära strängen till korrekt UTF-8 med TextDecoder. Det sista steget spelar roll eftersom atob() returnerar Latin-1. Multi-byte-tecken — emoji, CJK-text, accenterade tecken utanför Latin-1-intervallet — kommer ut som skräp utan steget med JavaScript text decoder.
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 }Utfyllnadssteget är lätt att förbise. JWT:er tar bort avslutande = tecken från sina Base64url-segment eftersom JWT-specifikationen (RFC 7515) definierar base64url utan utfyllnad. Men atob() i vissa webbläsarmotorer kastar ett InvalidCharacterError om inmatningslängden inte är delbar med 4. Defensiv utfyllnad med padEnd() undviker det kantfallet i alla miljöer. Här är en återanvändbar version som avkodar både header och payload till separata objekt:
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"När du väl har dessa två funktioner är det värt att placera dem i en delad verktygsmodul snarare än att kopiera logiken över filer. En src/lib/jwt.ts eller utils/jwt-decode.ts fil med en typad returform gör avsikten tydlig i kodbasen. I TypeScript kan du typa returvärdet som { header: JwtHeader; payload: JwtPayload } där JwtHeader inkluderar alg, typ, och valfritt kid, och JwtPayload utökar RFC 7519:s registrerade claims med en indexsignatur för anpassade claims. Att centralisera avkodningslogiken innebär att när du senare vill lägga till felhantering (fånga felaktigt formade segment) eller telemetri (logga avkodningsfel), har du bara ett ställe att uppdatera.
TextDecoder är det som gör detta flöde säkert för icke-ASCII-claims. Utan det returnerar atob() en Latin-1-sträng där multi-byte UTF-8-sekvenser delas upp över tecken. Du kommer att se skräp istället för emoji eller CJK-text. Skicka alltid genom new TextDecoder("utf-8") efter atob().Avkodning av UTF-8 JWT-claims med multi-byte-tecken
JWT-payloads är UTF-8 JSON kodad som base64url. De flesta payloads innehåller ASCII-bara fält som användar-ID och tidsstämplar, så utvecklare märker aldrig att atob() returnerar Latin-1 istället för UTF-8. Problemet uppstår i samma stund som ett claim innehåller emoji, japanska tecken, kyrilliska, eller någon kodpunkt ovanför U+00FF. Mönstret JavaScript decode UTF-8 kräver att den binära strängen konverteras till en byte-array först, sedan körs den genom TextDecoder.
// Simulerar en JWT-payload med emoji och CJK-tecken
const payloadObj = {
sub: "usr_e821",
display_name: "田中太郎",
team: "Platform 🚀",
region: "ap-northeast-1"
};
// Koda: objekt → JSON → UTF-8-bytes → base64url
const jsonStr = JSON.stringify(payloadObj);
const utf8Bytes = new TextEncoder().encode(jsonStr);
const base64 = btoa(String.fromCharCode(...utf8Bytes))
.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
// Avkoda: base64url → base64 → binär sträng → bytes → UTF-8-sträng
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); // "田中太郎" — korrekt
console.log(result.team); // "Platform 🚀" — korrektDet finns ett äldre reservmönster du kommer att se i äldre kodbaser som använder decodeURIComponent kombinerat med ett procentkodningstrick. Detta JavaScript decodeURIComponent-tillvägagångssätt fungerar eftersom det om-kodar varje byte som ett procenthex-par, sedan decodeURIComponent sätter ihop multi-byte UTF-8-sekvenserna igen:
function decodeBase64UrlLegacy(segment) {
const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
const binary = atob(base64);
// Konvertera varje tecken till %XX hex, sedan sätter decodeURIComponent ihop UTF-8
const utf8 = decodeURIComponent(
binary.split("").map(c =>
"%" + c.charCodeAt(0).toString(16).padStart(2, "0")
).join("")
);
return utf8;
}
// Fungerar för icke-ASCII-claims utan TextDecoder
const payload = decodeBase64UrlLegacy(token.split(".")[1]);
console.log(JSON.parse(payload));decodeURIComponent(escape(atob(segment))) i äldre JWT-verktygskodsnuttar. Funktionen escape() är föråldrad och icke-standard. Ersätt den med TextDecoder-metoden som visas ovan. Mönstret JavaScript unescape decoder har samma problem: unescape() är föråldrad. Båda funktionerna kan tas bort från framtida JavaScript-motorer.JWT-avkodningsflöde — Stepreferens
Varje steg i det webbläsarinbyggda JWT-avkodningsflödet, med det JavaScript-API som används och vad det producerar:
Node.js-motsvarigheten kollapsar steg 2 till 4 i ett enda anrop: Buffer.from(segment, "base64url").toString("utf-8"). Kodningsalternativet "base64url" hanterar alfabetkonverteringen och utfyllnaden internt.
Buffer.from() — Node.js strängavkodare för JWT:er
Node.js har en mycket enklare väg. Klassen Buffer accepterar en "base64url" kodning direkt, så du hoppar över den manuella teckenbytesprocessen och utfyllnadsberäkningen. Det här är vägen för JavaScript string decoder för serversidekod. En rad omvandlar ett JWT-segment till en UTF-8-sträng, och det hanterar multi-byte-tecken korrekt utan några extra steg.
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 }Det här är tillvägagångssättet jag väljer i varje Node.js-projekt. Det är kortare, snabbare och hanterar redan UTF-8 korrekt. Ingen TextDecoder behövs, ingen teckenbyte, ingen utfyllnadsberäkning. Klassen Buffer är en JavaScript-strängavkodare som hanterar base64url-alfabetet inbyggt, vilket eliminerar en hel klass av buggar relaterade till teckenbyte. Om din kod behöver köras i både webbläsaren och Node.js, kolla FAQ:en längst ner för en isomorfisk omslagsfunktion som identifierar miljön vid körning.
Här är ett mer komplett exempel som visar hur man extraherar vanliga JWT-claims och konverterar tidsstämplar till läsbara datum, vilket är mönstret du oftast använder i middleware och API-routehanterare:
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"]
// }I produktions-Node.js-tjänster dyker avkodningsmönstret Buffer.from() upp på tre återkommande ställen. Det första är request-loggnings-middleware: du avkodar den inkommande Authorization headern för att bifoga userId och org till varje strukturerad loggpost utan ett extra nätverksanrop till auth-servern. Det andra är felsökning: du skriver ut avkodade token-claims till konsolen under utveckling för att bekräfta att rätt scopes utfärdades innan du skriver testpåståenden. Det tredje är proaktiv tokenuppdatering i API-gateways. Snarare än att vidarebefordra en token uppströms och låta den nedströms tjänsten returnera ett 401 när token går ut mitt i en begäran, avkodar gatewayen token vid kanten, läser exp claimet, och utlöser en uppdatering om utgångstiden är inom de närmaste 30 sekunderna. Detta eliminerar en klass av övergående auth-fel som är svåra att reproducera och frustrerande att felsöka.
"base64url" lades till i Node.js 15.7.0. Om du är fast på Node.js 14 eller tidigare, fall tillbaka till Buffer.from(segment.replace(/-/g, "+").replace(/_/g, "/"), "base64") som fungerar på samma sätt men kräver den manuella teckenbytesprocessen.Avkoda JWT från en fil och ett API-svar
Två scenarier dyker ständigt upp. Det första är att läsa en JWT från en lokal fil: en sparad token under utveckling, ett testfixtur, eller en fil dumpat under en incident för post-mortem-analys. Det andra är att extrahera en JWT från ett HTTP-svar, typiskt access_token fältet i en OAuth-tokensvarstext eller en Authorization header. Båda behöver felhantering eftersom felaktigt formade tokens, trunkerade filer och nätverksfel är vardagsrealiteter. En token som var giltig förra veckan kan ha efterföljande blanksteg eller radbrytningar från kopiera-klistra in. En svarstext kan vara HTML istället för JSON om auth-servern returnerade en felsida.
Läs JWT från en fil (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);
}Extrahera JWT från ett API-svar (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);
}
// Användning
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);
}JWT-avkodning via kommandoraden
Ibland vill du bara titta på en token från terminalen utan att skriva ett skript. Node.js finns tillgängligt på de flesta utvecklarmaskiner, så en one-liner fungerar bra. jq hanterar snygg utskrift.
# Avkoda JWT-payload med Node.js one-liner
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'))))"
# Skicka till jq för snygg utskrift
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 .
# Avkoda både header och 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()));
});
"Om du föredrar ren bash utan Node.js, skicka segmentet genom base64 -d efter att ha fixat base64url-tecknen med tr:
# Ren bash: avkoda JWT-payload utan Node.js echo "$JWT_TOKEN" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null | jq . # macOS-variant (base64 -D istället för -d) echo "$JWT_TOKEN" | cut -d. -f2 | tr '_-' '/+' | base64 -D 2>/dev/null | jq .
För snabb visuell inspektion utan någon terminal alls, klistra in din token i ToolDeck JWT-avkodaren för en sida-vid-sida-nedbrytning av alla tre segment med färgkodade claim-etiketter och utgångsstatus.
jose — Verifiering och avkodning i ett bibliotek
För produktions-autentiserings-middleware behöver du signaturverifiering, inte bara avkodning. Biblioteket jose är det bästa alternativet. Det fungerar i både Node.js och webbläsare (via Web Crypto API), stöder HS256, RS256, ES256, EdDSA och JWE (krypterade tokens), och har inga inbyggda beroenden. Installera med 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";
// Hämta den offentliga nyckeluppsättningen från identitetsleverantören
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, etc. är nu verifierade
req.userId = payload.sub;
} catch (err) {
return res.status(401).json({ error: "Invalid token" });
}När du väljer mellan jose och det äldre paketet jsonwebtoken är den viktigaste skillnaden körtidsskopet. jsonwebtoken är enbart Node.js — det förlitar sig på den inbyggda modulen crypto och kan inte paketeras för webbläsaren. jose är fullt isomorfiskt: det använder Web Crypto API, som finns tillgängligt i alla moderna webbläsare, Node.js 16+, Deno, Bun och Cloudflare Workers. Om din auth-logik finns i en Next.js middleware-fil (som körs i Edge Runtime), eller i en Cloudflare Worker, eller i ett delat verktyg som importeras av både server- och klientkod, jose är det rätta valet eftersom det inte har några inbyggda beroenden och installeras utan ett byggsteg. jsonwebtoken är fortfarande rimligt för rena Node.js-serverapplikationer där du behöver dess bredare ekosystem av signeringshjälpare och du inte planerar att köra koden i en edge-miljö. I ett greenfield-projekt 2026, standard till jose såvida du inte har ett specifikt skäl att föredra det äldre API:et.
Om du bara behöver avkodning utan verifiering tillhandahåller jose jose.decodeJwt(token) som returnerar payload och jose.decodeProtectedHeader(token) för headern. Dessa är bekvämlighetsfunktioner som gör Base64url-avkodningen internt. Men hela anledningen till att använda jose är att du sällan bör avkoda utan att också verifiera. Om du är på klientsidan och bara behöver visa användaren deras eget visningsnamn eller avatar-URL från token-claims är avkodning utan verifiering okej. På serversidan, verifiera alltid. Jag har sett produktionssystem som avkodade JWT-claims för åtkomstkontrollbeslut utan att kontrollera signaturen, och det är en öppen dörr för varje angripare som förstår JWT-formatet.
import * as jose from "jose";
// Enbart avkodning: ingen hemlighet behövs, ingen verifiering
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"
// Kontrollera utgångstid utan verifiering (visning på klientsidan)
if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
console.log("Token has expired — redirect to login");
}Terminalutdata med syntaxmarkering
När du felsöker JWT-tokens i ett Node.js CLI-verktyg eller under en incident, gör färgkodad utdata en verklig skillnad. Biblioteket chalk parat med JSON.stringify löser problemet. Installera med 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)));
// Markera utgångsstatus
if (payload.exp) {
const expiresAt = new Date(payload.exp * 1000);
const isExpired = expiresAt < new Date();
console.log(
chalk.bold("\nExpires:"),
isExpired
? chalk.red(`EXPIRED at ${expiresAt.toISOString()}`)
: chalk.green(`Valid until ${expiresAt.toISOString()}`)
);
}
console.log(chalk.dim("\nSignature: " + segments[2].substring(0, 20) + "..."));
}
printJwt(process.argv[2]);
// Kör: node jwt-debug.mjs "eyJhbGci..."Bearbetning av JWT:er från stora loggfiler
Modern API-infrastruktur sänder ut strukturerade åtkomstloggar i NDJSON-format — ett JSON-objekt per rad, där varje rad innehåller request-sökvägen, svarsstatus, latens och den avkodade eller råa Authorization headern. I en trafikintensiv tjänst växer dessa filer snabbt: en gateway som hanterar 10 000 förfrågningar per minut producerar över 14 miljoner loggposter per dag. Säkerhets- och efterlevnadsanvändningsfall kräver regelbundet att dessa filer genomsöks i efterhand — identifiering av varje förfrågan gjord av ett komprometterat tjänstekonto (post-incident-analys), bekräftelse att en specifik användares tokens gick ut innan ett dataåtkomstfönster (efterlevnadsgranskning), eller extraktion av den fullständiga uppsättningen ämnen som åtkommit en känslig slutpunkt under ett underhållsfönster. Eftersom en enstaka loggfil kan överstiga flera gigabyte är det inte möjligt att ladda den i minnet med readFileSync. Node.js readline-strömmar bearbetar filen en rad i taget med konstant minnesutnyttjande, vilket gör det praktiskt att söka igenom godtyckligt stora loggar på en standard utvecklarlaptop.
Du kommer inte stöta på problemet med "fil för stor för minnet" med enskilda JWT:er, eftersom en enstaka token sällan är mer än några kilobyte. Scenariot som faktiskt uppstår är att söka igenom en stor åtkomstlogg eller granskningsspår efter JWT-tokens, avkoda var och en, och extrahera specifika claims. Node.js-strömmar hanterar detta utan att ladda hela filen.
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 {
// Hoppa över felaktigt formade rader
}
}
console.log(`\nScanned ${lineCount} lines, found ${expiredCount} expired tokens`);
}
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 {
// Inte en giltig JWT
}
}
}
console.log(`Found ${subjects.size} unique subjects:`);
for (const sub of subjects) console.log(` ${sub}`);
}
extractUniqueSubjects("./logs/gateway-2026-03.log");readFileSync kommer att blockera minnet och utlösa GC-pauser. Metoden med readline bearbetar en rad i taget med konstant minnesutnyttjande.Vanliga misstag
Problem: atob() returnerar en Latin-1-sträng. Multi-byte UTF-8-tecken (emoji, CJK, accenterade tecken) delas upp över tecken och kommer ut som skräp.
Lösning: Konvertera atob()-utdata till en Uint8Array, skicka sedan den genom new TextDecoder('utf-8').
// Kraschar på icke-ASCII payload-claims
const payload = JSON.parse(atob(token.split(".")[1]));
// display_name visas som "ç°ä¸å¤ªé\x83\x8E" istället för "田中太郎"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 visas korrekt som "田中太郎"Problem: atob() kastar "InvalidCharacterError" eftersom base64url använder - och _ istället för + och /.
Lösning: Ersätt - med + och _ med / innan du anropar atob(). Node.js Buffer.from() med 'base64url' hanterar detta automatiskt.
// Kastar: 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); // fungerar nuProblem: Vem som helst kan skapa en JWT med valfri payload. Avkodning läser bara data — det bevisar inte att token utfärdades av din auth-server.
Lösning: På serversidan, verifiera alltid signaturen med jose.jwtVerify() eller jsonwebtoken.verify(). Enbart avkodning är acceptabelt för klientsidesvisning av användar-claims.
// FARLIGT: avkodad men inte verifierad
const claims = JSON.parse(atob(token.split(".")[1]));
if (claims.role === "admin") {
grantAdminAccess(); // angripare kan förfalska detta
}import * as jose from "jose";
const { payload } = await jose.jwtVerify(token, secretKey);
if (payload.role === "admin") {
grantAdminAccess(); // säkert — signaturen är verifierad
}Problem: JWT exp är i sekunder sedan epoch, men Date.now() returnerar millisekunder. Jämförelsen kommer alltid att säga att token är giltig eftersom millisekundstidsstämpeln är 1000 gånger större.
Lösning: Dividera Date.now() med 1000 och avrunda nedåt innan du jämför med exp.
// Bugg: Date.now() är millisekunder, exp är sekunder
if (payload.exp > Date.now()) {
console.log("Token is valid"); // alltid sant — fel!
}const nowSeconds = Math.floor(Date.now() / 1000);
if (payload.exp > nowSeconds) {
console.log("Token is valid"); // korrekt jämförelse
}JWT-avkodningsmetoder — Snabb jämförelse
Använd atob() + TextDecoder för avkodning på webbläsarsidan när du bara behöver visa claims för användaren. Använd Buffer.from() i Node.js-skript och CLI-verktyg. Välj jose i samma stund som du behöver verifiera en signatur, vilket är all serversides-auth-middleware. Paketet jwt-decode är ett lättviktigt alternativ om du vill ha ett enkelt funktions-API för enbart avkodning i webbläsaren. För snabb visuell inspektion utan att skriva kod, klistra in din token i JWT Decoder-verktyget.
Vanliga frågor
Hur avkodar jag en JWT-token i JavaScript utan ett bibliotek?
Dela upp token på ".", ta det andra segmentet (payload), normalisera base64url-kodningen genom att ersätta - med + och _ med /, fyll ut med =-tecken och anropa sedan atob() följt av TextDecoder för att få UTF-8 JSON-strängen. Skicka resultatet genom JSON.parse() så har du claims-objektet. Inget npm-paket behövs. Detta fungerar i alla moderna webbläsare och i Node.js 18+. Om du även behöver läsa headern, tillämpa samma avkodningssteg på det första segmentet. Tänk på att detta ger dig rådata utan signaturverifiering — behandla resultatet som visningsdata såvida du inte verifierar signaturen på serversidan.
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" }Vad är skillnaden mellan atob() och Buffer.from() för JWT-avkodning?
atob() är ett webbläsar-API som avkodar standard Base64 till en Latin-1 binär sträng. Det förstår inte base64url-kodning direkt, så du måste ersätta - och _ tecken först. Buffer.from(segment, "base64url") är ett Node.js-API som hanterar base64url-alfabetet inbyggt och returnerar en Buffer som du kan anropa .toString("utf-8") på. Använd atob() i webbläsaren, Buffer.from() i Node.js. Ett tredje alternativ — som är långsammare men historiskt vanligt — är decodeURIComponent procentkodningstricket, men det mönstret förlitar sig på den föråldrade escape()-funktionen i vissa äldre kodsnuttar och bör undvikas i ny kod. För isomorfisk kod som körs i båda miljöer, kontrollera typeof Buffer !== "undefined" och förgrena därefter.
// Webbläsare
const json = atob(payload.replace(/-/g, "+").replace(/_/g, "/"));
// Node.js
const json2 = Buffer.from(payload, "base64url").toString("utf-8");Varför kraschar atob() på JWT-claims med icke-ASCII-tecken?
atob() returnerar en Latin-1-sträng där varje tecken motsvarar en enda byte. Multi-byte UTF-8-sekvenser (emoji, CJK-tecken, accenterade bokstäver utanför Latin-1) delas upp över flera tecken och producerar skräptext. Lösningen är att konvertera den binära strängen till en Uint8Array först och sedan skicka den arrayen till new TextDecoder("utf-8").decode(). TextDecoder-API:et sätter ihop multi-byte-sekvenser korrekt. Det här problemet är lätt att missa under utveckling eftersom de flesta JWT-payloads endast innehåller ASCII-användar-ID:n, tidsstämplar och rollnamn — felet uppstår först när en claim innehåller ett icke-ASCII-visningsnamn eller en lokaliserad sträng. Använd alltid TextDecoder-vägen i ny kod även när dina nuvarande payloads endast är ASCII, eftersom claims kan förändras allteftersom applikationen utvecklas.
// Trasig: atob returnerar Latin-1, multi-byte-tecken blir skräp
const broken = atob(base64); // "ð\x9F\x8E\x89" istället för emoji
// Fixad: konvertera till byte-array, sedan TextDecoder
const bytes = Uint8Array.from(atob(base64), c => c.charCodeAt(0));
const fixed = new TextDecoder("utf-8").decode(bytes);Kan jag verifiera en JWT-signatur i JavaScript?
Avkodning och verifiering är olika operationer. Avkodning läser bara payload, som inte är krypterad. Verifiering kontrollerar signaturen mot en hemlighet (HMAC) eller offentlig nyckel (RSA/ECDSA). Biblioteket jose stöder båda i webbläsaren via Web Crypto API och i Node.js. Paketet jsonwebtoken fungerar enbart i Node.js. Lita aldrig på avkodade claims utan att verifiera signaturen på serversidan. På klientsidan är det acceptabelt att avkoda en JWT för att läsa användarens visningsnamn eller utgångstid, men alla åtkomstkontrollbeslut — att kontrollera om en användare har en viss roll eller behörighet — måste ske i serversidekod efter verifiering. En angripare som förstår JWT-formatet kan skapa en token med godtyckliga claims och din klientsidekontroll kommer att godkännas.
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); // verifierade claimsHur kontrollerar jag om en JWT har gått ut i JavaScript?
Avkoda payload och läs exp-claimet, vilket är en Unix-tidsstämpel i sekunder. Jämför det med aktuell tid med Math.floor(Date.now() / 1000). Om aktuell tid är större än exp har token gått ut. Kom ihåg: exp-värdet är sekunder sedan epoch, inte millisekunder, så att dividera Date.now() med 1000 är nödvändigt. I praktiken, bygg in en liten klocktoleransbuffert — att kontrollera om token går ut inom de närmaste 30 sekunderna snarare än strikt i det förflutna förhindrar kantfall där token fortfarande är tekniskt giltig när du avkodar den men går ut vid den tidpunkt då nästa nedströms API-anrop bearbetar den. Hantera även fallet där exp saknas helt, vilket innebär att token aldrig går ut.
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 falseHur skriver jag isomorfisk JWT-avkodningskod som fungerar i både Node.js och webbläsaren?
Kontrollera förekomsten av globalThis.Buffer. Om den finns är du i Node.js och kan använda Buffer.from(segment, "base64url").toString("utf-8"). Om den inte finns är du i en webbläsare och bör använda atob() med TextDecoder-metoden. Omslut denna kontroll i en enda decodeBase64Url-funktion och använd den överallt. Detta är viktigast för verktygspaket, designsystemkomponenter och all delad kod som lever i ett monorepo-paket som importeras av både en Next.js-serverkomponent och en webbläsar-React-komponent. Att hålla miljödetektionen på ett ställe innebär att du bara behöver uppdatera det på ett ställe om körtiden ändras — till exempel när Deno lägger till fullt Buffer-stöd eller en ny edge-körtid kräver en annan kodsökväg.
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);
}Relaterade verktyg
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.