JWT Decoder JavaScript — atob(), TextDecoder e jose

·JavaScript Performance Engineer·Revisado porSophie Laurent·Publicado

Use o Decodificador JWT gratuito diretamente no seu navegador — sem instalação.

Experimentar Decodificador JWT online →

Todo fluxo de autenticação que já construí chega eventualmente ao mesmo ponto: você tem um JWT em um cookie, um cabeçalho ou uma URL de callback OAuth, e precisa ler o que está dentro. Um decodificador de JWT em JavaScript não requer nenhum pacote npm. O cabeçalho e o payload do token são apenas JSON codificado em Base64url, e tanto o browser quanto o Node.js já possuem tudo o que é necessário para decodificá-los. Este guia abrange todo o pipeline do decodificador de texto JavaScript para JWTs: divisão do token, normalização do base64url para Base64 padrão, atob() e TextDecoder para tratamento adequado de UTF-8, o Buffer.from() do Node.js, verificação de assinatura com jose, e os erros comuns que travam desenvolvedores todos os dias. Para uma inspeção rápida pontual, experimente o Decodificador JWT online em vez disso. Todos os exemplos têm como alvo ES2020+ e Node.js 18+.

  • Divida o JWT em "." — o índice 0 é o cabeçalho, o índice 1 é o payload, o índice 2 é a assinatura.
  • atob() decodifica Base64 mas retorna Latin-1, não UTF-8. Use TextDecoder ou Buffer.from() para claims não-ASCII.
  • Buffer.from(segmento, "base64url") trata base64url nativamente no Node.js — sem substituição manual de caracteres.
  • Decodificar NÃO é verificar. Nunca confie em claims de um JWT decodificado sem verificar a assinatura no servidor.
  • A biblioteca jose faz os dois: verifica assinaturas HS256/RS256/ES256 e retorna o payload decodificado em uma única chamada.

O que é Decodificação de JWT?

Um JSON Web Token é composto por três segmentos codificados em Base64url separados por pontos. O primeiro segmento é o cabeçalho, o segundo é o payload (os claims que você realmente precisa), e o terceiro é a assinatura criptográfica. O cabeçalho é um pequeno objeto JSON que descreve o próprio token. Seu campo mais importante é alg — o algoritmo de assinatura (por exemplo, HS256, RS256, ES256). O campo typ é quase sempre "JWT", e o campo opcional kid identifica qual chave foi usada para assinar o token — essencial quando um provedor de identidade rotaciona chaves e publica um endpoint JWKS com múltiplas chaves públicas.

O payload carrega os claims. O RFC 7519 define sete nomes de claims registrados: sub (sujeito — geralmente o ID do usuário), iss (emissor — a URL do servidor de autenticação), aud (audiência — a API para a qual o token se destina), iat (timestamp de emissão), exp (timestamp de expiração), nbf (timestamp de não-antes) e jti (ID do JWT — usado para prevenir ataques de replay). Todos os timestamps são segundos da época Unix, não milissegundos. O segmento de assinatura é binário bruto — um digest HMAC com chave ou uma assinatura digital assimétrica. Ele é codificado em Base64url como os outros segmentos, mas seus bytes não são JSON e não têm estrutura legível por humanos.

Na prática, você decodifica JWTs em JavaScript por três razões comuns. Primeiro, depuração: você tem um token de um fluxo OAuth ou de um ambiente de teste e quer confirmar que os claims correspondem ao que o servidor de autenticação deveria ter emitido. Segundo, leitura de claims do usuário para fins de exibição no lado do cliente — mostrar o nome, URL do avatar ou badge de papel do usuário logado a partir do payload do token sem uma chamada de API extra. Terceiro, verificar a expiração antes de tentar uma atualização: se exp estiver nos próximos 60 segundos, acionar uma atualização silenciosa antes da próxima chamada de API em vez de aguardar uma resposta 401.

Decodificar não verifica se o token é válido ou foi adulterado. Essa é uma operação separada chamada verificação, que requer o segredo HMAC ou a chave pública RSA/ECDSA. Qualquer pessoa pode decodificar um JWT. Somente o detentor da chave correta pode verificar um. Essa distinção confunde muitos desenvolvedores, especialmente ao construir fluxos de autenticação do lado do cliente onde claims decodificados são exibidos, mas nunca devem ser confiados para decisões de autorização sem uma verificação de backend confirmada.

