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
- Autenticação: Verificar “quem você é”, como a verificação do nome de usuário e da senha
- Autorização: verifica “o que você pode fazer”, como determinar permissões com base em funções
- Os dois costumam ser usados em conjunto: primeiro, verifica-se a identidade; depois, autoriza-se a operação.
(2) Visão geral dos princípios do JWT
- Um JWT (JSON Web Token) é um token sem estado composto por três segmentos codificados em Base64
- Uma vez emitido pelo servidor, ele não precisa ser armazenado; o cliente o salva e o inclui na solicitação.
- Adequado para sistemas distribuídos, evitando problemas relacionados ao compartilhamento de sessões
(3) Hash de senha com bcrypt
- O bcrypt é um algoritmo de hash desenvolvido especificamente para senhas, com um salt integrado
- É possível ajustar o tempo de cálculo usando
cost factorpara se proteger contra ataques de força bruta. - O mesmo texto em claro gera um resultado de hash diferente a cada vez, tornando-o muito mais seguro do que o MD5/SHA
3. Detalhes técnicos
(1) A estrutura e os princípios de funcionamento do JWT
Um JWT é composto por três partes interligadas por .:
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:
- O usuário faz login; o servidor verifica as credenciais
- Se a autenticação for bem-sucedida, gere um JWT e retorne-o ao cliente
- O cliente armazena o token no localStorage ou em um cookie
- As solicitações subsequentes incluem o seguinte no cabeçalho
Authorization: Bearer <token>: - O middleware do lado do servidor verifica a assinatura e a data de validade
▶ Exemplo: Emissão e verificação de um JWT
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);
▶ Exemplo: Decodificando um JWT para visualizar a carga útil (sem verificar a assinatura)
const decoded = jwt.decode(token, { complete: true });
console.log('Header:', decoded.header);
console.log('Payload:', decoded.payload);
(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
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);
})();
(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
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);
(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)
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 });
});
▶ Exemplo: Prevenção de XSS (escapamento de dados de saída)
function escapeHtml(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
const userInput = '<script>alert("xss")</script>';
console.log(escapeHtml(userInput));
▶ Exemplo: Proteção contra CSRF (middleware csurf)
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' });
});
4. Exercícios práticos
(1) API de registro
▶ Exemplo: API de cadastro de usuário
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 });
});
(2) API de login
▶ Exemplo: API de login do usuário
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
});
});
(3) Middleware de autenticação JWT
▶ Exemplo: O middleware authenticate
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' });
}
}
(4) Middleware de controle de acesso baseado em funções
▶ Exemplo: middleware da função “authorize”
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' });
});
5. Exemplo abrangente: um sistema completo de autenticação
project/
├── server.js
├── middleware/
│ ├── auth.js
│ └── role.js
├── routes/
│ ├── auth.js
│ └── users.js
└── パッケージ.json
middleware/auth.js:
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:
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:
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:
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:
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:
# 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>"
# 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
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
httpOnlypara 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-Onlydurante o desenvolvimento para monitorar os relatórios de violação e tornar a política mais restritiva gradualmente.
📖 Resumo
- Artigo: Conceitos básicos e uso de APIs desprotegidas
- Conceitos fundamentais: definições e aplicações
- Conceitos-chave e métodos de uso em explicações técnicas
- Conceitos fundamentais e aplicação em exercícios práticos
- Exemplo abrangente: conceitos fundamentais e uso de um sistema completo de autenticação
- Conceitos-chave e uso do fluxograma de autenticação JWT
- Conceitos-chave e uso de comparação e referência
📝 Exercícios
- 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.
- 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
userIderole. - Escreva o middleware
authenticatepara extrair e validar o JWT doAuthorization: Bearer <token> - Escreva o middleware
authorize('admin')para restringir o acesso aoDELETE /api/users/:idapenas aos usuários com a função “admin”. - 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