OAuth do MCP End User para ChatGPT Apps ======================================= Status ------ Documento tecnico da issue ``#141``. Registra os requisitos para o MCP Rubrix ser aceito como app/connector no ChatGPT. As issues ``#137`` (validacao OAuth no ``mcp-enduser``) e ``#152``/``#159`` (facade OAuth e tela de consentimento "Conectar Rubrix") ja foram implementadas com base neste documento — ver checklist ao final. Falta apenas ``#140`` (publicacao HTTPS/cadastro no ChatGPT), que e infraestrutura de deploy. O formato inicial do app e ``tool-only``: o MCP expoe ferramentas de produto e nao exige widget no ChatGPT nesta etapa. Fontes oficiais usadas ---------------------- - OpenAI Apps SDK - Authentication: https://developers.openai.com/apps-sdk/build/auth - OpenAI Apps SDK - Define tools: https://developers.openai.com/apps-sdk/plan/tools - OpenAI Apps SDK - Connect from ChatGPT: https://developers.openai.com/apps-sdk/deploy/connect-chatgpt - OpenAI Apps SDK - Reference: https://developers.openai.com/apps-sdk/reference Decisoes -------- Provider OAuth ~~~~~~~~~~~~~~ A decisao para a primeira implementacao e usar uma facade OAuth propria do Rubrix para o ``mcp-enduser``. Nao vamos introduzir Keycloak, Auth0, Cognito, Okta ou outro IdP externo nesta fase. O motivo e manter o modelo atual de confianca do produto: - o backend Rubrix continua protegido internamente por ``X-Api-Key``; - as credenciais por plataforma e escopo criadas na issue ``#151`` continuam sendo a raiz de autorizacao para o MCP; - o OAuth existe na borda do ``mcp-enduser`` para atender ChatGPT e demais clientes MCP que esperam fluxo OAuth. Essa facade deve emitir access tokens curtos associados a uma credencial Rubrix de plataforma. O ``mcp-enduser`` valida o token OAuth, resolve a credencial Rubrix associada e chama o backend com ``X-Api-Key``. Identificador de recurso e audience ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ O recurso protegido canonico deve ser o endpoint publico HTTPS do MCP End User: .. code-block:: text https://mcp.rubrix.lat/mcp Esse valor deve aparecer como: - campo ``resource`` do protected resource metadata; - parametro ``resource`` recebido no authorization request; - parametro ``resource`` recebido no token request; - ``aud`` do access token emitido pela facade OAuth. O ``mcp-enduser`` deve rejeitar tokens sem ``aud`` igual ao recurso protegido. Redirect URI do ChatGPT ~~~~~~~~~~~~~~~~~~~~~~~ Para apps novos, a redirect URI esperada deve ser cadastrada exatamente como o ChatGPT mostrar na tela de gerenciamento do app: .. code-block:: text https://chatgpt.com/connector/oauth/{callback_id} Essa URI deve entrar na allowlist da facade OAuth Rubrix. Apps legados podem continuar usando a URI antiga ``https://chatgpt.com/connector_platform_oauth_redirect``, mas ela nao deve ser usada como padrao novo. Mapeamento usuario OAuth para tenant Rubrix ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ O modelo inicial nao mapeia diretamente um usuario ChatGPT para todas as contas Rubrix daquele e-mail. O mapeamento deve ser feito por credencial de plataforma: 1. Um usuario Rubrix com permissao para emitir API key cria uma credencial para a plataforma, por exemplo ``CHATGPT``. 2. Essa credencial define os escopos permitidos. 3. No fluxo futuro ``Conectar Rubrix``, o usuario escolhe ou cria essa credencial. 4. A facade OAuth emite um token associado ao ``api_key_id`` dessa credencial, ao ``tenant_id`` e aos escopos autorizados. 5. O ``mcp-enduser`` valida o token e encaminha chamadas ao backend com a API key associada. Esse desenho preserva o controle por tenant e evita reescrever o controle de acesso do backend nesta fase. Endpoints obrigatorios ---------------------- MCP ~~~ Endpoint publico do MCP: .. code-block:: text POST https://mcp.rubrix.lat/mcp Em desenvolvimento local, o modulo usa: .. code-block:: text http://localhost:8003/mcp Para cadastro no ChatGPT, o endpoint precisa estar publicado em HTTPS. O caminho do modulo e ``/mcp`` (mesmo prefixo ja usado pelo `mcp-server`, em porta diferente); se a infraestrutura publicar um proxy com outro path, o proxy deve preservar o mesmo recurso canonico usado no OAuth. Protected resource metadata ~~~~~~~~~~~~~~~~~~~~~~~~~~~ O MCP deve expor: .. code-block:: text GET https://mcp.rubrix.lat/.well-known/oauth-protected-resource Resposta esperada: .. code-block:: json { "resource": "https://mcp.rubrix.lat/mcp", "authorization_servers": ["https://mcp.rubrix.lat"], "scopes_supported": [ "rubrix.signature.read", "rubrix.signature.flows.create", "rubrix.signature.reminders.resend" ], "resource_documentation": "https://rubrix.lat/docs/mcp-enduser" } Esse endpoint e necessario para o ChatGPT descobrir onde iniciar OAuth e quais escopos existem. Authorization server metadata ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A facade OAuth Rubrix deve expor pelo menos um destes endpoints: .. code-block:: text GET https://mcp.rubrix.lat/.well-known/oauth-authorization-server GET https://mcp.rubrix.lat/.well-known/openid-configuration Resposta minima: .. code-block:: json { "issuer": "https://mcp.rubrix.lat", "authorization_endpoint": "https://mcp.rubrix.lat/oauth/authorize", "token_endpoint": "https://mcp.rubrix.lat/oauth/token", "registration_endpoint": "https://mcp.rubrix.lat/oauth/register", "code_challenge_methods_supported": ["S256"], "token_endpoint_auth_methods_supported": ["none"], "grant_types_supported": ["authorization_code", "refresh_token"], "scopes_supported": [ "rubrix.signature.read", "rubrix.signature.flows.create", "rubrix.signature.reminders.resend" ] } Para a primeira versao, ``token_endpoint_auth_methods_supported`` deve incluir ``none`` para suportar cliente publico com authorization-code + PKCE. **Atualizacao**: Dynamic Client Registration (RFC 7591, ``registration_endpoint``) foi implementada. Motivo: clientes MCP genericos (Codex CLI, Claude Code, MCP Inspector, ...) nao tem um ``client_id`` pre-combinado como o ChatGPT (que registra o app manualmente no dev console e recebe um ``client_id`` fixo) - eles esperam se auto-registrar em ``POST /oauth/register`` no primeiro uso. Sem isso, qualquer cliente que nao seja o ChatGPT falha imediatamente com "Dynamic client registration not supported". O registro e aberto (sem autenticacao) por design: registrar um cliente so gera um ``client_id`` e grava quais ``redirect_uris`` ele pode usar em ``/oauth/authorize`` - isso nao concede nenhum acesso por si so. A fronteira de autorizacao real continua sendo o login + consentimento na tela "Conectar Rubrix"; um client_id registrado dinamicamente ainda precisa de um usuario Rubrix de verdade logando e clicando "Permitir". So sao aceitos clientes publicos (``token_endpoint_auth_method=none`` - sem ``client_secret``), que e o unico modelo que essa facade suporta hoje (PKCE). Cada client_id so pode usar os ``redirect_uris`` que ele mesmo registrou - um client_id nao pode usar o redirect_uri de outro. Endpoints OAuth ~~~~~~~~~~~~~~~ A facade deve implementar: .. code-block:: text GET /oauth/authorize POST /oauth/token POST /oauth/register Requisitos do fluxo: - authorization-code flow com PKCE ``S256``; - receber e preservar o parametro ``resource``; - validar redirect URI contra allowlist; - emitir access token curto (15min) junto com um ``refresh_token`` opaco de vida mais longa (30 dias), rotacionado a cada uso; - incluir ``iss``, ``aud``, ``exp``, ``sub``, ``tenant_id``, ``api_key_id``, ``platform`` e ``scope`` no token ou em metadados introspectaveis; - rejeitar token expirado, issuer incorreto, audience incorreto ou escopo insuficiente; - no refresh, aceitar estreitar o ``scope`` concedido mas rejeitar qualquer escopo que nao estivesse no grant original. Escopos OAuth finais -------------------- Escopos v1 ~~~~~~~~~~ Os escopos OAuth do ChatGPT devem mapear para os escopos de credencial da issue ``#151``: .. list-table:: :header-rows: 1 * - OAuth scope - Escopo interno - Uso * - ``rubrix.signature.read`` - ``SIGNATURE_READ`` - Consultar fluxos, pendencias, finalizados e link do documento assinado. * - ``rubrix.signature.flows.create`` - ``SIGNATURE_CREATE_FLOW`` - Criar e enviar novo fluxo de assinatura. * - ``rubrix.signature.reminders.resend`` - ``SIGNATURE_RESEND_REMINDER`` - Reenviar lembretes para signatarios pendentes. Escopos futuros ~~~~~~~~~~~~~~~ Os escopos abaixo devem ficar reservados para issues futuras, sem bloquear a primeira integracao: .. list-table:: :header-rows: 1 * - OAuth scope - Motivo * - ``rubrix.documents.write`` - Criacao/edicao de rascunhos de documentos. * - ``rubrix.signers.write`` - Inclusao/alteracao de signatarios fora do fluxo simplificado. * - ``rubrix.signature.flows.cancel`` - Cancelamento de fluxo; deve usar ``destructiveHint``. * - ``rubrix.signature.sign_own_invite`` - Caso do assinante que so precisa assinar, registrado na issue ``#154``. Security schemes por tool ------------------------- Todas as tools atuais sao privadas. Nao ha ``noauth`` nesta etapa. .. list-table:: :header-rows: 1 * - Tool - OAuth scope - Annotation * - ``rubrix_get_signature_flow`` - ``rubrix.signature.read`` - ``readOnlyHint=true`` * - ``rubrix_list_pending_signatures`` - ``rubrix.signature.read`` - ``readOnlyHint=true`` * - ``rubrix_list_finalized_signature_flows`` - ``rubrix.signature.read`` - ``readOnlyHint=true`` * - ``rubrix_get_signed_document_link`` - ``rubrix.signature.read`` - ``readOnlyHint=true`` * - ``rubrix_start_signature_flow`` - ``rubrix.signature.flows.create`` - ``openWorldHint=true`` * - ``rubrix_resend_signature_reminder`` - ``rubrix.signature.reminders.resend`` - ``openWorldHint=true`` Quando o MCP framework permitir declarar ``securitySchemes`` por tool, cada tool deve declarar ``oauth2`` com o escopo correspondente. Nao usar default global como unica fonte, porque a politica por tool precisa evoluir separadamente. Challenge de autenticacao ------------------------- Quando token estiver ausente, invalido, expirado ou sem escopo, o retorno deve conter challenge compatavel com MCP/ChatGPT: .. code-block:: json { "content": [ { "type": "text", "text": "Authentication required." } ], "_meta": { "mcp/www_authenticate": [ "Bearer resource_metadata=\"https://mcp.rubrix.lat/.well-known/oauth-protected-resource\", error=\"insufficient_scope\", error_description=\"Rubrix credential does not include the required scope.\"" ] }, "isError": true } Tambem deve haver ``WWW-Authenticate`` HTTP em respostas ``401`` fora de tool-call quando aplicavel: .. code-block:: text WWW-Authenticate: Bearer resource_metadata="https://mcp.rubrix.lat/.well-known/oauth-protected-resource", scope="rubrix.signature.read" Variaveis de ambiente implementadas ------------------------------------ Nomes finais implementados no ``mcp-enduser`` (ver ``mcp-enduser/README.md`` para a lista completa, incluindo os aliases ``MCP_OAUTH_*`` sem o prefixo ``ENDUSER``): .. list-table:: :header-rows: 1 * - Variavel - Exemplo - Uso * - ``MCP_ENDUSER_OAUTH_ISSUER`` - ``https://mcp.rubrix.lat`` - Issuer esperado dos tokens (tambem usado como ``resource``/``aud`` canonico junto de ``_AUDIENCE``). * - ``MCP_ENDUSER_OAUTH_AUDIENCE`` - ``https://mcp.rubrix.lat/mcp`` - ``aud`` exigido no access token. * - ``MCP_ENDUSER_OAUTH_JWKS_URL`` - ``https://mcp.rubrix.lat/oauth/jwks`` - Chaves publicas para validar a assinatura RS256 do JWT. * - ``MCP_ENDUSER_OAUTH_RESOURCE_METADATA_URL`` - ``https://mcp.rubrix.lat/.well-known/oauth-protected-resource`` - URL enviada no challenge ``mcp/www_authenticate``. * - ``MCP_ENDUSER_OAUTH_REDIRECT_URIS`` - ``https://chatgpt.com/connector/oauth/...`` - Allowlist de redirect URIs (separadas por virgula); vazio bloqueia todo ``/oauth/authorize``. * - ``MCP_ENDUSER_OAUTH_SIGNING_KEY_ID`` / ``_SIGNING_PRIVATE_KEY_PEM`` - ``rubrix-mcp-enduser-2026-06`` / PEM PKCS#8 - Chave RSA persistente para assinar os access tokens; sem ela o modulo gera uma chave em memoria (dev only, invalidada a cada restart). * - ``MCP_ENDUSER_OAUTH_FRONTEND_URL`` - ``https://rubrix.lat/frontend/connect/rubrix`` - Tela "Conectar Rubrix" para onde ``/oauth/authorize`` redireciona o navegador. * - ``MCP_ENDUSER_REDIS_URL`` - ``redis://redis:6379/1`` - Redis onde codes/refresh-tokens/consentimento-pendente/clientes-DCR/vault sao persistidos. Obrigatorio - o modulo nao sobe sem Redis alcancavel. O TTL do access token e fixo em 15 minutos e o do authorization code em 5 minutos (constantes em ``EndUserOAuthAuthorizationService``); nao ha variavel de ambiente para isso na primeira versao. Compatibilidade com outros clientes MCP --------------------------------------- O desenho nao deve depender de recursos exclusivos do ChatGPT. ChatGPT exige ``_meta["mcp/www_authenticate"]`` para abrir a UI de linking, mas os elementos base sao padroes OAuth/MCP: - endpoint MCP HTTP publico; - metadata de recurso protegido; - metadata OAuth/OIDC do authorization server; - ``Authorization: Bearer `` nas chamadas; - validacao de issuer, audience, expiracao e escopos. Claude, Copilot, Gemini e outros clientes devem conseguir usar o mesmo endpoint e o mesmo token bearer quando suportarem MCP remoto com OAuth. Quando algum cliente nao suportar OAuth completo, o modo manual com credencial de plataforma pode continuar existindo como fallback operacional. Checklist para as proximas issues --------------------------------- Issue ``#137`` (concluida): - [x] protected resource metadata (``GET /.well-known/oauth-protected-resource``); - [x] authorization server metadata (``GET /.well-known/oauth-authorization-server`` e ``/openid-configuration``); - [x] validacao de token OAuth no ``mcp-enduser`` (``OAuthTokenValidator``: issuer, audience, expiracao, assinatura RS256, escopo); - [x] challenge ``_meta["mcp/www_authenticate"]`` (``EndUserAuthenticationException``); - [ ] ``securitySchemes`` por tool: nao suportado pela anotacao ``@Tool`` do Quarkiverse MCP na versao usada; validacao/challenge acontece em runtime (ver nota em ``EndUserOAuthResource``/PR #158); - [x] testes para token ausente, invalido, expirado e sem escopo (``OAuthTokenValidatorTest``, ``EndUserAuthResolverTest``). Issue ``#152``/``#159`` (concluidas): - [x] tela ``Conectar Rubrix`` (frontend ``/connect/rubrix``, atras de login); - [x] criacao de credencial por plataforma no consentimento (``EndUserOAuthAuthorizationService#approveConsent`` cria uma API key nova via ``POST /api-keys`` com o Bearer do usuario logado — nao ha *selecao* de credencial existente porque o Rubrix so retorna o valor bruto da API key uma vez, na criacao); - [x] consentimento por escopo (tela lista a descricao de cada escopo OAuth solicitado antes de aprovar); - [x] emissao de authorization code e access token (``/oauth/authorize`` -> consentimento -> ``/oauth/token``); - [x] **refresh token**: ``/oauth/token`` aceita ``grant_type=refresh_token``. Todo ``authorization_code`` grant emite um ``refresh_token`` opaco (30 dias), rotacionado (uso unico) a cada refresh; o cliente pode estreitar o escopo pedindo um ``scope`` menor no refresh, nunca ampliar. ``grant_types_supported`` na authorization server metadata anuncia ``authorization_code`` e ``refresh_token``. Revogacao direta do access/refresh token (fora de deixar expirar) continua nao implementada. - [ ] **acumulo de API keys**: cada aprovacao na tela "Conectar Rubrix" ainda cria uma API key nova (``MCP OAuth ()``) sem reaproveitar uma existente. Com refresh token isso deixa de acontecer a cada 15 minutos (só ocorre quando o cliente perde o refresh token ou ele expira/e revogado, ex. apos os 30 dias), mas ainda nao ha expiracao/limpeza automatica dessas linhas em ``/admin/api-keys`` — debito remanescente se o volume de reconexoes distintas crescer. - [x] **Dynamic Client Registration** (``POST /oauth/register``, RFC 7591): implementada apos falha real testando com o Codex CLI - clientes MCP genericos nao tem ``client_id`` pre-combinado como o ChatGPT e esperam se auto-registrar. Aberta (sem auth), so aceita clientes publicos (``token_endpoint_auth_method=none``), cada ``client_id`` so valida contra os ``redirect_uris`` que ele mesmo registrou. - [x] **persistencia em Redis**: authorization codes, refresh tokens, consentimento pendente, clientes DCR e o vault ``api_key_id -> raw API key`` sao gravados no Redis (``MCP_ENDUSER_REDIS_URL``, DB index 1) com TTL, nao mais em ``ConcurrentHashMap``. Sobrevive a restart/redeploy - testado de verdade: registrar um client_id, matar o processo, subir um processo novo, e o ``/oauth/authorize`` com aquele client_id continuou funcionando. A chave de assinatura RSA continua fora dessa persistencia (ver linha "chave RSA" na tabela acima) - o restante do estado persistido faz esse ponto ser cosmetico na pratica (um refresh gera token novo, corretamente assinado com a chave atual). Debito adicional descoberto testando com clientes reais ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Dois bugs de boot que so apareceram ao efetivamente subir o ``mcp-enduser`` (os testes automatizados usam fakes manuais e nunca exercitam o binding real de configuracao do Quarkus, entao nenhum dos dois falhava em CI): - ``EndUserMcpConfig`` e um ``@ConfigMapping(prefix = "rubrix.mcp-enduser")``. O SmallRye Config valida que **toda** property sob esse prefixo mapeia pra um membro da interface. As properties OAuth usavam ``rubrix.mcp-enduser.oauth.*``, sob o mesmo prefixo mas sem existir na interface -> ``SRCFG00050`` derruba o boot antes de abrir qualquer porta. Corrigido renomeando o prefixo interno para ``rubrix.mcp-enduser-oauth.*`` (hifen, nao aninhado). Nenhuma variavel de ambiente mudou. - Um ``@ConfigProperty`` tipado ``String`` (nao ``Optional``) com ``defaultValue = ""`` ainda falha se alguma outra ConfigSource (aqui, a expressao ``${MCP_ENDUSER_OAUTH_REDIRECT_URIS:}`` no ``application.properties``) fornecer explicitamente uma string vazia: o SmallRye Config trata valor vazio como "sem valor" e o converter lanca excecao, e o ``defaultValue`` da anotacao so entra quando a key esta *totalmente ausente* de todas as fontes, nao quando ela resolve pra ``""``. ``redirect-uris`` foi trocado pra ``Optional`` (mesmo padrao ja usado em ``signing-key-id`` e ``signing-private-key-pem``). Issue ``#140`` (pendente, infraestrutura de deploy, fora do escopo deste repositorio): - [ ] HTTPS publico; - [ ] TLS valido; - [ ] CORS restrito em producao; - [ ] cadastro no ChatGPT Developer Mode; - [ ] refresh de metadata apos mudanca de tools. As variaveis ``MCP_ENDUSER_OAUTH_*`` ja estao plugadas no ``docker-compose.yml`` e ``.env.example`` prontas para receber os valores do dominio publico quando o ``#140`` for executado; nenhuma mudanca de codigo deveria ser necessaria alem de configuracao de ambiente/proxy reverso.