Ir para o conteúdo
OneKitly

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

Daniel OkonkwoProgramador front-end e redator de Tecnologia na OneKitly

Desempenho web · Formatos de ficheiro

Verificado a partir de 6 fontes

Ver perfil
Em resumo

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.

As três partes de um token HS256 real de 264 carateres: o que cada uma contém, quem a pode ler e o que a assinatura cobre
ParteConteúdoTamanho neste tokenLegível sem chave?Coberta pela assinatura?
Cabeçalhoalg e typ — que algoritmo o assinou36 carateresSim, totalmenteSim — mas o verificador não deve acreditar cegamente no alg
Carga útilAs reivindicações: sub, iss, exp, iat, jti e o que acrescentar183 carateres para 137 bytes de JSONSim — é o que quase todos falhamSim — não pode ser editada sem a chave
AssinaturaHMAC-SHA-256 sobre cabeçalho.carga, ou uma assinatura RSA/ECDSA43 carateres para 32 bytes em brutoSim, mas sozinha nada significaÉ a assinatura
O que faltaA confidencialidade e qualquer forma de anular o token antesZero bytes são gastos nissoNão se aplicaUse JWE para a primeira, um token de renovação ou uma lista de recusa para o segundo
Gerador de JWTConstrua e assine com HMAC um JSON Web Token no seu navegador — HS256, HS384 ou HS512.Experimentar a ferramenta

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
ExplicaçãoO que é um JWT (JSON Web Token)?Um JWT é um token compacto e assinado que transporta identidade entre serviços. Eis as suas três partes, o uso para autenticação e os limites de segurança.ExplicaçãoEntropia de palavras-passe: o que um medidor de robustez não pode saberA entropia mede o processo que produziu uma palavra-passe, não os carateres que a compõem. H = L x log2(R) só é verdade quando cada carácter foi escolhido ao acaso — e é precisamente por isso que um medidor que pontua uma palavra-passe inventada por um humano pelas suas classes de carateres está a medir a coisa errada.ComparaçãoMD5, SHA-1, SHA-256: que hash e para quêO MD5 está partido e o MD5 serve muito bem, conforme qual das três propriedades de segurança precisava. Eis o que significam realmente resistência a colisões, a segunda pré-imagem e a pré-imagem, que algoritmo mantém qual, e porque nenhum deles deve chegar perto de uma palavra-passe.ExplicaçãoO que é uma função de hash? (MD5, SHA-256)Uma função de hash transforma qualquer entrada numa impressão de tamanho fixo. Eis o que faz, as suas propriedades-chave, usos comuns e que algoritmos são seguros.ExplicaçãoO que é um UUID (e quando usá-lo)?Um UUID é um identificador de 128 bits único sem autoridade central. Eis como se parece, porque é útil, as versões e quando usar um.GuiaConstruir um URL com parâmetros que sobrevive a um copiar e colarTrês codificações, uma diferença visível: %20 ou +. O modo formulário do gerador reproduz URLSearchParams byte a byte em dezassete valores — mas dê-lhe um URL base com um fragmento e todos os parâmetros vão parar dentro do hash, onde nenhum servidor os vê.

Ferramentas relacionadas

Fontes

Detetaste um erro neste artigo?