Before · json
After · json
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3JfOTIxZiIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTcxMTYxMDAwMH0.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
// Cabeçalho
{ "alg": "HS256" }

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

atob() + TextDecoder — Decodificação JWT Nativa do Browser

O pipeline nativo do browser para decodificar um JWT tem quatro etapas. Primeiro, divida a string do token em "." para obter os três segmentos. Segundo, normalize o segmento base64url substituindo - por + e _ por /, depois preenchendo com caracteres = até que o comprimento seja múltiplo de 4. Terceiro, chame atob() para decodificar o Base64 em uma string binária. Quarto, converta a string binária para UTF-8 adequado usando TextDecoder. Essa última etapa importa porque atob() retorna Latin-1. Caracteres de múltiplos bytes — emoji, texto CJK, caracteres acentuados além da faixa Latin-1 — ficam corrompidos sem a etapa do decodificador de texto JavaScript.

JavaScript — decodificação JWT mínima
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 }

A etapa de preenchimento é fácil de ignorar. Os JWTs removem os caracteres = finais de seus segmentos Base64url porque a especificação JWT (RFC 7515) define base64url sem preenchimento. Mas atob() em alguns motores de browser lança um InvalidCharacterError se o comprimento da entrada não for divisível por 4. Preencher defensivamente com padEnd() evita esse caso extremo em todos os ambientes. Aqui está uma versão reutilizável que decodifica cabeçalho e payload em objetos separados:

JavaScript — decodificar cabeçalho e payload
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"

Depois de ter essas duas funções, vale a pena colocá-las em um módulo utilitário compartilhado em vez de copiar a lógica em vários arquivos. Um arquivo src/lib/jwt.ts ou utils/jwt-decode.ts com um tipo de retorno definido torna a intenção explícita em toda a base de código. Em TypeScript, você pode tipar o retorno como { header: JwtHeader; payload: JwtPayload } onde JwtHeader inclui alg, typ e opcional kid, e JwtPayload estende os claims registrados do RFC 7519 com uma assinatura de índice para claims personalizados. Centralizar a lógica de decodificação significa que quando você quiser adicionar tratamento de erros (capturando segmentos malformados) ou telemetria (registrando falhas de decodificação), você tem apenas um lugar para atualizar.

Nota:A etapa TextDecoder é o que torna este pipeline seguro para claims não-ASCII. Sem ela, atob() retorna uma string Latin-1 onde sequências UTF-8 de múltiplos bytes são divididas entre caracteres. Você verá lixo em vez de emoji ou texto CJK. Sempre passe por new TextDecoder("utf-8") após atob().

Decodificando Claims JWT UTF-8 com Caracteres Multi-Byte

Os payloads JWT são JSON UTF-8 codificados como base64url. A maioria dos payloads contém campos apenas ASCII como IDs de usuário e timestamps, então os desenvolvedores nunca percebem que atob() retorna Latin-1 em vez de UTF-8. O problema surge no momento em que um claim contém emoji, caracteres japoneses, cirílico, ou qualquer ponto de código acima de U+00FF. O padrão de decodificação UTF-8 em JavaScript requer converter a string binária em um array de bytes primeiro, depois passá-la pelo TextDecoder.

JavaScript — round-trip UTF-8 com emoji no payload JWT
// Simulando um payload JWT com emoji e caracteres CJK
const payloadObj = {
  sub: "usr_e821",
  display_name: "田中太郎",
  team: "Platform 🚀",
  region: "ap-northeast-1"
};

// Codificação: objeto → JSON → bytes 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ção: base64url → base64 → string binária → bytes → string 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); // "田中太郎" — correto
console.log(result.team);         // "Platform 🚀" — correto

Existe um padrão de fallback legado que você verá em bases de código mais antigas que usa decodeURIComponent combinado com um truque de codificação percentual. Essa abordagem de JavaScript decodeURIComponent funciona porque recodifica cada byte como um par percent-hex, então decodeURIComponent remonta as sequências UTF-8 de múltiplos bytes:

JavaScript — fallback decodeURIComponent para UTF-8
function decodeBase64UrlLegacy(segment) {
  const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
  const binary = atob(base64);
  // Converte cada char para %XX hex, depois decodeURIComponent remonta UTF-8
  const utf8 = decodeURIComponent(
    binary.split("").map(c =>
      "%" + c.charCodeAt(0).toString(16).padStart(2, "0")
    ).join("")
  );
  return utf8;
}

