Security
Escopo: JavaScript (setup). Princípios transversais em shared/platform/security.md.
Esta página cobre apenas o que é específico do ecossistema Node: onde colocar o quê, quais ferramentas usar, quais patterns idiomáticos. As regras conceituais (segredos fora do repositório, validação no servidor, HttpOnly + Secure + SameSite) vivem em shared/platform/security.md e não são repetidas aqui.
Conceitos fundamentais
| Conceito | O que é |
|---|---|
| Secret (segredo) | Credencial, chave ou token; nunca vai para o repositório |
| PaaS (Platform as a Service, Plataforma como Serviço) | Ambiente gerenciado (Heroku, Railway, Fly) que injeta variáveis no deploy |
| JWT (JSON Web Token, Token Web em JSON) | Token assinado usado para autenticação stateless |
| Middleware (componente de pipeline) | Função que intercepta a requisição antes ou depois do handler |
| Payload (corpo da mensagem) | Dados que acompanham a requisição ou o token |
Onde cada coisa vai
| Camada | Arquivo / mecanismo | Valor |
|---|---|---|
| Contrato público (commitado) | .env.example | Chaves esperadas, sem valores |
| Dev local (fora do repositório) | .env + dotenv | Connection string local, chave de teste |
| Staging/produção | Variáveis do host (container, PaaS) | Segredos reais, injetados no deploy |
| Secrets gerenciados | Vault, AWS Secrets Manager, Doppler | Rotação automática, auditoria |
.env.example documenta a superfície de configuração do projeto. Clonar o repo e rodar cp .env.example .env dá ao dev a lista do que precisa preencher.
.env.example
DATABASE_URL=
JWT_SECRET=
JWT_AUDIENCE=
PORT=3000
RATE_LIMIT_MAX=100
RATE_LIMIT_WINDOW_MS=60000
Chaves com valor zero indicam segredos (devem vir do ambiente). Chaves com valor default indicam config não sensível.
dotenv: apenas em desenvolvimento
dotenv carrega .env para process.env. Importar uma vez no bootstrap, antes de qualquer módulo que leia process.env.
npm install dotenv
// server.js
import "dotenv/config";
import { config } from "./config.js";
import { createApp } from "./app.js";
const app = createApp(config);
app.listen(config.port);
Em staging e produção, dotenv não entra no bundle. As variáveis são injetadas pelo container ou PaaS diretamente.
config.js: único ponto de leitura
process.env é o limite entre o ambiente externo e o código. Ler diretamente no meio da lógica espalha dependência global e dificulta teste. O padrão é um config.js que lê uma vez, valida e exporta um objeto consumido por injeção.
❌ Ruim: process.env espalhado pela aplicação
// orders.service.js
export function createOrderService() {
const db = new Pool({ connectionString: process.env.DATABASE_URL });
const rate = parseInt(process.env.RATE_LIMIT_MAX, 10);
// ...
}
✅ Bom: leitura em um lugar, módulos recebem por parâmetro
// config.js
export const config = {
port: parseInt(process.env.PORT, 10) || 3000,
database: {
url: process.env.DATABASE_URL,
},
auth: {
secret: process.env.JWT_SECRET,
audience: process.env.JWT_AUDIENCE,
},
};
// features/orders/orders.module.js
export function registerOrders(app, config) {
const orderService = createOrderService(config.database);
// ...
}
JWT: verify, nunca decode
jwt.decode() extrai o payload (corpo da mensagem) sem verificar assinatura. Qualquer token fabricado ou vencido passa. Em produção, use sempre jwt.verify().
❌ Ruim: decode aceita token forjado
export function authenticate(request, response, next) {
const token = request.headers.authorization?.split(" ")[1];
if (!token) return response.status(401).json({ error: "Unauthorized" });
const claims = jwt.decode(token); // não verifica assinatura
request.user = claims;
next();
}
✅ Bom: verify valida assinatura e expiração
export function authenticate(request, response, next) {
const token = request.headers.authorization?.split(" ")[1];
if (!token) {
const body = { error: "Unauthorized" };
response.status(401).json(body);
return;
}
try {
const claims = jwt.verify(token, config.auth.secret);
request.user = claims;
next();
} catch {
const body = { error: "Invalid token" };
response.status(401).json(body);
}
}
Autorização: middleware reutilizável
Checar role dentro de cada handler duplica lógica. Um middleware (componente de pipeline) authorize(roles) aplica a regra uma vez na definição da rota.
// middleware/authorize.js
export function authorize(allowedRoles) {
return function checkRole(request, response, next) {
const isAllowed = allowedRoles.includes(request.user.role);
if (!isAllowed) {
const body = { error: "Forbidden" };
response.status(403).json(body);
return;
}
next();
};
}
// routes/orders.routes.js
app.delete("/orders/:id", authenticate, authorize(["admin", "manager"]), cancelOrderHandler);
Session cookie com flags
express-session aceita as três flags obrigatórias diretamente no binding. Ver shared/platform/security.md para o racional de cada uma.
const sessionConfig = {
secret: config.auth.secret,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: true,
sameSite: "strict",
maxAge: 1000 * 60 * 60 * 8,
},
};
app.use(session(sessionConfig));
.gitignore
.env
.env.*
!.env.example
*.key
*.pem
secrets.json
O !.env.example garante que o contrato público seja sempre commitado.
Desenvolvido por @thiagocajadev · Fork baseado no repositório pmndrs/docs · Poimandres.