JWT Decoder JavaScript — atob(), TextDecoder a jose

·JavaScript Performance Engineer·ZkontrolovánoSophie Laurent·Publikováno

Používejte bezplatný JWT Decoder přímo v prohlížeči — bez instalace.

Vyzkoušet JWT Decoder online →

Každý autentizační tok, který jsem postavil, dospěje nakonec do stejného bodu: máte JWT sedící v cookie, hlavičce nebo OAuth callback URL a potřebujete přečíst, co je uvnitř. JWT dekodér v JavaScriptu nevyžaduje žádný npm balíček. Hlavička a payload tokenu jsou jen Base64url-kódovaný JSON a prohlížeč i Node.js mají vše potřebné k jejich dekódování. Tento průvodce pokrývá celý pipeline JavaScriptového textového dekodéru pro JWT: rozdělení tokenu, normalizaci base64url na standardní Base64, atob() a TextDecoder pro správné zpracování UTF-8, Node.js Buffer.from(), ověření podpisu pomocí jose, a běžné chyby, na které vývojáři narážejí každý den. Pro rychlou jednorázovou inspekci vyzkoušejte online JWT Dekodér místo toho. Všechny příklady cílí na ES2020+ a Node.js 18+.

  • Rozdělte JWT na "." — index 0 je hlavička, index 1 je payload, index 2 je podpis.
  • atob() dekóduje Base64, ale vrací Latin-1, ne UTF-8. Použijte TextDecoder nebo Buffer.from() pro non-ASCII claims.
  • Buffer.from(segment, "base64url") zpracovává base64url nativně v Node.js — není potřeba ruční nahrazení znaků.
  • Dekódování NENÍ ověření. Nikdy nedůvěřujte claims z dekódovaného JWT bez kontroly podpisu na straně serveru.
  • Knihovna jose dělá oboje: ověřuje podpisy HS256/RS256/ES256 a vrací dekódovaný payload v jednom volání.

Co je JWT dekódování?

JSON Web Token se skládá ze tří Base64url-kódovaných segmentů oddělených tečkami. První segment je hlavička, druhý je payload (claims, na kterých vám záleží), a třetí je kryptografický podpis. Hlavička je malý JSON objekt, který popisuje samotný token. Jeho nejdůležitější pole je alg — podpisový algoritmus (např. HS256, RS256, ES256). Pole typ je téměř vždy "JWT", a volitelné pole kid identifikuje, který klíč byl použit k podpisu tokenu — klíčové, když poskytovatel identity rotuje klíče a zveřejňuje JWKS endpoint s více veřejnými klíči.

Payload nese claims. RFC 7519 definuje sedm registrovaných názvů claims: sub (subject — obvykle ID uživatele), iss (issuer — URL autentizačního serveru), aud (audience — API, pro které je token určen), iat (časové razítko vydání), exp (časové razítko vypršení), nbf (časové razítko not-before), a jti (JWT ID — slouží k prevenci replay útoků). Všechna časová razítka jsou sekundy Unix epochy, ne milisekundy. Segment podpisu je surový binární — keyed HMAC digest nebo asymetrický digitální podpis. Je Base64url-kódován jako ostatní segmenty, ale jeho bajty nejsou JSON a nemají žádnou čitelnou strukturu.

V praxi dekódujete JWT v JavaScriptu ze tří běžných důvodů. Za prvé, ladění: máte token z OAuth flow nebo testovacího prostředí a chcete potvrdit, že claims odpovídají tomu, co měl autentizační server vydat. Za druhé, čtení uživatelských claims pro účely zobrazení na straně klienta — zobrazení jména přihlášeného uživatele, URL avataru nebo odznaku role z payload tokenu bez dalšího API volání. Za třetí, kontrola vypršení před pokusem o obnovení: pokud je exp v příštích 60 sekundách, spusťte tiché obnovení před dalším API voláním, místo čekání na odpověď 401.

