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
#151continuam sendo a raiz de autorizacao para o MCP;o OAuth existe na borda do
mcp-enduserpara 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
resourcedo protected resource metadata;parametro
resourcerecebido no authorization request;parametro
resourcerecebido no token request;auddo 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:
Um usuario Rubrix com permissao para emitir API key cria uma credencial para a plataforma, por exemplo
CHATGPT.Essa credencial define os escopos permitidos.
No fluxo futuro
Conectar Rubrix, o usuario escolhe ou cria essa credencial.A facade OAuth emite um token associado ao
api_key_iddessa credencial, aotenant_ide aos escopos autorizados.O
mcp-enduservalida 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.
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_tokenopaco de vida mais longa (30 dias), rotacionado a cada uso;incluir
iss,aud,exp,sub,tenant_id,api_key_id,platformescopeno token ou em metadados introspectaveis;rejeitar token expirado, issuer incorreto, audience incorreto ou escopo insuficiente;
no refresh, aceitar estreitar o
scopeconcedido 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 |
|---|---|---|
|
|
Consultar fluxos, pendencias, finalizados e link do documento assinado. |
|
|
Criar e enviar novo fluxo de assinatura. |
|
|
Reenviar lembretes para signatarios pendentes. |
Escopos futuros
Os escopos abaixo devem ficar reservados para issues futuras, sem bloquear a primeira integracao:
OAuth scope |
Motivo |
|---|---|
|
Criacao/edicao de rascunhos de documentos. |
|
Inclusao/alteracao de signatarios fora do fluxo simplificado. |
|
Cancelamento de fluxo; deve usar |
|
Caso do assinante que so precisa assinar, registrado na issue |
Security schemes por tool
Todas as tools atuais sao privadas. Nao ha noauth nesta etapa.
Tool |
OAuth scope |
Annotation |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|---|---|---|
|
|
Issuer esperado dos tokens (tambem usado como |
|
|
|
|
|
Chaves publicas para validar a assinatura RS256 do JWT. |
|
|
URL enviada no challenge |
|
|
Allowlist de redirect URIs (separadas por virgula); vazio bloqueia todo |
|
|
Chave RSA persistente para assinar os access tokens; sem ela o modulo gera uma chave em memoria (dev only, invalidada a cada restart). |
|
|
Tela “Conectar Rubrix” para onde |
|
|
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-servere/openid-configuration);[x] validacao de token OAuth no
mcp-enduser(OAuthTokenValidator: issuer, audience, expiracao, assinatura RS256, escopo);[x] challenge
_meta["mcp/www_authenticate"](EndUserAuthenticationException);[ ]
securitySchemespor tool: nao suportado pela anotacao@Tooldo Quarkiverse MCP na versao usada; validacao/challenge acontece em runtime (ver nota emEndUserOAuthResource/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#approveConsentcria uma API key nova viaPOST /api-keyscom 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/tokenaceitagrant_type=refresh_token. Todoauthorization_codegrant emite umrefresh_tokenopaco (30 dias), rotacionado (uso unico) a cada refresh; o cliente pode estreitar o escopo pedindo umscopemenor no refresh, nunca ampliar.grant_types_supportedna authorization server metadata anunciaauthorization_codeerefresh_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 temclient_idpre-combinado como o ChatGPT e esperam se auto-registrar. Aberta (sem auth), so aceita clientes publicos (token_endpoint_auth_method=none), cadaclient_idso valida contra osredirect_urisque ele mesmo registrou.[x] persistencia em Redis: authorization codes, refresh tokens, consentimento pendente, clientes DCR e o vault
api_key_id -> raw API keysao gravados no Redis (MCP_ENDUSER_REDIS_URL, DB index 1) com TTL, nao mais emConcurrentHashMap. Sobrevive a restart/redeploy - testado de verdade: registrar um client_id, matar o processo, subir um processo novo, e o/oauth/authorizecom 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):
EndUserMcpConfige 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 usavamrubrix.mcp-enduser.oauth.*, sob o mesmo prefixo mas sem existir na interface ->SRCFG00050derruba o boot antes de abrir qualquer porta. Corrigido renomeando o prefixo interno pararubrix.mcp-enduser-oauth.*(hifen, nao aninhado). Nenhuma variavel de ambiente mudou.Um
@ConfigPropertytipadoString(naoOptional<String>) comdefaultValue = ""ainda falha se alguma outra ConfigSource (aqui, a expressao${MCP_ENDUSER_OAUTH_REDIRECT_URIS:}noapplication.properties) fornecer explicitamente uma string vazia: o SmallRye Config trata valor vazio como “sem valor” e o converter lanca excecao, e odefaultValueda anotacao so entra quando a key esta totalmente ausente de todas as fontes, nao quando ela resolve pra"".redirect-urisfoi trocado praOptional<String>(mesmo padrao ja usado emsigning-key-idesigning-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.