Índice
- Autenticação ≠ autorização: a diferença que custa caro em produção
- Middleware de autenticação no Express e Next.js App Router
- Autorização por role (RBAC): quem pode fazer o quê
- Os erros que deixam rotas expostas mesmo com JWT configurado
- Rate limiting, logging e validação: o que vem depois
- Checklist de proteção de rotas
- FAQ
De acordo com o OWASP API Security Top 10, Broken Object Level Authorization é a vulnerabilidade número um em APIs há anos consecutivos. O problema quase nunca é falta de autenticação — é autenticação sem autorização. O token JWT está lá, o middleware verifica a assinatura, o 200 OK chega. E qualquer usuário autenticado acessa dados que não são dele.
Esse artigo cobre a separação real entre autenticação e autorização, com implementações prontas para Express e Next.js App Router, controle de acesso por role, as falhas mais comuns que passam despercebidas em code review — e o que você deve implementar logo depois que a autorização estiver no lugar.
O que você vai encontrar aqui:
- A diferença prática entre autenticação e autorização — com o bug exato que a confusão gera
- Middleware de autenticação reutilizável no Express e Next.js App Router
- RBAC (Role-Based Access Control) sem transformar o código num
ifaninhado - Os erros mais comuns que expõem rotas mesmo com JWT configurado corretamente
- Rate limiting, logging estruturado e validação: a camada que a maioria esquece
Autenticação ≠ autorização: a diferença que custa caro em produção
Autenticação responde: quem é esse usuário? Autorização responde: esse usuário pode fazer isso?
São camadas distintas e independentes. Você pode ter autenticação perfeita e autorização completamente inexistente — que é exatamente o cenário mais comum em APIs construídas com prazo curto.
O erro mais frequente em code review:
router.get('/admin/relatorios', verificarToken, async (req, res) => {
const relatorios = await db.relatorio.findAll();
res.json(relatorios);
});
O middleware verificarToken fez seu trabalho: confirmou que o JWT é legítimo e não está expirado. Mas ninguém verificou se aquele JWT pertence a um admin. Qualquer usuário autenticado — incluindo o cliente comum da plataforma — chega nessa rota sem bloqueio.
A separação correta funciona assim:
Requisição → [Autenticação] → [Autorização] → Handler
| |
"Quem é você?" "Você pode isso?"
→ 401 se não → 403 se não
autenticado autorizado
Um detalhe que parece pequeno mas importa: 401 é para quem não está autenticado. 403 é para quem está autenticado mas não tem permissão. Usar 401 para os dois casos é um erro comum — e faz o cliente tratar como "preciso me logar de novo" quando na verdade o acesso está negado por política.
Middleware de autenticação no Express e Next.js App Router
Express
O middleware de autenticação tem responsabilidade única: verificar o token, extrair os dados do usuário e injetá-los na requisição. Nada além disso.
import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';
export interface RequisicaoAutenticada extends Request {
usuario?: {
id: string;
email: string;
role: string;
};
}
export function autenticar(
req: RequisicaoAutenticada,
res: Response,
next: NextFunction
) {
const token =
req.cookies.access_token ??
req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({
error: 'Não autenticado',
code: 'UNAUTHENTICATED',
});
}
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!, {
algorithms: ['HS256'],
}) as jwt.JwtPayload;
req.usuario = {
id: payload.sub as string,
email: payload.email,
role: payload.role ?? 'user',
};
next();
} catch (err) {
if (err instanceof jwt.TokenExpiredError) {
return res.status(401).json({
error: 'Token expirado',
code: 'TOKEN_EXPIRED',
});
}
return res.status(401).json({
error: 'Token inválido',
code: 'TOKEN_INVALID',
});
}
}
O algorithms: ['HS256'] explícito não é opcional. Versões antigas do jsonwebtoken aceitavam alg: none no header do token se o algoritmo não fosse fixado — uma vulnerabilidade que permitia forjar tokens sem assinatura.
Next.js App Router
No App Router, a autenticação fica em middleware.ts na raiz do projeto. O middleware do Next.js é executado no Edge Runtime, então não há acesso ao Node.js nativo — você precisa de uma biblioteca compatível com Web Crypto API ou do jose:
import { NextRequest, NextResponse } from 'next/server';
import { jwtVerify } from 'jose';
const rotasPublicas = ['/login', '/registro', '/api/auth'];
export async function middleware(req: NextRequest) {
const pathname = req.nextUrl.pathname;
if (rotasPublicas.some((rota) => pathname.startsWith(rota))) {
return NextResponse.next();
}
const token =
req.cookies.get('access_token')?.value ??
req.headers.get('authorization')?.split(' ')[1];
if (!token) {
return NextResponse.json(
{ error: 'Não autenticado', code: 'UNAUTHENTICATED' },
{ status: 401 }
);
}
try {
const secret = new TextEncoder().encode(process.env.JWT_SECRET!);
const { payload } = await jwtVerify(token, secret);
const headers = new Headers(req.headers);
headers.set('x-user-id', payload.sub ?? '');
headers.set('x-user-role', (payload.role as string) ?? 'user');
return NextResponse.next({ request: { headers } });
} catch {
return NextResponse.json(
{ error: 'Token inválido', code: 'TOKEN_INVALID' },
{ status: 401 }
);
}
}
export const config = {
matcher: ['/api/:path*', '/dashboard/:path*'],
};
Nos Route Handlers, você recupera os dados do usuário pelos headers injetados pelo middleware:
import { NextRequest } from 'next/server';
export async function GET(req: NextRequest) {
const userId = req.headers.get('x-user-id');
const role = req.headers.get('x-user-role');
if (role !== 'admin') {
return Response.json(
{ error: 'Acesso não autorizado', code: 'FORBIDDEN' },
{ status: 403 }
);
}
const relatorios = await db.relatorio.findAll({ userId });
return Response.json(relatorios);
}
Autorização por role (RBAC): quem pode fazer o quê
Com os dados do usuário disponíveis em req.usuario, o middleware de autorização é uma factory que retorna um middleware configurado para as roles exigidas:
import { Response, NextFunction } from 'express';
import { RequisicaoAutenticada } from './autenticar';
export function autorizar(...rolesPermitidas: string[]) {
return (req: RequisicaoAutenticada, res: Response, next: NextFunction) => {
const role = req.usuario?.role;
if (!role || !rolesPermitidas.includes(role)) {
return res.status(403).json({
error: 'Acesso não autorizado',
code: 'FORBIDDEN',
});
}
next();
};
}
Nas rotas, a cadeia fica limpa e explícita:
import { autenticar } from '../middleware/autenticar';
import { autorizar } from '../middleware/autorizar';
router.get('/perfil', autenticar, handler);
router.get('/admin/usuarios', autenticar, autorizar('admin'), handler);
router.delete('/conteudo/:id', autenticar, autorizar('admin', 'moderador'), handler);
A ordem importa: autenticar sempre antes de autorizar. Tentar autorizar sem saber quem é o usuário resulta em req.usuario indefinido.
Autorização por recurso: verificação de posse (IDOR)
Role-based resolve a maioria dos casos. Mas quando o usuário precisa acessar recursos próprios — pedidos, documentos, configurações — você precisa verificar se o recurso pertence a quem está pedindo:
router.get('/pedidos/:id', autenticar, async (req: RequisicaoAutenticada, res) => {
const pedido = await db.pedido.findUnique({
where: { id: req.params.id },
});
if (!pedido) {
return res.status(404).json({ error: 'Pedido não encontrado' });
}
const ehAdmin = req.usuario!.role === 'admin';
const ehDono = pedido.userId === req.usuario!.id;
if (!ehAdmin && !ehDono) {
return res.status(403).json({
error: 'Acesso não autorizado',
code: 'FORBIDDEN',
});
}
res.json(pedido);
});
Esse é o padrão para evitar IDOR (Insecure Direct Object Reference) — quando um usuário altera o ID na URL e acessa dados de outro usuário. É o vetor número um de vazamento de dados em APIs REST com IDs sequenciais.
Os erros que deixam rotas expostas mesmo com JWT configurado
1. Rotas internas sem nenhuma verificação
router.post('/internal/sync-usuarios', async (req, res) => {
await sincronizarUsuarios();
res.json({ ok: true });
});
"Rota interna, ninguém vai achar" não é estratégia de segurança. Se a rota está registrada, está acessível. Rotas internas precisam de pelo menos validação de IP de origem ou um token de serviço fixo:
function validarTokenServico(req: Request, res: Response, next: NextFunction) {
const token = req.headers['x-service-token'];
if (token !== process.env.SERVICE_TOKEN) {
return res.status(403).json({ error: 'Acesso não autorizado' });
}
next();
}
router.post('/internal/sync-usuarios', validarTokenServico, handler);
2. Autorização feita só no frontend
// Frontend esconde o botão — mas a rota não verifica nada
// { role === 'admin' && <BotaoDeletar /> }
router.delete('/usuarios/:id', autenticar, handler);
Segurança no frontend é UX, não segurança. Qualquer pessoa com acesso às ferramentas de desenvolvedor — ou com curl — ignora o que o React renderiza. A verificação de autorização precisa estar no servidor, sempre.
3. Expor IDs sequenciais sem verificar posse
router.get('/faturas/:id', autenticar, async (req, res) => {
const fatura = await db.fatura.findUnique({ where: { id: req.params.id } });
res.json(fatura);
});
Com IDs sequenciais (/faturas/1001, /faturas/1002...), um usuário autenticado percorre os IDs e acessa faturas de outros clientes. Se você usa IDs sequenciais, adicione a verificação de posse. Se estiver projetando agora, considere UUIDs — eles não eliminam a necessidade da verificação, mas reduzem a superfície de enumeração.
4. Role mutável presa no payload do token
const payload = jwt.decode(token); // não verifica assinatura
if (payload.role === 'admin') { ... }
O payload do JWT é fixado no momento da emissão. Se você promover ou rebaixar um usuário no banco, o token antigo ainda carrega a role antiga até expirar. Para operações críticas — deleção de dados, pagamentos, mudanças de permissão — consulte o banco para garantir que a role do token ainda reflete a realidade. Para o fluxo padrão, tokens com expiração curta (15 minutos) com refresh token rotation reduzem a janela de risco sem consulta adicional ao banco.
5. Algoritmo de JWT não fixado
jwt.verify(token, process.env.JWT_SECRET!);
Sem { algorithms: ['HS256'] }, versões vulneráveis de bibliotecas JWT aceitam alg: none — um header manipulado que bypassa completamente a verificação de assinatura. Sempre fixe o algoritmo explicitamente.
Rate limiting, logging e validação: o que vem depois
Rate limiting
Autenticação e autorização protegem quem acessa e o que acessa. Rate limiting protege contra volume abusivo — força bruta, scraping e ataques de enumeração.
Para memória local (projetos pequenos ou single-instance):
import rateLimit from 'express-rate-limit';
const limiteLogin = rateLimit({
windowMs: 15 * 60 * 1000,
max: 10,
message: { error: 'Muitas tentativas. Tente novamente em 15 minutos.' },
standardHeaders: true,
legacyHeaders: false,
});
router.post('/auth/login', limiteLogin, loginHandler);
router.post('/auth/registro', limiteLogin, registroHandler);
router.post('/auth/recuperar-senha', limiteLogin, recuperarSenhaHandler);
Para múltiplas instâncias atrás de um load balancer, o contador em memória local não funciona — cada instância conta separadamente. Use rate-limit-redis com um store centralizado:
import RedisStore from 'rate-limit-redis';
import { createClient } from 'redis';
const redisClient = createClient({ url: process.env.REDIS_URL });
await redisClient.connect();
const limiteLogin = rateLimit({
windowMs: 15 * 60 * 1000,
max: 10,
store: new RedisStore({
sendCommand: (...args: string[]) => redisClient.sendCommand(args),
}),
});
Logging de acesso negado
Acessos 403 sem log são pontos cegos. Alguém tentando acessar /admin/usuarios repetidamente com tokens de usuário comum pode ser reconhecimento — e você não saberá sem registro:
export function autorizar(...roles: string[]) {
return (req: RequisicaoAutenticada, res: Response, next: NextFunction) => {
const role = req.usuario?.role;
if (!role || !roles.includes(role)) {
console.warn('[AUTHZ] Acesso negado', {
userId: req.usuario?.id,
role,
rolesExigidas: roles,
rota: req.originalUrl,
method: req.method,
ip: req.ip,
timestamp: new Date().toISOString(),
});
return res.status(403).json({
error: 'Acesso não autorizado',
code: 'FORBIDDEN',
});
}
next();
};
}
Em produção, substitua console.warn por um logger estruturado como pino ou winston — o JSON gerado vai direto para ferramentas de observabilidade como Datadog, Loki ou CloudWatch.
Validação de entrada
Usuário autenticado e autorizado não significa que o body da requisição é seguro. Valide tudo que chega antes de chegar ao banco. Com zod, a validação e a inferência de tipos ficam no mesmo lugar:
import { z } from 'zod';
const schemaCriarPedido = z.object({
produtoId: z.string().uuid(),
quantidade: z.number().int().positive().max(100),
enderecoId: z.string().uuid(),
});
router.post('/pedidos', autenticar, async (req: RequisicaoAutenticada, res) => {
const resultado = schemaCriarPedido.safeParse(req.body);
if (!resultado.success) {
return res.status(400).json({
error: 'Dados inválidos',
detalhes: resultado.error.flatten(),
});
}
const pedido = await db.pedido.create({
data: {
...resultado.data,
userId: req.usuario!.id,
},
});
res.status(201).json(pedido);
});
Checklist de proteção de rotas
| Item | O que verificar |
|---|---|
| ✅ Autenticação separada de autorização | Middleware de auth só identifica o usuário — autorização fica em camada própria? |
| ✅ 401 vs 403 | 401 para não autenticado, 403 para não autorizado — sem misturar? |
| ✅ Algoritmo de JWT fixado | jwt.verify usa { algorithms: ['HS256'] } explícito? |
| ✅ Rotas internas protegidas | Toda rota registrada tem alguma forma de verificação, mesmo que interna? |
| ✅ Verificação de posse de recurso | IDs de recursos verificam se pertencem ao usuário da requisição? |
| ✅ Autorização no servidor | Nenhuma decisão de autorização depende só do que o frontend envia? |
| ✅ Rate limiting em endpoints sensíveis | Login, registro e reset de senha têm limite de tentativas? |
| ✅ Rate limiting com store centralizado | Em múltiplas instâncias, o contador usa Redis ou equivalente? |
| ✅ Logs de acesso negado | Tentativas 403 são registradas com userId, rota, método e IP? |
| ✅ Validação de entrada | Bodies de requisições de escrita são validados com schema antes de chegar ao banco? |
FAQ
Qual a diferença entre RBAC e ABAC?
RBAC (Role-Based Access Control) controla acesso por papéis fixos: admin, user, moderador. É direto de implementar e cobre a maioria das aplicações com menos de 10 roles distintas. ABAC (Attribute-Based Access Control) controla acesso por atributos combinados do usuário, do recurso e do contexto — por exemplo, "gerente do departamento financeiro pode acessar relatórios da sua regional apenas durante horário comercial". É muito mais expressivo, mas também muito mais complexo de manter. Para APIs com regras de negócio simples, RBAC resolve. ABAC faz sentido quando as regras de acesso variam por múltiplos eixos independentes, como sistemas ERP ou plataformas multitenancy com permissões por organização.
Devo buscar a role no banco a cada requisição ou confiar no token?
Depende da operação. Para leitura e operações de baixo risco, confiar no payload do token com expiração curta (15 minutos) é aceitável e mais eficiente — evita uma query adicional por requisição. Para operações críticas — deleção permanente de dados, pagamentos, promoção de usuários, exportação em massa — consulte o banco para confirmar que a role atual do usuário ainda corresponde ao que está no token. O custo de uma query extra é insignificante comparado ao risco de uma role desatualizada executar uma operação irreversível.
Como proteger rotas em microsserviços onde o token não chega diretamente?
O serviço de autenticação valida o token e repassa os dados do usuário via headers internos para os serviços downstream. Cada serviço confia nesses headers somente quando a requisição vem do gateway ou de um proxy interno — nunca de um cliente externo diretamente. Para comunicação entre serviços, combine isso com mTLS ou tokens de serviço com escopo limitado. Sem essa separação, qualquer serviço interno pode forjar headers com x-user-role: admin.
express-jwt resolve tudo isso automaticamente?
Ele cuida da verificação do token e injeta o payload em req.auth. É um atalho útil para o middleware de autenticação — menos código de verificação para manter. Mas não resolve autorização: você ainda precisa implementar a camada de roles, a verificação de posse de recursos e o logging de acessos negados. Use como ponto de partida para autenticação, não como solução completa de segurança.
Como testar o middleware de autenticação?
Com Supertest no Express, você consegue testar os cenários críticos sem subir o servidor completo:
import request from 'supertest';
import app from '../app';
import jwt from 'jsonwebtoken';
describe('Middleware autenticar', () => {
it('retorna 401 quando não há token', async () => {
const res = await request(app).get('/api/perfil');
expect(res.status).toBe(401);
expect(res.body.code).toBe('UNAUTHENTICATED');
});
it('retorna 401 quando o token está expirado', async () => {
const token = jwt.sign(
{ sub: '123', role: 'user' },
process.env.JWT_SECRET!,
{ expiresIn: -1 }
);
const res = await request(app)
.get('/api/perfil')
.set('Authorization', `Bearer ${token}`);
expect(res.status).toBe(401);
expect(res.body.code).toBe('TOKEN_EXPIRED');
});
it('retorna 403 quando o role é insuficiente', async () => {
const token = jwt.sign(
{ sub: '123', role: 'user' },
process.env.JWT_SECRET!,
{ expiresIn: '15m' }
);
const res = await request(app)
.get('/api/admin/usuarios')
.set('Authorization', `Bearer ${token}`);
expect(res.status).toBe(403);
expect(res.body.code).toBe('FORBIDDEN');
});
});
Próximos passos
Se a sua API tem middleware de autenticação mas não tem middleware de autorização separado, esse é o ponto com maior retorno imediato. O padrão autenticar → autorizar(...roles) em cadeia no Express é simples de implementar, não exige refatoração do que já existe e elimina o vetor mais comum de acesso indevido.
Depois que a autorização estiver no lugar: rate limiting nos endpoints de autenticação, logging estruturado de acessos negados e validação de entrada com zod antes de qualquer operação de escrita no banco.