Autenticação JWT segura no NestJS com refresh tokens rotativos e Redis
Por John Pires · 06 de março de 2026

Contexto
JWTs (JSON Web Tokens) são amplamente usados para autenticação em APIs. Em aplicações modernas, adota-se um par de tokens: access token de curta duração e refresh token de longa duração. Access tokens são usados para autorizar requests. Refresh tokens permitem obter novos access tokens sem que o usuário faça login novamente.
No entanto, refresh tokens de longa duração introduzem risco: se um refresh token vazar, um atacante pode trocar indefinidamente por access tokens. A solução prática é usar refresh tokens rotativos (rotation) e um armazenamento server-side (Redis) para controlar validade e prevenir replay.
Explicação técnica
Princípios
Resumo do mecanismo que vamos implementar:
- Access token curto (por exemplo, 15 minutos) assinado com JWT.
- Refresh token rotativo: a cada uso do refresh token, você emite um novo refresh token (com novo jti) e invalida o anterior.
- Redis armazena o identificador do refresh token (jti) ligado a uma sessão ou usuário e tem TTL igual ao tempo de vida do refresh token.
- Detecção de replay: se um refresh token já usado for reapresentado, isso indica comprometimento e você revoga toda a sessão e outros refresh tokens relacionados.
- Armazenamento do refresh token no cliente em cookie httpOnly+secure+SameSite ou em armazenamento seguro no cliente, mas preferencialmente cookie.
Estrutura do payload JWT
Recomendações para claims:
- sub: id do usuário
- typ: 'access' ou 'refresh'
- jti: uuidv4 único por token
- sid: session id (opcional) para agrupar tokens da mesma autenticação
- iat, exp
O jti do refresh token é a chave primária para controle em Redis.
Redis: chave, valor e TTL
Convenção de chave:
refresh:jti:{jti} -> { "uid": userId, "sid": sessionId }
TTL da chave = tempo de expiração do refresh token (ex: 30 dias). Ao rotacionar, você cria uma nova chave com novo jti e deleta a chave anterior. Para detectar replay, em vez de apagar imediatamente a chave antiga, marque-a como usada por um curto período adicional (ou mantenha um registro de eventos de revogação) para distinguir reuse versus primeiro uso legítimo.
Detecção de replay e resposta
Fluxo seguro:
- Cliente envia refresh token R_old (com jti J1).
- Server verifica assinatura JWT; extrai J1 e procura refresh:jti:J1 em Redis.
- Se encontrado e estado = valid: emitir novo access token e novo refresh token (J2). Persistir refresh:jti:J2 e deletar ou marcar J1 como used e criar um marker revogado:refresh:jti:J1 com TTL longo o suficiente para detectar reuse.
- Se não encontrado, mas existir marker revogado para J1: isso indica reuse -> revogar sessão inteira e forçar logout dos dispositivos.
- Se não encontrado e sem marker, pode ser token expirado ou manipulado -> negar e exigir login.
Exemplos
Abaixo exemplos práticos em NestJS usando ioredis. Dois blocos principais: AuthService e AuthController com endpoints /login, /refresh, /logout. Também mostro um guard para refresh tokens.
Exemplo 1: AuthService (emitir, rotacionar, validar)
import { Injectable, UnauthorizedException } from '@nestjs/common'
import { JwtService } from '@nestjs/jwt'
import Redis from 'ioredis'
import { v4 as uuidv4 } from 'uuid'
@Injectable()
export class AuthService {
constructor(private jwtService: JwtService, private readonly redis: Redis) {}
private ACCESS_TTL = 60 * 15 // 15 minutes
private REFRESH_TTL = 60 * 60 * 24 * 30 // 30 days
async issueTokens(userId: string, sessionId: string) {
const accessPayload = { sub: userId, typ: 'access', sid: sessionId }
const accessToken = this.jwtService.sign(accessPayload, { expiresIn: this.ACCESS_TTL })
const jti = uuidv4()
const refreshPayload = { sub: userId, typ: 'refresh', jti, sid: sessionId }
const refreshToken = this.jwtService.sign(refreshPayload, { expiresIn: this.REFRESH_TTL })
const key = `refresh:jti:${jti}`
await this.redis.set(key, JSON.stringify({ uid: userId, sid: sessionId }), 'EX', this.REFRESH_TTL)
return { accessToken, refreshToken }
}
async rotateRefreshToken(oldJti: string, tokenPayload: any) {
const keyOld = `refresh:jti:${oldJti}`
const record = await this.redis.get(keyOld)
if (!record) {
// could be replay
const marker = await this.redis.get(`revoked:refresh:jti:${oldJti}`)
if (marker) {
throw new UnauthorizedException('Refresh token reuse detected')
}
throw new UnauthorizedException('Invalid refresh token')
}
const data = JSON.parse(record)
if (data.uid !== tokenPayload.sub) throw new UnauthorizedException('Token user mismatch')
// revoke old and record as revoked short-term to detect reuse
await this.redis.del(keyOld)
await this.redis.set(`revoked:refresh:jti:${oldJti}`, JSON.stringify({ uid: data.uid, sid: data.sid }), 'EX', 60 * 60 * 24) // keep revocation marker for 1 day
// issue new
const newJti = uuidv4()
const refreshPayload = { sub: data.uid, typ: 'refresh', jti: newJti, sid: data.sid }
const newRefresh = this.jwtService.sign(refreshPayload, { expiresIn: this.REFRESH_TTL })
const keyNew = `refresh:jti:${newJti}`
await this.redis.set(keyNew, JSON.stringify({ uid: data.uid, sid: data.sid }), 'EX', this.REFRESH_TTL)
const accessPayload = { sub: data.uid, typ: 'access', sid: data.sid }
const newAccess = this.jwtService.sign(accessPayload, { expiresIn: this.ACCESS_TTL })
return { accessToken: newAccess, refreshToken: newRefresh }
}
}
Exemplo 2: Controller e RefreshGuard
import { Controller, Post, Req, Res, Body, UseGuards, UnauthorizedException } from '@nestjs/common'
import { Response, Request } from 'express'
import { AuthService } from './auth.service'
import { JwtService } from '@nestjs/jwt'
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService, private jwtService: JwtService) {}
@Post('login')
async login(@Body() body: any, @Res() res: Response) {
// autenticar credenciais (omitido)
const userId = 'user:123'
const sessionId = 'sess:' + Date.now()
const { accessToken, refreshToken } = await this.authService.issueTokens(userId, sessionId)
// set cookie httpOnly
res.cookie('refresh_token', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 1000 * 60 * 60 * 24 * 30,
})
return res.json({ accessToken })
}
@Post('refresh')
async refresh(@Req() req: Request, @Res() res: Response) {
const token = req.cookies['refresh_token']
if (!token) throw new UnauthorizedException()
let payload: any
try { payload = this.jwtService.verify(token) } catch (e) { throw new UnauthorizedException('Invalid token') }
if (payload.typ !== 'refresh') throw new UnauthorizedException('Not a refresh token')
const oldJti = payload.jti
const tokens = await this.authService.rotateRefreshToken(oldJti, payload)
// overwrite cookie with new refresh token
res.cookie('refresh_token', tokens.refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 1000 * 60 * 60 * 24 * 30,
})
return res.json({ accessToken: tokens.accessToken })
}
@Post('logout')
async logout(@Req() req: Request, @Res() res: Response) {
const token = req.cookies['refresh_token']
if (token) {
try {
const payload: any = this.jwtService.verify(token)
const key = `refresh:jti:${payload.jti}`
await this.authService.redis.del(key)
await this.authService.redis.set(`revoked:refresh:jti:${payload.jti}`, 'logout', 'EX', 60 * 60 * 24)
} catch (e) {
// ignore invalid tokens
}
}
res.clearCookie('refresh_token')
return res.json({ ok: true })
}
}
Notas sobre implementação
Use variáveis de ambiente para chaves JWT e TTLs. Proteja a rota /refresh com rate limiting para reduzir ataques de brute force sobre tokens assinados.
Erros comuns
- Não usar jti: sem jti fica difícil invalidar tokens individuais.
- Apagar a chave antiga em Redis sem gravar um marcador de revogação: assim, quando o token antigo for reapresentado você não consegue distinguir entre token expirado e replay.
- Manter refresh tokens com TTL muito longo sem rotação: aumenta janela de comprometimento.
- Guardar refresh token no localStorage: suscetível a XSS. Prefira cookie httpOnly com SameSite.
- Não limitar o uso de refresh: permitir muitos refreshes em curto período sem proteção facilita abuso. Use rate limit por IP/e dispositivo.
- Não invalidar sessão inteira ao detectar replay: se reuse detectado, revogue todos os refresh tokens associados ao sessionId e force logout.
Checklist final
- Usar access token curto (ex: 15min) e refresh token rotativo.
- Incluir jti único em cada refresh token e armazenar no Redis com TTL.
- Ao trocar refresh token: criar novo jti, persistir novo registro e invalidar/registrar revogação do antigo.
- Manter um marcador de revogação para detectar reuse e responder revogando sessão inteira.
- Armazenar refresh token no cliente em cookie httpOnly+secure+SameSite (evitar localStorage).
- Aplicar rate limiting e monitoramento em endpoint de refresh.
- Logar eventos de refresh e de revogação para auditoria e detecção de anomalias.
- Considerar binding do refresh token ao fingerprint do dispositivo (opcional) e limitar sessões por usuário/dispositivo.
Considerações finais
Refresh tokens rotativos combinados com um armazenamento server-side (Redis) oferecem um equilíbrio entre usabilidade e segurança. A chave é tornar os refresh tokens single-use e observar reuse como um indicador de comprometimento. Implementação cuidadosa em NestJS com Redis e verificação de jti permite mitigar replay attacks e limitar o impacto de vazamentos.
Assuntos: NestJS, jwt, NoSQL, authentication, refresh-tokens
Leia também
Autorização por permissões no NestJS usando decorators e guards
Aprenda a controlar autorização por permissões no NestJS usando decorators e guards. Tutorial prático, passo a passo, com exemplos funcionais, integração JWT…
Implementando Circuit Breaker e Bulkhead em microservices NestJS com Cockatiel
Guia prático para integrar circuit breaker e bulkhead com Cockatiel em NestJS: módulo centralizado, wrapper Http resiliente, tratamento de erros e otimizações…
Interceptor global de logs e tempo no NestJS: implementar e testar
Implemente um interceptor global no NestJS para logs e medição de tempo, registre-o no bootstrap e teste com Jest e supertest. Artigo prático passo a passo…
Rate limiting por usuário no NestJS com Guard + Redis (sliding window)
Aprenda a implementar rate limiting por usuário no NestJS usando Guard + Redis com sliding window. Guia passo a passo, exemplos práticos (Lua + ioredis)…
Limitador de requisições no NestJS com Throttler + Redis: passo a passo para iniciantes
Guia prático para iniciantes: configure rate limiting no NestJS usando @nestjs/throttler e Redis. Passo a passo com exemplos funcionais, Docker, dicas de…
API REST com NestJS, Prisma e PostgreSQL: JWT, testes e otimização de performance
Guia avançado para APIs com NestJS, Prisma e PostgreSQL. Cobre autenticação JWT, testes automáticos (unit/e2e) e otimizações de performance para produção…