Node.js: Autenticação e Segurança

Última atualização: 2026-08-26

1. História: Uma API desprotegida

Alice passou duas semanas implementando a API de gerenciamento de usuários, apenas para descobrir no dia seguinte que o banco de dados havia sido excluído — sua API não possuía autenticação, de modo que qualquer pessoa podia chamá-la diretamente DELETE /api/users. Tendo aprendido a lição, ela implementou a autenticação JWT: os usuários devem primeiro fazer login para obter um token, e as solicitações subsequentes devem incluir o token para acessar rotas protegidas. Ao mesmo tempo, ela adicionou um middleware baseado em funções para operações de administrador, garantindo que mesmo usuários com um token não pudessem realizar ações perigosas. Uma semana depois, ela usou o Helmet para adicionar medidas de segurança aos cabeçalhos de resposta, e a API ficou finalmente tão segura quanto uma fortaleza.



2. Conceitos fundamentais

(1) A diferença entre autenticação e autorização

(2) Visão geral dos princípios do JWT

(3) Hash de senha com bcrypt



3. Detalhes técnicos

(1) A estrutura e os princípios de funcionamento do JWT

Um JWT é composto por três partes interligadas por .:

TEXT 📖 Somente leitura
Header.Payload.Signature
Seção Conteúdo Descrição
Cabeçalho { "alg": "HS256", "typ": "JWT" } Algoritmos e Estruturas de Dados
Carga útil { "userId": 1, "role": "admin", "exp": ... } Declaração personalizada + Declaração padrão
Assinatura HMACSHA256(base64(header) + "." + base64(payload), secret) Assinatura à prova de adulteração

Fluxo de trabalho:

  1. O usuário faz login; o servidor verifica as credenciais
  2. Se a autenticação for bem-sucedida, gere um JWT e retorne-o ao cliente
  3. O cliente armazena o token no localStorage ou em um cookie
  4. As solicitações subsequentes incluem o seguinte no cabeçalho Authorization: Bearer <token>:
  5. O middleware do lado do servidor verifica a assinatura e a data de validade

▶ Exemplo: Emissão e verificação de um JWT

JAVASCRIPT
const jwt = require('jsonwebtoken');

const SECRET = 'my_super_secret_key';

const token = jwt.sign(
  { userId: 42, role: 'admin' },
  SECRET,
  { expiresIn: '2h' }
);

console.log('Token:', token);

const decoded = jwt.verify(token, SECRET);
console.log('Decoded:', decoded);
▶ Experimente

▶ Exemplo: Decodificando um JWT para visualizar a carga útil (sem verificar a assinatura)

JAVASCRIPT
const decoded = jwt.decode(token, { complete: true });
console.log('Header:', decoded.header);
console.log('Payload:', decoded.payload);
▶ Experimente

(2) Parâmetros e uso do bcrypt

Parâmetro Valor recomendado Descrição
saltRounds 10-12 Número de rodadas; quanto maior o número, mais seguro, mas mais lento
Comprimento da senha ≥8 caracteres Validada pelo front-end
Algoritmos Blowfish Algoritmos subjacentes ao bcrypt

▶ Exemplo: Hash e verificação de senha

JAVASCRIPT
const bcrypt = require('bcrypt');

async function hashPassword(plainPassword) {
  const saltRounds = 10;
  const hash = await bcrypt.hash(plainPassword, saltRounds);
  console.log('Hash:', hash);
  return hash;
}

async function verifyPassword(plainPassword, hash) {
  const match = await bcrypt.compare(plainPassword, hash);
  console.log('Match:', match);
  return match;
}

(async () => {
  const hash = await hashPassword('MyPassword123');
  await verifyPassword('MyPassword123', hash);
  await verifyPassword('WrongPassword', hash);
})();
▶ Experimente

(3) Cabeçalhos de segurança HTTP e Helmet

O Helmet aumenta a segurança ao definir cabeçalhos de resposta HTTP:

Cabeçalho de segurança Finalidade Ativado por padrão
Content-Security-Policy Impede ataques XSS e restringe as fontes das quais os recursos podem ser carregados Não (requer configuração manual)
X-Frame-Options Impedir clickjacking Sim
X-Content-Type-Options Impedir a detecção de MIME Sim
Strict-Transport-Security Exigir HTTPS Sim
X-XSS-Protection Filtragem XSS do navegador Obsoleto (o Helmet não habilita mais isso por padrão)

