Auth0 + MCP CaptainDNS: nuestro REX completo

Por CaptainDNS
Publicado el 4 de diciembre de 2025

  • #Auth0
  • #MCP
  • #OAuth2
  • #DNS
  • #Arquitectura
Diagrama de arquitectura con Auth0, el servidor MCP CaptainDNS, la API backend y los clientes MCP
TL;DR

En este retorno de experiencia explicamos cómo conectamos Auth0 al servidor MCP de CaptainDNS sin romper lo que ya existía:

  • Un servidor MCP expuesto en /stream (dev y prod) que habla JSON-RPC con los clientes MCP (MCP Inspector, mañana ChatGPT).
  • Una API backend ya ligada a Auth0 vía el frontend, que había que alinear con un nuevo flujo OAuth2 MCP.
  • Un requisito de auth opcional: ejecutar los tools sin login, pero aprovechar la identidad cuando hay un Bearer.
  • Una API Auth0 dedicada al MCP, un PRM (/.well-known/oauth-protected-resource) y el Resource Parameter Compatibility Profile.
  • Propagación fina de identidad (sub, email, scopes) hacia profiles y logs api_requests, con la posibilidad de endurecer el acceso más adelante con tools protegidos.

1. Contexto: ¿por qué enchufar Auth0 al MCP CaptainDNS?

CaptainDNS ya tiene:

  • un frontend Next.js conectado a Auth0 (login clásico, tokens RS256 para la API),
  • una API backend,
  • un modelo de datos que almacena los perfiles y las peticiones.

La llegada del servidor MCP cambia el juego:

  • un nuevo punto de entrada en dev y luego en prod;
  • clientes MCP (MCP Inspector, luego ChatGPT) que quieren obtener un access token vía OAuth2 para llamar a este servidor MCP;
  • la necesidad de propagar la identidad hasta el backend para asociar acciones a un perfil existente.

Restricciones:

  • no romper lo que existe en frontend (audience histórica, tokens ya en circulación);
  • mantener la auth opcional en los tools v0 (lookup, propagation, audit e-mail);
  • preparar una base sólida para futuros tools con "auth obligatoria" (histórico, resolve watch list, features premium, etc.).

2. Vista de conjunto Auth0 + MCP

Al final la cadena se ve así:

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

  • declara dos APIs:

  • la API histórica frontend,

  • la API MCP (dev y prod);

  • emite access tokens JWT RS256 para esas audiences.

  • Servidor MCP CaptainDNS:

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

  • expone un PRM (Protected Resource Metadata) en /.well-known/oauth-protected-resource;

  • valida los tokens Auth0 via issuer y JWKS;

  • llama a la API backend propagando Authorization: Bearer <access_token> y un header source (frontend_mcp_anonymous o frontend_mcp_authenticated).

  • API backend:

  • acepta varias audiences (frontend + MCP);

  • mapea sub Auth0 → profiles;

  • registra todas las peticiones en apirequests con user_id y source.

  • Clientes MCP:

  • MCP Inspector en dev,

  • ChatGPT (Connectors) en prod, actuando como cliente OAuth2 MCP.

Diagrama de arquitectura Auth0 + MCP CaptainDNS

Objetivo: para un tools/call CaptainDNS:

  • en modo anónimo: la petición se ejecuta normalmente;
  • en modo autenticado: la petición se enlaza a un perfil.

3. API Auth0 dedicada al MCP y alineamiento de audiences

3.1. API Auth0 MCP: audience = MCP /stream

En lugar de reutilizar la audience histórica, creamos una API Auth0 dedicada para el MCP:

  • en dev:
  • audience = XXX-audience-dev;
  • en prod:
  • audience = XXX-audience-prod;
  • alg = RS256;
  • formato de access token = JWT (firmado, no cifrado).

Lado MCP:

  • AUTH0_AUDIENCE apunta a esa audience (dev o prod);
  • el PRM (/.well-known/oauth-protected-resource) repite esos valores en resource, audience y default_audience.

