O que está dentro de um JWT — e o que não protege
Publicado a 06/07/2026 · 17 min de leitura · Ferramentas para programadores
Daniel Okonkwo — Programador front-end e redator de Tecnologia na OneKitly
Desempenho web · Formatos de ficheiro
Verificado a partir de 6 fontes
Um JSON Web Token são três cadeias codificadas em base64url unidas por pontos: cabeçalho, carga útil, assinatura. A assinatura prova que as duas primeiras partes não foram alteradas por alguém sem a chave. Não as esconde. Pegue neste token HS256 real — eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFkYSBMb3ZlbGFjZSIsInJvbGUiOiJlZGl0b3IiLCJpYXQiOjE3ODY1NzkyMDAsImV4cCI6MTc4NjU4MjgwMCwiaXNzIjoiaHR0cHM6Ly9hbGxpbi5leGFtcGxlIiwianRpIjoiYTFiMmMzIn0.bu5qB6d_D3--WUj4b77tnh-wdH6FhKhLcbh2nvNmyl4 — e descodifique a secção do meio sem chave nenhuma. Sai {"sub":"1234567890","name":"Ada Lovelace","role":"editor","iat":1786579200,"exp":1786582800,"iss":"https://allin.example","jti":"a1b2c3"}. O base64url é uma codificação, não uma cifra. A primeira parte nomeia o algoritmo, a terceira é uma etiqueta de 43 carateres sobre as duas primeiras, e o token inteiro são 264 carateres que viajam em cada pedido. Tudo o que puser numa carga útil JWT — endereços de correio, identificadores internos, sinalizadores de permissões — é legível pelo navegador, por qualquer proxy que registe o cabeçalho e por quem leia o token num ecrã. Daí duas consequências. Não ponha nada confidencial numa carga útil; use JWE se precisar mesmo de esconder o conteúdo, o que é outra especificação. E lembre-se de que a assinatura não revoga: um token roubado continua válido até passar o seu exp, e é por isso que os tokens de acesso de vida curta são emparelhados com um token de renovação revogável.
Um JWT é assinado, não cifrado. Quem tiver o token pode descodificar a carga útil e ler todas as suas reivindicações. Eis um token real, descodificado sem chave nenhuma, mais os três ataques que a assinatura deve travar e o único problema que não consegue resolver.
Três partes, dois pontos
Todos os JWT têm o mesmo esqueleto: cabeçalho, ponto, carga útil, ponto, assinatura. O cabeçalho é um objeto JSON minúsculo que nomeia o algoritmo — aqui {"alg":"HS256","typ":"JWT"}, que em base64url fica eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. A carga útil é outro objeto JSON com as reivindicações. A assinatura é calculada sobre as duas primeiras partes codificadas unidas pelo seu ponto, e é por isso que nunca as pode reordenar nem reformatar: o que foi assinado são esses bytes exatos.
A RFC 7519 reserva sete nomes de reivindicação, e conhecê-los poupa muita reinvenção. iss é o emissor, sub o sujeito, aud o público pretendido, exp a expiração, nbf o instante antes do qual não vale, iat a hora de emissão e jti um identificador único do token. As três reivindicações temporais são valores NumericDate: segundos simples desde a época Unix, não milissegundos, o que é uma fonte fiável de erros de fator 1000 em JavaScript. No token acima, iat é 1786579200 e exp é 1786582800: uma diferença de 3 600, ou seja uma hora de vida.
O tamanho sobe mais depressa do que se espera. Essa carga útil são 137 bytes de JSON e passa a 183 carateres depois de codificada, porque o base64 custa sempre quatro carateres de saída por cada três bytes de entrada — uma sobretaxa de 33 %. O token inteiro são 264 carateres, e viaja em cada pedido. Mude para RS256 com uma chave de 2048 bits e só a assinatura salta de 32 bytes em bruto para 256, ou seja 342 carateres base64url: o token quase duplica. Enfiar um vetor de permissões na carga útil é como as equipas acabam a bater nos limites de cabeçalho dos proxies.
Descodifique-o por si mesmo: a carga útil está em claro
Copie a secção do meio do token — a sequência de carateres entre os dois pontos — e descodifique-a em base64url. Sem chave, sem biblioteca, sem permissão. O que volta é {"sub":"1234567890","name":"Ada Lovelace","role":"editor","iat":1786579200,"exp":1786582800,"iss":"https://allin.example","jti":"a1b2c3"}. Todas as reivindicações, em texto simples, incluindo o papel. Se tivesse sido um endereço de correio, um número interno de cliente, um escalão de subscrição ou um sinalizador de funcionalidade, seria igualmente visível.
A confusão é compreensível, porque o token parece texto cifrado. Não é. O base64url existe para mover bytes arbitrários por canais que só toleram um conjunto de carateres restrito — URL, cabeçalhos, nomes de ficheiro. Não há chave, logo não há nada a guardar em segredo nem nada a quebrar. A assinatura protege a integridade: se alguém editar a carga útil, a terceira parte deixa de verificar e o servidor rejeita o token. A integridade não é a confidencialidade, e um JWT entrega só a primeira.
A regra prática decorre diretamente. Trate uma carga útil JWT como um placard público que por acaso é à prova de adulteração. Ponha lá identificadores, não segredos. Ponha um identificador de utilizador em vez de um correio; um nome de papel em vez do raciocínio por trás dele; nada que não imprimisse por fora de um envelope. E guarde-o em conformidade: um cookie HttpOnly, Secure, SameSite põe-no fora do alcance dos scripts da página, ao passo que o localStorage entrega-o a qualquer script que consiga correr na sua origem.
Porquê base64url e não base64
O base64 padrão usa sessenta e quatro carateres terminados em + e /, e preenche a saída com = até um múltiplo de quatro. Os três são hostis a URL e cabeçalhos: + significa espaço em dados de formulário, / é um separador de caminho e = é um separador chave-valor numa cadeia de consulta. A RFC 4648 define por isso um alfabeto compatível com URL que troca + por -, / por _ e elimina totalmente o preenchimento. É o base64url, e é o que todas as partes de um JWT usam.
A assinatura do token acima mostra-o a descoberto: bu5qB6d_D3--WUj4b77tnh-wdH6FhKhLcbh2nvNmyl4 contém um sublinhado e três hífenes. Convertidos de volta para base64 padrão, esses mesmos 32 bytes leem-se bu5qB6d/D3++WUj4b77tnh+wdH6FhKhLcbh2nvNmyl4=. Se alguma vez descodificar uma parte de JWT com uma função base64 normal e sair lixo, é por isto: tem de substituir primeiro - por + e _ por /, e voltar a acrescentar o preenchimento se o descodificador o exigir.
O que a assinatura trava e o que a derrota
Mude um carácter da carga útil e a assinatura deixa de bater certo, porque o HMAC-SHA-256 sobre a cadeia cabeçalho.carga modificada produz algo completamente diferente. Voltar a assinar a carga adulterada exige o segredo, que o cliente não tem. Até aí, tudo bem. As falhas históricas não são ataques contra a criptografia; são ataques contra o verificador, e os dois clássicos vêm de confiar no cabeçalho.
A primeira é alg: none. A RFC 7515 define um JWS não seguro cujo cabeçalho diz {"alg":"none"} e cuja parte de assinatura está simplesmente vazia — o token termina num ponto nu. As bibliotecas que liam o alg do cabeçalho e despachavam em conformidade aceitavam esse token e tratavam as suas reivindicações como verificadas. Um atacante reescreve a carga à vontade, põe alg a none, retira a assinatura e entra. A correção não é rejeitar a cadeia none; é deixar de perguntar ao token que algoritmo usar. O verificador já sabe que algoritmo e que chave espera, e tudo o resto é rejeitado antes de a análise prosseguir.
A segunda é a confusão HS256 contra RS256, e é mais subtil. Com RS256, o servidor guarda uma chave privada para assinar e publica a pública correspondente para verificar. Se um verificador tirar o algoritmo do cabeçalho, um atacante pode pôr alg a HS256 e assinar o token com a chave pública como se fosse um segredo HMAC. O verificador executa então obedientemente o HMAC com essa mesma chave pública — que tem, porque é pública — e a etiqueta bate certo. Funciona: usar como chave de HMAC-SHA-256 o texto PEM de uma chave pública produz um token que um verificador ingénuo aceita. Fixar o algoritmo esperado fecha os dois ataques de uma vez, e é por isso que a RFC 8725 faz disso uma recomendação de destaque.
Um terceiro erro não exige criptografia nenhuma: verificar a assinatura e depois esquecer-se de conferir as reivindicações. Um token estruturalmente válido cujo exp passou na semana passada continua estruturalmente válido. O mesmo vale para um emitido por outro inquilino, ou destinado a outro público. Confira sempre o exp contra a hora atual com uma pequena tolerância de desvio de relógio, o iss contra o emissor esperado e o aud contra o seu próprio identificador — a assinatura diz quem escreveu as reivindicações, não se elas ainda se lhe aplicam.
O problema que nenhuma assinatura resolve: a revogação
Todo o encanto de um JWT é que o servidor não tem de consultar nada. O token transporta as suas próprias reivindicações e a sua própria prova, pelo que qualquer nó com a chave o verifica em microssegundos sem tocar numa base de dados. É também a sua fraqueza estrutural, e as duas coisas são inseparáveis: um servidor que não consulta estado nenhum não pode saber que despediu o utilizador há cinco minutos. O token continua válido até chegar o exp, e nada na especificação oferece forma de encurtar isso.
A resposta padrão é uma divisão em dois tokens. O token de acesso é um JWT com um exp deliberadamente curto — cinco a quinze minutos é o intervalo habitual — e é verificado sem estado em cada pedido. O token de renovação é de vida longa, opaco, guardado no servidor e trocado por um novo token de acesso quando o curto se esgota. A revogação acontece sobre o token de renovação, que tem estado e é portanto cancelável. A janela de exposição encolhe até à vida restante do token de acesso, exatamente o que o exp foi escolhido para limitar.
Se precisar de uma revogação mais rápida do que essa, tem de reintroduzir estado, e deve fazê-lo de propósito. Uma lista de recusa indexada pela reivindicação jti permite cancelar tokens individuais; as entradas podem ser removidas assim que o exp correspondente passar, pelo que a lista se mantém pequena. Rodar a chave de assinatura invalida todos os tokens de uma vez, que é o instrumento contundente perante uma suspeita de comprometimento de chave. Ambos lhe custam uma consulta, e nesse ponto é justo perguntar se um identificador de sessão opaco num cookie não teria sido mais simples desde o início.
O JWE é a outra especificação, e não é uma opção do JWT
Quando as reivindicações têm mesmo de ser escondidas, a resposta é o JSON Web Encryption, definido na RFC 7516. Um JWE é uma serialização diferente com cinco partes em vez de três — cabeçalho protegido, chave cifrada, vetor de inicialização, texto cifrado e etiqueta de autenticação — e fornece confidencialidade e integridade em conjunto, porque usa cifra autenticada. Pode aninhar os dois, assinando um JWT e cifrando depois o resultado, que é o que a RFC 7519 chama um JWT aninhado.
Na prática, a maioria das equipas não precisa de JWE e não deve recorrer a ele primeiro. Se a carga útil contém algo que preferiria que ninguém lesse, a resposta certa habitual é retirá-lo da carga útil. Substitua o valor sensível por um identificador opaco que o servidor de recursos saiba resolver, e o problema de confidencialidade desaparece junto com a gestão de chaves extra, a superfície de biblioteca extra e os modos de falha extra. Recorra ao JWE quando um token tiver de atravessar uma parte que o deve reencaminhar sem o ler — é o caso para que foi desenhado.
Uma lista de verificação para o verificador
Fixe o algoritmo antes de analisar seja o que for e rejeite um token cujo cabeçalho discorde. Verifique a assinatura com uma comparação em tempo constante. Depois confira exp, e nbf se existir, contra a hora atual com uma tolerância de desvio não superior a um minuto. Confira iss contra o emissor exato em que confia e aud contra o seu próprio identificador. Só depois de tudo isso deve ler as reivindicações da aplicação e, mesmo assim, trate role ou scope como uma afirmação do emissor e não como a última palavra: o servidor de recursos continua dono das suas decisões de autorização.
| Parte | Conteúdo | Tamanho neste token | Legível sem chave? | Coberta pela assinatura? |
|---|---|---|---|---|
| Cabeçalho | alg e typ — que algoritmo o assinou | 36 carateres | Sim, totalmente | Sim — mas o verificador não deve acreditar cegamente no alg |
| Carga útil | As reivindicações: sub, iss, exp, iat, jti e o que acrescentar | 183 carateres para 137 bytes de JSON | Sim — é o que quase todos falham | Sim — não pode ser editada sem a chave |
| Assinatura | HMAC-SHA-256 sobre cabeçalho.carga, ou uma assinatura RSA/ECDSA | 43 carateres para 32 bytes em bruto | Sim, mas sozinha nada significa | É a assinatura |
| O que falta | A confidencialidade e qualquer forma de anular o token antes | Zero bytes são gastos nisso | Não se aplica | Use JWE para a primeira, um token de renovação ou uma lista de recusa para o segundo |
Perguntas frequentes
- Um JWT está cifrado?
- Não. Um JWT normal é assinado, o que é uma garantia diferente. O cabeçalho e a carga útil são codificados em base64url, uma codificação sem chave nem segredo, pelo que quem tiver o token os pode ler. Descodificar a carga útil do token de exemplo deste artigo sem chave nenhuma devolve {"sub":"1234567890","name":"Ada Lovelace","role":"editor","iat":1786579200,"exp":1786582800,"iss":"https://allin.example","jti":"a1b2c3"} — todas as reivindicações em texto simples. O que a assinatura lhe compra é evidência de adulteração: mude um carácter e a terceira parte deixa de verificar, e o servidor rejeita. Se precisar mesmo de esconder o conteúdo, o JSON Web Encryption (RFC 7516) é a especificação para isso, e é um formato separado com cinco partes, não uma opção que se liga num JWT. Na maioria dos desenhos o melhor é manter os valores confidenciais fora da carga útil e transportar um identificador opaco.
- É possível alterar a carga útil de um JWT sem o segredo?
- Pode mudar os carateres, mas o resultado não verificará — desde que o seu verificador esteja bem escrito. A assinatura é calculada sobre a cadeia exata cabeçalho.carga, pelo que qualquer edição produz uma discrepância e o token é rejeitado. Dois erros do verificador anulam essa proteção. O primeiro é confiar no campo alg do cabeçalho: um token que declarava {"alg":"none"} com a parte de assinatura vazia foi historicamente aceite por bibliotecas que despachavam pelo cabeçalho, deixando o atacante reescrever a carga à vontade. O segundo é a confusão HS256/RS256, em que o atacante passa um token assimétrico para HMAC e o assina com a chave pública do próprio servidor, que o verificador usa então como segredo HMAC; isso produz mesmo uma etiqueta coincidente. Ambos se fecham com a mesma medida: decida no seu próprio código o algoritmo e a chave esperados antes de analisar, e rejeite tudo o que discorde. A RFC 8725 dá isto como recomendação principal.
- Como termino a sessão de um utilizador se um JWT não pode ser revogado?
- Apagar o token no cliente termina a sessão nesse navegador, e para muitos produtos isso chega mesmo. Não chega se o token puder ter sido copiado, porque um token assinado continua válido até ao seu exp faça o cliente o que fizer. A estrutura padrão é um JWT de acesso de vida curta — cinco a quinze minutos — emparelhado com um token de renovação opaco de vida longa guardado no servidor. Terminar sessão apaga o token de renovação, pelo que a sessão não pode ser renovada e morre dentro da vida restante do token de acesso. Se precisar de mais rapidez, acrescente estado de propósito: uma lista de recusa indexada pelo jti cancela tokens individuais, e as entradas são purgadas assim que o exp correspondente passa, pelo que nunca cresce sem limite. Rodar a chave de assinatura mata de uma vez todos os tokens em circulação e é a resposta certa a uma suspeita de comprometimento. Cada uma delas reintroduz uma consulta, que é o preço da revogação que queria.
- Onde devo guardar um JWT num navegador?
- Num cookie marcado HttpOnly, Secure e SameSite, em quase todos os casos. O HttpOnly põe o token fora do alcance do JavaScript, pelo que uma falha de scripting entre sítios em qualquer ponto da sua origem não o consegue ler nem enviar para fora; o Secure mantém-no fora de ligações em claro; o SameSite trava a falsificação de pedido entre sítios a que os cookies de outro modo o exporiam. O localStorage é a alternativa comum e a mais fraca, porque qualquer script a correr na sua página — incluindo um trazido por uma dependência comprometida — pode ler todas as suas chaves. O argumento a favor do localStorage costuma ser que o token tem de ser anexado à mão em chamadas de API de outra origem, o que os cookies tornam incómodo; é uma restrição real, mas vale mais resolvê-la com um proxy da mesma origem do que tornando o token legível por scripts. Escolha o que escolher, mantenha a carga útil livre de qualquer coisa sensível: o armazenamento decide quem pode roubar o token, não quem o pode ler depois de roubado.
- Quanto tempo deve durar um JWT?
- Curto o suficiente para que a sua vida incancelável seja uma exposição aceitável. Como um token assinado continua válido até ao exp aconteça o que acontecer do seu lado, o exp é toda a largura da janela em que um token roubado ainda funciona. Cinco a quinze minutos é o intervalo comum para tokens de acesso, e o token de exemplo deste artigo usa uma hora — iat 1786579200, exp 1786582800, uma diferença de exatamente 3 600 segundos. Qualquer duração medida em dias é na prática uma credencial permanente com uma assinatura colada. A vida longa é levada pelo token de renovação, e pode ser longa precisamente porque é opaco, guardado no servidor e revogável. Dois detalhes de implementação importam. Os valores NumericDate são segundos, não milissegundos, pelo que comparar o exp com Date.now() em JavaScript sem dividir por 1000 é o erro clássico. E preveja uma pequena tolerância de desvio de relógio, da ordem dos trinta a sessenta segundos, ou haverá tokens rejeitados por um servidor cujo relógio adianta um pouco em relação ao emissor.
- Devo usar um JWT ou um cookie de sessão simples?
- Para uma única aplicação com uma base de dados, um simples identificador de sessão num cookie costuma ser a escolha mais simples e mais sólida. É uma cadeia opaca aleatória, nada revela, e terminar sessão é apagar uma linha com efeito imediato. O JWT justifica a sua complexidade quando a verificação tem de acontecer onde o armazém de sessões não está — vários serviços atrás de um gateway, uma API de terceiros que aceita as suas afirmações de identidade, uma função na periferia que não pode pagar uma ida e volta à base de dados. É uma vantagem arquitetónica real e é a razão de ser dos JWT. Não adote um para um monólito que já tem uma tabela de sessões, e seja honesto quanto à troca: a ausência de estado compra-lhe verificação distribuída rápida e custa-lhe a revogação instantânea; não pode ficar com as duas. As equipas que acrescentam uma consulta a uma lista de recusa em cada pedido pagaram a complexidade do JWT e devolveram o seu único benefício estrutural.
Artigos que podem interessar-lhe
Todos os guias →Ferramentas relacionadas
Fontes
Detetaste um erro neste artigo?