▶ Exemplo: Integração do Helmet

JAVASCRIPT
const express = require('express');
const helmet = require('helmet');

const app = express();

app.use(helmet());

app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'"],
    styleSrc: ["'self'", "'unsafe-inline'"],
  }
}));

app.get('/', (req, res) => {
  res.send('Secured with helmet!');
});

app.listen(3000);
▶ Experimente

(4) Ameaças comuns à segurança e contramedidas

Ameaça Mecanismo Contramedidas
Injeção de SQL Concatenação de entradas do usuário para construir SQL malicioso Consultas parametrizadas / ORM
XSS (Cross-Site Scripting) Injeção de scripts maliciosos em páginas Escapamento de saída / CSP / Helmet
CSRF (Falsificação de Solicitação entre Sites) Envio de solicitações fingindo ser um usuário autenticado Token CSRF / Cookie SameSite
Ataques de força bruta Tentativas repetidas de senha Limitação de taxa / Número elevado de rodadas do bcrypt / Bloqueio de conta
Ataques “man-in-the-middle” Interceptação de dados de comunicação HTTPS / HSTS

▶ Exemplo: Prevenção de injeção de SQL (consultas parametrizadas)

JAVASCRIPT
const { Pool } = require('pg');
const pool = new Pool();

app.post('/login', async (req, res) => {
  const { username, password } = req.body;

  const result = await pool.query(
    'SELECT * FROM users WHERE username = $1',
    [username]
  );

  if (result.rows.length === 0) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  const user = result.rows[0];
  const match = await bcrypt.compare(password, user.password_hash);
  if (!match) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  const token = jwt.sign({ userId: user.id, role: user.role }, SECRET, { expiresIn: '2h' });
  res.json({ token });
});
▶ Experimente

▶ Exemplo: Prevenção de XSS (escapamento de dados de saída)

JAVASCRIPT
function escapeHtml(str) {
  return str
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;');
}

const userInput = '<script>alert("xss")</script>';
console.log(escapeHtml(userInput));
▶ Experimente

▶ Exemplo: Proteção contra CSRF (middleware csurf)

JAVASCRIPT
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });

app.get('/form', csrfProtection, (req, res) => {
  res.json({ csrfToken: req.csrfToken() });
});

app.post('/submit', csrfProtection, (req, res) => {
  res.json({ message: 'Form submitted successfully' });
});
▶ Experimente

4. Exercícios práticos

(1) API de registro

▶ Exemplo: API de cadastro de usuário

JAVASCRIPT
app.post('/api/register', async (req, res) => {
  const { username, password, role } = req.body;

  if (!username || !password) {
    return res.status(400).json({ error: 'Username and password are required' });
  }

  if (password.length < 8) {
    return res.status(400).json({ error: 'Password must be at least 8 characters' });
  }

  const existingUser = await pool.query(
    'SELECT id FROM users WHERE username = $1',
    [username]
  );
  if (existingUser.rows.length > 0) {
    return res.status(409).json({ error: 'Username already exists' });
  }

  const saltRounds = 10;
  const passwordHash = await bcrypt.hash(password, saltRounds);

  const result = await pool.query(
    'INSERT INTO users (username, password_hash, role) VALUES ($1, $2, $3) RETURNING id, username, role',
    [username, passwordHash, role || 'user']
  );

  const user = result.rows[0];
  const token = jwt.sign(
    { userId: user.id, role: user.role },
    SECRET,
    { expiresIn: '2h' }
  );

  res.status(201).json({ user: { id: user.id, username: user.username, role: user.role }, token });
});
▶ Experimente

(2) API de login

▶ Exemplo: API de login do usuário

