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

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:

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:

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:

POST https://mcp.rubrix.lat/mcp

Em desenvolvimento local, o modulo usa:

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:

GET https://mcp.rubrix.lat/.well-known/oauth-protected-resource

Resposta esperada:

{
  "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:

GET https://mcp.rubrix.lat/.well-known/oauth-authorization-server
GET https://mcp.rubrix.lat/.well-known/openid-configuration

Resposta minima:

{
  "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:

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:

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:

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.

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:

{
  "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:

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):

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 <token> 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 (<client_id>)) 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<String>) 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<String> (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.