Anatomia de um JWT: cabeçalho, carga útil, assinatura, base64url e por que decodificar não é verificar
Três pontos: a estrutura de um JSON Web Token
Um JSON Web Token é uma string composta por exatamente três valores codificados em base64url separados por pontos: base64url(cabeçalho).base64url(carga útil).base64url(assinatura). A RFC 7519, publicada em maio de 2015, define o formato. Seja num cabeçalho Authorization, num cookie ou num parâmetro de consulta de URL, todo JWT segue esta estrutura. As três partes podem ser decodificadas de forma independente, e a assinatura é o que as une com garantias criptográficas.
O formato foi projetado para ser compacto e seguro em URLs. Antes do JWT, a autenticação de API dependia frequentemente de identificadores de sessão opacos armazenados no servidor, exigindo uma consulta ao banco de dados em cada requisição. Os JWTs incorporam as claims diretamente no token, de modo que um servidor sem estado pode verificá-las sem consulta. Essa conveniência traz responsabilidades: ao contrário de um token de sessão que o servidor pode revogar imediatamente, um JWT assinado permanece válido até que sua claim exp expire, a menos que o servidor mantenha uma lista de negação.
Codificação base64url: a variante URL-segura do base64
O base64 padrão codifica dados binários com 64 caracteres: A–Z, a–z, 0–9, + e /, com preenchimento =. Dois desses caracteres (+ e /) têm significado especial em URLs e strings de consulta HTTP. Base64url, definido na RFC 4648, Seção 5, substitui + por - e / por _, e omite completamente o preenchimento =. O resultado pode ser usado diretamente em URLs, cabeçalhos HTTP e cookies sem codificação adicional.
A taxa de expansão é a mesma do base64 padrão: cada 3 bytes de entrada tornam-se 4 caracteres base64url — um fator de 4/3, aproximadamente 33% de expansão. Como a codificação é reversível sem qualquer chave, qualquer pessoa que obtenha um JWT pode ler seu cabeçalho e carga útil decodificando — base64url é codificação, não criptografia.
O cabeçalho: seleção de algoritmo e tipo de token
O cabeçalho é um objeto JSON que informa ao receptor como processar o token. Um cabeçalho mínimo contém dois campos: alg (o algoritmo de assinatura) e typ (sempre "JWT" para tokens padrão). A RFC 7518 define os identificadores de algoritmo. Os mais comuns são: HS256 — HMAC-SHA256, um algoritmo simétrico onde o mesmo segredo é usado para assinar e verificar; RS256 — RSA-PKCS1v1.5 com SHA-256, assimétrico, onde uma chave privada assina e a chave pública correspondente verifica; ES256 — ECDSA com P-256 e SHA-256, também assimétrico, mas com assinaturas mais curtas que RSA.
A escolha entre algoritmos simétricos e assimétricos tem consequências arquitetônicas reais. Com HS256, cada serviço que precisa verificar tokens deve possuir o segredo compartilhado. Com RS256 ou ES256, o emissor mantém a chave privada em sigilo e publica apenas a chave pública (frequentemente via endpoint JWKS), de modo que os serviços verificadores nunca manipulam material de assinatura. Para integrações multi-serviço ou de terceiros, algoritmos assimétricos são preferíveis.
A carga útil: claims registradas e seu significado
A carga útil é um objeto JSON contendo claims — afirmações sobre uma entidade, geralmente o usuário autenticado, mais metadados sobre o próprio token. A Seção 4.1 da RFC 7519 define sete nomes de claims registradas com semântica padronizada: iss (emissor), sub (sujeito, frequentemente um ID de usuário), aud (audiência, os destinatários pretendidos), exp (tempo de expiração, uma NumericDate após a qual o token não deve ser aceito), nbf (não antes de, uma NumericDate antes da qual o token não deve ser aceito), iat (emitido em) e jti (ID do JWT, identificador único útil para prevenir ataques de repetição).
NumericDate é o número de segundos desde 1970-01-01T00:00:00Z UTC (o epoch Unix), não milissegundos. Esta é uma fonte comum de bugs para desenvolvedores familiarizados com Date.now() do JavaScript. A carga útil de um JWT padrão é assinada mas não criptografada — qualquer parte que intercepte o token pode ler todas as claims. Se a carga contiver dados sensíveis, o token deve ser criptografado com JWE (RFC 7516) ou transmitido apenas via TLS.
A assinatura: o que ela protege — e a armadilha alg:none
A assinatura é calculada sobre a entrada de assinatura: base64url(cabeçalho) + "." + base64url(carga útil). Para HS256, é HMAC-SHA256(segredo, entrada_assinatura) codificado em base64url. Como a entrada inclui tanto o cabeçalho quanto a carga útil, qualquer modificação em qualquer das partes — mesmo um único caractere — invalida a assinatura. Um servidor que verifica a assinatura corretamente pode ter certeza de que o token não foi adulterado.
Em 2015, uma classe de vulnerabilidades foi descoberta em múltiplas bibliotecas JWT: o ataque alg:none. A RFC 7519 tecnicamente permite "alg":"none" para indicar um JWT não seguro sem assinatura. A falha: algumas bibliotecas liam o campo alg do cabeçalho (não verificado) para selecionar o caminho de verificação — se um atacante mudasse o cabeçalho para "alg":"none" e removesse a assinatura, a biblioteca declarava o token válido sem verificar nenhuma assinatura. CVE-2015-9235 documenta isso na popular biblioteca node-jsonwebtoken. A solução: a parte verificadora deve especificar o algoritmo esperado explicitamente e rejeitar qualquer token cujo cabeçalho indique um algoritmo diferente.
Decodificar vs. verificar: por que a distinção importa
Decodificar um JWT significa aplicar base64url-decode nas três partes para recuperar o JSON do cabeçalho, a carga útil e os bytes brutos da assinatura. Nenhuma chave é necessária. Qualquer pessoa com uma string JWT pode decodificá-la e ler as claims. Isso é intencional — JWTs não são contêineres opacos. Um desenvolvedor depurando um problema de autenticação, um engenheiro lendo um log de acesso, um pesquisador de segurança auditando um sistema: todos podem decodificar qualquer JWT que possuam.
Verificar um JWT significa checar que a assinatura foi produzida pela chave correta sobre os bytes exatos de cabeçalho e carga útil. Uma verificação bem-sucedida prova duas coisas: o token foi criado pelo detentor da chave de assinatura (autenticidade), e nem o cabeçalho nem a carga útil foram alterados desde a assinatura (integridade). A verificação requer a chave — o segredo compartilhado para HMAC, a chave pública para RSA/ECDSA.
O erro crítico é tratar um JWT decodificado como confiável sem verificação. Um cliente que lê claims de um JWT no localStorage e age sobre eles sem verificação de assinatura no servidor está confiando em dados que qualquer usuário pode fabricar livremente. Um atacante pode criar {"sub":"admin","role":"superuser"}, codificá-lo em base64url com qualquer cabeçalho e anexar uma assinatura falsa ou vazia — ele será decodificado perfeitamente. Decisões de autorização devem sempre ser tomadas após verificação criptográfica no servidor. Um decodificador JWT é a ferramenta certa para inspeção e depuração; nunca substitui a verificação do lado do servidor em uma aplicação em produção.