Dekódování nekontroluje, zda je token platný nebo byl pozměněn. To je samostatná operace zvaná ověření, která vyžaduje HMAC tajný klíč nebo RSA/ECDSA veřejný klíč. Kdokoli může dekódovat JWT. Pouze držitel správného klíče ho může ověřit. Tento rozdíl přivádí do problémů mnoho vývojářů, zejména při budování autentizačních toků na straně klienta, kde jsou dekódované claims zobrazovány, ale nikdy nesmí být důvěryhodné pro rozhodnutí o autorizaci bez ověřeného backendového ověření.

Before · json
After · json
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3JfOTIxZiIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTcxMTYxMDAwMH0.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
// Hlavička
{ "alg": "HS256" }

// Payload
{
  "sub": "usr_921f",
  "role": "admin",
  "iat": 1711610000
}

atob() + TextDecoder — Nativní dekódování JWT v prohlížeči

Nativní pipeline prohlížeče pro dekódování JWT má čtyři kroky. Za prvé, rozdělte řetězec tokenu na "." pro získání tří segmentů. Za druhé, normalizujte segment base64url nahrazením - za + a _ za /, pak doplněním = znaky, dokud délka není násobkem 4. Za třetí, zavolejte atob() pro dekódování Base64 do binárního řetězce. Za čtvrté, převeďte binární řetězec na správné UTF-8 pomocí TextDecoder. Tento poslední krok je důležitý, protože atob() vrací Latin-1. Vícebajtové znaky — emoji, text CJK, znaky s diakritikou nad rámec Latin-1 — bez kroku JavaScriptového textového dekodéru vychází poškozené.

JavaScript — minimální dekódování JWT
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 }

Krok doplnění je snadno přehlédnutelný. JWT odstraňuje koncové = znaky ze svých Base64url segmentů, protože specifikace JWT (RFC 7515) definuje base64url bez doplnění. Ale atob() v některých prohlížečích vyvolá InvalidCharacterError, pokud délka vstupu není dělitelná 4. Defenzivní doplnění pomocí padEnd() se vyhýbá tomuto hraničnímu případu ve všech prostředích. Zde je znovupoužitelná verze, která dekóduje hlavičku i payload do samostatných objektů:

JavaScript — dekódování hlavičky a payloadu
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("Algoritmus:", header.alg);   // "HS256"
console.log("Subject:", payload.sub);     // "usr_921f"
console.log("Role:", payload.role);       // "admin"

Jakmile máte tyto dvě funkce, vyplatí se je umístit do sdíleného utility modulu místo kopírování logiky napříč soubory. Soubor src/lib/jwt.ts nebo utils/jwt-decode.ts s typovaným návratovým tvarem dělá záměr napříč kódbází explicitním. V TypeScriptu můžete typovat návrat jako { header: JwtHeader; payload: JwtPayload }, kde JwtHeader zahrnuje alg, typ a volitelné kid, a JwtPayload rozšiřuje registrované claims RFC 7519 o index signaturu pro vlastní claims. Centralizace logiky dekódování znamená, že když budete chtít přidat zpracování chyb (zachycení malformovaných segmentů) nebo telemetrii (logování selhání dekódování), stačí aktualizovat jen jedno místo.

Poznámka:Krok TextDecoder je to, co dělá tento pipeline bezpečným pro non-ASCII claims. Bez něj atob() vrací Latin-1 řetězec, kde jsou vícebajtové UTF-8 sekvence rozděleny napříč znaky. Uvidíte nesmysly místo emoji nebo textu CJK. Vždy vedejte přes new TextDecoder("utf-8") po atob().

Dekódování UTF-8 JWT claims s vícebajtovými znaky

JWT payloady jsou UTF-8 JSON kódovaný jako base64url. Většina payloadů obsahuje pouze ASCII pole jako uživatelská ID a časová razítka, takže vývojáři nikdy nezpozorují, že atob() vrací Latin-1 místo UTF-8. Problém se projeví v okamžiku, kdy claim obsahuje emoji, japonské znaky, cyrilici nebo jakýkoli kódový bod nad U+00FF. Vzor JavaScriptového dekódování UTF-8 vyžaduje nejprve převést binární řetězec na pole bajtů, pak ho projít přes TextDecoder.

