Back

Cookies vs localStorage para Autenticação com JWT

Cookies vs localStorage para Autenticação com JWT

Você construiu seu fluxo de autenticação, os JWTs estão funcionando, e agora você se depara com a mesma questão que todo desenvolvedor frontend eventualmente enfrenta: onde eu realmente coloco esse token? A resposta importa mais do que a maioria dos tutoriais sugere, e o conselho comum — “use apenas cookies HttpOnly” — ignora tradeoffs reais que você precisa entender.

A seguir, apresentamos uma análise clara de ambas as opções, o que cada uma realmente protege e como aplicações modernas lidam com isso na prática.

Principais Conclusões

  • localStorage é totalmente acessível a qualquer JavaScript em execução na sua página, tornando-o vulnerável ao roubo de tokens via XSS.
  • Cookies HttpOnly bloqueiam completamente o acesso via JavaScript, mas introduzem risco de CSRF, que os atributos SameSite e Secure mitigam.
  • O padrão moderno armazena tokens de acesso de curta duração em memória e tokens de atualização em cookies HttpOnly, Secure e SameSite.
  • As diretrizes da OWASP e do OAuth para aplicações baseadas em navegador desaconselham o armazenamento de tokens de longa duração em localStorage.
  • A escolha correta depende do seu modelo de ameaças, do controle que você tem sobre o backend e se sua API exige cabeçalhos Authorization.

O Que Está Realmente em Jogo no Armazenamento de JWTs

O local onde você armazena um JWT determina quais vetores de ataque se aplicam à sua aplicação. As duas principais ameaças são:

  • XSS (Cross-Site Scripting): JavaScript malicioso executado no contexto da sua aplicação.
  • CSRF (Cross-Site Request Forgery): Induzir o navegador do usuário a realizar requisições autenticadas não intencionais.

Nenhuma das opções de armazenamento elimina ambos os riscos simultaneamente. O objetivo é entender qual risco você está aceitando e como mitigá-lo.

localStorage: Conveniente, mas Acessível via JavaScript

Armazenar um JWT no localStorage é simples. Você o escreve, lê e o anexa manualmente aos cabeçalhos Authorization: Bearer. Funciona bem com APIs que esperam esse formato de cabeçalho.

O problema é que o localStorage é totalmente acessível a qualquer JavaScript em execução na sua página. Se um atacante conseguir injetar um script — por meio de uma vulnerabilidade em uma dependência, um CDN comprometido ou uma falha de XSS no seu próprio código — ele poderá ler o token diretamente e exfiltrá-lo. A OWASP desaconselha explicitamente o armazenamento de identificadores de sessão no localStorage por esse motivo.

Isso não é teórico. Aplicações web modernas carregam dezenas de scripts de terceiros, e cada um representa uma superfície de ataque potencial.

Cookies HttpOnly: Melhor Resistência a XSS, Novas Considerações

Um cookie HttpOnly não pode ser lido por JavaScript de forma alguma. Mesmo que um atacante execute código na sua página, ele não conseguirá extrair o valor do token. Isso representa uma melhoria significativa.

Porém, cookies introduzem exposição a CSRF. Os navegadores anexam automaticamente cookies às requisições correspondentes, incluindo aquelas disparadas por sites maliciosos de terceiros.

Três atributos de cookie trabalham em conjunto para fechar essa brecha:

  • HttpOnly — bloqueia completamente o acesso via JavaScript.
  • Secure — transmite o cookie somente via HTTPS.
  • SameSite — controla quando os cookies são enviados em requisições entre origens diferentes.

Para o SameSite, os navegadores modernos adotam Lax como padrão quando o atributo não está definido, o que bloqueia cookies em sub-requisições entre origens (como POSTs de outra origem), mas os permite em navegações de nível superior. Strict é mais conservador e impede o envio do cookie em qualquer requisição entre origens, incluindo navegações de nível superior. Sempre defina esse atributo explicitamente em vez de depender dos padrões do navegador. O suporte ao SameSite é excelente nos navegadores modernos e pode ser verificado no Can I Use.

Com SameSite=Strict ou Lax corretamente configurado, o risco de CSRF é substancialmente reduzido para a maioria das configurações de autenticação no mesmo domínio. Para endpoints sensíveis que alteram estado, combine isso com um token anti-CSRF para uma defesa em profundidade.

O Padrão Utilizado pela Maioria das Aplicações Modernas

