A autenticação JWT (JSON Web Token) é o padrão para proteger APIs REST em aplicativos Node.js. Em 2026, os padrões de implementação estão bem estabelecidos – mas muitos tutoriais ignoram detalhes críticos de segurança, como rotação de token de atualização, gerenciamento adequado de segredos e invalidação de token. Este guia cobre a implementação completa e pronta para produção.
📋 Table of Contents
Como funciona a autenticação JWT
- O usuário faz login com credenciais → o servidor as valida
- O servidor emite dois tokens:token de acesso (curta duração, 15min) eatualizar token (vida longa, 7 dias)
- Cliente envia token de acesso no cabeçalho de autorização para cada solicitação
- Quando o token de acesso expira, o cliente envia o token de atualização para obter um novo token de acesso
- Ao sair, o token de atualização é invalidado no lado do servidor
Configuração
npm install express jsonwebtoken bcryptjs cookie-parser ioredis
npm install -D @types/jsonwebtoken @types/bcryptjs
Serviço de token
// services/tokenService.js
const jwt = require('jsonwebtoken');
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
const ACCESS_SECRET = process.env.JWT_ACCESS_SECRET;
const REFRESH_SECRET = process.env.JWT_REFRESH_SECRET;
// CRITICAL: Use different secrets for access and refresh tokens
function generateAccessToken(payload) {
return jwt.sign(payload, ACCESS_SECRET, {
expiresIn: '15m',
issuer: 'myapp',
audience: 'myapp-users',
});
}
function generateRefreshToken(payload) {
return jwt.sign(payload, REFRESH_SECRET, {
expiresIn: '7d',
issuer: 'myapp',
audience: 'myapp-users',
jwtid: crypto.randomUUID(), // unique ID for invalidation
});
}
function verifyAccessToken(token) {
return jwt.verify(token, ACCESS_SECRET, {
issuer: 'myapp',
audience: 'myapp-users',
});
}
async function verifyRefreshToken(token) {
const payload = jwt.verify(token, REFRESH_SECRET, {
issuer: 'myapp',
audience: 'myapp-users',
});
// Check token hasn't been revoked
const isRevoked = await redis.get(`revoked:${payload.jti}`);
if (isRevoked) throw new Error('Token revoked');
return payload;
}
async function revokeRefreshToken(token) {
try {
const payload = jwt.decode(token);
if (payload?.jti && payload?.exp) {
const ttl = payload.exp - Math.floor(Date.now() / 1000);
if (ttl > 0) {
await redis.setex(`revoked:${payload.jti}`, ttl, '1');
}
}
} catch (err) {
// Ignore decode errors on logout
}
}
module.exports = {
generateAccessToken, generateRefreshToken,
verifyAccessToken, verifyRefreshToken, revokeRefreshToken
};
Rotas de autenticação
// routes/auth.js
const express = require('express');
const bcrypt = require('bcryptjs');
const router = express.Router();
const { generateAccessToken, generateRefreshToken,
verifyRefreshToken, revokeRefreshToken } = require('../services/tokenService');
const { findUserByEmail } = require('../db/users');
// POST /auth/login
router.post('/login', async (req, res) => {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ error: 'Email and password required' });
}
const user = await findUserByEmail(email);
if (!user || !(await bcrypt.compare(password, user.hashedPassword))) {
// Same error for missing user and wrong password (prevent user enumeration)
return res.status(401).json({ error: 'Invalid credentials' });
}
const payload = { sub: user.id, email: user.email, role: user.role };
const accessToken = generateAccessToken(payload);
const refreshToken = generateRefreshToken(payload);
// Store refresh token in httpOnly cookie (not accessible to JS)
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days in ms
});
res.json({ accessToken, user: { id: user.id, email: user.email, role: user.role } });
});
// POST /auth/refresh
router.post('/refresh', async (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) return res.status(401).json({ error: 'No refresh token' });
try {
const payload = await verifyRefreshToken(refreshToken);
// Refresh token rotation: revoke old, issue new
await revokeRefreshToken(refreshToken);
const newRefreshToken = generateRefreshToken({
sub: payload.sub, email: payload.email, role: payload.role
});
const newAccessToken = generateAccessToken({
sub: payload.sub, email: payload.email, role: payload.role
});
res.cookie('refreshToken', newRefreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000,
});
res.json({ accessToken: newAccessToken });
} catch (err) {
res.clearCookie('refreshToken');
res.status(401).json({ error: 'Invalid or expired refresh token' });
}
});
// POST /auth/logout
router.post('/logout', async (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (refreshToken) await revokeRefreshToken(refreshToken);
res.clearCookie('refreshToken');
res.json({ message: 'Logged out successfully' });
});
module.exports = router;
Middleware de autenticação
// middleware/authenticate.js
const { verifyAccessToken } = require('../services/tokenService');
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Access token required' });
}
const token = authHeader.split(' ')[1];
try {
const payload = verifyAccessToken(token);
req.user = payload; // { sub, email, role, iat, exp }
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Access token expired', code: 'TOKEN_EXPIRED' });
}
res.status(401).json({ error: 'Invalid access token' });
}
}
// Role-based authorization
function authorize(...roles) {
return (req, res, next) => {
if (!roles.includes(req.user?.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
module.exports = { authenticate, authorize };
Protegendo Rotas
const { authenticate, authorize } = require('./middleware/authenticate');
// Public route
app.get('/health', (req, res) => res.json({ status: 'ok' }));
// Authenticated route
app.get('/api/profile', authenticate, (req, res) => {
res.json({ userId: req.user.sub, email: req.user.email });
});
// Role-restricted route
app.delete('/api/users/:id', authenticate, authorize('admin'), async (req, res) => {
await deleteUser(req.params.id);
res.json({ deleted: true });
});
Lista de verificação crítica de segurança
- Nunca armazene tokens em localStorage – vulnerável ao XSS. Use cookies httpOnly para tokens de atualização.
- Use segredos separados para tokens de acesso e atualização. Se o segredo de acesso vazar, os tokens de atualização permanecerão seguros.
- Expiração curta do token de acesso (15 minutos no máximo). Os tokens de acesso de longa duração não podem ser invalidados sem uma lista de bloqueios.
- Atualizar rotação de token — cada atualização emite um novo token de atualização e revoga o antigo. Detecta roubo de token.
- Somente HTTPS em produção — os JWTs em trânsito devem ser criptografados.
- Validar reivindicações iss e aud — evita que tokens de um serviço sejam usados em outro.
Perguntas Frequentes
P: Sessões vs JWT – o que é melhor?
R: As sessões (estado do lado do servidor) são mais fáceis de invalidar imediatamente. Os JWTs não têm estado e são dimensionados horizontalmente sem um armazenamento de sessão compartilhado. Use sessões para aplicativos mais simples; JWT para APIs consumidas por clientes ou microsserviços móveis/SPA.
P: Como invalido um JWT antes de expirar?
R: Duas abordagens: (1) Manter uma lista de bloqueios do Redis de JTIs de tokens revogados – verificada em cada solicitação. (2) Reduza a expiração do token de acesso para que a janela de danos seja pequena. A opção 1 é mais segura; a opção 2 é mais simples.
P: Onde o token de acesso deve ser armazenado no cliente?
R: Na memória (variável JavaScript) — não localStorage ou sessionStorage. O armazenamento de memória significa que ele é perdido na atualização da página, exigindo uma chamada de token de atualização para obter um novo — esse é o comportamento pretendido.
P: Devo usar RS256 ou HS256?
R: HS256 (simétrico, segredo compartilhado) para serviços que você controla totalmente. RS256 (chaves assimétricas, públicas/privadas) quando terceiros precisam verificar tokens sem conhecer seu segredo — como microsserviços ou parceiros.
P: Como lidar com “token expirado” no frontend?
A: Intercepte as respostas 401 com umcode: "TOKEN_EXPIRED" cabeçalho, chame automaticamente/auth/refresh, obtenha um novo token de acesso e repita a solicitação original. A maioria dos clientes HTTP (axios, ky) oferece suporte a interceptadores de solicitação para esse padrão.
Conclusão
A autenticação JWT de produção requer mais do que apenasjwt.sign() ejwt.verify(). A implementação completa abrange: cookies httpOnly para tokens de atualização, rotação de token em cada atualização, revogação apoiada por Redis para logout, segredos separados para cada tipo de token e middleware de autorização baseado em função. Essa arquitetura é dimensionada horizontalmente (tokens de acesso sem estado), lida com roubo de token (a rotação detecta a reutilização) e oferece suporte ao logout adequado (lista de revogação). Comece com esta base e você não precisará refatorar seu sistema de autenticação posteriormente.
🔗 Share this article
✍️ Leave a Comment