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

Tokens opacos (opaque bearer tokens)

Um crachá te dá acesso a um prédio comercial. Ele não tem nome, não tem foto, e não tem nada legível por dentro. Você aproxima o crachá, um leitor confere numa lista, e a porta abre ou não abre.

Um opaque bearer token (token opaco de portador) é esse crachá. Uma string aleatória e impossível de adivinhar, sem nada dentro dela para ser lido.

Sem estado não significa que o servidor esquece tudo

Essa expressão confunde bastante gente, então vale esclarecer isso logo de cara.

Sem estado (stateless) significa que o servidor não mantém sessão: nenhum objeto rastreando um usuário entre requisições. Ele ainda tem um banco de dados, ainda tem usuários, ainda armazena coisas. O que ele deixa de manter é um registro de que essa pessoa está logada no momento.

A requisição precisa trazer tudo que o servidor precisa para descobrir quem está perguntando.

O servidor para de lembrar de você; o cliente passa a se provar a cada requisição.

Duas palavras explicam o nome. Opaco significa que o servidor não consegue ler nada dentro do token, porque não há nada lá dentro. Não é codificado nem estruturado, é uma string aleatória. Bearer (portador) significa que quem o detém, o usa.

JunoSem estado não significa que o servidor esquece tudo "Sem estado" soa como se o servidor tivesse amnésia, e não é bem assim. Ele sabe tudo sobre a sua conta.

A única coisa que ele deixa de guardar é uma anotação dizendo que você está logado agora. Essa anotação é o que o token substitui.

JunoSem estado não significa que o servidor esquece tudo Esse modelo é só parcialmente sem estado, e vale dizer isso claramente. O servidor ainda mantém uma tabela de consulta de token para usuário, então o estado não desapareceu, ele encolheu.

Um objeto de sessão guarda identidade, papel (role) e o que mais se acumulou. A tabela de consulta guarda uma linha: essa string significa esse usuário. É algo muito menor para armazenar, replicar e manter sincronizado.

JunoSem estado não significa que o servidor esquece tudo O nome do cabeçalho é um resquício genuíno e engana o tempo todo. O token viaja em Authorization: Bearer <token>, e nesse modelo ele faz autenticação: prova quem você é e não concede nada por si só. As permissões continuam sendo decididas depois, a partir do usuário para o qual o token aponta.

Vale lembrar disso ao mexer num código desconhecido, porque o nome do cabeçalho induz à suposição de que posse implica permissão. Nunca implica, e essa segunda verificação é a que as pessoas esquecem de escrever.

O fluxo

Seis passos, e o formato vai parecer familiar:

  1. O usuário envia um nome de usuário e uma senha.
  2. O servidor os verifica.
  3. Em caso de sucesso, o servidor gera um token aleatório.
  4. O servidor armazena um pequeno mapeamento de token para usuário e envia o token ao cliente.
  5. O cliente o guarda e o anexa em toda requisição posterior no cabeçalho Authorization.
  6. O servidor consulta o token, encontra o usuário, e a requisição é autenticada.
http
GET /api/orders
Authorization: Bearer 7f3a9c1e5b28d4a06e91f7c3

Comparado com sessões e cookies, as diferenças estão em três pontos:

Sessão com estadoToken opaco de portador
O servidor armazenaUm objeto de sessão completoUma linha de token para usuário
O cliente armazenaUm cookie contendo um id de sessãoO próprio token
Ele viajaAutomaticamente, pelo navegadorExplicitamente, no código que você escreve
Serve paraAplicações web tradicionaisAPIs e aplicativos mobile

Essa terceira linha importa mais do que parece. Um cookie viaja junto sem que ninguém peça. Um bearer token é anexado pelo seu próprio código de cliente, em toda requisição, de forma deliberada.

JunoO fluxo Se isso parece bastante com sessões e cookies, é uma reação justa. Algo é armazenado pelo cliente, algo é verificado pelo servidor, e uma consulta conecta os dois.

A diferença está em quanto o servidor guarda e quem faz o envio. Mesmo esqueleto, distribuição de peso diferente.

JunoO fluxo Enviar o token explicitamente é uma vantagem, não um incômodo. Um cookie se anexa a toda requisição para aquele domínio, inclusive as disparadas por outro site. É isso que torna o CSRF possível, para começar.

Um bearer token é anexado pelo seu código, então uma requisição que seu código não fez não carrega nada. É por isso que esse modelo é o padrão para APIs servidas a clientes que você não controla.

JunoO fluxo O passo três traz a exigência que as pessoas costumam pular. O token precisa vir de uma fonte criptograficamente segura, crypto.randomBytes e não Math.random, com comprimento suficiente para tornar adivinhar impossível. 32 bytes é o mínimo usual.

O passo quatro tem algo que vale adotar: armazene um hash do token, não o token. O cliente guarda o original, o servidor guarda um digest, e faz a consulta aplicando hash ao que chega.

A tabela de consulta se torna então inútil para quem a lê, pelo mesmo motivo que senhas nunca são armazenadas em texto puro. Muitos sistemas em produção guardam bearer tokens em texto puro, e isso é a primeira coisa que um atacante com acesso ao banco de dados coleta.

