JWT vs cookies de sessão: diferenças e quando usar cada um

Um cookie de sessão é a chave para um estado guardado no servidor; um JWT leva um estado assinado até o servidor. Essa única diferença explica todos os prós e contras.

Experimente: Decodificador de JWT

A comparação costuma ser apresentada como JWT contra cookies, mas as duas coisas não são do mesmo tipo. Cookie é uma forma de enviar dados junto com cada requisição. JWT é um formato de dados. A escolha real é entre uma sessão no servidor, em que o navegador guarda um ID aleatório e o servidor consulta a quem ele pertence, e um token autocontido, em que o cliente guarda claims assinadas que o servidor consegue verificar sem consulta nenhuma. Com isso claro, as dúvidas sobre revogação, armazenamento e CSRF se encaixam.

Como funciona uma sessão no servidor

Depois de um login bem-sucedido, o servidor cria um registro de sessão (ID do usuário, papéis, validade) na memória, em um banco de dados ou em um armazenamento como o Redis, e envia ao navegador, em um cookie, um ID impossível de adivinhar. O navegador anexa o cookie a todas as requisições seguintes para aquele site, e o servidor carrega o registro a cada uma delas. O ID não significa nada sozinho; 32 bytes aleatórios viram 43 caracteres depois de codificados.

Set-Cookie: __Host-session=p-sLYX7J3ziMKxq-dadVMuQJNSVBHM5hrw7MJPihGQ0; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=86400

O prefixo __Host- faz os navegadores rejeitarem o cookie a menos que ele seja Secure, tenha Path=/ e não defina Domain, então um subdomínio não consegue sobrescrevê-lo.

Como funciona um JWT

Um JSON Web Token (RFC 7519) são três partes codificadas em Base64URL e unidas por pontos: um cabeçalho que indica o algoritmo, um payload com claims como sub e exp, e uma assinatura sobre as duas primeiras partes. O servidor verifica a assinatura e a validade com uma chave e confia nas claims sem perguntar a um banco de dados. Normalmente o token é enviado em um cabeçalho Authorization: Bearer.

O payload é apenas codificado, não criptografado. Qualquer pessoa com o token consegue lê-lo, como você pode ver colando um no Decodificador de JWT. Nunca coloque segredos ou dados pessoais em um JWT que você não mostraria ao próprio usuário. Existem tokens criptografados (JWE, RFC 7516), mas a maioria dos JWTs em uso é apenas assinada. Para entender por que as partes usam - e _ em vez de + e /, veja Base64 vs Base64URL.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzg0MTIiLCJpYXQiOjE3NjAwMDAwMDAsImV4cCI6MTc2MDAwMDkwMH0.08BFm85KQIp34VSetcibqwBVLpHKR3IZhI_IPNeTif4

// Reading the payload needs no key at all (browser):
const part = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
const bytes = Uint8Array.from(atob(part), c => c.charCodeAt(0));
JSON.parse(new TextDecoder().decode(bytes));
// { sub: 'user_8412', iat: 1760000000, exp: 1760000900 }

Como mostra o código acima, rodado no navegador, ler o payload não exige chave nenhuma.

Prós e contras lado a lado

Sessão no servidorJWT
O cliente guardaUm ID aleatório e sem significadoClaims assinadas e legíveis
O servidor precisa deUm armazenamento de sessões, compartilhado por todos os servidoresSó da chave para verificar assinaturas
Desconectar um usuário em todos os lugaresApagar a sessão: efeito imediatoImpossível antes do exp sem estado extra
Mudança de papel ou permissãoVale na próxima requisiçãoVale quando um novo token é emitido
Transporte típicoCookie, enviado automaticamenteCabeçalho Authorization, ou um cookie
Tamanho por requisiçãoID de 43 caracteres no nosso exemploDe 152 a 540 caracteres nos nossos exemplos

Revogação, expiração e refresh tokens

Não guardar estado (stateless) é o principal atrativo dos JWTs e também a principal fraqueza. Um token válido continua válido até expirar, mesmo depois que o usuário faz logout, troca a senha ou é banido. A resposta habitual é manter os access tokens com vida curta, muitas vezes minutos em vez de horas, e combiná-los com um refresh token de vida mais longa, que o cliente troca por novos access tokens. O refresh token é conferido com um estado no servidor, então revogá-lo impede a emissão de novos access tokens.

O OAuth 2.0 Security Best Current Practice (RFC 9700) descreve a rotação de refresh tokens: cada renovação devolve um novo refresh token e invalida o anterior, de modo que um token roubado que seja reutilizado pode ser detectado. Se você também precisa revogar access tokens na hora, acaba mantendo uma lista de bloqueio de IDs de token (jti) ou uma versão de token por usuário, o que é, de novo, estado no servidor. Nesse ponto, vale verificar se uma sessão comum não seria mais simples.

Onde guardar o token: cookies, localStorage e XSS

Tudo o que está em localStorage ou sessionStorage pode ser lido por qualquer script que rode na sua página. Um único bug de cross-site scripting (XSS) ou um único script de terceiros comprometido basta para copiar o token e usá-lo de qualquer lugar até que ele expire.

Um cookie marcado como HttpOnly não pode ser lido por JavaScript de jeito nenhum, então um XSS não consegue roubá-lo. O XSS ainda consegue enviar requisições a partir da página aberta da vítima, e o navegador vai anexar o cookie, então o HttpOnly limita o estrago em vez de impedi-lo. Prevenir XSS continua sendo essencial nos dois casos; cookies HttpOnly apenas tornam um ataque bem-sucedido menos útil para o atacante.