JavaScript — UTF-8 round-trip s emoji v JWT payloadu
// Simulace JWT payloadu s emoji a znaky CJK
const payloadObj = {
  sub: "usr_e821",
  display_name: "田中太郎",  // demonstrace vícebajtového kódování
  team: "Platform 🚀",
  region: "ap-northeast-1"
};

// Kódování: objekt → JSON → UTF-8 bajty → base64url
const jsonStr = JSON.stringify(payloadObj);
const utf8Bytes = new TextEncoder().encode(jsonStr);
const base64 = btoa(String.fromCharCode(...utf8Bytes))
  .replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");

// Dekódování: base64url → base64 → binární řetězec → bajty → UTF-8 řetězec
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); // "田中太郎" — správně
console.log(result.team);         // "Platform 🚀" — správně

Existuje starší záložní vzor, který uvidíte ve starších kódbázích, který používá decodeURIComponent kombinovaný s trikem percent-kódování. Tento přístup JavaScriptového decodeURIComponent funguje, protože znovu zakóduje každý bajt jako percent-hex pár, pak decodeURIComponent znovu sestaví vícebajtové UTF-8 sekvence:

JavaScript — záložní decodeURIComponent pro UTF-8
function decodeBase64UrlLegacy(segment) {
  const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
  const binary = atob(base64);
  // Převede každý znak na %XX hex, pak decodeURIComponent sestaví UTF-8
  const utf8 = decodeURIComponent(
    binary.split("").map(c =>
      "%" + c.charCodeAt(0).toString(16).padStart(2, "0")
    ).join("")
  );
  return utf8;
}

// Funguje pro non-ASCII claims bez TextDecoder
const payload = decodeBase64UrlLegacy(token.split(".")[1]);
console.log(JSON.parse(payload));
Varování:Ve starších utility snippetech JWT můžete narazit na starší vzor decodeURIComponent(escape(atob(segment))). Funkce escape() je zastaralá a nestandardní. Nahraďte ji přístupem s TextDecoder uvedeným výše. Vzor JavaScriptového unescape dekodéru má stejný problém: unescape() je zastaralá. Obě funkce mohou být odstraněny z budoucích JavaScriptových enginů.

Pipeline dekódování JWT — přehled kroků

Každý krok v nativní pipeline dekódování JWT v prohlížeči, s použitým JavaScriptovým API a co produkuje:

Parametr / Krok
Typ
Popis
token.split(".")
string[]
Rozdělí JWT na segmenty [hlavička, payload, podpis]
base64url → base64
nahrazení řetězce
Nahradí - za +, _ za /, doplní = na násobek 4
atob(base64)
string
Dekóduje standardní Base64 řetězec do binárního řetězce (Latin-1)
TextDecoder("utf-8")
TextDecoder
Převede Uint8Array surových bajtů na správný UTF-8 řetězec
JSON.parse()
object
Parsuje výsledný JSON řetězec do JavaScriptového objektu

Node.js ekvivalent sjednotí kroky 2 až 4 do jediného volání: Buffer.from(segment, "base64url").toString("utf-8"). Možnost kódování "base64url" zpracovává konverzi abecedy a doplnění interně.

Buffer.from() — Node.js string dekodér pro JWT

Node.js má mnohem jednodušší cestu. Třída Buffer přijímá kódování "base64url" přímo, takže přeskočíte ruční nahrazení znaků a doplnění. Toto je cesta JavaScriptového string dekodéru pro kód na straně serveru. Jeden řádek přemění segment JWT na UTF-8 řetězec a zpracovává vícebajtové znaky správně bez jakýchkoli dalších kroků.

Node.js 18+ — dekódování JWT s Buffer
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 }

To je přístup, po kterém sáhnu v každém Node.js projektu. Je kratší, rychlejší a správně zpracovává UTF-8. Není potřeba TextDecoder, žádné nahrazování znaků, žádná matematika doplnění. Třída Buffer je JavaScriptový string dekodér, který zpracovává abecedu base64url nativně, čímž eliminuje celou třídu chyb souvisejících se substitucí znaků. Pokud váš kód potřebuje běžet v prohlížeči i Node.js, viz FAQ níže pro isomorfní obalovací funkci, která detekuje prostředí za běhu.