JAVASCRIPT
app.post('/api/login', async (req, res) => {
  const { username, password } = req.body;

  if (!username || !password) {
    return res.status(400).json({ error: 'Username and password are required' });
  }

  const result = await pool.query(
    'SELECT id, username, password_hash, role FROM users WHERE username = $1',
    [username]
  );

  if (result.rows.length === 0) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  const user = result.rows[0];
  const match = await bcrypt.compare(password, user.password_hash);

  if (!match) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  const token = jwt.sign(
    { userId: user.id, username: user.username, role: user.role },
    SECRET,
    { expiresIn: '2h' }
  );

  res.json({
    user: { id: user.id, username: user.username, role: user.role },
    token
  });
});
▶ Experimente

(3) Middleware de autenticação JWT

▶ Exemplo: O middleware authenticate

JAVASCRIPT
function authenticate(req, res, next) {
  const authHeader = req.headers.authorization;

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Access denied. No token provided.' });
  }

  const token = authHeader.split(' ')[1];

  try {
    const decoded = jwt.verify(token, SECRET);
    req.user = decoded;
    next();
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expired' });
    }
    return res.status(403).json({ error: 'Invalid token' });
  }
}
▶ Experimente

(4) Middleware de controle de acesso baseado em funções

▶ Exemplo: middleware da função “authorize”

JAVASCRIPT
function authorize(...roles) {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ error: 'Authentication required' });
    }

    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ error: 'Insufficient permissions' });
    }

    next();
  };
}

app.get('/api/profile', authenticate, (req, res) => {
  res.json({ user: req.user });
});

app.delete('/api/users/:id', authenticate, authorize('admin'), async (req, res) => {
  await pool.query('DELETE FROM users WHERE id = $1', [req.params.id]);
  res.json({ message: 'User deleted' });
});

app.get('/api/admin/dashboard', authenticate, authorize('admin'), (req, res) => {
  res.json({ message: 'Welcome to admin dashboard' });
});
▶ Experimente

5. Exemplo abrangente: um sistema completo de autenticação

TEXT 📖 Somente leitura
project/
├── server.js
├── middleware/
│   ├── auth.js
│   └── role.js
├── routes/
│   ├── auth.js
│   └── users.js
└── パッケージ.json

middleware/auth.js:

JAVASCRIPT
const jwt = require('jsonwebtoken');

const SECRET = process.env.JWT_SECRET || 'fallback_dev_secret';

function authenticate(req, res, next) {
  const authHeader = req.headers.authorization;
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Access denied. No token provided.' });
  }

  const token = authHeader.split(' ')[1];
  try {
    const decoded = jwt.verify(token, SECRET);
    req.user = decoded;
    next();
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expired. Please login again.' });
    }
    return res.status(403).json({ error: 'Invalid token.' });
  }
}

module.exports = { authenticate, SECRET };

Middleware/role.js:

JAVASCRIPT
function authorize(...roles) {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ error: 'Authentication required.' });
    }
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ error: 'Forbidden. Insufficient permissions.' });
    }
    next();
  };
}

module.exports = { authorize };

routes/auth.js:

JAVASCRIPT
const express = require('express');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const { authenticate, SECRET } = require('../middleware/auth');

const router = express.Router();

const users = [];

router.post('/register', async (req, res) => {
  const { username, password, role } = req.body;
  if (!username || !password) {
    return res.status(400).json({ error: 'Username and password required.' });
  }
  if (password.length < 8) {
    return res.status(400).json({ error: 'Password must be at least 8 characters.' });
  }
  const exists = users.find(u => u.username === username);
  if (exists) {
    return res.status(409).json({ error: 'Username already exists.' });
  }

  const saltRounds = 10;
  const passwordHash = await bcrypt.hash(password, saltRounds);
  const newUser = {
    id: users.length + 1,
    username,
    passwordHash,
    role: role || 'user'
  };
  users.push(newUser);

  const token = jwt.sign(
    { userId: newUser.id, username: newUser.username, role: newUser.role },
    SECRET,
    { expiresIn: '2h' }
  );

  res.status(201).json({
    user: { id: newUser.id, username: newUser.username, role: newUser.role },
    token
  });
});

router.post('/login', async (req, res) => {
  const { username, password } = req.body;
  if (!username || !password) {
    return res.status(400).json({ error: 'Username and password required.' });
  }

  const user = users.find(u => u.username === username);
  if (!user) {
    return res.status(401).json({ error: 'Invalid credentials.' });
  }

  const match = await bcrypt.compare(password, user.passwordHash);
  if (!match) {
    return res.status(401).json({ error: 'Invalid credentials.' });
  }

  const token = jwt.sign(
    { userId: user.id, username: user.username, role: user.role },
    SECRET,
    { expiresIn: '2h' }
  );

  res.json({
    user: { id: user.id, username: user.username, role: user.role },
    token
  });
});

router.get('/profile', authenticate, (req, res) => {
  const user = users.find(u => u.id === req.user.userId);
  if (!user) {
    return res.status(404).json({ error: 'User not found.' });
  }
  res.json({ id: user.id, username: user.username, role: user.role });
});

module.exports = router;

routes/users.js:

JAVASCRIPT
const express = require('express');
const { authenticate } = require('../middleware/auth');
const { authorize } = require('../middleware/role');

const router = express.Router();

const users = [];

router.get('/', authenticate, authorize('admin'), (req, res) => {
  const safeList = users.map(u => ({ id: u.id, username: u.username, role: u.role }));
  res.json(safeList);
});

router.delete('/:id', authenticate, authorize('admin'), (req, res) => {
  const index = users.findIndex(u => u.id === parseInt(req.params.id));
  if (index === -1) {
    return res.status(404).json({ error: 'User not found.' });
  }
  users.splice(index, 1);
  res.json({ message: 'User deleted.' });
});

module.exports = router;

servidor.js:

JAVASCRIPT
const express = require('express');
const helmet = require('helmet');
const authRoutes = require('./routes/auth');
const userRoutes = require('./routes/users');

const app = express();

app.use(helmet());
app.use(express.json());

app.use('/api/auth', authRoutes);
app.use('/api/users', userRoutes);

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: 'Internal server error.' });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Processo de teste:

BASH
# Register
curl -X POST http://localhost:3000/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{"username":"alice","password":"SecurePass123","role":"admin"}'

# Log In
curl -X POST http://localhost:3000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"alice","password":"SecurePass123"}'

# Accessing Protected Routes
curl http://localhost:3000/api/auth/profile \
  -H "Authorization: Bearer <your_token>"

# Administrator Actions
curl -X DELETE http://localhost:3000/api/users/2 \
  -H "Authorization: Bearer <admin_token>"