CSRF e SameSite

A conveniência do cookie também é o seu risco: o navegador o anexa automaticamente, inclusive em requisições disparadas por outro site. Isso é cross-site request forgery (CSRF). Tokens enviados em um cabeçalho Authorization não ficam expostos ao CSRF clássico, porque o navegador não anexa um bearer token por conta própria; é o seu código que precisa adicioná-lo.

  • SameSite=Strict: o cookie nunca é enviado em requisições iniciadas a partir de outro site, nem mesmo quando alguém segue um link para o seu.
  • SameSite=Lax: enviado quando o usuário navega até o seu site por um link normal (um GET de nível superior), mas não em envios de formulário entre sites, fetches ou recursos incorporados. O Chrome aplica Lax aos cookies que não definem o atributo; defina-o explicitamente em vez de depender disso.
  • SameSite=None: enviado em qualquer situação, e precisa vir junto com Secure (caso contrário, o Chrome o rejeita). Só é necessário para uso realmente entre sites, como widgets incorporados.

"Mesmo site" significa o mesmo domínio registrável, então app.example.com e forum.example.com contam como o mesmo site. O SameSite não protege você de um subdomínio comprometido. Para requisições que alteram estado, mantenha também um token CSRF ou verifique o cabeçalho Origin.

Tamanho do token

Um ID de sessão continua minúsculo, não importa quanto você saiba sobre o usuário. Um JWT cresce a cada claim e conforme o algoritmo de assinatura. Os tokens que geramos para este guia tinham 152 caracteres com HS256 e três claims, 241 caracteres depois de acrescentar nome, endereço de e-mail e papel, e 540 caracteres com as mesmas claims assinadas com RS256 e uma chave de 2048 bits. A RFC 6265 só exige que os navegadores aceitem cookies de pelo menos 4.096 bytes, contando nome e atributos, então um JWT cheio de permissões pode não caber em um cookie. E todo esse tamanho vai junto em cada requisição.

Erros que quebram a segurança do JWT

  • Confiar no cabeçalho alg. Fixe os algoritmos que o seu servidor aceita. Aceitar none, ou deixar um token trocar de RS256 para HS256 para que a chave pública seja usada como segredo HMAC, permite que atacantes forjem tokens.
  • Decodificar em vez de verificar. Ler o payload não prova nada; verifique a assinatura primeiro, com uma biblioteca mantida.
  • Pular a checagem das claims. Valide exp, nbf, iss e aud, para que um token emitido para um serviço não seja aceito por outro.

A RFC 8725, JWT Best Current Practices, trata desses e de outros problemas.

Quando usar cada um

SituaçãoNormalmente a melhor opção
Aplicação web renderizada no servidor, em um único domínioCookie de sessão
Single-page app servida no mesmo site que a sua APICookie de sessão, ou um backend-for-frontend que mantém os tokens no servidor
Aplicativo mobile nativoAccess token de vida curta mais refresh token no armazenamento seguro da plataforma
Microsserviços que verificam requisições sem chamar um serviço de autenticaçãoAccess tokens JWT assinados com chave assimétrica, como RS256 ou ES256
Acesso de terceiros à sua APIAccess tokens OAuth 2.0, que muitas vezes são JWTs

O modelo híbrido: um JWT dentro de um cookie HttpOnly

Muitas aplicações colocam o próprio JWT em um cookie HttpOnly; Secure; SameSite. Os scripts não conseguem lê-lo, e os servidores continuam podendo verificá-lo sem consultar uma sessão. Os contras vêm junto: o cookie é enviado automaticamente, então as defesas contra CSRF voltam a ser necessárias; o limite de 4 KB dos cookies se aplica; e a revogação é tão difícil quanto com qualquer outro JWT. É um bom projeto para uma aplicação de navegador apoiada em APIs stateless, desde que você o trate como cookie quando o assunto é CSRF e como JWT quando o assunto é revogação.

Perguntas Frequentes

JWT é mais seguro que cookie de sessão?

Não, nenhum dos dois é mais seguro por natureza. Sessões são mais fáceis de revogar e não revelam nada ao cliente. JWTs dispensam a consulta de sessão, mas são difíceis de revogar antes de expirar. Onde a credencial fica guardada e como CSRF e XSS são tratados importam mais do que o formato.

Devo guardar o JWT no localStorage?

É simples, mas expõe o token a qualquer script da página, então um único bug de XSS pode vazá-lo. Um cookie HttpOnly e Secure, com o atributo SameSite definido explicitamente, deixa o token fora do alcance do JavaScript, ao custo de exigir proteção contra CSRF.

Dá para invalidar um JWT?

Não por conta própria. Um token assinado continua válido até o horário do exp. Para cortar o acesso antes, mantenha os access tokens com vida curta, revogue o refresh token ou confira os IDs de token em uma lista de bloqueio, o que acrescenta estado no servidor.

Os dados de um JWT são criptografados?

Normalmente não. O cabeçalho e o payload são JSON codificado em Base64URL que qualquer pessoa consegue decodificar; só a assinatura impede alterações. Use JWE se o conteúdo precisar ser confidencial, ou mantenha dados sensíveis fora do token.
Experimente: Decodificador de JWT