Capa do Ebook gratuito Roteamento e Autenticação em React: Construindo SPAs Seguras com React Router e JWT

Roteamento e Autenticação em React: Construindo SPAs Seguras com React Router e JWT

Novo curso

20 páginas

Armazenamento de tokens no front-end: localStorage, sessionStorage e cookies com trade-offs

Capítulo 8

Tempo estimado de leitura: 12 minutos

+ Exercício
Audio Icon

Ouça em áudio

0:00 / 0:00

Ao autenticar uma SPA com JWT (ou qualquer token de acesso), uma decisão prática aparece rapidamente: onde armazenar o token no front-end. Essa escolha afeta segurança, experiência do usuário, comportamento em múltiplas abas, persistência após fechar o navegador e até o desenho do fluxo de renovação (refresh). Não existe uma opção “perfeita”; existem trade-offs que precisam ser entendidos para escolher conscientemente.

Neste capítulo, você vai comparar localStorage, sessionStorage e cookies (incluindo cookies HttpOnly) sob a ótica de SPAs com React e APIs com JWT, com exemplos práticos e um passo a passo de implementação para cada abordagem.

O que exatamente estamos armazenando?

Em autenticação baseada em tokens, normalmente existem dois “tipos” de token:

  • Access token: curto (minutos), enviado em cada requisição para autorizar acesso a recursos.
  • Refresh token: mais longo (dias/semanas), usado para obter um novo access token quando ele expira.

Um padrão comum e mais seguro é: access token em memória (não persistente) e refresh token em cookie HttpOnly (persistente e inacessível ao JavaScript). Mas há cenários em que você pode optar por persistir o access token também, ou não usar refresh token e exigir login novamente.

Independentemente do padrão, o ponto central é: qual superfície de ataque você aceita (XSS, CSRF, roubo por extensões, vazamento por logs) e qual UX você deseja (lembrar login, manter sessão entre abas, etc.).

Continue em nosso aplicativo

Você poderá ouvir o audiobook com a tela desligada, ganhar gratuitamente o certificado deste curso e ainda ter acesso a outros 5.000 cursos online gratuitos.

ou continue lendo abaixo...
Download App

Baixar o aplicativo

Critérios para decidir: segurança e comportamento

Ameaças principais

  • XSS (Cross-Site Scripting): se um atacante conseguir executar JavaScript na sua página, ele pode ler dados acessíveis via JS (localStorage, sessionStorage, cookies não-HttpOnly) e exfiltrar tokens.
  • CSRF (Cross-Site Request Forgery): se sua autenticação depende de cookies enviados automaticamente pelo navegador, um site malicioso pode disparar requisições para sua API usando o cookie da vítima (a menos que você mitigue com SameSite/CSRF token).
  • Persistência indevida: tokens que ficam no dispositivo podem ser usados por outra pessoa (computador compartilhado) ou por malware/extensões.
  • Exposição acidental: tokens em storage podem parar em prints, logs, backups do navegador, ferramentas de debug, etc.

Comportamento esperado

  • Persistir após fechar o navegador? localStorage e cookies persistem; sessionStorage não.
  • Compartilhar sessão entre abas? localStorage é compartilhado entre abas do mesmo origin; sessionStorage é isolado por aba; cookies valem para todas as abas.
  • Sincronizar logout entre abas? localStorage facilita via evento storage; cookies exigem outra estratégia (ex.: BroadcastChannel, polling, ou resposta 401 global).
  • Enviar token automaticamente? cookies são enviados automaticamente (com regras); localStorage/sessionStorage exigem adicionar header manualmente.

localStorage: simples e persistente, mas exposto a XSS

localStorage é um armazenamento chave-valor persistente por origin. Ele é fácil de usar, sobrevive a reload e fechamento do navegador, e é compartilhado entre abas. O grande problema: qualquer JavaScript rodando na página pode ler. Em caso de XSS, o token pode ser roubado.

Quando faz sentido

  • Aplicações internas/ambientes controlados com baixo risco e forte mitigação de XSS.
  • Protótipos e MVPs (com consciência do risco).
  • Quando você precisa de persistência simples e não quer lidar com cookies/CSRF.

Trade-offs

  • Pró: simples; persistente; fácil de depurar; não depende de configuração de cookie/CORS.
  • Contra: vulnerável a XSS; token fica acessível a extensões; exige adicionar header manualmente; revogação é difícil se o token for longo.

Passo a passo: armazenar e usar access token no localStorage

1) Defina funções utilitárias para set/get/remove:

const TOKEN_KEY = "access_token";export function setAccessToken(token) {  localStorage.setItem(TOKEN_KEY, token);}export function getAccessToken() {  return localStorage.getItem(TOKEN_KEY);}export function clearAccessToken() {  localStorage.removeItem(TOKEN_KEY);}

2) Configure um cliente HTTP (ex.: fetch wrapper) para anexar o token:

export async function apiFetch(url, options = {}) {  const token = getAccessToken();  const headers = new Headers(options.headers || {});  if (token) headers.set("Authorization", `Bearer ${token}`);  const res = await fetch(url, { ...options, headers });  if (res.status === 401) {    // opcional: limpar token e sinalizar logout    clearAccessToken();  }  return res;}

3) No login, após receber o token, persista:

async function login(email, password) {  const res = await fetch("/api/auth/login", {    method: "POST",    headers: { "Content-Type": "application/json" },    body: JSON.stringify({ email, password })  });  if (!res.ok) throw new Error("Falha no login");  const data = await res.json();  setAccessToken(data.accessToken);  return data;}

4) No logout, remova:

function logout() {  clearAccessToken();}

Mitigações mínimas se você usar localStorage

  • Evite token de longa duração (curto + refresh em canal mais seguro).
  • Reduza risco de XSS: sanitize de HTML, evite dangerouslySetInnerHTML, valide entradas, use CSP (Content Security Policy).
  • Não armazene dados sensíveis extras junto do token (perfil completo, permissões detalhadas, etc.).

sessionStorage: reduz persistência, mantém exposição a XSS

sessionStorage é parecido com localStorage, mas com duas diferenças importantes: ele é isolado por aba e é apagado ao fechar a aba (ou o navegador, dependendo do comportamento). Isso diminui o risco em dispositivos compartilhados e reduz o tempo de exposição do token em repouso, mas não resolve XSS, porque ainda é acessível via JavaScript.

Quando faz sentido

  • Você quer que o usuário precise logar novamente ao fechar a aba.
  • Você quer evitar que uma aba “herde” a sessão de outra (isolamento por aba).
  • Você quer persistência apenas durante a sessão atual, mas ainda precisa sobreviver a reload.

Trade-offs

  • Pró: menos persistente; reduz impacto em máquinas compartilhadas; simples.
  • Contra: ainda vulnerável a XSS; não compartilha sessão entre abas; UX pode ser pior (login mais frequente).

Passo a passo: trocar localStorage por sessionStorage

1) Troque as funções utilitárias:

const TOKEN_KEY = "access_token";export function setAccessToken(token) {  sessionStorage.setItem(TOKEN_KEY, token);}export function getAccessToken() {  return sessionStorage.getItem(TOKEN_KEY);}export function clearAccessToken() {  sessionStorage.removeItem(TOKEN_KEY);}

2) O resto do fluxo (anexar no header Authorization, limpar em 401, etc.) permanece igual.

3) Se você precisa sincronizar logout entre abas, sessionStorage não ajuda (cada aba tem o seu). Uma alternativa é: ao receber 401, forçar logout local e redirecionar; outra é usar BroadcastChannel para comunicar eventos entre abas:

const authChannel = new BroadcastChannel("auth");export function broadcastLogout() {  authChannel.postMessage({ type: "LOGOUT" });}export function onAuthMessage(handler) {  authChannel.onmessage = (event) => handler(event.data);}

Ao fazer logout em uma aba, você envia a mensagem e as outras abas limpam seu sessionStorage e atualizam a UI.

Cookies: convenientes, mas exigem atenção a CSRF e atributos

Cookies podem armazenar tokens e são enviados automaticamente pelo navegador em requisições para o domínio/rota correspondente. Isso pode simplificar o cliente (você não precisa anexar Authorization manualmente), mas muda o modelo de ameaça: CSRF passa a ser uma preocupação central.

Além disso, cookies têm atributos que impactam diretamente segurança e comportamento:

  • HttpOnly: impede leitura via JavaScript. Ajuda muito contra exfiltração por XSS (o atacante não consegue “ler” o cookie), embora XSS ainda possa fazer ações em nome do usuário se a sessão estiver ativa.
  • Secure: cookie só é enviado via HTTPS.
  • SameSite: controla envio em navegação cross-site. Valores comuns: Lax, Strict, None (este exige Secure). É uma mitigação importante contra CSRF.
  • Path e Domain: restringem onde o cookie é enviado.

Cookie não-HttpOnly (token acessível via JS)

Armazenar token em cookie sem HttpOnly é, na prática, parecido com localStorage do ponto de vista de XSS (o JS consegue ler). A diferença é que o cookie pode ser enviado automaticamente, o que pode aumentar risco de CSRF se você também depender dele para autenticar.