// Funciona para claims não-ASCII sem TextDecoder
const payload = decodeBase64UrlLegacy(token.split(".")[1]);
console.log(JSON.parse(payload));
Aviso:Você pode encontrar o padrão legado decodeURIComponent(escape(atob(segment))) em trechos de utilitários JWT mais antigos. A função escape() é obsoleta e não padronizada. Substitua-a pela abordagem com TextDecoder mostrada acima. O padrão de JavaScript unescape decoder tem o mesmo problema: unescape() é obsoleta. Ambas as funções podem ser removidas de motores JavaScript futuros.

Pipeline de Decodificação JWT — Referência de Etapas

Cada etapa do pipeline nativo do browser para decodificação de JWT, com a API JavaScript usada e o que ela produz:

Parâmetro / Etapa
Tipo
Descrição
token.split(".")
string[]
Divide o JWT nos segmentos [cabeçalho, payload, assinatura]
base64url → base64
substituição de string
Substitui - por +, _ por /, preenche com = até múltiplo de 4
atob(base64)
string
Decodifica uma string Base64 padrão em uma string binária (Latin-1)
TextDecoder("utf-8")
TextDecoder
Converte um Uint8Array de bytes brutos em uma string UTF-8 correta
JSON.parse()
object
Converte a string JSON resultante em um objeto JavaScript

O equivalente no Node.js colapsa as etapas 2 a 4 em uma única chamada: Buffer.from(segment, "base64url").toString("utf-8"). A opção de codificação "base64url" trata a conversão de alfabeto e o preenchimento internamente.

Buffer.from() — O Decodificador de String Node.js para JWTs

O Node.js tem um caminho muito mais simples. A classe Buffer aceita uma codificação "base64url" diretamente, então você pula a substituição manual de caracteres e o preenchimento. Este é o caminho do decodificador de string JavaScript para código do lado do servidor. Uma linha transforma um segmento JWT em uma string UTF-8, e trata caracteres multi-byte corretamente sem etapas extras.

Node.js 18+ — decodificação JWT com 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 }

Essa é a abordagem que uso em todo projeto Node.js. É mais curta, mais rápida, e já trata UTF-8 corretamente. Sem necessidade de TextDecoder, sem substituição de caracteres, sem cálculo de preenchimento. A classe Buffer é um decodificador de string JavaScript que trata o alfabeto base64url nativamente, eliminando toda uma classe de bugs relacionados à substituição de caracteres. Se seu código precisar rodar tanto no browser quanto no Node.js, consulte o FAQ ao final para uma função wrapper isomórfica que detecta o ambiente em tempo de execução.

Aqui está um exemplo mais completo mostrando como extrair claims JWT comuns e converter timestamps em datas legíveis, que é o padrão mais usado em middleware e handlers de rotas de API:

Node.js — extração prática de claims JWT
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"]
// }

Em serviços Node.js de produção, o padrão de decodificação Buffer.from() aparece em três lugares recorrentes. O primeiro é middleware de registro de requisições: você decodifica o cabeçalho Authorization recebido para anexar userId e org a cada entrada de log estruturado sem uma viagem de rede extra ao servidor de autenticação. O segundo é depuração: você imprime claims de tokens decodificados no console durante o desenvolvimento para confirmar que os escopos corretos foram emitidos antes de escrever asserções de teste. O terceiro é a atualização proativa de tokens em gateways de API. Em vez de encaminhar um token para upstream e deixar o serviço downstream retornar um 401 quando o token expira no meio da requisição, o gateway decodifica o token na borda, lê o claim exp, e aciona uma atualização se a expiração estiver nos próximos 30 segundos. Isso elimina uma classe de falhas transitórias de autenticação que são difíceis de reproduzir e frustrantes de depurar.

Nota:A codificação "base64url" foi adicionada no Node.js 15.7.0. Se você estiver preso no Node.js 14 ou anterior, use o fallback Buffer.from(segment.replace(/-/g, "+").replace(/_/g, "/"), "base64") que funciona da mesma forma, mas requer a substituição manual de caracteres.

Decodificar JWT de um Arquivo e Resposta de API

