Auth0 + MCP CaptainDNS: nosso REX completo

Por CaptainDNS
Publicado em 4 de dezembro de 2025

  • #Auth0
  • #MCP
  • #OAuth2
  • #DNS
  • #Arquitetura
Diagrama de arquitetura com Auth0, o servidor MCP CaptainDNS, a API backend e os clientes MCP
TL;DR

Neste retorno de experiência explicamos como ligamos o Auth0 ao servidor MCP do CaptainDNS sem quebrar o que já existia:

  • Um servidor MCP exposto em /stream (dev e prod) que fala JSON-RPC com clientes MCP (MCP Inspector, amanhã ChatGPT).
  • Uma API backend já ligada ao Auth0 via frontend, a alinhar com um novo fluxo OAuth2 MCP.
  • Auth opcional: executar os tools sem login, mas usar a identidade quando houver Bearer.
  • Uma API Auth0 dedicada ao MCP, um PRM (/.well-known/oauth-protected-resource) e o Resource Parameter Compatibility Profile.
  • Propagação fina da identidade (sub, email, scopes) para profiles e logs api_requests, com possibilidade de endurecer o acesso depois com tools protegidos.

1. Contexto: por que ligar o Auth0 ao MCP CaptainDNS?

O CaptainDNS já tem:

  • um frontend Next.js ligado ao Auth0 (login clássico, tokens RS256 para a API),
  • uma API backend,
  • um modelo de dados que armazena perfis e requisições.

A chegada do servidor MCP muda o jogo:

  • um novo ponto de entrada em dev e depois em prod;
  • clientes MCP (MCP Inspector, depois ChatGPT) que querem buscar um access token via OAuth2 para chamar esse servidor MCP;
  • a necessidade de propagar a identidade até o backend para ligar ações a um perfil existente.

Restrições:

  • não quebrar o que já existe no frontend (audience histórica, tokens em circulação);
  • manter a auth opcional nos tools v0 (lookup, propagation, audit de e-mail);
  • preparar uma base sólida para futuros tools com "auth obrigatória" (histórico, resolve watch list, features premium etc.).

2. Visão geral Auth0 + MCP

No fim a cadeia fica assim:

  • Auth0 (XXX-prod.eu.auth0.com):

  • declara duas APIs:

  • a API histórica do frontend,

  • a API MCP (dev e prod);

  • emite access tokens JWT RS256 para essas audiences.

  • Servidor MCP CaptainDNS:

  • endpoint JSON-RPC: /stream (dev e prod);

  • expõe um PRM (Protected Resource Metadata) em /.well-known/oauth-protected-resource;

  • valida os tokens Auth0 via issuer e JWKS;

  • chama a API backend propagando Authorization: Bearer <access_token> e um header source (frontend_mcp_anonymous ou frontend_mcp_authenticated).

  • API backend:

  • aceita várias audiences (frontend + MCP);

  • mapeia sub Auth0 → profiles;

  • registra todas as requisições em apirequests com user_id e source.

  • Clientes MCP:

  • MCP Inspector em dev,

  • ChatGPT (Connectors) em prod, atuando como cliente OAuth2 MCP.

Diagrama de arquitetura Auth0 + MCP CaptainDNS

Objetivo: para um tools/call CaptainDNS:

  • em modo anônimo: a requisição roda normalmente;
  • em modo autenticado: a requisição é ligada a um perfil.

3. API Auth0 dedicada ao MCP e alinhamento de audiences

3.1. API Auth0 MCP: audience = MCP /stream

Em vez de tentar reutilizar a audience histórica, criamos uma API Auth0 dedicada para o MCP:

  • em dev:
  • audience = XXX-audience-dev;
  • em prod:
  • audience = XXX-audience-prod;
  • alg = RS256;
  • formato do access token = JWT (assinado, não cifrado).

Lado MCP:

  • AUTH0_AUDIENCE aponta para essa audience (dev ou prod);
  • o PRM (/.well-known/oauth-protected-resource) repete esses valores em resource, audience e default_audience.

