Skip to content
This page has been auto-translated and may contain errors.View in English

JSON Web Tokens

Um token opaco exige uma consulta: a string não significa nada, então o servidor precisa perguntar ao seu próprio armazenamento a quem ela pertence.

Tire a consulta e alguma coisa precisa substituí-la. Um JSON Web Token, ou JWT, faz isso colocando a resposta dentro do próprio token.

Três partes, uma string separada por pontos

Um JWT parece um amontoado de caracteres aleatórios, mas tem uma estrutura rígida. Três partes, unidas por pontos:

text
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjQ4MjEsInJvbGUiOiJ0ZWFjaGVyIn0.4pcPyMD09olPSyXnrXCjTwXyr4BsezdI1AVTmud2fU4
└────────── header ──────────┘ └────────── payload ─────────┘ └────────── signature ──────────┘
  • Header. Qual algoritmo assinou o token e que se trata de um JWT.
  • Payload. As informações de identidade, chamadas de claims: quem é o usuário, qual função ele exerce, quando o token expira, quem o emitiu.
  • Signature. Um carimbo criptográfico que comprova que nada foi alterado.

O servidor o monta pegando o header e o payload, combinando-os com um segredo que só ele conhece, e produzindo a signature. As três partes são então codificadas e unidas.

Qualquer pessoa pode ler um JWT. Só quem tem o segredo consegue criar um que o servidor vá aceitar.

É isso que o torna confiável sem precisar de uma consulta, e é essa a ideia central resumida em uma frase.

JunoTrês partes, uma string separada por pontos Um selo de cera em um envelope é uma boa comparação. Qualquer um pode olhar para o selo, mas só quem tem o carimbo consegue fazer um novo.

Ou seja, o selo não esconde a carta. Ele prova quem a fechou, e que ninguém a abriu depois disso.

JunoTrês partes, uma string separada por pontos Cole um token de verdade em um decodificador e você verá o header e o payload em JSON puro na hora, sem precisar do segredo. Faça isso pelo menos uma vez com um token de algo que você esteja construindo; isso ajuda a próxima seção fazer mais sentido.

A signature é a parte que resiste. Ela só te diz sim ou não, e apenas se você tiver o segredo.

JunoTrês partes, uma string separada por pontos O fato de o header ser visível e manipulável pelo atacante é a origem da falsificação clássica de JWT. Basta definir alg como none, remover a signature, e qualquer servidor que decodifique o token em vez de verificá-lo vai aceitar o payload que você escolheu.

Isso já foi demonstrado contra um middleware jwt.decode, e o resultado é uma sessão válida como quem quer que você tenha alegado ser.

A correção é fixar o que você aceita em jwt.verify: especifique os algoritmos explicitamente, e fixe também issuer e audience. Nunca deixe o token dizer como ele deve ser verificado.

Uma unidade para lembrar enquanto você mexe nisso: clockTolerance é em segundos, não em milissegundos.

O fluxo, e o que mudou

  1. O usuário faz login com um nome de usuário e senha.
  2. O servidor os verifica.
  3. Em caso de sucesso, o servidor cria um JWT e o assina.
  4. O token vai para o cliente, que o armazena.
  5. Toda requisição seguinte o carrega no header Authorization, exatamente como um token opaco faria.
  6. O servidor verifica a signature e lê os claims. Sem consulta, sem armazenamento.

O passo seis é o único que difere dos tokens opacos do tipo bearer, e isso muda completamente o modelo:

Token opaco bearerJWT
ConteúdoNada, uma string aleatóriaClaims codificados: id do usuário, função, expiração
Para identificar o usuárioConsultarVerificar a signature, ler os claims
Armazenamento no servidorUma pequena tabela token-usuárioNenhum
Onde a identidade ficaNo servidorDentro do token

Um token opaco aponta para a identidade mantida no servidor. Um JWT contém essa identidade.

JunoO fluxo, e o que mudou Só um passo mudou, e é ele que decide tudo o mais.

Fazer uma consulta significa que o servidor pode verificar a resposta mais atual. Ler o token significa que o servidor recebe a resposta que era verdadeira no momento em que o token foi criado.

JunoO fluxo, e o que mudou Sem armazenamento significa nenhum repositório de sessão compartilhado para manter, nenhuma coordenação entre instâncias, e escalabilidade horizontal de graça. É uma vantagem operacional real, e é por isso que os JWTs costumam ser a escolha padrão para microsserviços.

O custo aparece no dia em que você precisar encerrar uma sessão imediatamente. Não existe registro para excluir, então o token continua funcionando até expirar. Planeje isso com antecedência, não no meio de um incidente.

JunoO fluxo, e o que mudou As funções dentro do token são o ponto mais delicado na prática. Rebaixe um admin e o token já emitido para ele ainda vai dizer admin até expirar, então sua mudança de permissão tem um atraso medido pelo tempo de vida que você escolheu.

Há duas saídas, e cada uma tem seu custo. Manter os tokens com vida muito curta, na casa dos minutos, e renová-los com frequência, o que reintroduz uma consulta a cada renovação. Ou manter uma lista de permissão ou de bloqueio dos ids dos tokens, o que reintroduz a consulta que você havia eliminado.