Lado API backend:

  • AUTH0_ALLOWED_AUDIENCES contiene todas las audiences aceptadas:
  • la audience histórica frontend,
  • la audience MCP.

3.2. Problema resource vs audience y JWE cifrado

Primer problema con MCP Inspector:

  • el cliente solo enviaba resource=<url> en la URL de autorización;
  • Auth0 esperaba un audience=<url> explícito;
  • resultado típico:
  • o un access token cifrado (JWE de 5 partes, alg=dir, enc=A256GCM) inutilizable para el MCP,
  • o un error en Auth0 ("service not found") si la audience no correspondía a ninguna API.

La solución fue activar el Resource Parameter Compatibility Profile en Auth0:

  • Auth0 trata resource=<url> como audience=<url> para los clientes que solo conocen resource;
  • el token obtenido es un JWT RS256 clásico de 3 partes con aud = URL de recurso MCP (/stream).

Clave para conseguir un flujo OAuth2 que MCP Inspector entendiera sin cambiar su comportamiento.

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

Para que los clientes MCP sepan cómo hacer OAuth2 hacia el servidor, el MCP expone un PRM (Protected Resource Metadata) en:

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

Este JSON describe especialmente:

  • el resource / audience / default_audience;
  • los endpoints OAuth2 a usar en este contexto:
  • authorization_endpoint,
  • token_endpoint,
  • registration_endpoint opcional;
  • jwks_uri (vía la configuración OpenID de Auth0);
  • scopes_supported:
  • openid, profile, email, offline_access,
  • y los scopes aplicativos (captaindns:dns:read, captaindns:email:read, etc.);
  • default_scope:
  • típicamente "openid profile email captaindns:dns:read".

El PRM es así un contrato OAuth2 específico al MCP, complementario al discovery OpenID estándar de Auth0.

5. Validación del JWT en el servidor MCP

Una vez montado el flujo OAuth2, el servidor MCP debe verificar cada Bearer en /stream:

  • leer Authorization: Bearer <access_token> (si existe);
  • recuperar la configuración OpenID de Auth0:
  • issuer = XXX;
  • jwks_uri (clave pública);
  • validar:
  • firma RS256 via JWKS,
  • iss = tenant Auth0,
  • aud = audience MCP (dev o prod),
  • exp no expirado.

Además, el servidor MCP puede:

  • inspeccionar la claim scope (string, ej. "openid profile email captaindns:dns:read");
  • verificar que los scopes requeridos (por defecto profile, email) estén presentes si se quiere filtrar ya en la capa MCP.

Si el Bearer es inválido:

  • en modo "auth opcional", el MCP considera anónimo al usuario y no envía challenge;
  • en modo "auth obligatoria" de un tool (ver más abajo), el MCP devuelve un error JSON-RPC con _meta["mcp/www_authenticate"] para disparar el login en el cliente.

Secuencia de autenticación MCP con Auth0

6. Propagar la identidad hacia la API backend

Tras validar el JWT en el MCP, el servidor debe propagar la identidad hacia la API:

  • añadir el header Authorization: Bearer <access_token usuario>;
  • añadir un header source (o equivalente), tipado:
  • frontend_mcp_anonymous si no hay Bearer o el Bearer es inválido;
  • frontend_mcp_authenticated si el Bearer es válido.

Los logs MCP añaden un evento mcp_outbound_api con:

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

En la API, el middleware optional_auth:

  • vuelve a validar el token (issuer, audience ∈ AUTH0_ALLOWED_AUDIENCES, scopes requeridos);
  • si el token es válido:
  • enlaza la petición a un perfil (ver siguiente sección);
  • si no:
  • trata la petición como anónima, registrando el origen (source).

Este patrón de "doble validación" (MCP + API) mantiene la API autónoma respecto a cómo se la llama (frontend vs MCP).

