Refresh tokens permitem renovar access tokens sem exigir login completo a cada poucos minutos. Eles devem ser tratados como credenciais de longa duração: armazenados com segurança, rotacionados a cada uso, revogados no logout e protegidos contra replay.
Um bom desenho separa access token curto, usado nas APIs, de refresh token opaco, usado apenas no endpoint de renovação.
Gerando um token opaco
import crypto from 'node:crypto';
function createRefreshToken() {
return crypto.randomBytes(48).toString('base64url');
}
function hashToken(token) {
return crypto.createHash('sha256').update(token).digest('hex');
}Armazene somente o hash no banco. Se o banco vazar, os valores não podem ser usados diretamente.
Registro da sessão
await sessions.insert({
id: crypto.randomUUID(),
userId: user.id,
tokenHash: hashToken(refreshToken),
familyId: crypto.randomUUID(),
expiresAt,
createdAt: new Date(),
revokedAt: null,
});O familyId conecta tokens gerados pela mesma sessão e permite revogar toda a cadeia em caso de replay.
Cookie seguro
res.cookie('__Host-refresh', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
path: '/auth/refresh',
maxAge: 1000 * 60 * 60 * 24 * 30,
});Em aplicações realmente cross-site, SameSite pode precisar de none. Nesse caso, exija validação de Origin e proteção CSRF.
Rotação a cada uso
Quando um token válido é apresentado, marque-o como consumido e gere outro na mesma transação:
await database.transaction(async (tx) => {
const session = await tx.sessions.findByTokenHash(hashToken(token), {
forUpdate: true,
});
validateSession(session);
await tx.sessions.markRotated(session.id, new Date());
const nextToken = createRefreshToken();
await tx.sessions.insert({
userId: session.userId,
tokenHash: hashToken(nextToken),
familyId: session.familyId,
parentId: session.id,
expiresAt: session.expiresAt,
});
return nextToken;
});A operação precisa ser atômica para impedir duas renovações simultâneas com o mesmo token.
Detecção de replay
Se um token já rotacionado aparecer novamente, ele pode ter sido copiado. Revogue toda a família:
if (session.rotatedAt) {
await sessions.revokeFamily(session.familyId, 'refresh_token_reuse');
throw new AuthenticationError('Sessão revogada');
}O usuário precisará autenticar novamente. Registre o evento sem guardar o token.
Concorrência legítima
Duas abas podem tentar renovar ao mesmo tempo. Uma tolerância curta ou mecanismo de resultado reutilizável pode reduzir falsos positivos, mas amplia a janela de replay. Avalie o risco e mantenha a lógica simples sempre que possível.
Expiração absoluta e ociosa
Use dois limites:
- expiração absoluta da sessão;
- expiração por inatividade.
Rotacionar não deve renovar indefinidamente o limite absoluto, a menos que a política de negócio determine.
Endpoint de refresh
app.post('/auth/refresh', validateOrigin, async (req, res) => {
const token = req.cookies['__Host-refresh'];
if (!token) {
res.status(401).json({ error: 'missing_refresh_token' });
return;
}
const result = await rotateRefreshToken(token);
setRefreshCookie(res, result.refreshToken);
res.set('Cache-Control', 'no-store');
res.json({ accessToken: result.accessToken });
});Não aceite refresh token na query string. Limite body, frequência e tempo de processamento.
Access token curto
O endpoint deve emitir access token com validade curta, issuer e audience corretos. Não reutilize o refresh token como Bearer token de API.
Logout da sessão atual
await sessions.revokeByTokenHash(hashToken(token), 'logout');
res.clearCookie('__Host-refresh', {
secure: true,
sameSite: 'strict',
path: '/auth/refresh',
});Limpar o cookie sem revogar o servidor deixa uma cópia roubada válida.
Logout de todos os dispositivos
Revogue todas as famílias do usuário. Isso é útil após troca de senha, suspeita de invasão ou solicitação explícita.
Lista de sessões
Permita que o usuário veja dispositivos e revogue sessões. Armazene apenas metadados mínimos: data, último uso, nome aproximado do dispositivo e IP truncado quando permitido pela política de privacidade.
Vinculação ao dispositivo
User-Agent e IP mudam e podem gerar bloqueios indevidos. Use-os como sinal de risco, não como única prova. Fingerprinting agressivo pode violar privacidade.
MFA e elevação de privilégio
Uma sessão criada antes do MFA não deve receber privilégios elevados automaticamente. Registre nível de autenticação e exija step-up para operações críticas.
Troca de senha
Após troca ou recuperação de senha, revogue sessões antigas conforme a política. Se o usuário suspeita de invasão, a revogação global deve ser padrão.
Armazenamento em aplicativo móvel
Use armazenamento seguro da plataforma, como Keychain ou Keystore. Não grave tokens em arquivos, logs, backups não protegidos ou preferências comuns.
JWT como refresh token
É possível, mas um token opaco facilita revogação, rotação e redução de dados. Se usar JWT, ainda mantenha estado da sessão para detectar replay.
Rate limiting
Limite por sessão, usuário e IP. Muitas falhas podem indicar token expirado, aplicativo antigo ou ataque. Não bloqueie uma conta inteira por uma única origem sem considerar negação de serviço.
CSRF
Quando o refresh token está em cookie, valide Origin, use SameSite e considere token CSRF. CORS sozinho não impede envio cross-site.
Cache
Respostas de login e refresh devem incluir Cache-Control: no-store. Verifique proxies e CDNs para garantir que tokens não sejam armazenados.
Logs e observabilidade
Registre ID da sessão, usuário, resultado, reason code e request ID. Nunca registre token, cookie ou Authorization. Monitore replays, famílias revogadas, falhas e taxa de rotação.
Erros comuns
- refresh token sem rotação;
- armazenar token em texto puro no banco;
- não detectar reutilização;
- validade ilimitada;
- cookie sem Secure ou HttpOnly;
- não revogar no logout;
- renovação não atômica;
- registrar tokens;
- usar refresh token nas APIs;
- ignorar CSRF.
Testes
Teste token válido, expirado, revogado, rotacionado, reutilizado, concorrência, família revogada, logout, troca de senha, cookie ausente e Origin inválida.
Fluxo recomendado
- gere token opaco aleatório;
- armazene hash;
- envie em cookie seguro;
- rotacione atomicamente;
- detecte replay;
- revogue a família;
- use expiração absoluta;
- proteja refresh com CSRF;
- redija logs;
- permita revogação pelo usuário.
Combine refresh tokens com JWT no Node.js, Cookies Seguros, CSRF, Idempotency Keys e Secret Management.
Consulte o BCP de segurança OAuth e o guia de sessões da OWASP.



