DEV Community

Yuri Peixinho
Yuri Peixinho

Posted on

Refresh Tokens e Logout

Introdução

Muitos sistemas de autenticação começam com um único token (o JWT do login), sem refresh token nenhum. Este guia explica, do zero, o que é um refresh token, por que ele existe, como ele muda o que significa "logout", e como isso se encaixa numa arquitetura em camadas típica.

O problema: um token só não dá pra otimizar duas coisas ao mesmo tempo. Quando um usuário faz login, a API devolve um token JWT que vale por um tempo configurado, por exemplo 60 minutos. Esse token é a única prova de identidade que o front-end tem.

  • Segurança: se esse token vazar (XSS, um backup mal guardado, um log que capturou o header Authorization), quem pegou ele consegue agir como o usuário até o token expirar — e não tem botão de pânico. Não existe "revogar esse token específico" num JWT puro (explico o porquê na próxima seção). Então, quanto mais curto o tempo de vida, menor a janela de estrago.
  • Conveniência: se o token expira rápido (ex.: 5 minutos), o usuário é deslogado toda hora e precisa digitar a senha de novo o tempo todo, essa prática leva para o usuário péssima experiência.

Com um token só, você é obrigado a escolher um meio-termo ruim: nem seguro o suficiente, nem confortável o suficiente. A ideia do refresh token é separar essas duas responsabilidades em dois tokens diferentes, cada um otimizado pra uma coisa, ao invés de um token só tentando fazer as duas mal feitas.

Access token vs. refresh token

Access token Refresh token
Pra que serve Provar sua identidade em cada requisição à API Só pra pedir um access token novo, quando o antigo expira
Formato JWT (auto-contido: carrega os dados e a assinatura dentro dele) Normalmente uma string aleatória opaca — não é um JWT
Tempo de vida Curto: 5 a 15 minutos Longo: dias a semanas
Onde a API guarda Em lugar nenhum (é stateless: ela só confere a assinatura e a data) Numa tabela do banco, com hash, data de expiração e se foi revogado
Dá pra revogar antes de expirar? Não. Uma vez emitido, nenhuma ação do servidor o invalida antes da hora — ele só confere assinatura + prazo, sem olhar pra nenhuma lista Sim. É só marcar a linha dele como revogado no banco; a próxima vez que alguém tentar usá-lo, a API recusa
Onde fica guardado no navegador Normalmente em memória/localStorage Idealmente num cookie HttpOnly (próxima seção explica por quê)

O ponto central dessa tabela é a linha "dá pra revogar": um JWT é stateless de propósito, essa é a graça dele, permite validar sem bater no banco a cada requisição. Mas isso tem um preço: ninguém consegue "desligar" um JWT antes da hora. O refresh token existe justamente pra carregar esse fardo: ele tem estado no banco, porque sua função inteira depende de poder ser revogado.

Onde cada token deveria morar no navegador

Muitas aplicações front-end guardam tokens no localStorage. Isso funciona, mas tem uma vulnerabilidade conhecida: qualquer JavaScript que rode na sua página consegue ler o localStorage. Se um dia entrar uma dependência npm maliciosa, ou um ataque de XSS (alguém injeta um script via um campo mal sanitizado), esse script consegue roubar o token com uma linha de código que lê o localStorage diretamente.

Pra um access token de vida curta (5-15 min), esse risco é tolerável — na pior das hipóteses, o estrago dura poucos minutos. Mas um refresh token que vive por semanas não pode correr esse risco: se vazar, o atacante tem acesso por semanas, não minutos.

A solução padrão da indústria: guardar o refresh token num cookie HttpOnly. Um cookie marcado HttpOnly não pode ser lido por JavaScript nenhum — só o navegador o envia automaticamente nas requisições pro domínio certo, e só o servidor consegue lê-lo. Some mais duas flags:

  • Secure: só trafega em HTTPS (nunca em texto plano numa rede).
  • SameSite=Strict (ou Lax): o navegador não manda esse cookie em requisições vindas de outros sites, o que ajuda contra CSRF.

Ou seja: o access token continua vulnerável a XSS (tolerado, pela vida curta), mas o refresh token fica numa caixa que nem o próprio código da sua aplicação consegue abrir — só o navegador e a API.