Lado API backend:

  • AUTH0_ALLOWED_AUDIENCES contém todas as audiences aceitas:
  • a audience histórica do frontend,
  • a audience MCP.

3.2. Problema resource vs audience e JWE cifrado

Primeiro problema com o MCP Inspector:

  • o cliente só enviava resource=<url> na URL de autorização;
  • o Auth0 esperava um audience=<url> explícito;
  • resultado típico:
  • ou um access token cifrado (JWE de 5 partes, alg=dir, enc=A256GCM) inutilizável pelo MCP,
  • ou um erro do Auth0 ("service not found") se a audience não correspondesse a nenhuma API.

A solução foi ativar o Resource Parameter Compatibility Profile no Auth0:

  • o Auth0 passa a tratar resource=<url> como audience=<url> para clientes que só conhecem resource;
  • o token obtido é um JWT RS256 clássico de 3 partes com aud = URL da resource MCP (/stream).

Essa etapa foi chave para obter um fluxo OAuth2 que o MCP Inspector entendesse sem mudar o comportamento dele.

4. PRM MCP (/.well-known/oauth-protected-resource)

Para que os clientes MCP saibam como fazer OAuth2 com o servidor, o MCP expõe um PRM (Protected Resource Metadata) em:

  • /.well-known/oauth-protected-resource

Esse JSON descreve em especial:

  • o resource / audience / default_audience;
  • os endpoints OAuth2 a usar nesse contexto:
  • authorization_endpoint,
  • token_endpoint,
  • registration_endpoint opcional;
  • jwks_uri (via a configuração OpenID do Auth0);
  • scopes_supported:
  • openid, profile, email, offline_access,
  • além dos scopes da aplicação (captaindns:dns:read, captaindns:email:read, etc.);
  • default_scope:
  • tipicamente "openid profile email captaindns:dns:read".

O PRM é, portanto, um contrato OAuth2 específico do MCP, complementar ao discovery OpenID padrão do Auth0.

5. Validação do JWT no servidor MCP

Com o fluxo OAuth2 pronto, o servidor MCP precisa verificar cada Bearer em /stream:

  • ler Authorization: Bearer <access_token> (se existir);
  • recuperar a configuração OpenID do Auth0:
  • issuer = XXX;
  • jwks_uri (chave pública);
  • validar:
  • assinatura RS256 via JWKS,
  • iss = tenant Auth0,
  • aud = audience MCP (dev ou prod),
  • exp não expirado.

Além disso, o servidor MCP pode:

  • inspecionar a claim scope (string, ex. "openid profile email captaindns:dns:read");
  • verificar se os scopes requeridos (por padrão profile, email) estão presentes caso se queira filtrar já na camada MCP.

Se o Bearer for inválido:

  • em modo "auth opcional", o MCP considera o usuário anônimo e não envia challenge;
  • em modo "auth obrigatória" de um tool (veja abaixo), o MCP retorna um erro JSON-RPC com _meta["mcp/www_authenticate"] para disparar o login no cliente.

Sequência de autenticação MCP com Auth0

6. Propagar a identidade para a API backend

Depois que o JWT é validado pelo MCP, o servidor deve propagar a identidade para a API:

  • adicionar o header Authorization: Bearer <access_token do usuário>;
  • adicionar um header source (ou equivalente), com os valores:
  • frontend_mcp_anonymous se não houver Bearer ou se for inválido;
  • frontend_mcp_authenticated se o Bearer for válido.

Os logs do MCP também registram um evento mcp_outbound_api com:

  • path=/...;
  • has_bearer=true/false;
  • source=frontend_mcp_*.

Na API, o middleware optional_auth:

  • revalida o token (issuer, audience ∈ AUTH0_ALLOWED_AUDIENCES, scopes exigidos);
  • se o token é válido:
  • liga a requisição a um perfil (ver próxima seção);
  • caso contrário:
  • trata a requisição como anônima, registrando a origem (source).

Esse padrão de "dupla validação" (MCP + API) mantém a API autônoma quanto ao modo de chamada (frontend vs MCP).

7. Integração com profiles e api_requests

7.1. Criar / atualizar perfis

A API backend expõe POST /profile que:

  • sub (subject Auth0) e email nas claims;
  • cria ou atualiza um registro em profiles:
  • chave primária: auth0_sub;
  • email atualizado;
  • created_at, last_seen_at.

Pré-condição na API:

  • email precisa estar preenchido, senão 422 (impossível criar perfil incompleto).

Problema encontrado: o access token emitido para o MCP não trazia email por padrão.

7.2. Claim de email namespacada via Action Auth0

Para garantir um email utilizável, adicionamos uma Auth0 Post-Login Action:

exports.onExecutePostLogin = async (event, api) => {
  const ns = "NAMESPACE";
  if (event.user && event.user.email) {
    api.accessToken.setCustomClaim(`${ns}/email`, event.user.email);
  }
};

Explicação:

  • o Auth0 exige que as custom claims do access token sejam namespacadas (URL ou URN);
  • a claim NAMESPACE.email é adicionada a cada access token se o usuário tiver email.

No backend:

  • /profile lê primeiro email (se vier padrão);
  • senão, fallback para NAMESPACE/email.

A combinação das duas fontes mantém o código robusto, compatível com diferentes tipos de token (frontend vs MCP).

7.3. api_requests: ligar as requisições ao user_id correto

Para cada requisição tratada pela API, o middleware de logging:

  • cria uma linha em api_requests;
  • se existir sub e um perfil profiles.GetByAuth0ID(sub):
  • preenche user_id em apirequests;
  • usa source (frontend / MCP / job backend) para qualificar a origem.

Fluxo anônimo vs autenticado no MCP CaptainDNS

Resultado:

  • requisições anônimas (MCP sem Bearer) têm user_id = NULL, source=frontend_mcp_anonymous;
  • requisições autenticadas são ligadas a um perfil, com user_id preenchido.

8. Scopes e AUTH0_REQUIRED_SCOPES

8.1. Lado backend: AUTH0_REQUIRED_SCOPES

A API usa um parâmetro de configuração:

  • AUTH0_REQUIRED_SCOPES (por exemplo ["profile", "email"]),

para verificar se a claim scope do token contém todos os scopes exigidos.

O middleware optional_auth:

  • se houver Bearer:
  • valida assinatura e audience;
  • checa se scope contém todos os scopes exigidos;
  • caso contrário, marca a auth como inválida (missing required scope), mas deixa a requisição seguir como anônima (sem user_id).
  • se não houver Bearer:
  • trata diretamente como anônima.

Portanto, foi preciso garantir que os tokens MCP incluíssem profile e email:

  • via scopes_supported e default_scope no PRM MCP;
  • via os parâmetros scope enviados pelos clientes MCP durante o fluxo OAuth2.

8.2. Evitar "mágica" no Auth0

É tecnicamente possível adicionar scopes via uma Action Auth0, mas neste REX preferimos:

  • configuração limpa via:
  • o PRM MCP (default_scope);
  • a requisição /authorize (scope=),
  • um controle claro de scopes no lado API e MCP, sem "hardcoding" no tenant.

9. Auth opcional e tools protegidos (RequiresAuth)

9.1. Problema inicial: tudo virou "auth obrigatória"

Em uma primeira versão, assim que o servidor MCP detectava ausência de Bearer ou Bearer inválido, devolvia um challenge:

  • _meta["mcp/www_authenticate"] com realm e scope a obter.

Consequência:

  • alguns clientes MCP interpretavam essa resposta como falha geral,
  • enquanto o objetivo era manter os tools v0 usáveis em anônimo.

9.2. Flag RequiresAuth e modo duplo