Zde je úplnější příklad ukazující, jak extrahovat běžné JWT claims a převádět časová razítka na čitelné datum, což je vzor, který budete nejčastěji používat v middleware a obslužných rutinách API routes:

Node.js — praktická extrakce JWT claims
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"]
// }

V produkčních Node.js službách se vzor dekódování Buffer.from() vyskytuje na třech opakujících se místech. Prvním je middleware pro logování požadavků: dekódujete příchozí hlavičku Authorization pro přiložení userId a org ke každé strukturované položce logu bez dalšího síťového round-tripu na autentizační server. Druhým je ladění: vypisujete dekódované claims tokenu do konzole při vývoji pro potvrzení, že byly vydány správné scopy před psaním testovacích asercí. Třetím je proaktivní obnovení tokenu v API gateway. Místo přeposílání tokenu upstream a nechání downstream služby vrátit 401, když token vyprší uprostřed požadavku, gateway dekóduje token na okraji, přečte claim exp a spustí obnovení, pokud vypršení je do 30 sekund. Tím se eliminuje třída přechodných selhání autentizace, která jsou obtížně reprodukovatelná a frustrující při ladění.

Poznámka:Kódování "base64url" bylo přidáno v Node.js 15.7.0. Pokud jste uvázáni na Node.js 14 nebo starší, použijte záložní Buffer.from(segment.replace(/-/g, "+").replace(/_/g, "/"), "base64"), které funguje stejně, ale vyžaduje ruční záměnu znaků.

Dekódování JWT ze souboru a API odpovědi

Neustále se vyskytují dva scénáře. Prvním je čtení JWT z lokálního souboru: uložený token při vývoji, testovací fixture nebo soubor vyexportovaný při incidentu pro post-mortem analýzu. Druhým je extrakce JWT z HTTP odpovědi, typicky pole access_token v těle odpovědi OAuth token nebo hlavička Authorization. Oba vyžadují zpracování chyb, protože malformované tokeny, zkrácené soubory a síťové chyby jsou každodenní realitou. Token, který byl minulý týden platný, může mít trailing whitespace nebo nové řádky z kopírování. Tělo odpovědi může být HTML místo JSON, pokud autentizační server vrátil chybovou stránku.

Čtení JWT ze souboru (Node.js)

Node.js — dekódování JWT ze souboru se zpracováním chyb
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);
}

Extrakce JWT z API odpovědi (fetch)

JavaScript — dekódování JWT z API odpovědi
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);
}

// Použití
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);
}

Dekódování JWT z příkazové řádky

Někdy chcete jen nahlédnout do tokenu z terminálu bez psaní skriptu. Node.js je dostupný na většině vývojářských strojů, takže jednořádkový příkaz funguje dobře. jq zajišťuje hezké formátování.

bash — dekódování JWT payload z terminálu
# Dekódování JWT payload pomocí 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'))))"

# Roura do jq pro hezký výstup
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 .

# Dekódování hlavičky i payloadu
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()));
  });
"

Pokud dáváte přednost čistému bash bez Node.js, přesměrujte segment přes base64 -d po opravě base64url znaků pomocí tr:

bash — čisté bash dekódování JWT bez Node.js
# Čisté bash: dekódování JWT payload bez Node.js
echo "$JWT_TOKEN" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null | jq .

# Varianta pro macOS (base64 -D místo -d)
echo "$JWT_TOKEN" | cut -d. -f2 | tr '_-' '/+' | base64 -D 2>/dev/null | jq .

Pro rychlou vizuální inspekci zcela bez terminálu vložte token do JWT Dekodéru ToolDeck pro přehledné zobrazení všech tří segmentů s barevně odlišenými claim popisky a stavem vypršení platnosti.

jose — Ověření i dekódování v jedné knihovně

Pro produkční autentizační middleware potřebujete ověření podpisu, nejen dekódování. Knihovna jose je nejlepší volba. Funguje v Node.js i prohlížečích (přes Web Crypto API), podporuje HS256, RS256, ES256, EdDSA a JWE (šifrované tokeny) a nemá žádné nativní závislosti. Nainstalujte pomocí npm install jose.

