🌐 Detecting your location…
📢 Advertisement — Configure AdSense in Appearance → Customize → AdSense Settings

Como implementar a autenticação JWT em Express.js: guia passo a passo 2026

⏱️6 min read  ·  1,319 words

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.

Como funciona a autenticação JWT

  1. O usuário faz login com credenciais → o servidor as valida
  2. O servidor emite dois tokens:token de acesso (curta duração, 15min) eatualizar token (vida longa, 7 dias)
  3. Cliente envia token de acesso no cabeçalho de autorização para cada solicitação
  4. Quando o token de acesso expira, o cliente envia o token de atualização para obter um novo token de acesso
  5. 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.

✍️ Leave a Comment

Your email address will not be published. Required fields are marked *

🌐 Read in:🇩🇪 Deutsch🇧🇷 Português🇸🇦 العربية🇮🇳 हिन्दी🇧🇩 বাংলা