A solução foi introduzir um flag RequiresAuth nos tools MCP:

  • para todos os tools existentes (dns_lookup, dns_propagation, email_auth_audit):

  • RequiresAuth = false;

  • comportamento:

  • sem Bearer: execução anônima, sem challenge;

  • com Bearer válido: execução + ligação ao perfil.

  • para futuros tools "premium" (request_history, resolve_watch_list, etc.):

  • RequiresAuth = true;

  • comportamento:

  • se não houver Bearer ou se for inválido:

  • o MCP devolve um erro JSON-RPC com:

  • isError: true,

  • _meta["mcp/www_authenticate"] preenchido para disparar o login;

  • se o Bearer for válido:

  • execução normal com ligação ao perfil.

Esse mecanismo permite introduzir tools protegidos progressivamente sem quebrar a experiência dos tools públicos.

9.3. Metadados de segurança em tools/list

Para ser explícito com os clientes MCP, os tools agora expõem uma seção de segurança:

  • tipo oauth2;
  • scopes exigidos (para tools protegidos).

Foram adicionados testes para verificar:

  • que tools/list retorna as informações de segurança,
  • que os tools protegidos devolvem _meta["mcp/www_authenticate"] quando falta o Bearer.

10. Gerir várias audiences (frontend + MCP) em prod

10.1. Observações

Em produção coexistem dois tipos de tokens:

  • tokens do frontend:
  • aud = [XXX, YYY];
  • tokens MCP:
  • aud = "XXX".

Numa fase intermediária, a API só aceitava a audience MCP (AUTH0_ALLOWED_AUDIENCES="XXX"), o que quebrou o frontend (mismatch de audience).

10.2. Solução: AUTH0_ALLOWED_AUDIENCES com múltiplos valores

A solução foi permitir várias audiences no lado da API:

  • AUTH0_ALLOWED_AUDIENCES="XXX, YYY"

O middleware:

  • divide a string por ,;
  • aplica TrimSpace em cada entrada;
  • aceita tokens cujo aud (string ou array) contenha ao menos uma audience permitida.

Resultado:

  • a API aceita tanto:
  • tokens do frontend (audience histórica),
  • tokens MCP (audience MCP);
  • a transição para um mundo multi-cliente (frontend + MCP + outros) fica coberta.

Estrutura de um JWT Auth0 para o MCP CaptainDNS

FAQ: Perguntas frequentes sobre Auth0 + MCP CaptainDNS

Por que criar uma API Auth0 dedicada para o MCP em vez de reutilizar a audience histórica da API?

A audience histórica já era usada pelo frontend e por tokens existentes. Dar a mesma audience ao MCP tornaria a configuração mais ambígua e difícil de auditar.

Criando uma API Auth0 dedicada para o MCP (em dev e prod) obtemos uma separação clara dos usos, melhor rastreabilidade e a possibilidade de evoluir o contrato MCP de forma independente do frontend.

Por que o token inicial estava cifrado (JWE em 5 partes) e não um JWT legível?

Sem o Resource Parameter Compatibility Profile, o Auth0 interpreta mal certos fluxos em que só resource é enviado, principalmente quando nenhuma audience coerente é encontrada.

No nosso caso, o cliente MCP enviava resource=... sem audience=... explícito. O Auth0 respondia às vezes com um token JWE cifrado (5 partes) ou com uma mensagem de erro.

Ao ativar o Resource Parameter Compatibility Profile, o Auth0 trata resource como audience, permitindo obter um JWT RS256 clássico em 3 partes, utilizável pelo servidor MCP.

Para que serve exatamente o PRM (/.well-known/oauth-protected-resource) no MCP?

O PRM é um documento de metadados que descreve como um cliente deve fazer OAuth2 para acessar um recurso protegido:

  • qual é o resource / audience;
  • quais endpoints de autorização e de token usar;
  • quais scopes são suportados e recomendados.

Para o CaptainDNS, o PRM permite que clientes como MCP Inspector ou ChatGPT descubram automaticamente como obter um token Auth0 para o MCP, quais scopes pedir e se integrar sem configuração manual complexa.

Como gerenciar os scopes sem quebrar os clientes existentes?

Estratégia adotada:

  • definir um conjunto mínimo de scopes exigidos (profile, email) na API;
  • não recusar toda a requisição se esses scopes faltarem, mas cair no modo anônimo;
  • reservar a falha estrita (missing scope) para os tools MCP com RequiresAuth = true.