Cinco formas como isso dá errado

Quase nenhum dos riscos está em como o modelo funciona. Está em como o token é armazenado, quanto tempo ele dura, e o quão bem cuidada é a tabela de consulta.

O que dá erradoPor que isso importaA correção
1Armazenamento inseguroUm token no localStorage, numa variável simples ou num cookie desprotegido é legível por qualquer script na página. Um bug de XSS e o atacante fica indistinguível do usuárioGuarde-o em algum lugar mais difícil de alcançar, e limite a frequência com que ele fica exposto
2Tokens de vida longaUm token válido por semanas significa que um token vazado é válido por semanasJanelas de expiração curtas
3Sem revogaçãoTokens não expiram sozinhos nem param de funcionar no logout. Uma expiração que nada verifica é só decoraçãoVerifique a expiração em toda requisição, delete no logout
4Acúmulo na tabela de consultaTodo login grava uma linha, então as linhas se acumulam e as consultas ficam lentas. Um atacante pode forçar isso martelando o endpoint de login, o que é uma negação de serviçoUm job agendado removendo tokens expirados
5Uma tabela de consulta adulteradaO mapeamento é a fonte da verdade. Acesso de escrita via SQL injection ou credenciais vazadas redireciona um token para outro usuárioProteja o banco de dados e as ferramentas administrativas com o mesmo cuidado que a aplicação

O problema 5 é o que vale a pena examinar com calma. Nada muda no token; o que muda é o que ele significa. Isso é elevação de privilégio conseguida sem sequer tocar na credencial.

JunoCinco formas como isso dá errado Repare que quatro dos cinco problemas não têm nada a ver com o token em si. É sobre onde ele é guardado, quanto tempo dura, se alguém faz a limpeza, e quem pode editar a lista.

O token está bem. O trabalho todo está no que existe ao redor dele.

JunoCinco formas como isso dá errado O problema um não tem uma resposta limpa, e vale entender por quê, porque muito conselho por aí finge que tem.

O localStorage é legível por qualquer script na página. Um cookie com HttpOnly não é, mas cookies trazem o CSRF de volta, então você precisa de SameSite e possivelmente uma verificação de token.

Ambos são designs reais com fraquezas reais. A questão é contra qual ataque você prefere se defender, e a resposta honesta geralmente começa com não ter um bug de XSS.

JunoCinco formas como isso dá errado O formato ao qual a produção converge é um access token de vida curta, minutos, mais um refresh token de vida longa guardado em algum lugar mais difícil de alcançar. O access token é verificado o tempo todo e expira rápido; o refresh token é apresentado raramente e pode ser revogado.

Isso reintroduz uma consulta, só que no refresh em vez de em toda requisição. É a troca que vale entender: você está recomprando a capacidade de revogação por uma fração do custo.

A rotação de refresh token é o complemento que torna o roubo detectável. Emita um novo refresh token a cada uso e invalide o anterior, de modo que um token roubado sendo reutilizado aparece como um token reaproveitado, e toda a família pode ser revogada naquele momento.

Coloque em prática

Uma API emite tokens assim:

js
const token = Math.random().toString(36).slice(2)
tokens[token] = { userId: 4821 }
res.json({ token })

E os verifica assim:

js
const token = req.headers.authorization?.replace('Bearer ', '')
const entry = tokens[token]
if (!entry) return res.status(401).json({ error: 'Unauthorized' })
req.userId = entry.userId

Encontre quatro problemas.

Compare suas respostas

1. Math.random não é criptograficamente seguro. Sua saída é previsível a partir de valores anteriores, então um atacante que coleta alguns tokens consegue deduzir outros. crypto.randomBytes(32).toString('hex') é a correção, e só esse problema já torna todo o esquema falsificável.

2. Nenhuma expiração em lugar nenhum. Nada é armazenado sobre quando o token foi emitido e nada verifica isso na entrada, então todo token emitido é válido para sempre. Armazene uma expiração e verifique-a em toda requisição.

3. Os tokens são armazenados em texto puro. tokens[token] guarda a string original, então quem ler esse armazenamento consegue usar todo token nele. Armazene um hash e faça a consulta aplicando hash ao que chega.

4. tokens é um objeto simples, então a consulta é permeável. tokens['toString'] retorna Object.prototype.toString, um valor verdadeiro (truthy), então uma requisição trazendo o token literal toString passa direto pela verificação if (!entry).

entry.userId então é undefined. Se isso vira uma falha ou uma requisição autenticada como usuário undefined depende do código que vem depois. Use um Map, Object.create(null) ou Object.hasOwn().

O número 4 é o que parece inofensivo à primeira vista. Os outros três são coisas para lembrar; esse é uma propriedade dos objetos JavaScript que já causou problemas em sistemas reais.

Para onde isso vai a seguir

A tabela de consulta é o que torna esse modelo viável e também o que o limita. Ela te dá revogação, e significa que toda requisição toca um armazenamento compartilhado, exatamente o que a identidade sem estado deveria evitar.

Os JSON Web Tokens levam essa ideia até o fim removendo a consulta por completo, colocando a identidade dentro do próprio token.