A maioria dos tutoriais de JWT termina com jwt.sign() e um 200 OK. Produção começa onde o tutorial termina: como renovar tokens, como invalidar sessões, como armazenar tokens no cliente, e como proteger contra ataques.
Por que JWT + Refresh Token
Access tokens curtos (15 min) limitam a janela de exposição se um token for comprometido. Refresh tokens longos (7 dias) permitem renovar o access token sem pedir login novamente. O ciclo:
1. Usuário faz login → recebe access token (15min) + refresh token (7 dias)
2. Access token expira → cliente usa refresh token para renovar
3. Refresh token expira → usuário faz login novamente
Sem refresh tokens, você precisa de access tokens longos (perigoso) ou pedir login a cada 15 minutos (ruim para experiência).
Geração de tokens
import jwt from 'jsonwebtoken'
const ACCESS_SECRET = process.env.JWT_ACCESS_SECRET
const REFRESH_SECRET = process.env.JWT_REFRESH_SECRET
function generateTokens(user: { id: string; email: string }) {
const accessToken = jwt.sign(
{ sub: user.id, email: user.email, type: 'access' },
ACCESS_SECRET,
{ expiresIn: '15m' }
)
const refreshToken = jwt.sign(
{ sub: user.id, type: 'refresh' },
REFRESH_SECRET,
{ expiresIn: '7d' }
)
return { accessToken, refreshToken }
}
Dois secrets diferentes. Se o refresh secret vazar, o access token não é afetado. Nunca use o mesmo secret para ambos.
Storage seguro no cliente
O armazenamento do access token no cliente é debatido, mas a prática segura é:
- Access token: memória JavaScript (variável). Não localStorage (vulnerável a XSS), não cookie HttpOnly (compartilha entre abas, mas funciona para muitos casos).
- Refresh token: cookie HttpOnly, Secure, SameSite=Strict. Nunca JavaScript deve acessar o refresh token.
// No login, armazena access token em memória
let accessToken: string | null = null
async function login(email: string, password: string) {
const res = await fetch('/api/auth/login', {
method: 'POST',
credentials: 'include', // envia cookies
body: JSON.stringify({ email, password })
})
const data = await res.json()
accessToken = data.accessToken // em memória, não em storage
}
// Em cada request, usa o access token
async function apiRequest(url: string, options: RequestInit = {}) {
const res = await fetch(url, {
...options,
credentials: 'include',
headers: {
...options.headers,
Authorization: `Bearer ${accessToken}`
}
})
if (res.status === 401) {
// Access token expirado: tenta renovar
const renewed = await refreshAccessToken()
if (renewed) {
return apiRequest(url, options) // tenta novamente
}
window.location.href = '/login'
}
return res
}
Refresh token flow
async function refreshAccessToken(): Promise<boolean> {
try {
const res = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include' // envia cookie HttpOnly
})
if (!res.ok) return false
const data = await res.json()
accessToken = data.accessToken
return true
} catch {
return false
}
}
Invalidação de tokens
JWT por si só não suporta invalidação (é stateless). Para invalidar sessões, mantenha uma lista de tokens revogados no servidor:
// Ao fazer logout, armazena o token na blacklist
async function logout(token: string) {
const decoded = jwt.decode(token)
const ttl = decoded.exp - Math.floor(Date.now() / 1000)
// Armazena na blacklist com TTL igual ao tempo restante do token
await redis.setex(`blacklist:${token}`, ttl, 'revoked')
}
// Middleware verifica a blacklist
async function verifyToken(token: string) {
const isBlacklisted = await redis.get(`blacklist:${token}`)
if (isBlacklisted) throw new Error('Token revoked')
return jwt.verify(token, ACCESS_SECRET)
}
Proteções obrigatórias
- HTTPS em todas as rotas. Sem exceção. Tokens em HTTP são interceptados.
- HttpOnly + Secure + SameSite em cookies de refresh.
- Nunca armazenar tokens em localStorage. Vulnerável a XSS.
- Validar
iss,aud, eexpem cada request. Não confie apenas na assinatura. - Usar algoritmo forte (RS256) em produção. HS256 funciona mas é menos seguro para distribuído.
O fluxo completo
Login → access + refresh tokens. Request com access token. Se 401, tenta refresh. Se refresh falha, redireciona para login. Logout invalida ambos os tokens. Refresh tokens rodam em background antes de expirar.
Isso é o mínimo para autenticação JWT em produção. Tudo abaixo disso é prototype, não sistema de autenticação.
Curtiu o conteúdo?
Construo produtos web e soluções com IA do jeito certo — arquitetura sólida, código sustentável e entrega real.
Vamos conversar