Skip to content

fix(integrations): state do OAuth assinado e token só para o endpoint do provedor (CRM-29) - #82

Merged
daniloleonecarneiro merged 8 commits into
developfrom
feat/CRM-29-state-oauth-assinado
Nov 4, 2026
Merged

daniloleonecarneiro merged 8 commits into
developfrom
feat/CRM-29-state-oauth-assinado

Conversation

@nickoliveira23

@nickoliveira23 nickoliveira23 commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

O parâmetro state do OAuth das integrações dos agentes passa a ser emitido e conferido pelo servidor, e o endpoint MCP do fluxo passa a ser resolvido só no servidor:

  • State assinado (src/services/oauth_state.py): HMAC-SHA256 com chave derivada da ENCRYPTION_KEY, validade de 15 minutos e nonce. Ele carrega só agent_id, issued_at, nonce e sig. A assinatura também cobre quem iniciou o fluxo (usuário e, quando existe, o contexto da requisição), sem que esses ids viajem no state: um fluxo iniciado por um usuário só pode ser concluído por ele.
  • Endpoint resolvido no servidor. O endpoint MCP vem da configuração do serviço; o callback não lê endereço nenhum do state.
  • Callback só pelo POST. Os GET /<provedor>/callback saíram; o frontend conclui o fluxo pelo POST .../callback, autenticado e com integrations:connect.
  • Bearer só para o endpoint do provedor. No runtime (configs MCP, builder do agente e discover-tools), o token de uma integração nativa vai só ao endpoint MCP do próprio provedor (src/services/mcp_provider_endpoints.py). Servidores MCP customizados mantêm a URL configurada.

Também entram:

  • Mensagens de erro genéricas nas respostas de autorização e callback.
  • Logs sem trechos de token nem respostas de token.

Test plan

  • pytest tests/unit --ignore=tests/unit/test_exception_handlers.py → 698 passed
  • Testes novos:
    • test_oauth_state.py
    • test_oauth_callback_contract.py: os 12 provedores, incluindo Google e PayPal
    • test_oauth_caller.py
    • test_mcp_provider_endpoints.py
    • test_integrations_rbac_gates.py: atualizado
  • Mutações: assinatura, validade, agente, amarração ao chamador, dependências sem o wrapper, cada config de runtime, builder, discover-tools e tabela de endpoints. Todas deixam a suíte vermelha.
  • Ao vivo:
    • GET .../callback → 404
    • State emitido para um usuário e concluído por outro → recusado, sem requisição de saída
    • mcp_url fora da tabela gravado na configuração → o runtime usa o endpoint do provedor

Changelog

  • fix(integrations): state do OAuth assinado, com validade e amarrado a quem iniciou o fluxo
  • fix(integrations): callback só pelo POST autenticado; erros genéricos; sem token em log
  • fix(integrations): o token de integração nativa só vai ao endpoint MCP do provedor

Nota de operação: se algum ambiente cadastrou no app OAuth do provedor um redirect_uri apontando para o callback GET do processor (em vez da rota /<provedor>/callback do frontend), esse fluxo passa a dar 404. O caminho padrão (redirect_uri derivado do frontend) não muda. Conexões existentes não mudam de URL: a tabela de endpoints inclui todas as grafias que o código já usa.

…o no servidor (CRM-29)

O state das integrações OAuth dos agentes era um JSON em base64 sem
assinatura, e o complete_authorization confiava nos campos dele,
inclusive no endereço do servidor MCP usado na troca do código, na
descoberta de metadados e gravado na integração.

src/services/oauth_state.py emite o state com agent_id, emissão, nonce e
um HMAC-SHA256 com chave derivada do ENCRYPTION_KEY, e o valida em tempo
constante, com validade de 15 minutos e o agent_id esperado. O serviço
base, o PayPal e as duas integrações Google passam a emitir e validar por
ele, e o endereço do MCP é sempre o do próprio serviço. O agent_id segue
no topo do JSON, que é onde a tela de retorno do frontend o lê.

As falhas do complete_authorization respondem uma mensagem genérica e o
detalhe fica no log do servidor, de onde também saem a resposta da troca
de token, prévias e prefixos de access e refresh token.

Co-Authored-By: Claude Code <[EMAIL_REDACTED]>
… erros (CRM-29)

