JWT em Node.js: access token e refresh token
JWT (JSON Web Token) é o padrão para autenticação stateless em APIs REST — o servidor não precisa guardar sessões, a identidade do usuário viaja no próprio token. A combinação de access token de curta duração com refresh token rotativo é o padrão atual de segurança para APIs de produção.
Assinar e verificar JWTs com jose
jose é a biblioteca JWT recomendada para Node.js moderno (suporta Web Crypto API):
// npm install jose
import { SignJWT, jwtVerify, type JWTPayload } from 'jose'
const ACCESS_SECRET = new TextEncoder().encode(process.env.JWT_ACCESS_SECRET!)
const REFRESH_SECRET = new TextEncoder().encode(process.env.JWT_REFRESH_SECRET!)
export interface TokenPayload extends JWTPayload {
userId: string
email: string
role: string
}
// Gerar access token (curta duração — 15 minutos):
export async function gerarAccessToken(payload: Omit<TokenPayload, keyof JWTPayload>) {
return new SignJWT(payload)
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('15m')
.setIssuer('minha-api')
.setAudience('web-app')
.sign(ACCESS_SECRET)
}
// Gerar refresh token (longa duração — 30 dias):
export async function gerarRefreshToken(userId: string) {
return new SignJWT({ userId })
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('30d')
.setJti(crypto.randomUUID()) // JWT ID único para revogação
.sign(REFRESH_SECRET)
}
// Verificar e decodificar access token:
export async function verificarAccessToken(token: string): Promise<TokenPayload> {
const { payload } = await jwtVerify(token, ACCESS_SECRET, {
issuer: 'minha-api',
audience: 'web-app',
})
return payload as TokenPayload
}Middleware de autenticação no Express
Proteger rotas com middleware que valida o JWT:
// middleware/auth.ts
import { Request, Response, NextFunction } from 'express'
import { verificarAccessToken, type TokenPayload } from '../lib/jwt'
// Estender o tipo Request para incluir o usuário:
declare global {
namespace Express {
interface Request {
usuario?: TokenPayload
}
}
}
export async function autenticar(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Token não fornecido' })
}
const token = authHeader.slice(7)
try {
req.usuario = await verificarAccessToken(token)
next()
} catch (err) {
// Distinguir token expirado de token inválido:
const isExpired = (err as Error).name === 'JWTExpired'
return res.status(401).json({
error: isExpired ? 'Token expirado' : 'Token inválido',
code: isExpired ? 'TOKEN_EXPIRED' : 'TOKEN_INVALID',
})
}
}
// Middleware de autorização por role:
export function autorizar(...roles: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
if (!roles.includes(req.usuario?.role ?? '')) {
return res.status(403).json({ error: 'Permissão negada' })
}
next()
}
}
// Usar nas rotas:
app.get('/api/admin', autenticar, autorizar('admin'), handler)Refresh token rotativo com Redis
Renovar o access token sem exigir novo login, com proteção contra roubo:
// Armazenar refresh tokens no Redis para revogação:
import { redis } from '../lib/redis'
// Ao fazer login:
app.post('/api/auth/login', async (req, res) => {
const usuario = await autenticarUsuario(req.body.email, req.body.password)
const accessToken = await gerarAccessToken({
userId: usuario.id,
email: usuario.email,
role: usuario.role,
})
const refreshToken = await gerarRefreshToken(usuario.id)
// Armazenar refresh token no Redis (30 dias):
const jti = extrairJti(refreshToken) // decodificar sem verificar para pegar jti
await redis.setex(`refresh:${jti}`, 30 * 24 * 3600, usuario.id)
// Enviar refresh token como httpOnly cookie (mais seguro que body):
res.cookie('refresh_token', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 30 * 24 * 60 * 60 * 1000,
path: '/api/auth', // só enviado para rotas de auth
})
res.json({ accessToken, expiresIn: 900 })
})
// Renovar access token:
app.post('/api/auth/refresh', async (req, res) => {
const refreshToken = req.cookies.refresh_token
if (!refreshToken) return res.status(401).json({ error: 'Refresh token ausente' })
try {
const { payload } = await jwtVerify(refreshToken, REFRESH_SECRET)
const jti = payload.jti!
// Verificar se o token está no Redis (não foi revogado):
const userId = await redis.get(`refresh:${jti}`)
if (!userId) return res.status(401).json({ error: 'Refresh token inválido ou revogado' })
// Rotação: deletar o token atual e gerar um novo par:
await redis.del(`refresh:${jti}`)
const usuario = await buscarUsuario(userId)
const novoAccessToken = await gerarAccessToken({ userId, email: usuario.email, role: usuario.role })
const novoRefreshToken = await gerarRefreshToken(userId)
const novoJti = extrairJti(novoRefreshToken)
await redis.setex(`refresh:${novoJti}`, 30 * 24 * 3600, userId)
res.cookie('refresh_token', novoRefreshToken, { httpOnly: true, secure: true, sameSite: 'strict', path: '/api/auth', maxAge: 30 * 24 * 60 * 60 * 1000 })
res.json({ accessToken: novoAccessToken, expiresIn: 900 })
} catch {
res.status(401).json({ error: 'Refresh token expirado' })
}
})Logout e revogação de tokens
Invalidar tokens antes do vencimento natural:
// Logout: revogar refresh token e bloquear access token até expirar
app.post('/api/auth/logout', autenticar, async (req, res) => {
const refreshToken = req.cookies.refresh_token
// Revogar refresh token:
if (refreshToken) {
try {
const { payload } = await jwtVerify(refreshToken, REFRESH_SECRET)
await redis.del(`refresh:${payload.jti}`)
} catch {
// Token já expirado — não precisa fazer nada
}
}
// Adicionar access token à blacklist até ele expirar naturalmente:
// (necessário para logout imediato — sem isso, o access token ainda é válido)
const accessToken = req.headers.authorization!.slice(7)
const exp = req.usuario!.exp!
const ttl = exp - Math.floor(Date.now() / 1000)
if (ttl > 0) {
await redis.setex(`blacklist:${req.usuario!.jti ?? accessToken}`, ttl, '1')
}
// Limpar cookie:
res.clearCookie('refresh_token', { path: '/api/auth' })
res.json({ message: 'Logout realizado com sucesso' })
})
// Adicionar verificação de blacklist no middleware:
export async function autenticar(req, res, next) {
// ... verificar token ...
const naBlacklist = await redis.exists(`blacklist:${payload.jti}`)
if (naBlacklist) return res.status(401).json({ error: 'Token revogado' })
// ...
}
// Revogar TODOS os tokens de um usuário (ex: troca de senha):
async function revogarTodosTokens(userId: string) {
const keys = await redis.keys(`refresh:*`)
for (const key of keys) {
if (await redis.get(key) === userId) await redis.del(key)
}
}Boas práticas de segurança para JWT
Erros comuns e como evitá-los:
// ❌ ERRO: usar algoritmo "none" (sem assinatura):
// Nunca aceite tokens com alg: "none" — desative explicitamente
// ❌ ERRO: segredo fraco:
// JWT_SECRET=secret → facilmente quebrado por brute force
// ✅ CORRETO: segredo forte (32+ bytes aleatórios):
// openssl rand -base64 32
// ❌ ERRO: armazenar dados sensíveis no payload:
// { userId: '123', senha: 'hash' } ← payload é base64, não criptografado!
// O payload de um JWT é VISÍVEL para qualquer um que tenha o token.
// ✅ CORRETO: apenas dados não-sensíveis + identificadores:
// { userId: '123', role: 'user', email: 'user@example.com' }
// ❌ ERRO: access token de longa duração:
// .setExpirationTime('30d') ← se vazar, atacante tem 30 dias
// ✅ CORRETO: access token curto + refresh token:
// .setExpirationTime('15m') ← janela de exposição mínima
// ❌ ERRO: armazenar JWT no localStorage:
// localStorage.setItem('token', jwt) ← vulnerável a XSS
// ✅ CORRETO: access token em memória (variável JS), refresh em httpOnly cookie
// ❌ ERRO: não verificar issuer e audience:
// await jwtVerify(token, secret) ← aceita tokens de outras APIs
// ✅ CORRETO:
// await jwtVerify(token, secret, { issuer: 'minha-api', audience: 'web-app' })
// ✅ Rotação automática no cliente (React/Vue):
// Interceptar 401 com código TOKEN_EXPIRED,
// chamar /api/auth/refresh, repetir a requisição original,
// nunca pedir login novamente (seamless refresh)$ runstack deploy --plan starter
Não quer configurar manualmente?
Não quer configurar manualmente? Implante o VPS para APIs em menos de 3 minutos com a Runstack. Infraestrutura da OPEN DATACENTER, com servidores no Brasil.
Perguntas frequentes
Conteúdos relacionados