Autenticação e autorização em SaaS multi-tenant gerado com IA

O agente acerta autenticação e erra autorização. Como o IDOR aparece em código gerado por IA, por que os testes não pegam e onde colocar a checagem para não ser esquecida.

Autenticação e autorização em SaaS multi-tenant gerado com IA
Neste artigo
  1. Que diferença resolve metade dos bugs?
  2. Por que os testes não pegam esse erro?
  3. Onde colocar a checagem para ela não ser esquecida?
  4. O que ler linha por linha, sempre?
  5. Como um convite passou pela minha revisão?
  6. Como pedir isso para o agente?
  7. Perguntas frequentes

Autenticação o agente costuma acertar, porque é padrão e tem biblioteca boa. Autorização ele erra, e erra de um jeito que passa em todos os testes: o endpoint confirma que você está logado e esquece de confirmar que aquele recurso é seu. Num SaaS multi-tenant isso significa um cliente lendo o dado do outro.

Que diferença resolve metade dos bugs?

Autenticação responde “quem é você”. Autorização responde “você pode fazer isso com este recurso”. São duas perguntas, e o agente responde a primeira com competência e trata a segunda como detalhe.

O código problemático parece perfeitamente saudável:

// Parece certo. Tem guard, tem usuario logado.
@Get('projects/:id')
async findOne(@Param('id') id: string) {
  return this.projects.findById(id);   // <- e aqui que vaza
}

O guard garantiu que existe um usuário. Ninguém garantiu que o projeto id pertence ao tenant desse usuário. Trocar o número na URL devolve o projeto de outro cliente, com status 200 e sem nenhum erro no log.

Isso é IDOR, e é a falha mais comum que eu encontro em código gerado por agente.

Por que os testes não pegam esse erro?

Porque teste gerado por agente segue o caminho feliz: cria um usuário, cria um projeto desse usuário, busca o projeto, confirma que voltou. Tudo verde.

O teste que pega é o negativo, e ele quase nunca é escrito espontaneamente: cria dois tenants, cria um recurso no tenant A, autentica como tenant B, busca o recurso do A, e exige 404.

Repare no detalhe: 404, não 403. Responder 403 confirma que o recurso existe, o que já é vazamento de informação.

Onde colocar a checagem para ela não ser esquecida?

Se o filtro por tenant depende de alguém lembrar em cada endpoint, uma hora alguém esquece. As três camadas, da mais fraca pra mais forte:

Camada Como funciona Ponto fraco
No controller Cada rota checa Depende de disciplina, esquece fácil
No service ou repositório Um ponto central injeta o tenant em toda query Query crua escapa
Row Level Security no Postgres O banco recusa a linha Exige configurar sessão por request

Eu uso a segunda como padrão e a terceira quando o dado é sensível. A combinação transforma um esquecimento em erro de banco, em vez de vazamento silencioso, e essa diferença é o que interessa quando o código é escrito rápido.

O que ler linha por linha, sempre?

Não reviso tudo que o agente escreve. Reviso estes, sem exceção:

  • Todo endpoint que recebe um id na URL ou no corpo
  • Toda query crua, porque ela escapa do repositório central
  • Toda rota administrativa, porque "é só admin" é a desculpa mais comum pra pular a checagem
  • Todo lugar que monta filtro dinâmico a partir de parâmetro do cliente
  • Convite, troca de senha e transferência de posse, que mexem em quem pode o quê

Como um convite passou pela minha revisão?

Num projeto eu escrevi no PRD apenas que o admin convida pessoas pro workspace. O agente implementou convite por e-mail com link, que é razoável. Só que o link não expirava, aceitava múltiplos usos, e não checava se quem aceitou era o e-mail convidado.

Ou seja: qualquer pessoa com o link entrava no workspace, para sempre. Não apareceu em teste nenhum, porque o teste convidava e aceitava, e funcionava. Apareceu quando eu fui ler o arquivo por outro motivo.

A lição não foi "o agente é ruim", foi que eu não escrevi a restrição. Hoje a linha está no template do PRD: convite expira, é de uso único, e só é aceito pelo destinatário. Formato completo em como escrever um PRD para Claude Code e Codex.

Como pedir isso para o agente?

Implemente o CRUD de projects.

Autorizacao (obrigatorio):
- toda leitura e escrita filtra por tenant_id da sessao
- recurso de outro tenant responde 404, nunca 403
- rota de admin NAO ignora o filtro de tenant
- nenhuma query crua sem o filtro

Testes obrigatorios, alem do caminho feliz:
- tenant B tentando ler recurso do tenant A -> 404
- tenant B tentando editar recurso do tenant A -> 404
- usuario sem papel tentando rota de admin -> 403

Quando o teste negativo está no pedido, ele vem junto, e o próprio agente costuma corrigir o código pra fazer o teste passar.

Perguntas frequentes

RLS resolve sozinho?

Resolve o vazamento por query esquecida, que é o pior. Não resolve regra de papel, do tipo "membro só edita a tarefa dele", que continua sendo lógica de aplicação.

Dá pra confiar quando o agente diz que revisou a segurança?

Não como prova. Ele revisa contra o que você descreveu, e o buraco costuma estar no que ninguém descreveu. O teste negativo executado vale mais que qualquer confirmação em texto.

E se dois agentes concordarem que está seguro?

Continua não sendo prova. Eles compartilham viés de treino e concordam errado com frequência. Escrevi sobre isso em Claude Code e Codex juntos. O checklist antes de subir está em checklist de segurança.

WA in X