JavaScript — jose: ověření HS256 tokenu
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);
  }
}
JavaScript — jose: ověření RS256 s JWKS endpointem
import * as jose from "jose";

// Načtení sady veřejných klíčů od poskytovatele identity
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 atd. jsou nyní ověřeny
  req.userId = payload.sub;
} catch (err) {
  return res.status(401).json({ error: "Invalid token" });
}

Při rozhodování mezi jose a starším balíčkem jsonwebtoken je klíčovým rozdílem rozsah runtime. jsonwebtoken je pouze pro Node.js — spoléhá se na vestavěný modul crypto a nebude se bundlovat pro prohlížeč. jose je plně isomorfní: používá Web Crypto API, které je dostupné ve všech moderních prohlížečích, Node.js 16+, Deno, Bun a Cloudflare Workers. Pokud vaše autentizační logika žije v souboru Next.js middleware (který běží v Edge Runtime), v Cloudflare Worker nebo ve sdílené utility importované serverovým i klientským kódem, jose je správná volba, protože nemá žádné nativní závislosti a instaluje se bez build kroku. jsonwebtoken zůstává rozumnou volbou pro čistě Node.js serverové aplikace, kde potřebujete jeho širší ekosystém pomocných funkcí pro podepisování a neplánujete spouštět kód v edge prostředí. V novém projektu v roce 2026 defaultně volte jose, pokud nemáte konkrétní důvod preferovat starší API.

Pokud potřebujete pouze dekódování bez ověření, jose poskytuje jose.decodeJwt(token), který vrací payload, a jose.decodeProtectedHeader(token) pro hlavičku. Jsou to pomocné funkce, které provádějí Base64url dekódování interně. Ale celý důvod pro sáhnutí po jose je, že byste zřídka měli dekódovat bez ověřování. Pokud jste na straně klienta a potřebujete jen ukázat uživateli jeho vlastní zobrazované jméno nebo URL avataru z claims tokenu, dekódování bez ověření je v pořádku. Na straně serveru vždy ověřujte. Viděl jsem produkční systémy, které dekódovaly JWT claims pro rozhodnutí o řízení přístupu bez kontroly podpisu, a to je otevřené dveře pro každého útočníka, který rozumí formátu JWT.

JavaScript — jose.decodeJwt pro scénáře pouze s dekódováním
import * as jose from "jose";

// Pouze dekódování: není potřeba tajný klíč, bez ověření
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"

// Kontrola vypršení bez ověření (zobrazení na straně klienta)
if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
  console.log("Token has expired — redirect to login");
}

Výstup v terminálu se zvýrazněním syntaxe

Při ladění JWT tokenů v Node.js CLI nástroji nebo při incidentu barevně odlišený výstup skutečně pomáhá. Knihovna chalk v kombinaci s JSON.stringify odvede práci. Nainstalujte pomocí npm install chalk.

Node.js — barevný výstup dekódování JWT
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)));

  // Zvýraznění stavu vypršení
  if (payload.exp) {
    const expiresAt = new Date(payload.exp * 1000);
    const isExpired = expiresAt < new Date();
    console.log(
      chalk.bold("\nVyprší:"),
      isExpired
        ? chalk.red(`VYPRŠELO dne ${expiresAt.toISOString()}`)
        : chalk.green(`Platné do ${expiresAt.toISOString()}`)
    );
  }

  console.log(chalk.dim("\nPodpis: " + segments[2].substring(0, 20) + "..."));
}

printJwt(process.argv[2]);
// Spuštění: node jwt-debug.mjs "eyJhbGci..."
Poznámka:Barevný výstup je pouze pro terminál. Nepoužívejte chalk při zápisu JWT claims do log souborů, API odpovědí nebo databázových polí. ANSI escape kódy se budou zobrazovat jako nesmysly v mimoterminálovém kontextu.

Zpracování JWT z velkých log souborů