Vale ser honesto: as duas opções são o modelo stateless recomprando um pouco de estado. É um design perfeitamente válido, mas não é a vantagem gratuita que o discurso de venda promete.

Quatro formas de dar errado

Um JWT ainda é um token bearer, então toda armadilha dos tokens opacos continua valendo: roube-o e você vira o usuário. Ser autocontido acrescenta mais quatro.

O que dá erradoPor que isso importaA correção
1Presumir que está criptografadoEle está codificado, não criptografado. Quem quer que o tenha em mãos pode ler o payload. Divulgação de informação no STRIDE, exposição de dados sensíveis no OWASPNão coloque no payload nada que você não escreveria em um cartão-postal
2Um segredo fraco ou vazadoAdivinhar ou roubar o segredo permite que um atacante assine seus próprios tokens, incluindo um que diga admin. Elevação de privilégioUm segredo longo e aleatório, carregado do ambiente, nunca versionado
3Um payload sobrecarregadoTokens grandes significam headers grandes em toda requisição: requisições lentas, proxies insatisfeitos, logs inchados. E tudo que está ali vaza se o token vazarSó o que o servidor precisa em toda requisição
4Sem expiraçãoVocê não consegue revogar um JWT, então sem uma expiração ele funciona para sempre. Falha de autenticaçãoSempre defina uma, e mantenha-a curta

A armadilha 2 é a que transforma um pequeno erro em um comprometimento total. Um id de sessão vazado compromete uma conta; um segredo de assinatura vazado compromete todas as contas, inclusive as que ainda nem existem.

JunoQuatro formas de dar errado A armadilha um pega quase todo mundo, porque um JWT realmente parece um amontoado de caracteres sem sentido.

Mas não é embaralhado. Está escrito em um alfabeto que não é amigável para leitura, e qualquer decodificador o transforma de volta em texto simples na hora.

JunoQuatro formas de dar errado O segredo pertence a uma variável de ambiente, fora do repositório, e o "depois a gente rotaciona" precisa de um plano, porque rotacionar invalida todos os tokens ativos de uma vez.

Suportar dois segredos válidos durante uma janela de rotação é o que torna isso viável: verifique contra os dois, assine com o novo, e depois aposente o antigo.

JunoQuatro formas de dar errado A escolha do algoritmo decide quem pode emitir tokens. HS256 é simétrico, então o mesmo segredo assina e verifica, e todo serviço que confere um token também consegue criar um. RS256 é assimétrico: um serviço guarda a chave privada e assina, todos os outros verificam com a chave pública.

Para uma única aplicação, HS256 serve bem. Entre serviços diferentes, a assinatura simétrica acaba dando, sem alarde, a todo verificador o poder de forjar tokens, o que raramente é o que se pretendia.

Também vale saber que um JWT não precisa ser um token de acesso. É um formato de container, e usá-lo como link de redefinição de senha ou como token de verificação de e-mail é comum e faz sentido.

Cada um deles precisa de um claim de propósito, para que um token emitido para uma finalidade não possa ser apresentado para outra.

Mão na massa

Uma auditoria de segurança stateless, quatro cenários.

js
// 1. Um registro na tabela de consulta de tokens
const tokenTable = [
  { token: 'a91f...', userId: 4821, expires: '3000-01-01T00:00:00Z' },
]

// 2. Chamada em toda requisição
function authenticate(token) {
  const record = tokenTable.find((r) => r.token === token)
  return record ?? null
}

// 3. O payload que vai dentro de um JWT
{ id: 4821, name: 'Mara', admin: false, streetAddress: 'Rua Augusta, 1200',
  phone: '+55 11 91234-5678', tabSwitches: 14, windowResizes: 3 }

// 4. Configuração do servidor usada ao emitir JWTs
const config = { secret: 'secret123', algorithm: 'HS256' }
Compare suas respostas
#ArmadilhaCorreção
1Token de vida longa. Expira no ano 3000, quase certamente um erro de digitação, e continua sendo uma credencial válida por um milênioUma expiração de 30 minutos
2Sem revogação. A função encontra o registro e o retorna sem nunca olhar para expires, então um token expirado autentica normalmenteCompare expires com o momento atual, retorne null se já passou, e aproveite para excluir a linha
3Payload sobrecarregado, e divulgação de informação. Um endereço e um número de telefone ficam em algo que qualquer portador pode ler, junto com dados analíticos que o servidor nunca precisaid, name e admin, nada além disso
4Segredo fraco, e sem expiração. secret123 é adivinhável e está fixo no código, ou seja, está no repositórioUm segredo longo e aleatório vindo de uma variável de ambiente, além de um claim de expiração

O cenário 2 é o que se esconde melhor. A função parece correta: ela realmente encontra o token e realmente retorna null para um que não existe. Armazenar uma expiração e nunca checá-la equivale a não ter expiração nenhuma.

Para onde isso vai a seguir

Dois modelos stateless, um trocando uma consulta pela revogação e o outro trocando a revogação pela escala. Nos dois, sua aplicação continua responsável pelas senhas, pelas redefinições e por todo o aparato de provar quem alguém é.

OAuth e identidade delegada questiona se você sequer quer essa responsabilidade.