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.
# 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:
| Chamada | Esperado | Real |
|---|---|---|
| Token compartilhado legado → seu próprio projeto | 2xx | 201 |
| Token com escopo → seu próprio projeto | 2xx | 201 |
| Mesmo token com escopo → um projeto diferente | 403 | 403 |
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
- Lançar a permissão antes de auditar quem ia usar. Quase adicionei
can_publishcomo capacidade ativa antes de checar que ninguém precisava dela ainda. A auditoria transformou um chute em fato. - Testar só o caminho feliz. Um 201 na chamada certa não prova nada sobre o escopo. O 403 na chamada errada é o teste de verdade.
- Pular o passo de "mostrar seu trabalho" porque você já sabe que tá certo. É exatamente nesse momento que um segundo par de olhos, humano ou os próprios limites da ferramenta, pega o que você não viu.
Agora tenta isso
- Adiciona o booleano. Qualquer credencial de máquina que escreve devia ter sua permissão mais perigosa desligada por padrão.
- Audita antes de conceder. Antes de ligar uma capacidade nova pra qualquer coisa, checa se algo vivo realmente precisa dela.
- Testa a rejeição, não só o sucesso. Roda o caso negativo contra seu sistema real em produção e guarda a resposta: é o recibo que prova a promessa.
O que eu usei
- Claude Code: rodou as duas auditorias independentes e escreveu a migration e a verificação de escopo
- Branches da Neon: testou o esquema numa cópia descartável de produção antes de tocar na real
- As ferramentas MCP da Vercel: checaram o status do deploy ao vivo e rodaram o teste de fumaça sem precisar do painel
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.
Eu construo sites, automações e ferramentas de IA, e lanço em semanas o que antes levava trimestres.
Ver o estúdio ↗