Neste capítulo, o foco é lidar com um problema inevitável em SPAs autenticadas: o token de acesso (access token) expira. Se a aplicação não tratar isso corretamente, o usuário sofre com erros 401 no meio de uma ação, telas quebradas, perda de dados em formulários e uma sensação de “sessão instável”. A estratégia mais comum para reduzir atrito é combinar um access token de curta duração com um refresh token de duração maior, permitindo renovar a sessão de forma transparente e controlada.
Conceitos essenciais: access token, refresh token e expiração

Em uma arquitetura baseada em JWT (ou tokens opacos), normalmente existem dois “tipos” de credenciais:
Access token: é o token enviado em cada requisição para APIs protegidas (geralmente no header Authorization). Deve ter vida curta (por exemplo, 5 a 15 minutos). Se vazar, o impacto é limitado no tempo.
Refresh token: é usado para obter um novo access token quando o atual expira. Deve ter vida mais longa (dias ou semanas), mas com controles mais rígidos. Idealmente, ele não é enviado em todas as requisições, apenas no endpoint de renovação.
Expiração é o momento em que o servidor passa a rejeitar o access token. Mesmo que o cliente “ache” que o token ainda é válido (por relógio local), o servidor é a fonte de verdade. Por isso, o tratamento deve ser robusto para o caso em que o servidor responde 401/403 por token expirado ou inválido.
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...Baixar o aplicativo
Por que não usar apenas um token longo?
Um access token longo aumenta a janela de risco caso ele seja comprometido (por XSS, vazamento de logs, extensões maliciosas, etc.). Separar access e refresh permite que o access token seja curto e rotativo, enquanto o refresh token fica mais protegido e com regras de rotação e revogação.
O que significa “renovar sessão” na prática?
Renovar sessão significa: ao detectar que o access token expirou (ou está prestes a expirar), o front-end chama um endpoint de refresh, recebe um novo access token (e possivelmente um novo refresh token), atualiza o estado de autenticação e reexecuta a requisição que falhou, sem exigir que o usuário faça login novamente.
Estratégias de renovação: proativa vs reativa
Existem duas abordagens principais, que podem ser combinadas:
Renovação proativa: o cliente tenta renovar antes de expirar (por exemplo, quando faltam 60 segundos). Isso reduz a chance de uma requisição falhar no meio de uma ação.
Renovação reativa: o cliente tenta renovar quando recebe um 401/403 por token expirado. É mais simples, mas pode gerar uma primeira falha e precisa de lógica para repetir a requisição.
Na prática, muitas SPAs adotam renovação reativa com repetição automática, e opcionalmente adicionam um “timer” proativo para melhorar UX em fluxos críticos.
Requisitos no back-end (o que o front-end precisa assumir)
Mesmo sendo um capítulo focado em React, a estratégia de refresh token depende de contratos do servidor. O front-end precisa saber:
Endpoint de refresh: por exemplo,
POST /auth/refresh.Forma de envio do refresh token: comumente em cookie HttpOnly (recomendado) ou no corpo da requisição (menos recomendado).
Resposta do refresh: geralmente retorna um novo access token e, em rotação, um novo refresh token.
Erros esperados: se o refresh token expirou/revogado, o servidor retorna 401 e o cliente deve forçar logout.
Do ponto de vista de segurança, uma prática forte é refresh token rotation: a cada refresh, o servidor emite um novo refresh token e invalida o anterior. Isso reduz o impacto de roubo do refresh token, mas exige que o cliente atualize o token armazenado (ou confie no cookie atualizado automaticamente).
Passo a passo prático: fluxo completo de refresh no front-end