Muitas aplicações em produção dividem o problema da seguinte forma:

  1. Tokens de acesso de curta duração armazenados na memória JavaScript (uma variável em nível de módulo ou estado React).
  2. Tokens de atualização armazenados em um cookie HttpOnly, Secure e SameSite.

O token de acesso desaparece ao fechar a aba ou atualizar a página, mas uma chamada silenciosa ao endpoint /refresh recupera um novo token usando o cookie. O token de acesso nunca toca o armazenamento persistente, e o token de atualização nunca é legível via JavaScript.

Essa abordagem está alinhada com as diretrizes atuais para aplicações baseadas em navegador que utilizam OAuth 2.0 com PKCE (Authorization Code Flow com PKCE), que é o que as diretrizes do OAuth 2.0 para Aplicações Baseadas em Navegador recomendam. Se você estiver trabalhando com OpenID Connect (OIDC), o mesmo padrão se aplica — mantenha os ID tokens e tokens de atualização fora do localStorage.

Lista de Verificação de Segurança

Antes de publicar, verifique:

  • Flag HttpOnly definida em qualquer cookie que armazene tokens.
  • Flag Secure habilitada (HTTPS obrigatório).
  • SameSite explicitamente definido como Strict ou Lax.
  • Tokens de acesso com curta duração, tipicamente medida em minutos e não em horas.
  • Cabeçalhos de Content Security Policy configurados.
  • Nenhum JWT de longa duração armazenado no localStorage.

Escolhendo a Abordagem Correta para Sua Aplicação

Não existe uma resposta universal. Se você controla seu backend e serve sua aplicação a partir do mesmo domínio, cookies HttpOnly com configuração adequada de SameSite são o padrão mais seguro. Se você está integrando com uma API de terceiros que exige cabeçalhos Authorization e não pode definir cookies no lado do servidor, o armazenamento em memória com curta expiração é uma alternativa razoável — apenas nunca persista tokens de longa duração no localStorage.

Conclusão

JWTs de longa duração no localStorage são exatamente o que as diretrizes de segurança atuais desaconselham de forma consistente. Cookies HttpOnly com os atributos Secure e SameSite oferecem o padrão mais robusto para a maioria das configurações no mesmo domínio, enquanto o armazenamento em memória combinado com um cookie de token de atualização cobre os casos mais complexos. Uma vez que você compreende o modelo de ameaças — XSS de um lado, CSRF do outro — a escolha correta para sua aplicação se torna um tradeoff sobre o qual você pode raciocinar com clareza, em vez de uma suposição.

Perguntas Frequentes

O sessionStorage compartilha a mesma vulnerabilidade do localStorage: qualquer JavaScript em execução na página pode lê-lo. A única diferença é que o sessionStorage é limpo quando a aba é fechada. Isso reduz a janela de exposição, mas não protege contra XSS. Para armazenamento de tokens, trate o sessionStorage com o mesmo cuidado que o localStorage e evite armazenar tokens de longa duração nele.

O SameSite=Strict impede que cookies sejam enviados em requisições entre origens, o que bloqueia a maioria dos padrões de ataque CSRF. No entanto, para endpoints de alto valor que alteram estado, adicionar um token anti-CSRF oferece uma defesa em profundidade. O SameSite é aplicado pelo navegador, portanto clientes mais antigos ou casos extremos incomuns podem não respeitá-lo. O padrão de double-submit token continua sendo uma salvaguarda sensata.

Um intervalo comum é de 5 a 15 minutos. Curto o suficiente para que um token roubado tenha valor limitado, mas longo o suficiente para evitar sobrecarga no seu endpoint de atualização. Combine isso com um token de atualização de maior duração (horas a dias) em um cookie HttpOnly. Se sua aplicação lida com operações sensíveis, como pagamentos, prefira expiração mais curta e exija reautenticação para ações críticas.

Armazene o token de acesso na memória JavaScript — uma variável de módulo, estado React ou um closure — em vez de no localStorage. Mantenha-o com curta duração e atualize-o por meio de um endpoint de backend quando possível. Se você precisar persistir algo entre recarregamentos, direcione o fluxo de atualização pelo seu próprio backend, que mantém a credencial de longa duração no lado do servidor, e nunca exponha tokens de longa duração ao armazenamento do cliente.

Gain control over your UX

See how users are using your site as if you were sitting next to them, learn and iterate faster with OpenReplay. — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay