Os Refresh Tokens no Node.js permitem emitir novos access tokens sem pedir login completo a cada expiração. O access token permanece curto, reduzindo a janela de uso se for roubado, enquanto o refresh token representa uma sessão de longa duração e exige proteção reforçada.
Um refresh token não deve ser tratado como JWT comum armazenado indefinidamente. Rotação, detecção de reutilização, revogação por família, hash no banco, cookies seguros e auditoria são essenciais. Quando um token antigo aparece depois de ter sido rotacionado, a aplicação deve considerar possível roubo.
Neste guia, você aprenderá a gerar tokens opacos, criar famílias, rotacionar, detectar replay, armazenar com hash, usar cookies, revogar sessões, trabalhar com OAuth, dispositivos e testes concorrentes.
O que é um Refresh Token?
Refresh token é uma credencial usada para obter outro access token. O RFC 6749 do OAuth 2.0 define o conceito. O RFC 9700, OAuth 2.0 Security Best Current Practice descreve rotação e proteção moderna.
Para validação de access tokens, consulte JWT Seguro no Node.js. Para aplicações públicas, veja OAuth 2.0 com PKCE.
Access token curto
Um access token pode expirar em poucos minutos:
{
"sub": "user-42",
"aud": "api.example.com",
"exp": 1787930100,
"scope": "orders:read orders:write"
}O prazo depende do risco e da arquitetura.
Refresh token longo
O refresh token pode durar dias ou semanas, mas deve possuir expiração absoluta, inatividade e revogação.
Token opaco
const refreshToken = randomBytes(32)
.toString('base64url');Um valor aleatório é simples de revogar e não expõe claims.
Não use identificadores previsíveis
UUID comum pode ser adequado quando aleatório, mas uma sequência, timestamp ou user ID não possui entropia suficiente.
Armazenando com hash
const tokenHash = createHash('sha256')
.update(refreshToken)
.digest('hex');Como o token possui alta entropia, um hash criptográfico rápido pode ser usado. Se o banco vazar, o valor original não está disponível.
Tabela de sessões
CREATE TABLE refresh_sessions (
id UUID PRIMARY KEY,
family_id UUID NOT NULL,
user_id UUID NOT NULL,
token_hash TEXT NOT NULL UNIQUE,
parent_id UUID,
status TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
last_used_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ NOT NULL,
absolute_expires_at TIMESTAMPTZ NOT NULL,
revoked_at TIMESTAMPTZ,
replaced_by UUID,
user_agent TEXT,
ip_prefix TEXT
);Família de tokens
Todos os refresh tokens derivados de um login compartilham family_id. Isso permite revogar a sessão inteira após replay.
Rotação
- Cliente envia token atual.
- Servidor calcula o hash.
- Servidor localiza a sessão ativa.
- Servidor marca o token como usado ou substituído.
- Servidor cria um novo refresh token.
- Servidor emite novo access token.
- Cliente substitui o token antigo.
Transação de rotação
await withTransaction(pool, async client => {
const current = await findRefreshForUpdate(
client,
tokenHash
);
if (!current || current.status !== 'active') {
throw new InvalidRefreshTokenError();
}
const nextToken = generateRefreshToken();
const nextHash = hashRefreshToken(nextToken);
const nextId = crypto.randomUUID();
await client.query(`
UPDATE refresh_sessions
SET status = 'rotated',
last_used_at = now(),
replaced_by = $1
WHERE id = $2
AND status = 'active'
`, [nextId, current.id]);
await client.query(`
INSERT INTO refresh_sessions (
id, family_id, user_id, token_hash,
parent_id, status, created_at,
expires_at, absolute_expires_at
) VALUES ($1,$2,$3,$4,$5,'active',now(),$6,$7)
`, [
nextId,
current.familyId,
current.userId,
nextHash,
current.id,
slidingExpiry,
current.absoluteExpiresAt
]);
return nextToken;
});Lock da linha
Use SELECT FOR UPDATE ou update condicional para impedir que duas requisições rotacionem o mesmo token simultaneamente.
Reutilização detectada
Se um token com status rotated aparece novamente, ele pode ter sido copiado. Revogue a família:
UPDATE refresh_sessions
SET status = 'revoked',
revoked_at = now()
WHERE family_id = $1
AND status IN ('active', 'rotated');Concorrência legítima
Aplicativos podem enviar duas requisições de refresh quase simultâneas. Uma política estrita interpreta a segunda como replay. Para reduzir falsos positivos:
- serialize refresh no cliente;
- use uma janela curta de tolerância;
- retorne o mesmo resultado em retry idempotente;
- mantenha acesso controlado ao sucessor.
Grace period
Uma janela de poucos segundos pode aceitar o token anterior apenas quando contexto e sucessor correspondem. Janelas grandes enfraquecem a detecção.
Idempotência no refresh
Uma chave de operação pode permitir repetir a mesma rotação sem gerar múltiplos sucessores. Consulte Idempotência em APIs Node.js.
Expiração deslizante
Cada uso estende expires_at até um limite. Isso mantém sessões ativas, mas precisa de uma expiração absoluta.
Expiração absoluta
Mesmo com uso contínuo, a sessão deve exigir novo login após um período máximo:
absolute_expires_at = created_at + interval '90 days'Inatividade
Uma sessão não usada por 30 dias pode expirar antes do limite absoluto.
Revogação por usuário
Após mudança de senha, recuperação ou suspeita de comprometimento, revogue todas as famílias ou apenas sessões selecionadas.
Logout
Logout deve revogar a sessão no servidor, limpar cookie e impedir uso posterior do refresh token.
Logout de todos os dispositivos
UPDATE refresh_sessions
SET status = 'revoked',
revoked_at = now()
WHERE user_id = $1
AND status = 'active';Lista de dispositivos
Mostre sessões com nome aproximado, criação, último uso e localização genérica. Não transforme user agent em identidade confiável.
Revogação individual
O usuário deve conseguir encerrar uma sessão específica. Audite a ação.
Cookies seguros
Para aplicações web, armazene refresh token em cookie:
Set-Cookie: refresh_token=...;
HttpOnly;
Secure;
SameSite=Strict;
Path=/auth/refresh;
Max-Age=2592000A política SameSite depende da arquitetura.
HttpOnly
Impede leitura direta por JavaScript, reduzindo impacto de XSS sobre o token. XSS ainda pode executar requisições na sessão.
Secure
O cookie só deve trafegar por HTTPS.
Path restrito
Limitar ao endpoint de refresh reduz envio desnecessário para outras rotas.
Domain
Evite cookie em domínio pai amplo. Um subdomínio comprometido pode afetar a sessão.
SameSite
- Strict: maior proteção, pode afetar navegação externa.
- Lax: equilíbrio comum.
- None: exige Secure e proteção CSRF explícita.
CSRF
Se o refresh token está em cookie, proteja o endpoint contra CSRF. SameSite ajuda, mas integrações cross-site podem exigir token anti-CSRF.
Double submit
Um token CSRF legível pelo cliente pode ser enviado em header e comparado ao cookie correspondente, com validação adequada.
Aplicativos móveis
Armazene refresh tokens em Keychain, Keystore ou armazenamento seguro da plataforma, nunca em arquivo comum ou localStorage.
SPAs
Uma SPA pode usar BFF e cookie HttpOnly para evitar expor refresh token ao JavaScript. Consulte Backend for Frontend no Node.js.
LocalStorage
Tokens em localStorage ficam acessíveis a qualquer script executado na origem. Evite para credenciais duradouras.
Refresh token como JWT
É possível, mas a revogação e a rotação ainda exigem estado no servidor. Um JWT longo sem registro de sessão é difícil de invalidar.
Claims mínimos
Se usar JWT, não inclua dados sensíveis. Valide issuer, audience, assinatura, expiração e ID.
Token binding
DPoP ou mTLS podem vincular tokens a uma chave do cliente em arquiteturas OAuth avançadas. Isso reduz uso por quem apenas roubou o token.
PKCE
PKCE protege o authorization code, não substitui rotação de refresh tokens.
Escopos
O novo access token não deve obter escopos maiores que a sessão original. Reavalie permissões atuais se roles mudaram.
Revogação de autorização
Quando o usuário perde uma permission, o próximo refresh deve emitir claims atualizados ou negar a sessão.
MFA e nível de autenticação
A sessão pode registrar amr e auth_time. A rotação não deve fingir que MFA acabou de ocorrer.
Step-up
Para operações sensíveis, exija passkey ou TOTP recente, mesmo que a sessão possa renovar access tokens.
Consulte Passkeys no Node.js e TOTP no Node.js.
Alteração de senha
Decida se revoga todas as sessões ou preserva a atual após step-up. Para comprometimento, revogue todas.
Recuperação de conta
Depois de recovery, encerre sessões antigas, revogue tokens e notifique o usuário.
Auditoria
Registre:
- família criada;
- refresh bem-sucedido;
- reutilização detectada;
- família revogada;
- logout;
- logout global;
- sessão removida;
- recuperação de conta.
Nunca registre o token.
Veja Logs de Auditoria no Node.js.
Métricas
Monitore refresh por segundo, falhas, replay, famílias revogadas, idade das sessões e concorrência de rotação.
Alertas
Replay em várias famílias, refresh de localização muito diferente e recuperação seguida de uso antigo podem indicar ataque.
Rate limiting
Limite refresh por sessão e IP. Um loop de cliente com token inválido não deve sobrecarregar o banco.
Erros
{
"code": "SESSION_EXPIRED",
"message": "Entre novamente para continuar"
}Não revele se o token foi encontrado, rotacionado ou revogado.
Limpeza
Remova ou arquive sessões expiradas em lotes. Preserve apenas metadados necessários à auditoria.
Índices
CREATE INDEX idx_refresh_user_active
ON refresh_sessions (user_id, last_used_at DESC)
WHERE status = 'active';Transações
Rotação, sucessor e revogação precisam ser atômicos. Consulte Transações PostgreSQL no Node.js.
Testes unitários
Teste geração, hash, expiração e regras de status.
Teste de rotação
test('token antigo não permanece ativo', async () => {
const first = await login();
const second = await refresh(first.refreshToken);
await assert.rejects(() =>
refresh(first.refreshToken)
);
assert.ok(second.refreshToken);
});Teste de replay
Use o token antigo após a rotação e confirme revogação da família.
Teste concorrente
Envie duas rotações em paralelo. Confirme apenas um sucessor ou o comportamento idempotente previsto.
Teste de expiração absoluta
Mesmo com refresh recente, uma família além do limite deve exigir login.
Teste de revogação
Remova uma sessão e confirme que access token futuro não pode ser renovado.
Teste de cookie
Confirme HttpOnly, Secure, SameSite, Path e ausência do token no corpo.
Teste de CSRF
Uma requisição cross-site sem proteção deve falhar.
Erros comuns
- Refresh token sem rotação: roubo permanece válido por muito tempo.
- Token em texto claro no banco: vazamento permite sessões.
- JWT stateless longo: revogação fica difícil.
- Cookie sem HttpOnly: scripts leem a credencial.
- Sem CSRF: cookie pode ser usado por origem maliciosa.
- Sem expiração absoluta: sessão dura para sempre.
- Replay ignorado: token roubado continua circulando.
Boas práticas
- Use tokens opacos e aleatórios.
- Armazene somente hash.
- Rotacione em todo uso.
- Use famílias.
- Detecte reutilização.
- Revogue a família no replay.
- Defina inatividade e expiração absoluta.
- Use cookie HttpOnly ou storage seguro.
- Proteja CSRF.
- Audite sem registrar tokens.
Conclusão
Os Refresh Tokens no Node.js permitem access tokens curtos sem exigir login constante, mas representam uma sessão duradoura e valiosa para um atacante.
Rotação, hash, famílias e detecção de reutilização transformam o refresh token em uma credencial controlável. Com cookies seguros, CSRF, expiração absoluta e revogação por dispositivo, a aplicação mantém conveniência sem aceitar sessões invisíveis e impossíveis de encerrar.