Dois cenários aparecem constantemente. O primeiro é ler um JWT de um arquivo local: um token salvo durante o desenvolvimento, uma fixture de teste, ou um arquivo gerado durante um incidente para análise pós-mortem. O segundo é extrair um JWT de uma resposta HTTP, tipicamente o campo access_token no corpo de uma resposta de token OAuth ou em um cabeçalho Authorization. Ambos precisam de tratamento de erros porque tokens malformados, arquivos truncados e erros de rede são realidades do dia a dia. Um token que era válido semana passada pode ter espaços em branco ou quebras de linha no final por causa de copiar e colar. O corpo de uma resposta pode ser HTML em vez de JSON se o servidor de autenticação retornou uma página de erro.

Ler JWT de um Arquivo (Node.js)

Node.js — decodificar JWT de arquivo com tratamento de erros
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);
}

Extrair JWT de uma Resposta de API (fetch)

JavaScript — decodificar JWT de resposta de API
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);
}

// Uso
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ção de JWT pela Linha de Comando

Às vezes você quer apenas examinar um token no terminal sem escrever um script. O Node.js está disponível na maioria das máquinas de desenvolvimento, então um one-liner funciona bem. jq cuida da formatação visual.

bash — decodificar payload JWT pelo terminal
# Decodificar payload JWT com 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'))))"

# Redirecionar para jq para saída formatada
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 .

# Decodificar cabeçalho e 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 você preferir bash puro sem Node.js, redirecione o segmento pelo base64 -d após corrigir os caracteres base64url com tr:

bash — decodificação JWT em bash puro sem Node.js
# Bash puro: decodificar payload JWT sem Node.js
echo "$JWT_TOKEN" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null | jq .

# Variante macOS (base64 -D em vez de -d)
echo "$JWT_TOKEN" | cut -d. -f2 | tr '_-' '/+' | base64 -D 2>/dev/null | jq .

Para inspeção visual rápida sem nenhum terminal, cole seu token no Decodificador JWT do ToolDeck para uma análise lado a lado dos três segmentos com rótulos de claims codificados por cor e status de expiração.

jose — Verificação e Decodificação em Uma Única Biblioteca

Para middleware de autenticação em produção, você precisa de verificação de assinatura, não apenas decodificação. A biblioteca jose é a melhor opção. Funciona tanto no Node.js quanto em browsers (via Web Crypto API), suporta HS256, RS256, ES256, EdDSA e JWE (tokens criptografados), e tem zero dependências nativas. Instale com npm install jose.

JavaScript — jose: verificar token HS256
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: verificar RS256 com endpoint JWKS
import * as jose from "jose";

// Buscar o conjunto de chaves públicas do provedor de identidade
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. estão agora verificados
  req.userId = payload.sub;
} catch (err) {
  return res.status(401).json({ error: "Invalid token" });
}

Ao decidir entre jose e o pacote mais antigo jsonwebtoken, a diferença principal é o escopo de runtime. jsonwebtoken é exclusivo para Node.js — depende do built-in crypto e não fará bundle para o browser. jose é totalmente isomórfico: usa a Web Crypto API, disponível em todos os browsers modernos, Node.js 16+, Deno, Bun e Cloudflare Workers. Se sua lógica de autenticação estiver em um arquivo de middleware Next.js (que roda no Edge Runtime), em um Cloudflare Worker, ou em um utilitário compartilhado importado por código de servidor e cliente, jose é a escolha correta porque tem zero dependências nativas e instala sem etapa de build. jsonwebtoken ainda é razoável para aplicações de servidor Node.js puro onde você precisa de seu ecossistema mais amplo de helpers de assinatura e não planeja rodar o código em um ambiente de borda. Em um projeto novo em 2026, opte por jose a menos que tenha um motivo específico para preferir a API mais antiga.

Se você precisar apenas de decodificação sem verificação, o jose disponibiliza jose.decodeJwt(token) que retorna o payload e jose.decodeProtectedHeader(token) para o cabeçalho. Essas são funções de conveniência que fazem a decodificação Base64url internamente. Mas a razão principal para usar o jose é que você raramente deveria decodificar sem também verificar. Se você está no lado do cliente e só precisa mostrar ao usuário seu próprio nome de exibição ou URL de avatar a partir dos claims do token, decodificação sem verificação é aceitável. No lado do servidor, sempre verifique. Já vi sistemas em produção que decodificavam claims JWT para decisões de controle de acesso sem verificar a assinatura, e isso é uma porta aberta para qualquer atacante que entende o formato JWT.