Moderní API infrastruktura emituje strukturované přístupové logy ve formátu NDJSON — jeden JSON objekt na řádek, kde každý řádek obsahuje cestu požadavku, status odpovědi, latenci a dekódovanou nebo surovou hlavičku Authorization. V zaneprázdněné službě tyto soubory rychle rostou: gateway zpracovávající 10 000 požadavků za minutu produkuje přes 14 milionů log záznamů denně. Bezpečnostní a compliance případy použití pravidelně vyžadují prohledávání těchto souborů zpětně — identifikace každého požadavku provedeného kompromitovaným servisním účtem (post-incidentní analýza), potvrzení, že tokeny konkrétního uživatele vypršely před oknem přístupu k datům (compliance audit), nebo extrakce úplné sady subjects, kteří přistupovali k citlivému endpointu během servisního okna. Protože jeden log soubor může přesáhnout několik gigabajtů, načtení do paměti pomocí readFileSync není proveditelné. Node.js readline streamy zpracovávají soubor po jednom řádku s konstantní paměťovou spotřebou, takže je praktické prohledávat libovolně velké logy na standardním vývojářském laptopu.

Na problém "soubor příliš velký pro paměť" nenarazíte u jednotlivých JWT, protože jeden token je zřídkakdy větší než pár kilobajtů. Scénář, který se vyskytuje, je prohledávání velkého přístupového logu nebo auditní stopy pro JWT tokeny, dekódování každého a extrakce konkrétních claims. Node.js streamy to zvládají bez načtení celého souboru.

Node.js — stream NDJSON logu a dekódování vložených JWT
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 {
      // Přeskočit malformované řádky
    }
  }

  console.log(`\nNaskenováno ${lineCount} řádků, nalezeno ${expiredCount} vypršených tokenů`);
}

scanLogsForExpiredTokens("./logs/api-access-2026-03.ndjson");
Node.js — extrakce unikátních JWT subjects ze stream logu
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 {
        // Není platný JWT
      }
    }
  }

  console.log(`Nalezeno ${subjects.size} unikátních subjects:`);
  for (const sub of subjects) console.log(`  ${sub}`);
}

extractUniqueSubjects("./logs/gateway-2026-03.log");
Poznámka:Přepněte na streaming, když log soubor překročí 50 MB. Načtení 500 MB NDJSON souboru pomocí readFileSync zablokuje paměť a spustí GC pauzy. Přístup s readline zpracovává jeden řádek najednou s konstantní paměťovou spotřebou.

Běžné chyby

Použití atob() bez TextDecoder pro non-ASCII claims

Problém: atob() vrací Latin-1 řetězec. Vícebajtové UTF-8 znaky (emoji, CJK, znaky s diakritikou) jsou rozděleny napříč znaky a vychází poškozené.

Řešení: Převeďte výstup atob() na Uint8Array, pak ho předejte přes new TextDecoder('utf-8').

Before · JavaScript
After · JavaScript
// Selže na non-ASCII payload claims
const payload = JSON.parse(atob(token.split(".")[1]));
// display_name appears as "ç°ä¸­å¤ªé\x83\x8E" instead of "田中太郎"
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 correctly shows "田中太郎"
Zapomenutí nahrazení znaků base64url na base64

Problém: atob() vyvolá "InvalidCharacterError", protože base64url používá - a _ místo + a /.

Řešení: Nahraďte - za + a _ za / před voláním atob(). Node.js Buffer.from() s 'base64url' to řeší automaticky.

Before · JavaScript
After · JavaScript
// Vyvolá: 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); // nyní funguje
Důvěřování dekódovaným JWT claims bez ověření podpisu

Problém: Kdokoli může vytvořit JWT s libovolným payloadem. Dekódování pouze čte data — nedokazuje, že token vydal váš autentizační server.

Řešení: Na straně serveru vždy ověřujte podpis pomocí jose.jwtVerify() nebo jsonwebtoken.verify(). Dekódování bez ověření je přijatelné pro zobrazení uživatelských claims na straně klienta.

Before · JavaScript
After · JavaScript
// NEBEZPEČNÉ: dekódováno, ale neověřeno
const claims = JSON.parse(atob(token.split(".")[1]));
if (claims.role === "admin") {
  grantAdminAccess(); // útočník může toto zfalšovat
}
import * as jose from "jose";
const { payload } = await jose.jwtVerify(token, secretKey);
if (payload.role === "admin") {
  grantAdminAccess(); // bezpečné — podpis je ověřen
}
Porovnávání exp s Date.now() bez dělení 1000

