Cliente e admin no mesmo sistema: separe sessão, cookie e rota, e prove com teste
Dê a cliente e equipe sessões com cookies, segredos e middlewares próprios, e escreva o teste que prova que a sessão de cliente nunca abre o painel.
Terça, 10h, uma cliente da loja manda um print para o suporte: ela está vendo a lista de pedidos de todo mundo. Ninguém invadiu nada. Ela entrou com a conta dela, digitou um endereço que viu num e-mail antigo, e a tela de relatórios do admin abriu. O sistema checava o papel do usuário em quase todas as rotas do painel. Quase.
A resposta curta: se cliente e equipe são públicos diferentes, dê a cada um a sua própria sessão, com cookie de nome diferente, segredo diferente e um middleware aplicado ao grupo inteiro de rotas, não tela por tela. E escreva um teste automatizado que entra como cliente e prova que nenhuma URL do admin responde 200. Um campo is_admin na tabela de usuários e um if no menu não são isolamento.
Por que o if do papel sempre esquece uma tela
No modelo "um login só", todo mundo entra pelo mesmo formulário, recebe o mesmo cookie de sessão e o sistema decide o que mostrar olhando um campo do usuário. A segurança passa a depender de cada rota lembrar de checar esse campo. Cada rota nova, cada exportação de CSV, cada endpoint de API criado às pressas é uma chance de esquecer.
Há outros vazamentos menos óbvios. O "lembrar de mim" do cliente, que dura semanas, passa a valer para o painel. Uma falha de XSS na área do cliente roda com o mesmo cookie que dá acesso ao admin. O tempo de sessão que é razoável para quem compra uma camiseta fica longo demais para quem pode estornar pedidos. Misturar sessão é misturar poder.
Separar não significa duplicar o sistema. Significa que cada público tem seu próprio "mundo" de autenticação: tabela ou provedor de usuários, formulário de entrada, cookie, regras de expiração e um portão único na frente das rotas dele.
O que separar, camada por camada
- Usuários: idealmente tabelas diferentes, ou ao menos um tipo de conta que não muda por um campo editável no próprio perfil.
- Sessão e cookie: nomes diferentes, segredos de assinatura diferentes e, se possível, escopo diferente (um subdomínio como admin.loja.com.br, ou um caminho como /admin).
- Portão: um middleware aplicado ao grupo de rotas do admin inteiro, que só deixa passar quem tem sessão de equipe.
- Expiração: sessão de equipe curta e sem "lembrar de mim"; sessão de cliente pode ser mais longa.
- Prova: testes que tentam abrir rotas do admin com sessão de cliente e sem sessão.
Em Laravel, isso se faz com guards e providers separados, e o post sobre dois logins no Laravel mostra o passo a passo no framework. Aqui o exemplo é em Node.js com Express, para mostrar o mecanismo sem a mágica de configuração.
Como montar as duas sessões em Express
- Crie duas instâncias de
express-session, cada uma comnameesecretpróprios. - Restrinja o cookie do admin ao caminho
/admine usesameSite: 'strict'nele. - Monte cada área num
Routerseparado, cada um com a sua sessão. - No router do admin, registre as rotas de entrada e, logo depois, o middleware que exige sessão de equipe para todo o resto.
- Regenere a sessão depois do login, para que um identificador anterior não continue valendo.
- Escreva o teste que entra como cliente e tenta abrir o painel.
O app com dois mundos
const express = require('express');
const session = require('express-session');
const { verificaCliente, verificaEquipe } = require('./auth');
const producao = process.env.NODE_ENV === 'production';
const app = express();
app.use(express.urlencoded({ extended: false }));
const sessaoAdmin = session({
name: 'adm_sid',
secret: process.env.ADMIN_SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { path: '/admin', httpOnly: true, sameSite: 'strict', secure: producao, maxAge: 8 * 60 * 60 * 1000 }
});
const sessaoLoja = session({
name: 'loja_sid',
secret: process.env.LOJA_SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { path: '/', httpOnly: true, sameSite: 'lax', secure: producao }
});
function exigeEquipe(req, res, next) {
if (req.session.equipeId) return next();
return res.redirect('/admin/entrar');
}
const admin = express.Router();
admin.use(sessaoAdmin);
admin.get('/entrar', (req, res) => res.send('formulário da equipe'));
admin.post('/entrar', async (req, res, next) => {
const membro = await verificaEquipe(req.body.email, req.body.senha);
if (!membro) return res.status(401).send('Credenciais inválidas');
req.session.regenerate((err) => {
if (err) return next(err);
req.session.equipeId = membro.id;
res.redirect('/admin/pedidos');
});
});
admin.use(exigeEquipe);
admin.get('/pedidos', (req, res) => res.send('todos os pedidos'));
const loja = express.Router();
loja.use(sessaoLoja);
loja.post('/entrar', async (req, res, next) => {
const cliente = await verificaCliente(req.body.email, req.body.senha);
if (!cliente) return res.status(401).send('Credenciais inválidas');
req.session.regenerate((err) => {
if (err) return next(err);
req.session.clienteId = cliente.id;
res.redirect('/conta');
});
});
loja.get('/conta', (req, res) => res.send('meus pedidos'));
app.use('/admin', admin);
app.use('/', loja);
module.exports = app;
O módulo ./auth é onde você confere a senha com hash, cada função na sua tabela de usuários; o post sobre login com senha protegida cobre essa parte. Repare que exigeEquipe vem depois das rotas de entrada e antes de todas as outras: qualquer rota adicionada abaixo dele já nasce protegida, sem ninguém precisar lembrar.
O cookie loja_sid tem caminho /, então o navegador também o envia para /admin. Não tem problema: a sessão do admin só lê adm_sid, assinado com outro segredo. O cookie de cliente chega e é ignorado. Para isolamento ainda mais forte, coloque o painel num subdomínio próprio e deixe os cookies sem atributo de domínio, que ficam restritos ao host que os criou.
Em produção, troque o armazenamento em memória padrão do express-session por um store persistente, como Redis ou banco. O padrão não foi feito para produção e perde todas as sessões a cada reinício. Os detalhes de httpOnly, secure e sameSite estão no guia de cookie de sessão com HttpOnly.
O teste que prova o isolamento
const test = require('node:test');
const assert = require('node:assert');
const request = require('supertest');
process.env.ADMIN_SESSION_SECRET = 'segredo-admin-de-teste';
process.env.LOJA_SESSION_SECRET = 'segredo-loja-de-teste';
const app = require('./app');
test('sessão de cliente não abre o painel', async () => {
const agente = request.agent(app);
await agente.post('/entrar').type('form')
.send({ email: 'ana@exemplo.com', senha: 'senha-de-teste' })
.expect(302);
const resposta = await agente.get('/admin/pedidos');
assert.notStrictEqual(resposta.status, 200);
});
test('sem sessão, o painel redireciona para a entrada', async () => {
await request(app).get('/admin/pedidos').expect(302).expect('Location', '/admin/entrar');
});
O request.agent guarda cookies entre as chamadas, como um navegador. No ambiente de teste, verificaCliente precisa encontrar a conta de exemplo, seja num banco de teste ou num módulo substituto. O ponto é ter o teste rodando no CI: no dia em que alguém criar uma rota do admin fora do router protegido, ou trocar a ordem dos middlewares, o build quebra antes da cliente mandar o print.
Vale ampliar o teste com uma lista das rotas do painel, percorrida num laço. Mais importante que a quantidade é que ele rode a cada pull request.
Erros comuns
- Esconder o menu e chamar isso de permissão: o link some, mas a URL continua respondendo para quem a digitar.
- Checar o papel tela por tela: a primeira rota esquecida, geralmente uma exportação ou endpoint de API, vira vazamento de dados.
- Mesmo cookie e mesmo segredo para os dois públicos: uma falha na área do cliente passa a valer para o painel.
- "Lembrar de mim" compartilhado: o acesso de equipe ganha semanas de validade num computador que pode ser de uso comum.
- Não regenerar a sessão no login: um identificador de sessão plantado antes da autenticação continua válido depois dela.
- Sair só apagando o cookie no navegador: a sessão segue viva no servidor; o post sobre sair de todos os aparelhos mostra como invalidar de verdade.
Checklist
- Cliente e equipe têm cookies de sessão com nomes e segredos diferentes.
- O cookie do admin é restrito ao caminho ou subdomínio do painel.
- Um único middleware protege todo o grupo de rotas do admin.
- A sessão é regenerada depois de cada login.
- A sessão de equipe expira em horas e não tem "lembrar de mim".
- Há teste automatizado que tenta abrir o painel com sessão de cliente.
- O store de sessão em produção é persistente, não a memória do processo.
Entre agora no seu sistema com uma conta de cliente e cole na barra de endereço a URL de uma tela do painel: ela responde 200 ou manda você embora?