Em geral, se você vai usar cookie para token, prefira HttpOnly e desenhe o fluxo para não precisar ler o token no JS.

Cookie HttpOnly (recomendado para refresh token)

O padrão mais usado em SPAs seguras é:

  • Refresh token em cookie HttpOnly + Secure + SameSite.
  • Access token em memória (variável/estado) e renovado via endpoint de refresh.

Assim, mesmo que ocorra XSS, o atacante não consegue extrair o refresh token do cookie. Ainda assim, XSS é grave (pode disparar requisições enquanto a sessão existir), mas você reduz a chance de “roubo reutilizável” do token.

Trade-offs de cookies

  • Pró: com HttpOnly, token não fica acessível ao JS; envio automático; boa compatibilidade com SSR e APIs tradicionais.
  • Contra: exige tratar CSRF; CORS e credenciais precisam estar corretos; SameSite pode impactar cenários cross-site; depuração pode ser menos direta.

Passo a passo prático: autenticação com refresh token em cookie HttpOnly

O objetivo aqui é: o front-end não armazena refresh token em lugar nenhum acessível ao JS. O servidor define o cookie no login e o navegador o envia automaticamente no refresh.

1) Backend: endpoint de login setando cookie HttpOnly

Exemplo conceitual (Node/Express) do que o servidor precisa fazer:

// POST /api/auth/login// valida credenciais, gera accessToken e refreshTokenres.cookie("refresh_token", refreshToken, {  httpOnly: true,  secure: true,          // em produção  sameSite: "lax",       // ou "strict" dependendo do fluxo  path: "/api/auth/refresh"});res.json({ accessToken });

Pontos importantes:

  • path restrito ao endpoint de refresh reduz exposição do cookie.
  • sameSite: se seu front e API estão no mesmo site (mesmo “site” para o navegador), Lax costuma funcionar bem. Se você precisa de cross-site (domínios diferentes em contexto de terceiro), pode precisar de SameSite=None; Secure, o que aumenta exigência de mitigação de CSRF.

2) Front-end: guardar access token em memória

Você pode manter o access token em uma variável de módulo ou em estado (ex.: Context). O ponto é: não persistir. Exemplo simples com variável de módulo:

let inMemoryAccessToken = null;export function setInMemoryToken(token) {  inMemoryAccessToken = token;}export function getInMemoryToken() {  return inMemoryAccessToken;}export function clearInMemoryToken() {  inMemoryAccessToken = null;}

3) Front-end: login recebe access token e não persiste em storage

export async function login(email, password) {  const res = await fetch("/api/auth/login", {    method: "POST",    headers: { "Content-Type": "application/json" },    credentials: "include",    body: JSON.stringify({ email, password })  });  if (!res.ok) throw new Error("Falha no login");  const data = await res.json();  setInMemoryToken(data.accessToken);  return data;}

Note o credentials: "include": ele é necessário para o navegador aceitar cookies em cenários com CORS e para enviar cookies em requisições subsequentes quando apropriado.

4) Front-end: endpoint de refresh para obter novo access token

export async function refreshAccessToken() {  const res = await fetch("/api/auth/refresh", {    method: "POST",    credentials: "include"  });  if (!res.ok) throw new Error("Refresh falhou");  const data = await res.json();  setInMemoryToken(data.accessToken);  return data.accessToken;}

5) Front-end: wrapper de fetch com retry após 401

Quando uma requisição falhar com 401 por expiração do access token, você tenta refresh e repete a requisição uma vez:

export async function apiFetch(url, options = {}) {  const headers = new Headers(options.headers || {});  const token = getInMemoryToken();  if (token) headers.set("Authorization", `Bearer ${token}`);  let res = await fetch(url, { ...options, headers, credentials: "include" });  if (res.status !== 401) return res;  // tenta renovar e repetir  try {    const newToken = await refreshAccessToken();    const retryHeaders = new Headers(options.headers || {});    retryHeaders.set("Authorization", `Bearer ${newToken}`);    res = await fetch(url, { ...options, headers: retryHeaders, credentials: "include" });    return res;  } catch {    clearInMemoryToken();    return res;  }}

Esse padrão evita que o usuário seja desconectado imediatamente quando o access token expira, sem precisar persistir tokens em storage.

6) Backend: endpoint de logout limpando cookie

// POST /api/auth/logoutres.clearCookie("refresh_token", {  path: "/api/auth/refresh"});res.status(204).send();

E no front-end:

export async function logout() {  await fetch("/api/auth/logout", { method: "POST", credentials: "include" });  clearInMemoryToken();}

CSRF em autenticação por cookie: como pensar e mitigar

Se a sua API autentica o usuário com base em cookie (especialmente se você não usa Authorization header), você precisa mitigar CSRF. Mesmo usando Authorization header para recursos, o endpoint de refresh e logout normalmente depende do cookie.

Camadas comuns de mitigação:

  • SameSite: Lax ou Strict já reduz bastante CSRF em muitos cenários. None é mais permissivo e exige mais cuidado.
  • CSRF token: o servidor emite um token anti-CSRF (não HttpOnly) e o front envia em header (ex.: X-CSRF-Token) em requisições sensíveis. O servidor valida se o token confere com o esperado para aquela sessão/cookie.
  • Verificação de Origin/Referer: para requisições state-changing, validar Origin e/ou Referer contra domínios permitidos.
  • Evitar efeitos colaterais em GET: endpoints que alteram estado devem ser POST/PUT/PATCH/DELETE.

Exemplo de envio de CSRF token (conceitual):

// suponha que o backend disponibilize um endpoint que retorna csrfTokenexport async function getCsrfToken() {  const res = await fetch("/api/auth/csrf", { credentials: "include" });  const data = await res.json();  return data.csrfToken;}export async function apiFetchWithCsrf(url, options = {}) {  const csrf = await getCsrfToken();  const headers = new Headers(options.headers || {});  headers.set("X-CSRF-Token", csrf);  return fetch(url, { ...options, headers, credentials: "include" });}

Comparativo direto: qual escolher?

Se você prioriza segurança contra roubo de token

  • Preferência: refresh token em cookie HttpOnly + access token em memória.
  • Motivo: reduz exfiltração por XSS (não elimina XSS, mas diminui impacto de roubo persistente).

Se você prioriza simplicidade de implementação no front-end

  • Preferência: localStorage (ou sessionStorage) com Authorization header.
  • Cuidado: invista em mitigação de XSS e mantenha tokens curtos.

Se você quer “lembrar login” sem complexidade de refresh

  • Preferência: localStorage com token de duração moderada.
  • Risco: maior janela de exposição se o token for roubado.

Se você quer sessão que morre ao fechar a aba

  • Preferência: sessionStorage (ou apenas memória).
  • Observação: memória é ainda mais restritiva (perde no reload), sessionStorage sobrevive a reload.

Armadilhas comuns e boas práticas

1) Guardar token e dados sensíveis juntos

Evite armazenar no storage objetos completos de usuário com permissões detalhadas, e-mails, etc. Guarde o mínimo necessário. Se precisar de dados do usuário, busque do backend e mantenha em memória.

2) Token longo demais

Quanto maior a validade, maior o impacto de vazamento. Prefira access tokens curtos e, se necessário, refresh com rotação no servidor.

3) Falta de rotação/revogação

Se você usa refresh token, considere rotação (refresh token muda a cada uso) e invalidação no servidor em logout/comprometimento. Isso reduz reutilização em caso de vazamento.

4) CORS e credenciais mal configurados (cookies)

Para cookies funcionarem em chamadas cross-origin, o backend precisa responder com headers adequados e o front precisa usar credentials: "include". Além disso, Access-Control-Allow-Origin não pode ser * quando Allow-Credentials está habilitado.

5) Confundir “token inacessível ao JS” com “app imune a XSS”

Cookie HttpOnly impede leitura do token via JS, mas XSS ainda pode disparar ações autenticadas (porque o navegador envia cookies automaticamente). Por isso, XSS deve ser tratado como prioridade independentemente do mecanismo de armazenamento.

6) Sincronização entre abas

Se você usa localStorage, pode sincronizar logout com o evento storage:

window.addEventListener("storage", (e) => {  if (e.key === "access_token" && e.newValue === null) {    // outra aba fez logout: atualize a UI local  }});

Com cookies HttpOnly, você tende a depender mais de: resposta 401 global, BroadcastChannel, ou um endpoint de “me”/sessão para revalidar periodicamente.

Agora responda o exercício sobre o conteúdo:

Em uma SPA que busca reduzir o risco de roubo reutilizavel de tokens em caso de XSS, qual combinacao de armazenamento e fluxo tende a ser mais indicada?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

A combinacao de access token em memoria e refresh token em cookie HttpOnly reduz a chance de exfiltracao do refresh token via JavaScript em XSS e permite renovar o access token sem persistir tokens em storage.

Próximo capitúlo

Interpretação e validações no cliente: JWT, claims e limites do front-end

Arrow Right Icon
Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.