Pular para o conteúdo
segurança

JWT e Refresh Tokens: Autenticação Stateless Segura em Node.js

Implemente autenticação segura com JWT e refresh tokens em Node.js: geração, validação, rotação de tokens, revogação e proteção contra ataques comuns.

Douglas M. Pereira5 min de leitura
jwtautenticaçãorefresh tokennodejssegurançabackend

O problema com JWT puro

JWT (JSON Web Token) é frequentemente mal implementado. O pattern mais comum — emitir um JWT com expiração longa (7 dias, 30 dias) — é inseguro: se o token vazar, o atacante tem acesso por todo o período de validade, sem como revogar.

A solução é usar tokens de curta duração com refresh tokens.

O fluxo correto: Access Token + Refresh Token

AUTENTICAÇÃO INICIAL
  ↓ POST /auth/login (credenciais)
  ↓ servidor verifica usuário
  ← access_token (15 min) + refresh_token (7 dias, httpOnly cookie)

USO NORMAL
  ↓ GET /api/dados (Authorization: Bearer access_token)
  ← dados

RENOVAÇÃO (quando access_token expira)
  ↓ POST /auth/refresh (envia refresh_token via cookie)
  ← novo access_token (15 min) + novo refresh_token rotacionado

O refresh token vai no cookie httpOnly (inacessível ao JavaScript), prevenindo XSS. O access token vai no header Authorization para cada requisição.

Implementação em Node.js/TypeScript

Geração de tokens

import jwt from 'jsonwebtoken'
import { randomBytes } from 'crypto'

const ACCESS_TOKEN_SECRET = process.env.JWT_ACCESS_SECRET!
const REFRESH_TOKEN_SECRET = process.env.JWT_REFRESH_SECRET!

interface TokenPayload {
  sub: string  // userId
  email: string
  role: string
}

export function generateAccessToken(payload: TokenPayload): string {
  return jwt.sign(payload, ACCESS_TOKEN_SECRET, {
    expiresIn: '15m',
    algorithm: 'HS256',
    issuer: 'minha-empresa.com',
    audience: 'minha-empresa-api',
  })
}

export function generateRefreshToken(): string {
  // Refresh token é opaque — apenas um ID aleatório
  // Os dados ficam no banco, não no token
  return randomBytes(64).toString('hex')
}

Armazenando refresh tokens no banco

// prisma/schema.prisma (exemplo)
model RefreshToken {
  id          String   @id @default(cuid())
  token       String   @unique
  userId      String
  user        User     @relation(fields: [userId], references: [id])
  expiresAt   DateTime
  revokedAt   DateTime?
  createdAt   DateTime @default(now())
  userAgent   String?
  ipAddress   String?
}

// Salvar o refresh token
async function saveRefreshToken(
  userId: string,
  token: string,
  req: Request
) {
  const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)  // 7 dias
  
  await db.refreshToken.create({
    data: {
      token: await hash(token),  // nunca guardar o token em texto puro
      userId,
      expiresAt,
      userAgent: req.headers['user-agent'],
      ipAddress: req.ip,
    }
  })
}

Endpoint de login

// POST /auth/login
export async function login(req: Request, res: Response) {
  const { email, password } = req.body
  
  const user = await db.user.findUnique({ where: { email } })
  if (!user || !await bcrypt.compare(password, user.passwordHash)) {
    // Mesma mensagem para usuário não existente ou senha errada
    // (previne user enumeration)
    return res.status(401).json({ error: 'Credenciais inválidas' })
  }
  
  const accessToken = generateAccessToken({
    sub: user.id,
    email: user.email,
    role: user.role,
  })
  
  const refreshToken = generateRefreshToken()
  await saveRefreshToken(user.id, refreshToken, req)
  
  // Refresh token em cookie httpOnly
  res.cookie('refresh_token', refreshToken, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'strict',
    maxAge: 7 * 24 * 60 * 60 * 1000,  // 7 dias em ms
    path: '/auth',  // cookie só enviado para /auth/*
  })
  
  return res.json({ access_token: accessToken })
}

Endpoint de refresh

// POST /auth/refresh
export async function refresh(req: Request, res: Response) {
  const refreshToken = req.cookies['refresh_token']
  if (!refreshToken) return res.status(401).json({ error: 'Token faltando' })
  
  // Buscar no banco (comparar hash)
  const storedToken = await db.refreshToken.findFirst({
    where: {
      token: await hash(refreshToken),
      revokedAt: null,
      expiresAt: { gt: new Date() },
    },
    include: { user: true }
  })
  
  if (!storedToken) {
    // Token não encontrado: pode ser ataque de replay
    // Revogar todos os tokens do usuário (se conseguirmos identificá-lo)
    return res.status(401).json({ error: 'Token inválido' })
  }
  
  // Rotacionar: revogar o token atual e emitir novo
  await db.refreshToken.update({
    where: { id: storedToken.id },
    data: { revokedAt: new Date() }
  })
  
  const newAccessToken = generateAccessToken({
    sub: storedToken.user.id,
    email: storedToken.user.email,
    role: storedToken.user.role,
  })
  
  const newRefreshToken = generateRefreshToken()
  await saveRefreshToken(storedToken.user.id, newRefreshToken, req)
  
  res.cookie('refresh_token', newRefreshToken, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'strict',
    maxAge: 7 * 24 * 60 * 60 * 1000,
    path: '/auth',
  })
  
  return res.json({ access_token: newAccessToken })
}

Middleware de autenticação

export function authenticate(req: Request, res: Response, next: NextFunction) {
  const token = req.headers.authorization?.replace('Bearer ', '')
  if (!token) return res.status(401).json({ error: 'Token requerido' })
  
  try {
    const payload = jwt.verify(token, ACCESS_TOKEN_SECRET, {
      algorithms: ['HS256'],
      issuer: 'minha-empresa.com',
      audience: 'minha-empresa-api',
    }) as TokenPayload
    
    req.user = payload
    next()
  } catch (err) {
    if (err instanceof jwt.TokenExpiredError) {
      return res.status(401).json({ error: 'Token expirado', code: 'TOKEN_EXPIRED' })
    }
    return res.status(401).json({ error: 'Token inválido' })
  }
}

Checklist de segurança JWT

| Item | Por quê | |---|---| | Access token com TTL curto (15 min) | Limita janela de ataque se vazar | | Refresh token em httpOnly cookie | Inacessível ao JS — previne XSS | | Refresh token opaque + salvo no banco | Permite revogação imediata | | Rotação de refresh token a cada uso | Detecta replay attacks | | Hash do refresh token no banco | Previne exposição em dump de banco | | Algoritmo HS256 ou RS256 explícito | Previne ataque do algoritmo none | | issuer e audience validados | Previne cross-service token forgery | | Logout revoga o refresh token | Sessão termina de verdade |