7. Integración con profiles y api_requests

7.1. Creación / actualización de perfiles

La API backend expone POST /profile que:

  • lee sub (subject Auth0) y email en las claims;
  • crea o actualiza una fila en profiles:
  • clave principal: auth0_sub;
  • email actualizado;
  • timestamps created_at, last_seen_at.

Precondición en la API:

  • email debe ser no vacío, de lo contrario 422 (no se puede crear un perfil incompleto).

Problema encontrado: el access token emitido para MCP no incluía email por defecto.

7.2. Claim email namespacada vía Action Auth0

Para garantizar un email utilizable añadimos una 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);
  }
};

Explicación:

  • Auth0 exige que las custom claims de access token estén namespacadas (URL o URN);
  • la claim NAMESPACE.email se añade a cada access token si el usuario tiene email.

En el backend:

  • /profile lee primero email (si viene estándar);
  • si no, fallback a NAMESPACE/email.

La combinación de ambas fuentes mantiene un código robusto, compatible con varios tipos de token (frontend vs MCP).

7.3. api_requests: enlazar las peticiones al user_id correcto

Para cada petición tratada por la API, el middleware de logging:

  • crea una línea en api_requests;
  • si hay un sub y existe un perfil profiles.GetByAuth0ID(sub):
  • rellena user_id en apirequests;
  • usa source (frontend / MCP / job backend) para calificar el origen.

Flujo anónimo vs autenticado en el MCP CaptainDNS

Resultado:

  • las peticiones anónimas (MCP sin Bearer) tienen user_id = NULL, source=frontend_mcp_anonymous;
  • las peticiones autenticadas se enlazan a un perfil, con user_id informado.

8. Scopes y AUTH0_REQUIRED_SCOPES

8.1. Lado backend: AUTH0_REQUIRED_SCOPES

La API usa un parámetro de configuración:

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

para verificar que la claim scope del token contiene todos los scopes requeridos.

El middleware optional_auth:

  • si hay Bearer:
  • valida la firma y la audience;
  • comprueba que scope contenga todos los scopes requeridos;
  • si no, marca la auth como inválida (missing required scope), pero deja seguir la petición como anónima (sin user_id).
  • si no hay Bearer:
  • trata directamente la petición como anónima.

Por tanto hubo que asegurar que los tokens MCP contuvieran profile y email:

  • vía scopes_supported y default_scope en el PRM MCP;
  • vía los parámetros scope enviados por los clientes MCP durante el flow OAuth2.

8.2. Evitar "magia" en Auth0

Es técnicamente posible añadir scopes vía una Action Auth0, pero en este REX preferimos:

  • una configuración limpia vía:
  • el PRM MCP (default_scope);
  • la petición /authorize (scope=),
  • un control explícito de scopes en API y MCP, sin "hardcoding" en el tenant.

9. Auth opcional y tools protegidos (RequiresAuth)

9.1. Problema inicial: todo se volvía "auth obligatoria"

En una primera versión, en cuanto el servidor MCP detectaba ausencia de Bearer o Bearer inválido, devolvía un challenge:

  • _meta["mcp/www_authenticate"] con un realm y scope a obtener.

Consecuencia:

  • algunos clientes MCP interpretaban esta respuesta como un fallo global,
  • cuando el objetivo era mantener los tools v0 usables en anónimo.

9.2. Flag RequiresAuth y doble modo

La solución fue introducir un flag RequiresAuth a nivel de los tools MCP:

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

  • RequiresAuth = false;

  • comportamiento:

  • sin Bearer: ejecución anónima, sin challenge;

  • con Bearer válido: ejecución + enlace a perfil.

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

  • RequiresAuth = true;

  • comportamiento:

  • si no hay Bearer o Bearer inválido:

  • el MCP devuelve un error JSON-RPC con:

  • isError: true,

  • _meta["mcp/www_authenticate"] relleno para disparar el login;

  • si el Bearer es válido:

  • ejecución normal con enlace a perfil.