Problém: JWT exp je v sekundách od epochy, ale Date.now() vrací milisekundy. Porovnání bude vždy říkat, že token je platný, protože časové razítko v milisekundách je 1000x větší.

Řešení: Vydělte Date.now() číslem 1000 a zaokrouhlete dolů před porovnáním s exp.

Before · JavaScript
After · JavaScript
// Chyba: Date.now() je v milisekundách, exp je v sekundách
if (payload.exp > Date.now()) {
  console.log("Token is valid"); // vždy true — špatně!
}
const nowSeconds = Math.floor(Date.now() / 1000);
if (payload.exp > nowSeconds) {
  console.log("Token is valid"); // správné porovnání
}

Metody dekódování JWT — rychlé srovnání

Metoda
Prostředí
UTF-8 bezpečné
Ověření podpisu
Vlastní typy
Vyžaduje instalaci
atob() + TextDecoder
Prohlížeč
N/A (jen čtení)
Ne
Buffer.from()
Node.js
N/A (jen čtení)
Ne
decodeURIComponent()
Prohlížeč (starší)
N/A (jen čtení)
Ne
jose
Oboje
✓ (JWS/JWE)
npm install
jsonwebtoken
Node.js
npm install
jwt-decode
Oboje
N/A
npm install

Použijte atob() + TextDecoder pro dekódování na straně prohlížeče, když jen potřebujete zobrazit claims uživateli. Použijte Buffer.from() v Node.js skriptech a CLI nástrojích. Sáhněte po jose v okamžiku, kdy potřebujete ověřit podpis, což je jakýkoli serverový auth middleware. Balíček jwt-decode je lehká alternativa, pokud chcete API s jednou funkcí pro dekódování bez ověření v prohlížeči. Pro rychlou vizuální inspekci bez psaní kódu vložte token do nástroje JWT Decoder.

Často kladené otázky

Jak dekódovat JWT token v JavaScriptu bez knihovny?

Rozdělte token na ".", vezměte druhý segment (payload), normalizujte base64url kódování nahrazením - za + a _ za /, doplňte znaky =, poté zavolejte atob() následované TextDecoderem pro získání UTF-8 JSON řetězce. Výsledek předejte přes JSON.parse() a máte objekt claims. Žádný npm balíček není potřeba. Tento přístup funguje ve všech moderních prohlížečích a v Node.js 18+. Pokud potřebujete přečíst i hlavičku, použijte stejné kroky dekódování na první segment. Mějte na paměti, že tímto získáte surová data bez ověření podpisu — výsledek považujte jen pro zobrazení, pokud neověřujete podpis na straně serveru.

JavaScript
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" }

Jaký je rozdíl mezi atob() a Buffer.from() pro dekódování JWT?

atob() je prohlížečové API, které dekóduje standardní Base64 do binárního řetězce Latin-1. Nerozumí přímo base64url kódování, takže musíte nejprve nahradit znaky - a _. Buffer.from(segment, "base64url") je Node.js API, které zpracovává base64url abecedu nativně a vrací Buffer, na který lze zavolat .toString("utf-8"). Použijte atob() v prohlížeči, Buffer.from() v Node.js. Třetí možností — která je pomalejší, ale historicky rozšířená — je trik s percent-kódováním decodeURIComponent, ale tento vzor se v některých starších ukázkách spoléhá na zastaralou funkci escape() a měl by se v novém kódu vyvarovat. Pro isomorfní kód, který běží v obou prostředích, zkontrolujte typeof Buffer !== "undefined" a podle toho větvete.

JavaScript
// Prohlížeč
const json = atob(payload.replace(/-/g, "+").replace(/_/g, "/"));

// Node.js
const json2 = Buffer.from(payload, "base64url").toString("utf-8");

Proč atob() selhává na non-ASCII JWT claims?