JavaScript — jose.decodeJwt para cenários somente de decodificação
import * as jose from "jose";

// Somente decodificação: sem segredo necessário, sem verificação
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"

// Verificar expiração sem verificação (exibição no lado do cliente)
if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
  console.log("Token expirado — redirecionar para login");
}

Saída no Terminal com Destaque de Sintaxe

Ao depurar tokens JWT em uma ferramenta CLI Node.js ou durante um incidente, a saída com código de cores faz uma diferença real. A biblioteca chalk combinada com JSON.stringify resolve o problema. Instale com npm install chalk.

Node.js — saída JWT decodificada com cores
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)));

  // Destacar status de expiração
  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]);
// Execute: node jwt-debug.mjs "eyJhbGci..."
Nota:A saída colorida é somente para o terminal. Não use chalk ao escrever claims JWT em arquivos de log, respostas de API ou campos de banco de dados. Os códigos de escape ANSI aparecerão como lixo em contextos fora do terminal.

Processando JWTs de Grandes Arquivos de Log

A infraestrutura de API moderna emite logs de acesso estruturados no formato NDJSON — um objeto JSON por linha, com cada linha contendo o caminho da requisição, status da resposta, latência e o cabeçalho Authorization decodificado ou bruto. Em um serviço movimentado, esses arquivos crescem rapidamente: um gateway que processa 10.000 requisições por minuto produz mais de 14 milhões de entradas de log por dia. Casos de uso de segurança e conformidade frequentemente exigem varredura desses arquivos após o fato — identificar cada requisição feita por uma conta de serviço comprometida (análise pós-incidente), confirmar que os tokens de um usuário específico expiraram antes de uma janela de acesso a dados (auditoria de conformidade), ou extrair o conjunto completo de sujeitos que acessaram um endpoint sensível durante uma janela de manutenção. Como um único arquivo de log pode exceder vários gigabytes, carregá-lo na memória com readFileSync não é viável. Os streams readline do Node.js processam o arquivo uma linha por vez com overhead de memória constante, tornando prático varrer logs arbitrariamente grandes em um laptop comum de desenvolvedor.

Você não terá o problema de "arquivo grande demais para a memória" com JWTs individuais, pois um único token raramente tem mais de alguns kilobytes. O cenário que ocorre é varrer um grande log de acesso ou trilha de auditoria em busca de tokens JWT, decodificar cada um e extrair claims específicos. Os streams do Node.js lidam com isso sem carregar o arquivo inteiro.

Node.js — stream NDJSON de log e decodificar JWTs incorporados
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 {
      // Ignorar linhas malformadas
    }
  }

  console.log(`\nScanned ${lineCount} lines, found ${expiredCount} expired tokens`);
}

scanLogsForExpiredTokens("./logs/api-access-2026-03.ndjson");
Node.js — extrair sujeitos JWT únicos do stream de log
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 {
        // Não é um JWT válido
      }
    }
  }

  console.log(`Found ${subjects.size} unique subjects:`);
  for (const sub of subjects) console.log(`  ${sub}`);
}

extractUniqueSubjects("./logs/gateway-2026-03.log");
Nota:Mude para streaming quando o arquivo de log exceder 50 MB. Carregar um arquivo NDJSON de 500 MB com readFileSync irá fixar a memória e acionar pausas de GC. A abordagem com readline processa uma linha por vez com uso de memória constante.

Erros Comuns

Usar atob() sem TextDecoder para claims não-ASCII

Problema: atob() retorna uma string Latin-1. Caracteres UTF-8 de múltiplos bytes (emoji, CJK, caracteres acentuados) são divididos entre caracteres e ficam corrompidos.

Solução: Converta a saída de atob() em um Uint8Array, depois passe por new TextDecoder('utf-8').