Este mecanismo permite introducir tools protegidos progresivamente sin romper la experiencia de los tools públicos.

9.3. Metadata de seguridad en tools/list

Para ser explícitos de cara a los clientes MCP, los tools exponen ahora una sección de seguridad:

  • tipo oauth2;
  • scopes requeridos (para los tools protegidos).

Se añadieron tests para verificar:

  • que tools/list devuelve la información de seguridad,
  • que los tools protegidos devuelven _meta["mcp/www_authenticate"] si falta el Bearer.

10. Gestionar varias audiences (frontend + MCP) en prod

10.1. Observaciones

En producción coexisten dos tipos de tokens:

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

En una fase intermedia, la API solo aceptaba la audience MCP (AUTH0_ALLOWED_AUDIENCES="XXX"), lo que rompió el frontend (audience mismatch).

10.2. Solución: AUTH0_ALLOWED_AUDIENCES con múltiples valores

La solución fue permitir varias audiences en la API:

  • AUTH0_ALLOWED_AUDIENCES="XXX, YYY"

El middleware:

  • separa la cadena por ,;
  • aplica TrimSpace a cada entrada;
  • acepta tokens cuyo aud (string o array) contenga al menos una audience permitida.

Resultado:

  • la API acepta tanto:
  • tokens frontend (audience histórica),
  • tokens MCP (audience MCP);
  • la transición hacia un mundo multi-cliente (frontend + MCP + otros) queda gestionada.

Estructura de un JWT Auth0 para el MCP CaptainDNS

FAQ: Preguntas frecuentes sobre Auth0 + MCP CaptainDNS

¿Por qué crear una API Auth0 dedicada para el MCP en lugar de reutilizar la audience histórica de la API?

La audience histórica ya estaba usada por el frontend y por tokens existentes. Dar la misma audience al MCP habría hecho la configuración más ambigua y difícil de auditar.

Al crear una API Auth0 dedicada para el MCP (en dev y prod), obtenemos una separación clara de usos, mejor trazabilidad y la posibilidad de hacer evolucionar el contrato MCP de forma independiente al frontend.

¿Por qué el token inicial estaba cifrado (JWE de 5 partes) y no era un JWT legible?

Sin el Resource Parameter Compatibility Profile, Auth0 interpreta mal ciertos flujos donde solo se envía resource, sobre todo cuando no se encuentra una audience coherente.

En nuestro caso, el cliente MCP enviaba resource=... sin audience=... explícito. Auth0 respondía entonces a veces con un token JWE cifrado (5 partes) o con un mensaje de error.

Al activar el Resource Parameter Compatibility Profile, Auth0 trata resource como audience, lo que permite obtener un JWT RS256 clásico de 3 partes, utilizable por el servidor MCP.

¿Para qué sirve exactamente el PRM (/.well-known/oauth-protected-resource) en el MCP?

El PRM es un documento de metadatos que describe cómo un cliente debe hacer OAuth2 para acceder a un recurso protegido:

  • cuál es el resource / audience;
  • qué endpoints de autorización y de token usar;
  • qué scopes están soportados y recomendados.

Para CaptainDNS, el PRM permite a clientes como MCP Inspector o ChatGPT descubrir automáticamente cómo obtener un token Auth0 para el MCP, saber qué scopes pedir e integrarse sin configuración manual compleja.

¿Cómo gestionar los scopes sin romper a los clientes existentes?

La estrategia elegida:

  • definir un conjunto mínimo de scopes requeridos (profile, email) en la API;
  • no rechazar toda la petición si faltan esos scopes, sino pasar a modo anónimo;
  • reservar el fallo estricto (missing scope) para los tools MCP con RequiresAuth = true.

Así, los tools v0 siguen usables sin cambiar la configuración Auth0 de los clientes existentes, a la vez que se permite endurecer progresivamente para herramientas premium más adelante.

