Refresh tokens: estratégias seguras de renovação

1. Fundamentos dos Refresh Tokens

Refresh tokens são credenciais de longa duração utilizadas para obter novos access tokens sem exigir que o usuário se autentique novamente. Enquanto o access token (geralmente um JWT) tem validade curta — de 5 a 15 minutos — o refresh token pode durar dias ou semanas.

A principal diferença está no escopo de risco: um access token vaza? O dano é limitado ao seu TTL. Um refresh token vaza? O atacante pode renovar sessões indefinidamente. Por isso, refresh tokens exigem proteção extra.

Cenários típicos:
- APIs REST: servidores trocam refresh tokens por novos access tokens via endpoint dedicado
- SPAs: refresh tokens armazenados em HttpOnly cookies para mitigar XSS
- Mobile apps: refresh tokens protegidos por Secure Enclave (iOS) ou Android Keystore

2. Ciclo de Vida e Rotação Segura

O fluxo padrão de renovação segue este padrão:

POST /api/auth/refresh
Authorization: Bearer <refresh_token>

Resposta 200 OK:
{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "refresh_token": "dGhpcyBpcyBhIG5ldyByZWZyZXNo...",
  "expires_in": 900
}

Rotação automática (token rotation) é a prática de emitir um novo refresh token a cada renovação e invalidar o anterior. Isso limita a janela de exploração caso um token seja roubado.

TTL recomendado:
- Refresh tokens para aplicações web: 7 a 30 dias
- Refresh tokens para mobile: até 90 dias (com possibilidade de renovação silenciosa)
- Refresh tokens para serviços críticos: 24 horas (com reautenticação obrigatória)

3. Armazenamento Seguro do Refresh Token

No Backend

Nunca armazene refresh tokens em texto puro. Faça hash com SHA-256:

// Geração do hash
const crypto = require('crypto');
const hash = crypto.createHash('sha256').update(refreshToken).digest('hex');
// Armazene hash no banco junto com user_id, family_id, expiração

No Frontend (SPA)

Método Segurança Risco
HttpOnly cookie Alta CSRF (mitigável com SameSite=Strict)
localStorage Baixa XSS pode ler o token
sessionStorage Média Perde ao fechar aba

Recomendação: Sempre prefira HttpOnly cookies com atributos Secure, SameSite=Strict e HttpOnly.

Em Mobile

  • iOS: Keychain com kSecAttrAccessible = kSecAttrAccessibleWhenUnlockedThisDeviceOnly
  • Android: EncryptedSharedPreferences + Android Keystore

4. Proteção Contra Ataques Comuns

Replay Attacks

Cada refresh token deve conter um identificador único (jti - JWT ID). O servidor deve armazenar jtis já utilizados para evitar reuso:

Payload do refresh token:
{
  "sub": "user123",
  "jti": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "exp": 1700000000,
  "family_id": "fam_xyz789"
}

Roubo de Tokens

Implemente fingerprint do cliente — colete User-Agent, IP, device ID e armazene junto ao token. Na renovação, compare os dados:

// Validação de fingerprint
if (storedFingerprint.ip !== currentRequest.ip) {
  // Possível roubo: invalida toda a família de tokens
  invalidateFamily(token.family_id);
  return 403 Forbidden;
}

Binding do Token ao Cliente

Use token binding criptográfico: o refresh token é assinado com uma chave derivada do client_id. Assim, mesmo que vaze, não pode ser usado por outro cliente:

const clientSecret = getClientSecret(clientId);
const bindingKey = crypto.createHmac('sha256', clientSecret)
  .update(refreshToken).digest('hex');
// Armazene bindingKey junto ao token no banco

5. Revogação e Blacklist de Refresh Tokens

Blacklist em Cache (Redis)

Use Redis com TTL igual ao tempo restante do token:

// Adicionar à blacklist
SET blacklist:<jti> "revoked" EX <ttl_restante>

// Verificar na renovação
if (redis.exists(`blacklist:${jti}`)) {
  return 401 Unauthorized;
}

Revogação por Usuário

Ao alterar senha, invalide todos os tokens do usuário:

// Incrementa versão de segurança do usuário
UPDATE users SET token_version = token_version + 1 WHERE id = $1;

// Na validação do refresh token
if (token.token_version !== user.token_version) {
  return 401 "Token revogado por mudança de senha";
}

Revogação por Família

Quando um token é rotacionado, o token pai é invalidado. Se alguém tentar usar o token antigo, detectamos roubo:

// Na rotação
const parentJti = getParentJti(request.refreshToken);
if (redis.exists(`used_jti:${parentJti}`)) {
  // Token pai já foi usado — possivelmente roubado
  invalidateFamily(token.family_id);
  return 403 "Possível roubo de token detectado";
}
redis.set(`used_jti:${parentJti}`, "used", "EX", 86400);

6. Monitoramento e Logging de Renovações

Cada renovação deve gerar um log estruturado:

{
  "event": "token_refresh",
  "user_id": "user123",
  "jti": "a1b2c3d4...",
  "family_id": "fam_xyz789",
  "timestamp": "2024-01-15T10:30:00Z",
  "fingerprint": {
    "ip": "192.168.1.100",
    "user_agent": "Mozilla/5.0...",
    "device_id": "device_abc"
  },
  "status": "success"
}

Alertas para padrões anômalos:
- Mais de 3 renovações em 1 minuto
- Renovação com fingerprint diferente do original
- Tentativa de uso de jti já utilizado

Auditoria: Mantenha histórico de tokens ativos por usuário para rastreamento de sessões.

7. Considerações de Implementação

Estrutura do Refresh Token

// Payload completo
{
  "sub": "user_abc123",
  "jti": "uuid-v4",
  "exp": 1700100000,
  "iat": 1699500000,
  "family_id": "fam_001",
  "token_version": 3,
  "client_id": "web_app_1"
}

Endpoint de Renovação

POST /api/auth/refresh
Content-Type: application/json
Authorization: Bearer <current_refresh_token>

{
  "client_id": "web_app_1",
  "fingerprint": {
    "ip": "192.168.1.100",
    "user_agent": "Mozilla/5.0..."
  }
}

Respostas:
- 200 OK: { access_token, refresh_token, expires_in }
- 401: Token expirado ou revogado (cliente deve reautenticar)
- 403: Roubo suspeito (invalida família, cliente deve reautenticar)

Tratamento de Erros

Código Significado Ação do Cliente
401 Token expirado ou revogado Reautenticar
403 Roubo detectado Reautenticar + alerta de segurança
429 Muitas requisições Aguardar e tentar novamente

Referências