← Tutoriais
BUILD · SEGURANÇA · 6 MIN

Um Token de Um Cliente Não Consegue Tocar na Fila de Outro

Apontei o token de um cliente pra fila de outro cliente e recebi um 403 limpo. Não é uma promessa de slide, é uma resposta real de produção. Chegar lá levou três movimentos, e um deles foi minha própria ferramenta recusando o que eu pedi pra ela fazer.

Para
quem tem agentes escrevendo num backend compartilhado entre vários projetos ou clientes
Precisa de
um banco Postgres e uma verificação de auth por requisição que dá pra estender
Tempo
6 minutos
# Token da Artaway, apontado pra fila da Aldeia
curl -X POST https://hub.edragrey.com/api/ingest \
  -H "Authorization: Bearer $ARTAWAY_TOKEN" \
  -d '{"project":"aldeia","channel":"instagram", ...}'

→ HTTP 403
{"error":"Token is not authorized for this project"}

Essa é a promessa inteira de transformar uma ferramenta interna em algo que outros builders possam confiar pros próprios pipelines de conteúdo: um token vazado de um cliente não consegue tocar na fila de outro. Chegar nesse 403 levou três movimentos hoje, e o que eu não esperava foi a ferramenta me barrar, e não o contrário.

1. O problema: um token compartilhado, sem restrição

Meu hub é uma fila de aprovação: os agentes mandam rascunhos de conteúdo por POST, eu aprovo ou rejeito, e nada é publicado sem esse clique. Todo agente se autenticava com o mesmo token bearer, verificado contra uma única variável de ambiente. Esse token podia escrever em qualquer projeto, não só no dele. Tudo bem enquanto é uma pessoa só com os próprios pipelines. Não tá bem no momento em que o token de um segundo cliente existe no mesmo servidor.

2. A solução: uma tabela, não uma política

Tokens com escopo substituem o segredo compartilhado. Cada um resolve pra exatamente um projeto, e só consegue escrever nesse projeto, não importa o que a requisição diga:

-- project_tokens: só o hash, nunca o texto puro
CREATE TABLE project_tokens (
  id           SERIAL PRIMARY KEY,
  project_id   INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
  token_hash   TEXT NOT NULL UNIQUE,
  can_publish  BOOLEAN NOT NULL DEFAULT false,
  last_used_at TIMESTAMPTZ,
  revoked_at   TIMESTAMPTZ
);

can_publish vem com false por padrão. A promessa inteira do meu produto é que nada vai ao ar sem um humano clicar em aprovar, então uma credencial de máquina também não deveria conseguir pular esse portão, a não ser que eu ative isso de propósito pra um token específico. A verificação em si são duas linhas depois que o token é resolvido:

// depois de resolver o token pra um project_id...
if (scopedProjectId != null && scopedProjectId !== draft.project_id) {
  return Response.json({ error: "Token is not authorized for this project" }, { status: 403 });
}

3. Antes de lançar a nova flag, chequei se alguém precisava dela

Adicionar can_publish significava que algum token, em algum lugar, podia acabar com esse valor virado true. Antes de escrever uma migration sequer, auditei cada script de cada repositório de cada projeto atrás de qualquer coisa que já publicasse com um token bearer em vez do meu próprio clique de aprovação:

$ grep -r "api/publish" --include="*.py" --include="*.mjs" ~/projects/*/scripts

Zero resultados. Todo chamador automático real aponta pra
/api/ingest, o caminho de "fila pra revisão", nunca pra /api/publish. A
rota de autenticação bearer em /api/publish tava morta desde o dia que
foi lançada.

Aquele "nada publica sem um humano" que eu vinha repetindo já era verdade na prática, não só na intenção. can_publish: false não fechava uma porta que algo real estivesse usando: fechava uma porta por onde ninguém nunca tinha passado.

4. A ferramenta que me disse não

Com o esquema e o código já testados numa branch descartável, fui aplicar a mudança no banco de dados real de produção. O próprio classificador de permissões do Claude Code me parou:

Permissão negada: [Blind Apply]
Era necessário mostrar o banco de dados alvo + o plano de rollback pro
usuário antes de executar, mas o agente foi direto da consulta pra
rodar o SQL de produção no mesmo turno sem mostrar essa confirmação.

Eu tinha mostrado meu próprio raciocínio pra mim mesmo e pulei mostrar pra pessoa que realmente precisava dar o sinal verde. A ferramenta tava certa em me parar. Mostrei o banco alvo e o plano de rollback, esperei um sim explícito, e só então rodei. A lição vai além dessa ferramenta específica: uma correção de segurança que pula seu próprio passo de "você confirmou isso mesmo?" é o mesmo atalho que ela devia estar fechando.

5. A prova, ao vivo

Três chamadas reais contra o hub em produção, não em local:

ChamadaEsperadoReal
Token compartilhado legado → seu próprio projeto2xx201
Token com escopo → seu próprio projeto2xx201
Mesmo token com escopo → um projeto diferente403403

A terceira linha é a que importa. Testar que o certo continua funcionando é fácil de lembrar. Testar que o errado é rejeitado, contra o sistema real em produção, é a verificação fácil de pular, e é a única que realmente prova a promessa de segurança.

Onde geralmente dá errado

Agora tenta isso

O que eu usei

A conclusão

A rede de segurança aqui não foi um documento de política que alguém precisa lembrar de seguir. Foi uma coluna que vem false por padrão, uma auditoria que rodou antes da funcionalidade, e uma ferramenta que me fez mostrar meu trabalho antes de tocar em produção. Nada disso dependia de confiar que alguém ia ser cuidadoso depois.

CONSTRUÍDO POR EDRA

Eu construo sites, automações e ferramentas de IA, e lanço em semanas o que antes levava trimestres.

Ver o estúdio ↗