O fluxo completo, passo a passo

  1. Login. O usuário manda e-mail + senha. A API confere e devolve dois tokens: um access token (JWT, 10 min) no corpo da resposta, e um refresh token (string aleatória, 30 dias) num cookie HttpOnly. A API também salva um registro desse refresh token no banco (hash dele, não o valor puro).
  2. Uso normal. O front-end manda o access token no header Authorization: Bearer ... em toda chamada, como é comum. Enquanto ele não expira, nada muda.
  3. Expiração. Depois de 10 minutos, uma chamada qualquer volta 401 Unauthorized — o access token venceu.
  4. Refresh automático. O interceptor HTTP do seu cliente (ex.: Axios) percebe o 401, chama POST /auth/refresh sem precisar que o usuário faça nada (o cookie HttpOnly vai junto sozinho, o navegador cuida disso). A API confere o refresh token no banco: existe, não expirou, não foi revogado, então devolve um access token novo.
  5. Repete a chamada original. O interceptor pega esse access token novo e refaz a chamada que tinha tomado 401, automaticamente. O usuário não percebe nada — nenhuma tela de login aparece, a página continua funcionando.
  6. Isso se repete por até 30 dias (a vida do refresh token), sem o usuário nunca precisar digitar a senha de novo — contanto que ele volte a usar o sistema antes do refresh token também vencer.
  7. Se o refresh token também expirar (30 dias sem usar o sistema) ou for invalidado (logout, veja mais abaixo): o passo 4 falha, e aí sim o usuário é mandado pra tela de login.

Rotação: a parte que detecta roubo

Tem uma pergunta incômoda no fluxo acima: e se alguém roubar o refresh token (não o access token, o refresh)? Ele vale por 30 dias, isso é um problema sério.

A defesa chama-se rotação de refresh token: toda vez que o refresh token é usado pra pedir um access token novo (passo 4 do fluxo acima), a API não só devolve o access token, ela também invalida o refresh token usado e emite um novo no lugar. Ou seja, cada refresh token só serve uma vez; usar gera o próximo da cadeia.

Por que isso ajuda a detectar roubo? Pensa no cenário: alguém copiou seu refresh token (token A) sem você saber. Duas pessoas agora têm o mesmo token A: você e o atacante. Em algum momento, um dos dois vai usar o token A primeiro — digamos que seja você. Isso gera o token B, e A fica marcado como "já usado". Mais tarde, o atacante tenta usar o token A (que ele ainda tem, achando que ainda vale), a API vê um token já marcado como usado sendo reapresentado. Isso só acontece numa de duas situações: um bug, ou um roubo. Por segurança, a reação correta é tratar como roubo: revogar a família inteira de tokens daquela sessão (A, B e qualquer um que viesse depois), obrigando os dois — você e o atacante — a fazer login de novo. Incomoda você uma vez, mas tira o acesso do atacante.

Isso só é possível porque o refresh token tem estado no banco (volte à tabela comparátiva): sem isso, não teria como saber que um token já foi "gasto".

Logout: o que muda de sentido

Num sistema que usa só um JWT, sem refresh token, normalmente não existe endpoint de logout, porque não há nada no servidor pra desligar. O JWT é stateless, então "sair" é só apagar o token do armazenamento local do navegador.

Com refresh token, isso muda: agora existe uma linha no banco representando aquela sessão (o registro do refresh token). Um POST /auth/logout passaria a ter trabalho de verdade pra fazer: marcar aquele refresh token como revogado. A partir daí:

  • O refresh token não serve mais pra pedir um access token novo (passo 4 do fluxo falha).
  • O access token que ainda estiver circulando (se alguém tiver roubado, por exemplo) continua valendo até expirar sozinho — mas isso agora são minutos, não a sessão inteira.

É a primeira vez que "logout" significa revogar algo no servidor, e não só limpar o navegador. Esse é o ganho concreto de ter um refresh token: dá ao sistema um botão de pânico que ele simplesmente não tem pra um logout normal, nem pra reagir a um token roubado.

Top comments (1)

Collapse
 
alexshev profile image
Alex Shev •

Logout is where token design becomes a product promise. Rotating refresh-token families and revoking the family server-side gives you a concrete answer to theft and logout-from-all-devices, while short-lived access tokens keep the compromise window bounded. Client-side deletion alone is only cosmetic.