Enviar um arquivo
  1. base64hub
  2. Aprenda Base64
  3. Base64 em JWT
Explicação

Como o JWT usa Base64URL: decodifique um token à mão

Um JWT são três strings Base64URL unidas por pontos. Qualquer pessoa consegue decodificar as duas primeiras, e só a assinatura as protege. Veja como cada parte é montada e lida.

Anatomia de um token

Aqui está um token HS256 completo, assinado com o segredo de demonstração demo-secret-do-not-use. As quebras de linha foram acrescentadas para facilitar a leitura; um token real fica numa linha só:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiJ1c2VyLTQyIiwibmFtZSI6IkFkYSIsImFkbWluIjpmYWxzZSwiaWF0IjoxNzYwMDAwMDAwLCJleHAiOjE3NjAwMDM2MDB9
.nKTVXOM_r1JbE92WsFd2SSun0a1TByUV8ZpqrRUnIhk

As três partes, separadas por .:

ParteComprimento em Base64URLDecodifica para
Header36{"alg":"HS256","typ":"JWT"}
Payload104{"sub":"user-42","name":"Ada","admin":false,"iat":1760000000,"exp":1760003600}
Assinatura4332 bytes brutos de HMAC-SHA256

O header e o payload são JSON codificado em UTF-8 e depois em Base64URL. A assinatura é binária, então só faz sentido como bytes. iat e exp são timestamps Unix em segundos: este token foi emitido em 2025-10-09 08:53:20 UTC e expirou uma hora depois.

Por que Base64URL sem preenchimento

Um token passa por lugares onde +, / e = causam problemas: query strings (+ vira espaço), caminhos de URL (/ é separador), cookies e headers HTTP. Por isso o JWS, o formato de assinatura por trás do JWT, definido na RFC 7515, usa o alfabeto URL-safe da RFC 4648 e remove o preenchimento:

Base64 encoding using the URL- and filename-safe character set defined in Section 5 of RFC 4648, with all trailing ‘=’ characters omitted (as permitted by Section 3.2) and without the inclusion of any line breaks, whitespace, or other additional characters.

Em tradução livre: codificação Base64 com o conjunto de caracteres seguro para URLs e nomes de arquivo definido na seção 5 da RFC 4648, com todos os = do final omitidos (como a seção 3.2 permite) e sem quebras de linha, espaços em branco ou outros caracteres adicionais. Daí saem duas diferenças em relação ao Base64 padrão:

  • + vira - e / vira _. A assinatura acima contém _, que os decodificadores padrão rejeitam.
  • Nenhum = no final. O comprimento de um segmento nunca é 4n+1, então um decodificador sempre consegue deduzir quanto preenchimento foi removido: resto 2 na divisão por 4 significa ==, resto 3 significa =.

Mais sobre a diferença entre os alfabetos em Base64 vs Base64URL.

Decodificar as partes à mão

Divida nos pontos, restaure o alfabeto padrão e o preenchimento, decodifique e faça o parsing do JSON. No navegador:

function decodePart(part) {
  const b64 = part.replace(/-/g, "+").replace(/_/g, "/");
  const padded = b64 + "=".repeat((4 - (b64.length % 4)) % 4);
  const bytes = Uint8Array.from(atob(padded), (c) => c.charCodeAt(0));
  return JSON.parse(new TextDecoder().decode(bytes));
}

const [header, payload] = token.split(".").slice(0, 2).map(decodePart);

Sem a etapa do TextDecoder, o texto fora do ASCII quebra. Um payload com {"name":"Zoë 日本"} vira eyJuYW1lIjoiWm_DqyDml6XmnKwifQ. Passe a string convertida só pelo atob e o nome sai como mojibake começando com Zoë, porque o atob retorna um caractere por byte. Nos runtimes que têm o recurso, Uint8Array.fromBase64(part, { alphabet: "base64url" }) substitui as três primeiras linhas; veja Uint8Array.toBase64. No Node.js, Buffer.from(part, "base64url") faz o mesmo.

Em Python, o urlsafe_b64decode cuida do alfabeto, mas ainda exige o preenchimento:

import base64, json

def b64url_decode(s: str) -> bytes:
    return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))

header, payload, signature = token.split(".")
print(json.loads(b64url_decode(header)))   # {'alg': 'HS256', 'typ': 'JWT'}
print(json.loads(b64url_decode(payload)))  # {'sub': 'user-42', 'name': 'Ada', ...}

No shell, traduza o alfabeto com tr e complete o preenchimento com um laço:

jwt_part() {
  local s
  s=$(printf '%s' "$1" | tr '_-' '/+')
  while [ $(( ${#s} % 4 )) -ne 0 ]; do s="$s="; done
  printf '%s' "$s" | base64 -d
}
jwt_part "$(echo "$TOKEN" | cut -d. -f2)"

Se você só quer ver o que há dentro de um token, cole-o no decodificador de JWT. Ele roda no seu navegador, então um token de produção não sai da sua máquina.

Por que o payload é legível

Codificação não é criptografia. O Base64URL só muda a forma como os bytes são escritos, e o decodificador de JWT desfaz isso sem chave nenhuma. Um JWT assinado garante que o payload não foi alterado desde a assinatura. Ele não esconde o payload.

A codificação também custa espaço. Cada claim cresce um terço depois de codificada em Base64URL, e o token inteiro viaja a cada requisição, num header ou cookie, então mantenha o payload pequeno: um ID e algumas claims curtas em vez de um perfil de usuário completo.

Trate toda claim como pública. IDs de usuário, papéis e horários de expiração não têm problema. Senhas, chaves de API e dados pessoais que quem tem o token não deveria ver, sim. Se o conteúdo precisa ser confidencial, use um JWT criptografado (JWE). A forma compacta dele tem cinco partes Base64URL: header, chave criptografada, IV, texto cifrado e tag de autenticação. A explicação codificação vs criptografia trata da diferença de forma geral.

Como a assinatura é calculada

A assinatura cobre o texto ASCII exato header.payload, as duas strings Base64URL com o ponto entre elas. Para HS256:

import hmac, hashlib

signing_input = f"{header}.{payload}".encode("ascii")
mac = hmac.new(b"demo-secret-do-not-use", signing_input, hashlib.sha256).digest()
hmac.compare_digest(mac, b64url_decode(signature))   # True

Os bytes brutos da assinatura são então codificados em Base64URL sem preenchimento. O tamanho deles depende do algoritmo:

AlgoritmoBytes da assinaturaCaracteres Base64URL
HS2563243
ES2566486
RS256 (chave de 2048 bits)256342

As assinaturas ES256 no JWS são os valores brutos R || S, de 32 bytes cada. O OpenSSL e muitas bibliotecas de criptografia geram uma assinatura ECDSA codificada em DER, que começa com 30 e tem comprimento variável. Converter de uma forma para a outra é uma fonte comum de erros de “invalid signature”.

Para montar e assinar um token com chaves HS, RS, ES ou EdDSA, use o codificador de JWT. Para um HMAC bruto em Base64, veja a ferramenta HMAC.

Erros comuns

  • Usar o atob ou um decodificador Base64 padrão diretamente. Funciona com alguns tokens e lança erro com outros, dependendo de aparecer ou não um - ou _. O payload do nosso exemplo decodifica com atob; a assinatura, não.
  • Acrescentar preenchimento ao criar tokens. Algumas bibliotecas rejeitam segmentos que terminam com =. Remova-o.
  • Recodificar o header ou o payload antes de verificar. A assinatura cobre o texto Base64URL original. Fazer o parsing do JSON e serializá-lo de novo pode mudar a ordem das chaves ou os espaços, e a assinatura deixa de bater. Verifique sempre contra os segmentos como foram recebidos.
  • Confiar num payload decodificado. Decodificar não é verificar. Confira a assinatura com o algoritmo esperado e nunca aceite "alg":"none" (header eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0), a menos que seja essa a intenção.
  • Usar a string do token como chave única. 32 bytes de assinatura precisam de 256 bits, e 43 caracteres carregam 258. Os 2 últimos bits não são usados, então no nosso exemplo o k final pode ser trocado por l, m ou n, e decodificadores tolerantes ainda retornam os mesmos bytes. A assinatura continua válida. Isso não afeta a segurança, mas duas strings diferentes podem representar o mesmo token.
  • Ler timestamps como milissegundos. exp e iat estão em segundos. Multiplique por 1000 antes de passá-los ao Date do JavaScript.

Perguntas frequentes

O payload do JWT é criptografado?

Não. Um JWT assinado comum (JWS) só codifica o payload em Base64URL, então qualquer pessoa que tenha o token consegue lê-lo. Tokens criptografados usam JWE, que tem cinco partes em vez de três.

Por que um JWT não termina com sinais de =?

A RFC 7515 define a codificação como Base64URL com todos os = do final removidos, para que os tokens possam ir em URLs e headers sem escape. Os decodificadores precisam recolocar o preenchimento ou aceitar entrada sem ele.

É preciso a chave secreta para decodificar as três partes?

Não. O header e o payload são Base64URL simples. A chave só é necessária para verificar a assinatura ou criar uma nova. O decodificador de JWT mostra as duas partes e confere a assinatura se você fornecer a chave.

Por que todo JWT começa com eyJ?

O header é um objeto JSON que começa com {", e esses dois bytes sempre viram eyJ. Mais prefixos como esse estão listados em prefixos Base64 comuns.

Continue lendo