Tutorial de JWT costuma terminar em jwt.sign() e um 200 OK. Produção começa no ponto em que o tutorial para: renovação de token, revogação de sessão, armazenamento no cliente e os ataques que pegam implementação ingênua.
Dois tokens, duas durações
Access token de 15 minutos limita a janela de exposição se o token vazar. Refresh token de 7 dias renova o access token sem pedir login de novo. O RFC 6749, seção 6, define esse par como grant separado:
1. login -> access token (15 min) + refresh token (7 dias)
2. access expira -> cliente chama /auth/refresh com o cookie
3. refresh expira -> usuário faz login de novo
Access token longo amplia a janela de roubo. Pedir login a cada 15 minutos manda o usuário pro suporte. O par curto e longo resolve os dois lados.
Geração com assinaturas separadas
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 }
}
Segredo único para os dois tokens transfere o vazamento de um para o outro. Chave de assinatura sai de variável de ambiente e não entra em commit.
O RFC 8725, Best Current Practice para JWT, recomenda fixar o algoritmo esperado na verificação. Sem essa fixação, a implementação aceita o campo alg do token como instrução, e um atacante monta token com alg: none ou troca HS256 por RS256 com a chave pública como segredo.
// algoritmo vem da sua configuração, do token
const payload = jwt.verify(token, ACCESS_SECRET, {
algorithms: ['RS256'],
issuer: 'https://api.example.com',
audience: 'web-app',
})
Access token em memória, refresh token em cookie
- Access token: variável em memória JavaScript. localStorage persiste o token para qualquer script XSS ler junto.
- Refresh token: cookie com
HttpOnly,SecureeSameSite=Strict. A referência do Set-Cookie na MDN descreve cada flag: HttpOnly tira o cookie do alcance dedocument.cookie, Secure restringe o envio a HTTPS, SameSite bloqueia o envio em requisição cross-site.
let accessToken: string | null = null
async function login(email: string, password: string) {
const res = await fetch('/api/auth/login', {
method: 'POST',
credentials: 'include',
body: JSON.stringify({ email, password }),
})
const data = await res.json()
accessToken = data.accessToken
}
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) {
const renewed = await refreshAccessToken()
if (renewed) return apiRequest(url, options)
window.location.href = '/login'
}
return res
}
Renovação no 401
async function refreshAccessToken(): Promise<boolean> {
try {
const res = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include',
})
if (!res.ok) return false
const data = await res.json()
accessToken = data.accessToken
return true
} catch {
return false
}
}
O cookie viaja sozinho, então o refresh não depende de nenhum estado no cliente. Duas chamadas simultâneas que disparam o refresh ao mesmo tempo podem se enfrentar quando a rotação está ligada; serialize as renovações numa fila de uma vaga por aba.
Revogação com lista negra e TTL
JWT confere assinatura sem consultar o servidor, e essa propriedade é justamente a que impede revogação automática. O RFC 8725 recomenda lista de negação para token revogado. No logout, guarde o token com o TTL igual ao tempo que falta para o exp dele:
async function logout(token: string) {
const decoded = jwt.decode(token) as { exp: number }
const ttl = decoded.exp - Math.floor(Date.now() / 1000)
await redis.setex(`blacklist:${token}`, ttl, 'revoked')
}
async function verifyToken(token: string) {
const blacklisted = await redis.get(`blacklist:${token}`)
if (blacklisted) throw new Error('Token revoked')
return jwt.verify(token, ACCESS_SECRET, { algorithms: ['RS256'] })
}
Refresh token pede a mesma atenção no logout: apague o cookie de refresh no cliente e invalide o valor do refresh no servidor. Sem isso, o refresh sobrevive ao logout e renova access token para uma sessão que o usuário encerrou.
Rotação do refresh token
Cada uso do refresh emite um refresh novo e invalida o anterior. O reuso de um refresh já consumido indica que alguém copiou o token, e a resposta certa é derrubar a árvore inteira de sessão do usuário.
O fluxo completo
Login entrega os dois tokens. A request carrega o access token no header e o cookie de refresh. Um 401 dispara a renovação; quando a renovação falha, o cliente manda o usuário para o login. Logout apaga os dois lados. O OWASP Authentication Cheat Sheet cobre o resto da lista: HTTPS em todas as rotas, expiração curta, senha com hash adequado e limite de tentativa.
Referências
- RFC 7519, JSON Web Token (Jones, Bradley e Sakimura, 2015)
- RFC 7515, JSON Web Signature (2015)
- RFC 8725, Best Current Practice for JSON Web Tokens (2020)
- RFC 6749, seção 6, Refresh Token Grant (Hardt, 2012)
- node-jsonwebtoken (documentação da biblioteca)
- OWASP Authentication Cheat Sheet
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