TEXT 📖 Somente leitura
# Registration Response
{
  "user": { "id": 1, "username": "alice", "role": "admin" },
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

# Login Response
{
  "user": { "id": 1, "username": "alice", "role": "admin" },
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

# Insufficient Permissions Response
{ "error": "Forbidden. Insufficient permissions." }


6. Fluxograma de autenticação JWT

100%
sequenceDiagram
    participant Client as Client
    participant Server as Server-side
    participant DB as Database

    Client->>Server: POST /api/login {username, password}
    Server->>DB: Query User Records
    DB-->>Server: Return User Data
    Server->>Server: bcrypt.compare() Confirm Password
    alt The password is correct
        Server->>Server: jwt.sign() Issued Token
        Server-->>Client: Back { token }
        Client->>Client: Storage Token
        Client->>Server: GET /api/profile<br/>Authorization: Bearer <token>
        Server->>Server: jwt.verify() Verification Token
        alt Token Valid
            Server-->>Client: 200 Return User Data
        else Token Invalid or Expired
            Server-->>Client: 401/403 Access Denied
        end
    else Incorrect password
        Server-->>Client: 401 Invalid credentials
    end


7. Comparações e referências

(1) JWT x Sessão x OAuth

Dimensão JWT Sessão OAuth 2.0
Local de armazenamento Cliente Servidor Servidor + Cliente
Sem estado Sim Não Não
Escalabilidade Oferece suporte nativo a sistemas distribuídos Requer armazenamento compartilhado de sessões Requer um servidor de autorização
Casos de uso Autenticação por API, microsserviços Aplicativos web tradicionais Login por meio de serviços de terceiros
Riscos de segurança Vazamentos de tokens que não podem ser revogados Desvio de sessão Ataques de repetição
Complexidade Baixa Baixa Alta

(2) Seleção dos parâmetros do bcrypt

saltRounds Duração aproximada Casos de uso
8 ~40 ms Desenvolvimento e testes
10 ~160 ms Recomendado para ambientes de produção
12 ~640 ms Altos requisitos de segurança
14 ~2,5 s Cenários de segurança extrema

(3) Ameaças comuns à segurança e contramedidas

Ameaça Exemplo de ataque Método de defesa Ferramentas/Bibliotecas
Injeção de SQL ' OR 1=1 -- Consultas parametrizadas pg/mysql2
XSS <script>document.cookie</script> Escape + CSP helmet/xss
CSRF Falsificação de envio de formulário Token CSRF + SameSite csurf
Ataque de força bruta Ataque de dicionário a senhas Limitação de taxa + bcrypt de alta rodada express-rate-limit
Ataques “man-in-the-middle” Monitoramento do tráfego HTTP HTTPS + HSTS helmet/Let’s Encrypt

(4) Campos comuns em uma carga útil de JWT

Campo Nome completo Descrição
iss Emissor Identificador do emissor
sub Assunto Assunto (geralmente o ID do usuário)
aud Público ID do destinatário
exp Vencimento Hora de vencimento (timestamp Unix)
iat Emitido em Data de emissão
jti ID JWT Identificador exclusivo (anti-replay)
userId Personalizado ID do usuário corporativo
função Personalizada Função do usuário

❓ Perguntas Frequentes

P: Onde o JWT é armazenado? R: Recomenda-se armazená-lo em um cookie httpOnly para impedir que ataques XSS o leiam; localStorage é prático, mas vulnerável ao roubo por XSS, portanto, deve ser usado em conjunto com o CSP.

P: O que devo fazer se um token expirar? R: Uma abordagem comum com dois tokens consiste em usar um token de acesso de curta duração e um token de atualização de longa duração. Quando o token de acesso expirar, use o token de atualização para obter um novo. O token de atualização é armazenado no banco de dados para que possa ser revogado de forma proativa.

P: Por que o bcrypt é tão lento? R: O fator de custo do bcrypt controla o número de rodadas de hash; cada rodada adicional dobra o tempo de processamento. Essa “lentidão” é intencional, garantindo que os ataques de força bruta acarretem um custo computacional significativo a cada tentativa de adivinhar a senha.

P: O HTTPS é obrigatório? R: É obrigatório em um ambiente de produção. Com o HTTP, os tokens e senhas são transmitidos em texto simples e podem ser interceptados por qualquer intermediário; a transmissão criptografada via HTTPS é o requisito mínimo de segurança.

P: Como é possível prevenir ataques de força bruta? R: Defesa em várias camadas — o bcrypt aumenta o custo computacional por tentativa com mais rodadas + o express-rate-limit para limitar a taxa de tentativas + mecanismo de bloqueio de conta + resposta atrasada após tentativas malsucedidas de login.

P: É possível revogar um JWT de forma proativa? R: Como os JWTs são, por natureza, sem estado, não podem ser revogados de forma proativa. As soluções mais comuns incluem: manter uma lista negra (armazenando o JTI dos tokens revogados no Redis até que expirem), reduzir o período de validade em conjunto com um token de atualização e exigir autenticação secundária para operações confidenciais.

P: O que devo levar em consideração ao configurar o CSP para o Helmet? R: Se as regras do CSP forem muito rígidas, elas podem impedir o carregamento de recursos legítimos. Recomendamos usar o Content-Security-Policy-Report-Only durante o desenvolvimento para monitorar os relatórios de violação e tornar a política mais restritiva gradualmente.


📖 Resumo

📝 Exercícios

  1. Implemente a interface de cadastro. A senha deve ter pelo menos 8 caracteres e conter pelo menos um algarismo. Ela deve ser criptografada usando o bcrypt e armazenada no banco de dados.
  2. Implemente a API de login para emitir um JWT com validade de 1 hora após a verificação da senha; a carga útil deve incluir userId e role.
  3. Escreva o middleware authenticate para extrair e validar o JWT do Authorization: Bearer <token>
  4. Escreva o middleware authorize('admin') para restringir o acesso ao DELETE /api/users/:id apenas aos usuários com a função “admin”.
  5. Integrar o Helmet ao projeto, configurar o CSP para permitir que apenas scripts e estilos da mesma origem sejam carregados e usar o curl para verificar as alterações nos cabeçalhos da resposta

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%