¿Cómo pasar un tool MCP de 'auth opcional' a 'auth obligatoria'?

El cambio se hace en dos pasos:

  • marcar el tool MCP con RequiresAuth = true;
  • lado servidor MCP:
  • si no hay Bearer o Bearer inválido → devolver isError=true con _meta["mcp/www_authenticate"] para lanzar el login;
  • si el Bearer es válido y los scopes suficientes → ejecutar normalmente.

Este enfoque permite evolucionar un tool a "auth obligatoria" sin modificar la API backend, concentrando la lógica de seguridad en el MCP.

Glosario Auth0 + MCP CaptainDNS

MCP (Model Context Protocol)

Protocolo que estandariza cómo un modelo de IA dialoga con herramientas externas (APIs, servicios). En CaptainDNS permite a clientes como MCP Inspector o ChatGPT llamar tools DNS/email vía un servidor MCP dedicado.

PRM (Protected Resource Metadata)

Documento JSON publicado por un recurso protegido (aquí, el servidor MCP CaptainDNS) en /.well-known/oauth-protected-resource. Describe el resource, la audience, los scopes, y los endpoints de autorización y token.

Issuer (iss)

Claim de un token JWT que indica quién lo emitió. El servidor MCP y la API backend verifican que iss coincida con el tenant Auth0 esperado.

Audience (aud)

Claim que indica para qué recurso(s) va destinado el token. En nuestro caso: la API frontend o la API MCP. La API backend debe aceptar varias audiences para gestionar distintos clientes.

Subject (sub)

Identificador único del usuario en el tenant Auth0. Es la clave usada para encontrar o crear el perfil en la tabla profiles.

Scope

Lista de permisos asociados a un token (ej. openid profile email captaindns:dns:read). Los scopes se usan para controlar el acceso a ciertas funcionalidades, especialmente los tools MCP protegidos.

JWS vs JWE

  • JWS: JWT firmado (3 partes), legible y verificable lado servidor.
  • JWE: JWT cifrado (5 partes) que requiere una clave de descifrado. Para el MCP CaptainDNS elegimos JWS RS256, más sencillo de validar vía JWKS.

Namespaced claim

Claim custom de un access token cuyo nombre es una URL/URN, exigido por Auth0 para evitar colisiones con claims estándar. Ejemplo: NAMESPACE para almacenar el email en un token MCP.

Resource Parameter Compatibility Profile

Opción Auth0 que trata resource como audience para los clientes OAuth que solo conocen resource. Imprescindible para que MCP Inspector obtenga un JWT RS256 válido sin cambiar su comportamiento.

frontend_mcp_anonymous / frontend_mcp_authenticated

Valores del campo source usados en los logs y en la tabla api_requests para distinguir las peticiones MCP anónimas (sin Bearer o Bearer inválido) de las peticiones MCP autenticadas (Bearer válido, perfil identificado).

Artículos relacionados

CaptainDNS · 27 de noviembre de 2025

Diagrama de la arquitectura MCP de CaptainDNS entre ChatGPT, el servidor MCP y la API backend

Entre bastidores del MCP de CaptainDNS

Cómo conectamos CaptainDNS a las IA mediante MCP: arquitectura, transporte HTTP+SSE, JSON-RPC, errores 424 y timeouts, y lo que aprendimos por el camino.

  • #MCP
  • #Arquitectura
  • #DNS
  • #Correo
  • #Integraciones IA

CaptainDNS · 21 de noviembre de 2025

Esquema de un host de IA que habla con CaptainDNS a través de un conector MCP estandarizado

¿Un MCP para CaptainDNS?

Antes de conectar CaptainDNS a las IAs, hay que entender qué es el Model Context Protocol (MCP) y lo que permite de verdad. Un ABC del MCP y las primeras pistas para CaptainDNS.

  • #MCP
  • #IA
  • #DNS
  • #Correo
  • #Arquitectura