Programação Por Equipe DevBras

Capa do artigo: Cliente e admin no mesmo sistema: separe sessão, cookie e rota, e prove com teste

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

  1. Crie duas instâncias de express-session, cada uma com name e secret próprios.
  2. Restrinja o cookie do admin ao caminho /admin e use sameSite: 'strict' nele.
  3. Monte cada área num Router separado, cada um com a sua sessão.
  4. No router do admin, registre as rotas de entrada e, logo depois, o middleware que exige sessão de equipe para todo o resto.
  5. Regenere a sessão depois do login, para que um identificador anterior não continue valendo.
  6. 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?

Avalie este artigo

Referência: https://medium.com/@sayhicoelho/laravel-5-4-trabalhando-com-autentica%C3%A7%C3%B5es-independentes-sem-packages-adicionais-6e50c11a0b79