Assim, os tools v0 permanecem utilizáveis sem alterar a configuração Auth0 dos clientes existentes, ao mesmo tempo permitindo endurecer progressivamente para ferramentas premium mais tarde.

Como passar um tool MCP de 'auth opcional' para 'auth obrigatória'?

A mudança acontece em dois passos:

  • marcar o tool MCP com RequiresAuth = true;
  • lado servidor MCP:
  • se não houver Bearer ou for inválido → retornar isError=true com _meta["mcp/www_authenticate"] para disparar o login;
  • se o Bearer for válido e os scopes suficientes → executar normalmente.

Isso permite tornar um tool "auth obrigatória" sem modificar a API backend, concentrando a lógica de segurança no MCP.

Glossário Auth0 + MCP CaptainDNS

MCP (Model Context Protocol)

Protocolo que padroniza como um modelo de IA conversa com ferramentas externas (APIs, serviços). No CaptainDNS permite que clientes como MCP Inspector ou ChatGPT chamem ferramentas de DNS/email via um servidor MCP dedicado.

PRM (Protected Resource Metadata)

Documento JSON publicado por um recurso protegido (aqui, o servidor MCP CaptainDNS) em /.well-known/oauth-protected-resource. Descreve resource, audience, scopes e os endpoints de autorização e de token.

Issuer (iss)

Claim de um token JWT que indica quem o emitiu. O servidor MCP e a API backend verificam se iss corresponde ao tenant Auth0 esperado.

Audience (aud)

Claim que indica para quais recursos o token se destina. No nosso caso: a API do frontend ou a API MCP. A API backend deve aceitar múltiplas audiences para lidar com clientes diferentes.

Subject (sub)

Identificador único do usuário no tenant Auth0. É a chave usada para encontrar ou criar o perfil na tabela profiles.

Scope

Lista de permissões associadas a um token (ex. openid profile email captaindns:dns:read). Os scopes controlam o acesso a certas funcionalidades, especialmente os tools MCP protegidos.

JWS vs JWE

  • JWS: JWT assinado (3 partes), legível e verificável no servidor.
  • JWE: JWT cifrado (5 partes) que requer uma chave de descriptografia. Para o MCP CaptainDNS escolhemos JWS RS256, mais simples de validar via JWKS.

Namespaced claim

Claim custom em um access token cujo nome é uma URL/URN, exigida pelo Auth0 para evitar colisões com claims padrão. Exemplo: NAMESPACE para guardar o email em um token MCP.

Resource Parameter Compatibility Profile

Opção do Auth0 que trata resource como audience para clientes OAuth que só conhecem resource. Essencial para que o MCP Inspector obtenha um JWT RS256 válido sem mudar de comportamento.

frontend_mcp_anonymous / frontend_mcp_authenticated

Valores do campo source usados em logs e na tabela api_requests para distinguir requisições MCP anônimas (sem Bearer ou Bearer inválido) de requisições MCP autenticadas (Bearer válido, perfil identificado).

Artigos relacionados

CaptainDNS · 27 de novembro de 2025

Diagrama da arquitetura MCP do CaptainDNS entre o ChatGPT, o servidor MCP e a API backend

Nos bastidores do MCP do CaptainDNS

Como conectamos o CaptainDNS a IAs via MCP: arquitetura, transporte HTTP+SSE, JSON-RPC, erros 424 e timeouts, e o que aprendemos no caminho.

  • #MCP
  • #Arquitetura
  • #DNS
  • #E-mail
  • #Integrações IA

CaptainDNS · 21 de novembro de 2025

Esquema de um host de IA conversando com o CaptainDNS por um conector MCP padronizado

Um MCP para o CaptainDNS?

Antes de ligar o CaptainDNS a IAs, é preciso entender o que é o Model Context Protocol (MCP) e o que ele realmente permite. Um ABC do MCP e os primeiros passos para o CaptainDNS.

  • #MCP
  • #IA
  • #DNS
  • #E-mail
  • #Arquitetura