Before · JavaScript
After · JavaScript
// Quebra em claims de payload não-ASCII
const payload = JSON.parse(atob(token.split(".")[1]));
// display_name aparece como "ç°ä¸­å¤ªé\x83\x8E" em vez de "田中太郎"
const binary = atob(token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/"));
const bytes = Uint8Array.from(binary, c => c.charCodeAt(0));
const payload = JSON.parse(new TextDecoder("utf-8").decode(bytes));
// display_name mostra corretamente "田中太郎"
Esquecer a substituição de caracteres de base64url para base64

Problema: atob() lança "InvalidCharacterError" porque base64url usa - e _ em vez de + e /.

Solução: Substitua - por + e _ por / antes de chamar atob(). O Buffer.from() do Node.js com 'base64url' faz isso automaticamente.

Before · JavaScript
After · JavaScript
// Lança: 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); // agora funciona
Confiar em claims JWT decodificados sem verificação de assinatura

Problema: Qualquer pessoa pode criar um JWT com qualquer payload. Decodificar apenas lê os dados — não prova que o token foi emitido pelo seu servidor de autenticação.

Solução: No lado do servidor, sempre verifique a assinatura usando jose.jwtVerify() ou jsonwebtoken.verify(). A decodificação sem verificação é aceitável para exibição de claims do usuário no lado do cliente.

Before · JavaScript
After · JavaScript
// PERIGOSO: decodificado mas não verificado
const claims = JSON.parse(atob(token.split(".")[1]));
if (claims.role === "admin") {
  grantAdminAccess(); // atacante pode forjar isso
}
import * as jose from "jose";
const { payload } = await jose.jwtVerify(token, secretKey);
if (payload.role === "admin") {
  grantAdminAccess(); // seguro — assinatura verificada
}
Comparar exp com Date.now() sem dividir por 1000

Problema: JWT exp está em segundos desde a época, mas Date.now() retorna milissegundos. A comparação sempre dirá que o token é válido porque o timestamp em milissegundos é 1000x maior.

Solução: Divida Date.now() por 1000 e arredonde para baixo o resultado antes de comparar com exp.

Before · JavaScript
After · JavaScript
// Bug: Date.now() está em milissegundos, exp está em segundos
if (payload.exp > Date.now()) {
  console.log("Token is valid"); // sempre verdadeiro — errado!
}
const nowSeconds = Math.floor(Date.now() / 1000);
if (payload.exp > nowSeconds) {
  console.log("Token is valid"); // comparação correta
}

Métodos de Decodificação JWT — Comparação Rápida

Método
Ambiente
Seguro UTF-8
Verifica Assinatura
Tipos Personalizados
Requer Instalação
atob() + TextDecoder
Browser
N/A (somente leitura)
Não
Buffer.from()
Node.js
N/A (somente leitura)
Não
decodeURIComponent()
Browser (legado)
N/A (somente leitura)
Não
jose
Ambos
✓ (JWS/JWE)
npm install
jsonwebtoken
Node.js
npm install
jwt-decode
Ambos
N/A
npm install

Use atob() + TextDecoder para decodificação no browser quando você só precisa exibir claims ao usuário. Use Buffer.from() em scripts Node.js e ferramentas CLI. Recorra ao jose no momento em que precisar verificar uma assinatura, que é qualquer middleware de autenticação do lado do servidor. O pacote jwt-decode é uma alternativa leve se você quiser uma API de função única para decodificação sem verificação no browser. Para inspeção visual rápida sem escrever código, cole seu token na ferramenta JWT Decoder.

Perguntas Frequentes

Como decodifico um token JWT em JavaScript sem usar uma biblioteca?

Divida o token em ".", pegue o segundo segmento (o payload), normalize a codificação base64url substituindo - por + e _ por /, preencha com caracteres =, depois chame atob() seguido de TextDecoder para obter a string JSON em UTF-8. Passe o resultado por JSON.parse() e você terá o objeto de claims. Nenhum pacote npm é necessário. Essa abordagem funciona em todos os browsers modernos e no Node.js 18+. Se precisar também ler o cabeçalho, aplique os mesmos passos de decodificação ao primeiro segmento. Lembre-se de que isso fornece os dados brutos sem nenhuma verificação de assinatura — trate o resultado como somente para exibição, a menos que você verifique a assinatura no servidor.

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

Qual é a diferença entre atob() e Buffer.from() para decodificar JWT?

atob() é uma API do browser que decodifica Base64 padrão em uma string binária Latin-1. Ela não entende a codificação base64url diretamente, então você precisa substituir os caracteres - e _ primeiro. Buffer.from(segmento, "base64url") é uma API do Node.js que trata o alfabeto base64url nativamente e retorna um Buffer no qual você pode chamar .toString("utf-8"). Use atob() no browser, Buffer.from() no Node.js. Uma terceira opção — mais lenta, mas historicamente comum — é o truque de codificação percentual com decodeURIComponent, mas esse padrão depende da função obsoleta escape() em alguns trechos mais antigos e deve ser evitado em código novo. Para código isomórfico que roda em ambos os ambientes, verifique typeof Buffer !== "undefined" e ramifique adequadamente.

JavaScript
// Browser
const json = atob(payload.replace(/-/g, "+").replace(/_/g, "/"));

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

Por que atob() falha com claims JWT que contêm caracteres não-ASCII?

atob() retorna uma string Latin-1 onde cada caractere corresponde a um único byte. Sequências UTF-8 de múltiplos bytes (emoji, caracteres CJK, letras acentuadas além do Latin-1) são divididas em vários caracteres, produzindo saída corrompida. A solução é converter a string binária em um Uint8Array primeiro, depois passar esse array para new TextDecoder("utf-8").decode(). A API TextDecoder remonta corretamente as sequências de múltiplos bytes. Esse problema é fácil de ignorar no desenvolvimento porque a maioria dos payloads JWT contém apenas IDs de usuário ASCII, timestamps e nomes de papel — o bug só aparece quando um claim contém um nome de exibição não-ASCII ou uma string localizada. Sempre use o caminho com TextDecoder em código novo, mesmo quando seus payloads atuais são apenas ASCII, já que os claims podem mudar conforme a aplicação evolui.

JavaScript
// Quebrado: atob retorna Latin-1, caracteres multi-byte ficam corrompidos
const broken = atob(base64); // "ð\x9F\x8E\x89" em vez do emoji

// Corrigido: converter para array de bytes, depois TextDecoder
const bytes = Uint8Array.from(atob(base64), c => c.charCodeAt(0));
const fixed = new TextDecoder("utf-8").decode(bytes);

Posso verificar uma assinatura JWT em JavaScript?

Decodificar e verificar são operações diferentes. Decodificar apenas lê o payload, que não está criptografado. A verificação confere a assinatura contra um segredo (HMAC) ou chave pública (RSA/ECDSA). A biblioteca jose suporta ambos no browser via Web Crypto API e no Node.js. O pacote jsonwebtoken funciona somente no Node.js. Nunca confie em claims decodificados sem verificar a assinatura no lado do servidor. No lado do cliente, é aceitável decodificar um JWT para ler o nome de exibição ou o tempo de expiração do usuário, mas qualquer decisão de controle de acesso — verificar se um usuário possui um papel ou permissão específica — deve ocorrer em código do lado do servidor após a verificação. Um atacante que entende o formato JWT pode criar um token com claims arbitrários e sua verificação do lado do cliente passará.

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); // verified claims

Como verifico se um JWT está expirado em JavaScript?

Decodifique o payload e leia o claim exp, que é um timestamp Unix em segundos. Compare-o com o horário atual usando Math.floor(Date.now() / 1000). Se o horário atual for maior que exp, o token está expirado. Lembre-se: o valor de exp está em segundos desde a época, não em milissegundos, portanto dividir Date.now() por 1000 é obrigatório. Na prática, inclua uma pequena margem para diferença de relógio — verificar se o token expira nos próximos 30 segundos em vez de estritamente no passado evita casos extremos onde o token ainda é tecnicamente válido quando você o decodifica, mas expira quando a próxima chamada de API o processa. Também trate o caso em que exp está completamente ausente, o que significa que o token nunca expira.

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 or false

Como escrevo código JWT isomórfico que funciona tanto no Node.js quanto no browser?

Verifique a existência de globalThis.Buffer. Se existir, você está no Node.js e pode usar Buffer.from(segmento, "base64url").toString("utf-8"). Se não existir, você está em um browser e deve usar atob() com a abordagem TextDecoder. Envolva essa verificação em uma única função decodeBase64Url e use-a em todo o código. Isso é mais importante para pacotes utilitários, componentes de sistema de design e qualquer código compartilhado que vive em um pacote monorepo importado tanto por um componente de servidor Next.js quanto por um componente React do browser. Manter a detecção de ambiente em um único lugar significa que você só precisa atualizá-la em um ponto se o runtime mudar — por exemplo, quando o Deno adicionar suporte completo ao Buffer ou quando um novo runtime de borda exigir um caminho de código diferente.

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

Ferramentas Relacionadas

Também disponível em: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 LaurentRevisor técnico

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.