atob() vrací Latin-1 řetězec, kde každý znak odpovídá jednomu bajtu. Vícebajtové UTF-8 sekvence (emoji, znaky CJK, znaky s diakritikou nad rámec Latin-1) jsou rozloženy napříč více znaky, což produkuje poškozený výstup. Opravou je nejprve převést binární řetězec na Uint8Array, pak předat toto pole do new TextDecoder("utf-8").decode(). TextDecoder API vícebajtové sekvence správně sestaví. Tento problém je snadné přehlédnout při vývoji, protože většina JWT payloadů obsahuje pouze ASCII uživatelská ID, časová razítka a názvy rolí — chyba se projeví, až když claim obsahuje non-ASCII zobrazované jméno nebo lokalizovaný řetězec. Vždy používejte cestu přes TextDecoder v novém kódu, i když jsou vaše aktuální payloady jen ASCII, protože claims se mohou měnit s vývojem aplikace.

JavaScript
// Rozbité: atob vrací Latin-1, vícebajtové znaky jsou poškozené
const broken = atob(base64); // "ð\x9F\x8E\x89" místo emoji

// Opraveno: převod na pole bajtů, pak TextDecoder
const bytes = Uint8Array.from(atob(base64), c => c.charCodeAt(0));
const fixed = new TextDecoder("utf-8").decode(bytes);

Mohu ověřit JWT podpis v JavaScriptu?

Dekódování a ověření jsou různé operace. Dekódování pouze čte payload, který není zašifrován. Ověření kontroluje podpis oproti tajnému klíči (HMAC) nebo veřejnému klíči (RSA/ECDSA). Knihovna jose podporuje oboje v prohlížeči přes Web Crypto API i v Node.js. Balíček jsonwebtoken funguje pouze v Node.js. Nikdy nedůvěřujte dekódovaným claims bez ověření podpisu na straně serveru. Na straně klienta je přijatelné dekódovat JWT pro čtení zobrazovaného jména uživatele nebo doby vypršení platnosti, ale jakékoli rozhodnutí o přístupu — kontrola, zda má uživatel určitou roli nebo oprávnění — musí proběhnout v kódu na straně serveru po ověření. Útočník, který rozumí formátu JWT, může vytvořit token s libovolnými claims a vaše kontrola na straně klienta projde.

JavaScript
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); // ověřené claims

Jak zkontrolovat, zda JWT vypršel v JavaScriptu?

Dekódujte payload a přečtěte claim exp, což je Unix časové razítko v sekundách. Porovnejte ho s aktuálním časem pomocí Math.floor(Date.now() / 1000). Pokud je aktuální čas větší než exp, token vypršel. Pamatujte: hodnota exp je v sekundách od epochy, ne v milisekundách, takže je nutné dělit Date.now() číslem 1000. V praxi zabudujte malou rezervu pro odchylku hodin — kontrola, zda token vyprší během příštích 30 sekund, spíše než přísně v minulosti, předchází hraničním případům, kdy je token stále technicky platný při dekódování, ale vyprší do doby, než další downstream volání API ho zpracuje. Ošetřete také případ, kdy exp zcela chybí, což znamená, že token nikdy nevyprší.

JavaScript
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 nebo false

Jak napsat isomorfní kód pro dekódování JWT fungující v Node.js i prohlížeči?

Zkontrolujte existenci globalThis.Buffer. Pokud existuje, jste v Node.js a můžete použít Buffer.from(segment, "base64url").toString("utf-8"). Pokud neexistuje, jste v prohlížeči a měli byste použít atob() s přístupem TextDecoder. Zabalte tuto kontrolu do jediné funkce decodeBase64Url a používejte ji všude. To je nejdůležitější pro utility balíčky, komponenty designového systému a jakýkoliv sdílený kód žijící v balíčku monorepa, který je importován jak serverovou komponentou Next.js, tak prohlížečovou React komponentou. Udržování detekce prostředí na jednom místě znamená, že ho potřebujete aktualizovat jen na jednom místě, pokud se runtime změní — například když Deno přidá plnou podporu Buffer nebo nový edge runtime vyžaduje jinou cestu kódu.

JavaScript
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);
}

Související nástroje

Dostupné také v:Python
MW
Marcus WebbJavaScript Performance Engineer

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.

SL
Sophie LaurentTechnický recenzent

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.