A seguir está um passo a passo prático para implementar renovação de sessão com refresh token em uma SPA React. O exemplo usa axios para facilitar interceptação e repetição de requisições, mas a lógica é aplicável a fetch.
1) Defina o contrato de tokens no estado da aplicação
Você precisa de um lugar central para ler e atualizar o access token atual. Como capítulos anteriores já cobriram modelagem de estado e interceptação, aqui vamos focar no que muda para suportar refresh:
Um método
setAccessToken(token)para atualizar o token em memória.Um método
clearSession()para limpar estado e redirecionar quando refresh falhar.Um método
refreshSession()que chama o endpoint de refresh e atualiza o access token.
Um ponto importante: para reduzir exposição, é comum manter o access token em memória e usar refresh token em cookie HttpOnly. Assim, após reload, você pode “reidratar” a sessão chamando refresh (ou um endpoint de “me”) para obter um novo access token.
2) Crie uma função de refresh com controle de concorrência
Um erro comum é disparar múltiplos refresh simultâneos quando várias requisições recebem 401 ao mesmo tempo. Isso pode causar corrida, invalidar refresh tokens rotacionados e gerar loops. A solução é implementar um “lock” (promessa compartilhada) para garantir que apenas um refresh ocorra por vez.
import axios from 'axios'; let refreshPromise = null; export function createAuthApi({ getAccessToken, setAccessToken, clearSession }) { const api = axios.create({ baseURL: '/api', withCredentials: true }); async function refreshSession() { if (!refreshPromise) { refreshPromise = api.post('/auth/refresh') .then((res) => { const newAccessToken = res.data.accessToken; setAccessToken(newAccessToken); return newAccessToken; }) .catch((err) => { clearSession(); throw err; }) .finally(() => { refreshPromise = null; }); } return refreshPromise; } api.interceptors.request.use((config) => { const token = getAccessToken(); if (token) { config.headers = config.headers ?? {}; config.headers.Authorization = `Bearer ${token}`; } return config; }); api.interceptors.response.use( (response) => response, async (error) => { const originalRequest = error.config; const status = error.response?.status; const isAuthError = status === 401; const isRefreshEndpoint = originalRequest?.url?.includes('/auth/refresh'); if (!isAuthError || isRefreshEndpoint) { return Promise.reject(error); } if (originalRequest._retry) { return Promise.reject(error); } originalRequest._retry = true; try { const newToken = await refreshSession(); originalRequest.headers = originalRequest.headers ?? {}; originalRequest.headers.Authorization = `Bearer ${newToken}`; return api.request(originalRequest); } catch (e) { return Promise.reject(e); } } ); return { api, refreshSession }; }Nesse exemplo:
withCredentials: truepermite enviar cookies (onde o refresh token pode estar).refreshPromisegarante que múltiplos 401 aguardem o mesmo refresh.originalRequest._retryevita loop infinito se o refresh não resolver.Se o refresh falhar,
clearSession()é chamado para forçar logout e limpar UI.
3) Diferencie “token expirado” de “não autenticado” quando possível
Nem todo 401 é expiração. Pode ser token inválido, usuário desativado, sessão revogada, clock skew, etc. O ideal é o back-end retornar um código/erro específico (por exemplo, error: 'token_expired') para que o cliente só tente refresh quando fizer sentido. Se isso não existir, você pode tentar refresh em 401, mas com cuidado para não mascarar problemas.
Uma variação do interceptor acima é checar um campo no body:
const reason = error.response?.data?.error; const shouldTryRefresh = status === 401 && reason === 'token_expired';Se o servidor não fornece isso, uma estratégia é: tentar refresh uma vez em 401 e, se falhar, encerrar sessão.
4) Garanta que o endpoint de refresh não use o mesmo interceptor de refresh (evitar recursão)
Se o refresh endpoint retornar 401, o interceptor não deve tentar refresh novamente, senão cria recursão. Por isso o exemplo bloqueia quando isRefreshEndpoint é verdadeiro.
5) Reidratação de sessão ao carregar a aplicação
Quando o usuário recarrega a página, o access token em memória se perde. Se você usa refresh token em cookie HttpOnly, você pode reidratar chamando refresh ao iniciar a app (ou chamando um endpoint /auth/session / /me que devolva um access token novo).
Exemplo de fluxo em um provider:
import { useEffect, useState } from 'react'; export function useSessionBootstrap({ refreshSession, clearSession }) { const [bootstrapped, setBootstrapped] = useState(false); useEffect(() => { let cancelled = false; (async () => { try { await refreshSession(); } catch (e) { // Se não houver refresh válido, apenas garante estado limpo clearSession(); } finally { if (!cancelled) setBootstrapped(true); } })(); return () => { cancelled = true; }; }, [refreshSession, clearSession]); return { bootstrapped }; }O objetivo é evitar “piscar” entre área pública e autenticada de forma errática. Você pode usar bootstrapped para renderizar um estado de carregamento até decidir se há sessão válida.
Tratamento de expiração durante ações do usuário (UX e integridade)
Mesmo com refresh automático, há casos em que a expiração acontece no meio de uma ação sensível, como salvar um formulário grande. Boas práticas:
Repetir requisições idempotentes automaticamente: GETs e algumas PUTs podem ser repetidas com segurança, mas POSTs podem criar duplicidade se o servidor não for idempotente.
Para POSTs críticos, use idempotency key: envie um header como
Idempotency-Keygerado no cliente para que o servidor evite duplicar a operação se a requisição for reenviada após refresh.Preservar estado local do formulário: se o refresh falhar e você precisar redirecionar para login, mantenha rascunho local (state, storage temporário) para o usuário não perder dados.
Se você optar por repetir automaticamente qualquer requisição após refresh, combine com idempotência no back-end para evitar efeitos colaterais.
Renovação proativa com base em exp (quando fizer sentido)
Se o access token for um JWT e você tiver acesso ao payload, pode ler a claim exp para saber quando expira. Isso permite renovar antes. Atenção: isso é uma otimização de UX, não uma garantia de validade, pois o servidor pode revogar tokens antes do expirar.
Exemplo de utilitário simples para calcular tempo restante:
export function getJwtExpMs(accessToken) { try { const payload = JSON.parse(atob(accessToken.split('.')[1])); if (!payload.exp) return null; return payload.exp * 1000; } catch { return null; } } export function shouldRefreshSoon(accessToken, thresholdMs = 60_000) { const expMs = getJwtExpMs(accessToken); if (!expMs) return false; return expMs - Date.now() <= thresholdMs; }Você pode usar isso para disparar refresh em background quando o usuário está ativo. Evite rodar timers agressivos; prefira checar em eventos relevantes (troca de rota, foco na aba, antes de chamadas importantes) ou usar um intervalo moderado.
Evite armadilhas comuns: loops, tempestade de refresh e estados inconsistentes


Loop infinito de refresh
Acontece quando:
O refresh endpoint retorna 401 e o interceptor tenta refresh novamente.
O servidor retorna 401 por outro motivo e o cliente insiste em refresh sem limite.
Mitigações:
Marcar requisição como
_retrye tentar apenas uma vez.Não interceptar o endpoint de refresh.
Se refresh falhar, limpar sessão e parar tentativas.
Tempestade de refresh (múltiplas abas)
Se o usuário abre várias abas, cada uma pode tentar refresh ao mesmo tempo. Dependendo do back-end e rotação de refresh token, isso pode invalidar tokens e derrubar a sessão em uma das abas.
Mitigações possíveis:
Sincronização entre abas: usar
BroadcastChannelpara que uma aba “líder” faça refresh e compartilhe o novo access token com as demais.Back-end tolerante: permitir uma pequena janela de reutilização do refresh token anterior (grace period) em rotação, reduzindo falhas por corrida.
Exemplo de sincronização com BroadcastChannel (conceito):
const channel = new BroadcastChannel('auth'); export function broadcastAccessToken(token) { channel.postMessage({ type: 'ACCESS_TOKEN_UPDATED', token }); } export function listenAccessTokenUpdates(setAccessToken) { channel.onmessage = (event) => { if (event.data?.type === 'ACCESS_TOKEN_UPDATED') { setAccessToken(event.data.token); } }; }Ao concluir o refresh em uma aba, você chama broadcastAccessToken. As outras abas atualizam o token em memória e evitam refresh desnecessário.
Requisições pendentes durante logout forçado
Se o refresh falhar e você limpar sessão, ainda podem existir requisições em andamento que vão falhar e disparar mensagens de erro redundantes. Uma abordagem é cancelar requisições pendentes (axios cancel token / AbortController) quando clearSession() ocorre, ou pelo menos suprimir notificações quando a causa for “sessão encerrada”.
Refresh token em cookie HttpOnly: implicações práticas no front-end
Quando o refresh token está em cookie HttpOnly:
O JavaScript não consegue ler o refresh token diretamente (isso é bom para reduzir impacto de XSS).
Você precisa usar
withCredentials(axios) oucredentials: 'include'(fetch) para enviar o cookie ao endpoint de refresh.CORS e SameSite precisam estar corretamente configurados no servidor, especialmente se front e API estão em domínios diferentes.
Exemplo com fetch para refresh:
export async function refreshWithFetch() { const res = await fetch('/api/auth/refresh', { method: 'POST', credentials: 'include' }); if (!res.ok) throw new Error('Refresh failed'); const data = await res.json(); return data.accessToken; }Se você estiver em ambiente cross-site, o cookie pode exigir SameSite=None; Secure. Isso é configuração do servidor, mas o front-end precisa estar preparado para o fato de que, se o cookie não for enviado, o refresh sempre falhará.
Rotação e revogação: como o front-end deve reagir
Com rotação de refresh token, o servidor pode invalidar o refresh token anterior assim que emite um novo. Para o front-end:
Se o refresh token estiver em cookie, a rotação é transparente (o servidor atualiza o cookie).
Se o refresh token estiver armazenado no cliente (menos recomendado), você deve atualizar o valor sempre que o endpoint de refresh retornar um novo refresh token.
Revogação pode acontecer por logout em outro dispositivo, troca de senha, política de segurança, etc. O comportamento esperado do cliente é: ao falhar o refresh com 401, encerrar sessão local e exigir login novamente.
Checklist de implementação (para validar seu fluxo)
Ao receber 401 em endpoint protegido, a aplicação tenta refresh apenas uma vez e repete a requisição original.
O refresh é serializado (lock) para evitar múltiplas chamadas simultâneas.
O endpoint de refresh não entra em loop de interceptação.
Em falha de refresh, a sessão é encerrada e o estado da UI é limpo.
Ao carregar a aplicação, existe um bootstrap que tenta reidratar a sessão via refresh (quando aplicável).
Se houver múltiplas abas, existe estratégia para reduzir corrida (BroadcastChannel ou tolerância no back-end).
Requisições não idempotentes têm estratégia para evitar duplicidade (idempotency key ou não repetir automaticamente).