O que um JSON Web Token contém realmente
Um JSON Web Token (JWT) são três blocos de texto unidos por pontos finais. Cada bloco está codificado em Base64url, e os dois primeiros são apenas JSON. Qualquer pessoa que tenha o token consegue ler o que está lá dentro. Num JWT normal não há nada cifrado.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 <- header
eyJzdWIiOiIxMjM0NSIsIm5hbWUiOiJBbGV4In0 <- payload
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFW <- signature
Unidos por pontos finais, aquilo é o token que se cola num pedido. Descodifique as duas primeiras partes e obtém JSON vulgar:
{ "alg": "HS256", "typ": "JWT" }
{ "sub": "12345", "name": "Alex" }
Uma carga útil real transporta normalmente também timestamps e uma audiência:
{
"iss": "https://auth.example.com",
"sub": "12345",
"aud": "api.example.com",
"iat": 1756000000,
"exp": 1756003600,
"roles": ["editor"]
}
O cabeçalho
O cabeçalho descreve como o token está assinado. Há dois campos que contam mais:
algnomeia o algoritmo de assinatura, por exemploHS256(HMAC com SHA-256, usando um segredo partilhado) ouRS256(assinatura RSA com SHA-256, assinada com uma chave privada e verificada com uma pública).kid, o ID da chave, é opcional e diz a quem recebe qual das várias chaves foi usada, de modo que as chaves possam ser rodadas sem partir os tokens existentes.
typ é normalmente apenas JWT e não tem significado de segurança nenhum.
A carga útil e as suas claims
A carga útil é um objeto JSON de claims, que é simplesmente a palavra que a especificação usa para afirmações sobre o sujeito do token. Alguns nomes de claim estão registados pela norma, e são todos curtos:
| Claim | Nome | O que contém |
|---|---|---|
iss | Emissor | Quem criou o token, normalmente um URL ou o nome de um serviço |
sub | Sujeito | Sobre quem é o token, normalmente um ID de utilizador estável |
aud | Audiência | A quem se destina o token, para que um serviço possa rejeitar tokens destinados a outro |
exp | Hora de expiração | O instante a partir do qual o token tem de ser rejeitado |
nbf | Não antes de | O instante antes do qual o token tem de ser rejeitado |
iat | Emitido em | Quando o token foi criado |
jti | ID do JWT | Um identificador único, útil para seguir ou bloquear tokens individuais |
exp, nbf e iat são todos valores NumericDate, ou seja, segundos desde 1 de janeiro de 1970 UTC. São segundos, não milissegundos. Se um timestamp descodificar para uma data no ano 57 000, está a ler milissegundos e tem de dividir por mil.
Tudo o resto na carga útil é o que o emissor lá tiver posto: papéis, permissões, um endereço de email, um ID de tenant. Como um nome de claim privado pode um dia colidir com um nome registado, os emissores costumam dar às suas um espaço de nomes próprio, com um prefixo em forma de URL.
A assinatura
A assinatura é calculada sobre o texto exato das duas primeiras partes unidas por um ponto, usando o algoritmo indicado no cabeçalho e uma chave. Mude um único caráter do cabeçalho ou da carga útil e a assinatura deixa de corresponder.
Com HS256 o mesmo segredo assina e verifica, por isso quem consegue verificar um token também consegue emitir um. Com RS256 ou ES256 é uma chave privada que assina e uma chave pública que verifica, e é isso que permite a um fornecedor de identidade distribuir tokens que muitos serviços distintos conseguem validar sem guardar segredo nenhum.
Base64url não é cifra
O Base64url é a mesma ideia que o Base64, com duas diferenças: usa - e _ em vez de + e / para que o resultado seja seguro em URL, e o preenchimento final com = é normalmente retirado. É uma codificação, uma forma reversível de escrever bytes como texto. Não oferece confidencialidade nenhuma.
Por isso nunca ponha nada sensível na carga útil de um JWT: nem palavras-passe, nem números de cartão, nem notas internas sobre o utilizador. Parta do princípio de que quem tem o token, e quem o ler a partir de um ficheiro de registo ou do armazenamento do navegador, consegue ver todos os campos.
Existe um formato separado, o JWE, que cifra de facto a carga útil, e tem cinco partes em vez de três. Se o seu token tem três partes, está assinado e é legível.
Descodificar não é verificar
É este o ponto que conta. Um descodificador mostra-lhe o que um token diz. Não lhe diz se o token é genuíno. A verificação é um passo à parte e precisa da chave.
Para confiar mesmo num token, um servidor tem de:
- Verificar a assinatura contra a chave correta.
- Exigir o algoritmo que espera, em vez de confiar no campo
algdo token. Saltar este passo dá origem a dois ataques clássicos: pôralganonee retirar a assinatura, e pegar na chave pública RSA de um serviço e usá-la como segredo de HMAC, enganando um verificador deRS256para que executeHS256. - Verificar
expe, se existir,nbf, tolerando um pequeno desvio de relógio. - Verificar que
isseaudcorrespondem ao que este serviço espera.
Só então as claims significam alguma coisa. Até aí, a carga útil é uma cadeia de carateres não verificada que chegou pela rede.
Notas práticas
- Os tokens são normalmente enviados num cabeçalho HTTP como
Authorization: Bearer <token>, por isso mantenha a carga útil pequena. Cada claim que acrescenta é enviada em todos os pedidos. - Um token assinado mantém-se válido até expirar. Não existe forma nativa de o revogar, e é por isso que os tokens de acesso tendem a ter vidas curtas, muitas vezes de minutos, com um token de renovação separado para obter novos.
- Se um token parecer malformado, comece por contar os pontos. Dois pontos e três partes é um JWT assinado normal. Um token truncado colado a partir de um terminal é uma causa muito comum de "invalid signature".
- Descodificar um token para ler a sua expiração, o seu sujeito ou os seus papéis durante uma depuração é completamente seguro e não exige segredo nenhum. É exatamente para isso que a carga útil serve.