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.
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 servidor | JWT | |
|---|---|---|
| O cliente guarda | Um ID aleatório e sem significado | Claims assinadas e legíveis |
| O servidor precisa de | Um armazenamento de sessões, compartilhado por todos os servidores | Só da chave para verificar assinaturas |
| Desconectar um usuário em todos os lugares | Apagar a sessão: efeito imediato | Impossível antes do exp sem estado extra |
| Mudança de papel ou permissão | Vale na próxima requisição | Vale quando um novo token é emitido |
| Transporte típico | Cookie, enviado automaticamente | Cabeçalho Authorization, ou um cookie |
| Tamanho por requisição | ID de 43 caracteres no nosso exemplo | De 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 comSecure(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. Aceitarnone, 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,isseaud, 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ção | Normalmente a melhor opção |
|---|---|
| Aplicação web renderizada no servidor, em um único domínio | Cookie de sessão |
| Single-page app servida no mesmo site que a sua API | Cookie de sessão, ou um backend-for-frontend que mantém os tokens no servidor |
| Aplicativo mobile nativo | Access 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ção | Access tokens JWT assinados com chave assimétrica, como RS256 ou ES256 |
| Acesso de terceiros à sua API | Access 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.