As 12 integrações OAuth tinham, além do callback autenticado que o
frontend usa (POST /agents/{agent_id}/integrations/{provider}/callback),
um GET /integrations/{provider}/callback que tirava o agente e o
endereço do MCP do próprio state. Nenhum cliente chama essas rotas; elas
saem, com o registro no main.py.

Nas rotas de início e de callback, as falhas respondem mensagens
genéricas em vez do texto da exceção. O error_response recebia um
`request` que não existia no handler de início (NameError em 10
provedores) e o corpo da requisição no de callback; as chamadas deixam
de passá-lo.

Co-Authored-By: Claude Code <[EMAIL_REDACTED]>
… state traz (CRM-29)

test_oauth_state cobre o state emitido: valida para o próprio agente, o
frontend segue lendo o agent_id com um decode simples, não leva URL, e é
recusado se vier no formato sem assinatura, alterado em qualquer campo,
de outro agente, vencido, do futuro, assinado com outra chave ou sem
chave configurada.

test_oauth_callback_contract roda os 12 provedores com o cliente HTTP
substituído por um que grava as chamadas: state sem assinatura ou de
outro agente é recusado antes de qualquer requisição; a URL de
autorização leva o state assinado; a falha da troca de token não volta
na resposta; e mesmo um state assinado com mcp_url não escolhe o
endereço usado nem o gravado (base e PayPal).

O teste de RBAC deixa de exigir os callbacks sem permissão e passa a
garantir que eles não voltem e que o callback seja o POST com
integrations:connect. O oauth_state.py recebe a formatação do black.

Co-Authored-By: Claude Code <[EMAIL_REDACTED]>
…niciaram o fluxo (CRM-29)

Achado da review: o state assinado conferia o agente e a validade, mas
não quem completava o fluxo. Dentro da validade, um state emitido para
um usuário era aceito na sessão de outro, inclusive de outra conta; nas
integrações Google, sem PKCE, nada mais segurava isso.

A assinatura passa a cobrir também o usuário e a conta de quem chama
(src/api/oauth_caller.py), sem levá-los no state: o callback recalcula
a assinatura com o chamador do request, e outro usuário ou outra conta
não batem. Sem identidade de usuário, nada é emitido nem aceito. As 12
dependências que montam os serviços das integrações registram o
chamador no serviço (binds_oauth_caller).

Co-Authored-By: Claude Code <[EMAIL_REDACTED]>
… MCP do provedor (CRM-29)

Achado da review: com o callback corrigido, o runtime ainda mandava o
Bearer da integração ao mcp_url gravado no config do agente, e esse
valor pode ser escrito fora do callback (o PUT de configuração mescla o
que o corpo traz, e configs gravados antes da correção podem ter
qualquer coisa).

src/services/mcp_provider_endpoints.py lista os endpoints de cada
integração nativa, com todas as grafias que o código já usa, para que
uma conexão que funciona hoje não mude de URL. Um mcp_url fora da lista
é trocado pelo padrão do provedor nos configs do runtime das 10
integrações MCP, no carregamento de tools do agente e na descoberta de
tools. Servidores MCP customizados seguem com a URL configurada.

O builder do agente deixa de logar o config inteiro da integração (com
tokens) e passa a resolver a URL por integration_mcp_url_for.

Co-Authored-By: Claude Code <[EMAIL_REDACTED]>
Um mcp_url gravado de fora da lista nunca recebe o Bearer nos configs
de runtime que leem a integração (asana, canva, monday, paypal,
supabase, atlassian, hubspot), nem nos que recebem a URL de quem chama
(github, notion, linear), nem no carregamento de tools do agente, nem
na descoberta de tools. Os padrões espalhados (serviços, configs do
runtime e builder) são conferidos contra a lista, para que um novo
padrão não corte uma conexão em silêncio.

Co-Authored-By: Claude Code <[EMAIL_REDACTED]>
Co-Authored-By: Claude Code <[EMAIL_REDACTED]>
@sourcery-ai

sourcery-ai Bot commented Oct 13, 2026

Copy link
Copy Markdown

Sorry @nickoliveira23, your pull request is larger than the review limit of 150,000 diff characters

@daniloleonecarneiro
daniloleonecarneiro merged commit a88f4a3 into develop Nov 4, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants