1. De qué trata esta lección y qué no incluye
Será una lección muy interesante; en ella vamos a:
- formar en la cabeza la imagen del «triángulo de confianza» entre MCP Client, MCP Server y MCP Auth Server — con el usuario, que está «por encima» de ese triángulo como propietario de los recursos;
- recorrer el flow: quién envía el token a quién, dónde se autentica el usuario y por qué el servidor MCP nunca ve su contraseña;
- vincularlo con nuestro backend Next.js/MCP y la futura configuración de Keycloak/Auth0.
Qué no haremos hoy:
- no marcamos casillas en Keycloak ni configuramos un IdP concreto;
- no implementamos una validación completa de JWT ni introspección — son temas de las siguientes lecciones (sobre el Auth Server y sobre el MCP Server como recurso protegido).
El objetivo ahora es que puedas coger una hoja, dibujar flechas entre ChatGPT, tu servidor y Auth0/Keycloak y explicar sin dudar: dónde está el login, dónde el token y dónde los datos.
2. Triángulo de confianza: MCP Client, MCP Server, MCP Auth Server
Empecemos por los actores. El «triángulo de confianza» técnico lo forman MCP Client, MCP Server y MCP Auth Server; el usuario (User) es un rol aparte, propietario de los recursos, que está como por encima de ese triángulo y da su consentimiento de acceso. En la especificidad de MCP y Apps SDK esta arquitectura está bastante bien formalizada.
User (Resource Owner)
Es la persona al otro lado de la pantalla. Esta persona:
- entra en ChatGPT;
- escribe la petición «muéstrame mis pedidos / mis listas de regalos»;
- acepta «vincular la cuenta» de tu servicio a ChatGPT.
Lo principal: es quien posee los recursos (historial de pedidos, perfiles, listas de regalos) y quien otorga el consentimiento para acceder a ellos.
MCP Client
Para nosotros aquí es:
- ChatGPT con Apps SDK;
- a veces — MCP Jam Inspector (en depuración).
MCP Client puede:
- leer los metadatos de tu servidor MCP (a través de .well-known);
- iniciar el flow de OAuth en el navegador del usuario;
- almacenar y adjuntar tokens a las llamadas de las herramientas MCP.
Importante: MCP Client es un public client. No almacena tu client_secret, por lo que se comunica con el Auth Server como una SPA pública: Authorization Code + PKCE.
MCP Server (Resource Server)
Es tu backend que implementa MCP:
- establece la conexión con ChatGPT;
- declara herramientas (tools), recursos, prompts;
- en cada invocación de una herramienta revisa el encabezado Authorization: Bearer <token>;
- valida el token (firma, exp, aud, scope) y, si todo está bien, ejecuta la lógica de negocio.
Punto clave: el servidor MCP no gestiona el login. No ve contraseñas, no dibuja el formulario de login, no envía al usuario el correo de «confirma tu email». Confía solamente en tokens firmados criptográficamente por el Auth Server.
MCP Auth Server (Authorization Server / IdP)
Es un servicio separado de autenticación y autorización: Keycloak, Auth0, Ory Hydra+Kratos, Okta, Cognito, Azure AD, etc.
Se encarga de:
- la UI de login (email/contraseña, SSO, 2FA);
- el almacenamiento de las cuentas de usuario;
- la emisión de tokens (access token, refresh token);
- la publicación de metadatos OAuth/OIDC (/authorize, /token, jwks_uri, /registration, etc.).
Para MCP debe soportar OAuth 2.1 para public clients (PKCE S256, dynamic client registration, etc.).
Resumen de roles
| Quién | Qué hace | Qué no hace |
|---|---|---|
| User | Introduce login/contraseña, da su consentimiento para acceder a los datos | No se comunica directamente con el MCP Server |
| MCP Client (ChatGPT/Jam) | Inicia OAuth, almacena el token, invoca MCP tools | No valida contraseñas ni verifica la firma del token |
| MCP Server | Valida tokens, ejecuta la lógica de negocio de las tools | No muestra formulario de login ni almacena contraseñas |
| MCP Auth Server | Autentica al usuario y emite tokens | No conoce tus herramientas MCP ni su lógica de negocio |
Si en tu cabeza todo esto se mezclaba en un «gran servidor que lo hace todo», es hora de separarlo.
3. Cómo es el flow: de «no hay token» a la llamada protegida de herramientas
Veamos ahora el flujo de mensajes. En la especificación MCP llaman a este proceso «The Flow»: discovery → redirect → code → token → authorized calls.
Paso 0. Intento de invocar una herramienta protegida sin token
El usuario escribe: «Muestra mis ideas de regalos guardadas».
ChatGPT, como MCP Client, decide: «para esto hay que invocar la herramienta getUserGiftLists en nuestro servidor MCP». Hace la llamada sin token (el usuario aún no se ha autenticado).
Tu servidor MCP:
- detecta la ausencia o incorrección del encabezado Authorization;
- responde con 401 Unauthorized y añade el encabezado WWW-Authenticate: Bearer resource_metadata="https://api.giftgenius.com/.well-known/oauth-protected-resource" con el enlace a los metadatos del recurso protegido (resource metadata, más abajo).
Se ve aproximadamente así (lógica, no es HTTP completo):
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://api.giftgenius.com/.well-known/oauth-protected-resource"
ChatGPT ve ese encabezado y entiende: «ajá, el recurso está protegido por OAuth; hay que ejecutar el flow de OAuth y vincular la cuenta».
Discovery: .well-known/oauth-protected-resource
Después, el MCP Client solicita a tu servidor los metadatos:
GET /.well-known/oauth-protected-resource
El servidor responde con un documento JSON con el identificador del recurso y la lista de servidores de autorización de los que hay que obtener tokens.
Ejemplo mínimo (lo configuraremos en detalle más tarde; ahora importa la idea):
{
"resource": "https://api.giftgenius.com",
"authorization_servers": [
"https://auth.giftgenius.com"
],
"scopes_supported": ["gifts.read", "gifts.write"]
}
Aquí:
- resource — el ID canónico de tu recurso; luego debe usarse como audience o resource al emitir el token;
- authorization_servers — la lista de Auth Servers en los que ChatGPT puede solicitar un token;
- scopes_supported — qué «permisos» entiende tu servidor MCP.
Solicitud de autorización: redirección al Auth Server
Tras recibir los metadatos, el MCP Client va al Auth Server. Abre una pestaña en el navegador:
GET https://auth.giftgenius.com/authorize
?response_type=code
&client_id=chatgpt-giftgenius
&redirect_uri=... (URL de retorno del MCP Client)
&code_challenge=...
&code_challenge_method=S256
&scope=openid gifts.read
&resource=https://api.giftgenius.com
El usuario:
- ve una pantalla de login conocida (por ejemplo, Keycloak o Auth0);
- introduce login/contraseña y pasa 2FA;
- confirma que ChatGPT puede leer sus listas de regalos (scope gifts.read).
Code → Token: intercambio del code por el token con PKCE
Tras el login con éxito, el Auth Server redirige al usuario de vuelta al MCP Client con un code. El MCP Client:
- hace POST a /token;
- envía el code y el code_verifier (que corresponde al code_challenge del paso anterior).
El Auth Server verifica PKCE: hashea el code_verifier y lo compara con el code_challenge original. Si todo está bien y el cliente es realmente el mismo que inició el flow, entonces:
- emite un access_token de corta duración (normalmente JWT);
- en él indica:
- sub — el ID del usuario en el Auth Server;
- aud o resource — tu servidor MCP;
- scope — las acciones permitidas (gifts.read, openid, etc.).
Petición autenticada: invocación de la herramienta MCP con token
Ahora el MCP Client está listo para invocar de nuevo tu herramienta, pero ya con el encabezado:
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
El MCP server:
- verifica la firma del token (con el JWK del Auth Server) o mediante introspection;
- comprueba la expiración (exp);
- comprueba aud / resource — que el token realmente se emitió para https://api.giftgenius.com;
- mira el scope y decide si se puede invocar getUserGiftLists.
Después de esto ya va a tu base de datos con algún userId y devuelve las listas de regalos personales.
Fíjate que hasta este momento solo hemos hablado del flujo de red: cómo se obtiene el token y llega al servidor MCP. Luego es importante entender cómo de sub y otras claims del token se obtiene un userId concreto en tu base de datos — aquí entra en juego el identity bridge.
4. Identity Bridge: cómo el user de ChatGPT se convierte en userId en tu base de datos
La parte más interesante de la arquitectura es el «puente de identidad» (identity bridge). En la especificación MCP se subraya: el servidor MCP no conoce a los usuarios de ChatGPT; se apoya en los datos del token del Auth Server.
El esquema es algo así:
flowchart TD User[User en ChatGPT] -->|Login/SSO| Auth[Auth Server] Auth -->|JWT: sub, email, tenant| MCP[MCP Server] MCP -->|userId/tenantId| DB[(Tu base de datos)]
Paso a paso se ve así.
Primero, el Auth Server conoce a sus propios usuarios: tiene entidades user, email, id, posiblemente tenant, roles. Al iniciar sesión con éxito coloca esta información en el token (en las claims):
{
"sub": "auth0|abc123",
"email": "user@example.com",
"given_name": "Alice",
"https://giftgenius.com/tenant": "tenant-42",
"scope": "openid gifts.read",
"aud": "https://api.giftgenius.com"
}
Segundo, el MCP Server al validar el token extrae estas claims y decide quién es en su mundo. Por ejemplo:
- si sub ya existe en la tabla User.authProviderId, tomamos el userId asociado;
- si no, creamos un registro local (aprovisionamiento on-the-fly) y lo enlazamos.
Un fragmento típico de código TypeScript del lado del servidor MCP (simplificado, sin verificación de firma) puede verse así:
type TokenClaims = {
sub: string;
email?: string;
scope?: string;
};
async function mapClaimsToUserId(claims: TokenClaims): Promise<string> {
const user = await db.user.findUnique({ where: { authSub: claims.sub } });
if (user) return user.id;
const created = await db.user.create({
data: { authSub: claims.sub, email: claims.email ?? null }
});
return created.id;
}
Tercero, ya con su propio userId el servidor MCP obtiene todo lo que necesita: listas de regalos, historial de pedidos, ajustes, plan.
Así, el Auth Server se convierte en un «puente» entre el mundo externo (ChatGPT, Google, SSO) y tu mundo interno (customer_id en la base de datos de pedidos).
5. Por qué conviene separar Auth Server y MCP Server
Puede surgir la tentación: «hagamos que mi servidor MCP también muestre el login y emita el token». Formalmente es posible (puedes integrar dentro un mini IdP), pero arquitectónicamente es mala idea. Las razones son bastante pragmáticas.
Primero, seguridad y escalabilidad. Un Auth Server es una máquina pesada: 2FA, social login, políticas de contraseñas, bloqueo de cuentas, recuperación de acceso, auditoría de accesos y, posiblemente, certificaciones. Reescribir esto en cada microservicio (en cada servidor MCP) es el camino al infierno y al PCI‑DSS. Es mucho más sencillo delegarlo a Keycloak/Auth0 y simplemente validar su token.
Segundo, capacidad de cambiar o añadir clientes. Hoy solo tienes ChatGPT. Mañana conectarás Claude Desktop, tu propio frontend en Next.js, una app móvil. Todos ellos pueden usar el mismo Auth Server y el mismo esquema de OAuth 2.1, y tu servidor MCP solo seguirá validando tokens. No tendrás que reescribir la lógica de negocio para cada nuevo cliente.
Tercero, limpieza del código. Idealmente, el MCP Server:
- sabe publicar /.well-known/oauth-protected-resource;
- sabe validar el token Bearer y extraer de él userId, scopes, tenant;
- implementa herramientas de negocio (orders, gifts, profiles).
Toda la lógica de UI del login — formularios, maquetación, logins sociales — vive en el Auth Server y no sobrecarga el backend.
6. Cómo se ve en nuestra aplicación educativa GiftGenius
Volvamos a la aplicación que llevamos a lo largo del curso. Supongamos que tenemos:
- la ChatGPT App «GiftGenius» con un widget (Apps SDK) que sabe sugerir regalos;
- un servidor MCP en Node/Next.js que expone herramientas:
- searchGifts — anónima, no requiere login;
- getSavedGiftLists — personal, requiere autenticación;
- un Auth Server (más tarde — Keycloak/Auth0), donde cada usuario tiene una cuenta.
Escenario de usuario anónimo y autenticado
Si el usuario simplemente escribe «elige un regalo para mi hermano, 30 años, le gustan los juegos de mesa», nuestra App puede:
- invocar la herramienta anónima searchGifts;
- mostrar recomendaciones en la interfaz.
En ese caso:
- no se necesita token;
- el servidor MCP simplemente ejecuta la consulta (por ejemplo, a tu catálogo o a una API externa).
En cuanto el usuario dice «guarda esto en mis listas» o «muestra mis ideas guardadas», el modelo decide invocar la herramienta protegida getSavedGiftLists. El servidor responde con 401 + WWW-Authenticate con resource_metadata. ChatGPT lanza el asistente de OAuth «Link GiftGenius account», pasa al usuario por el login y obtiene el token.
Después, en cada llamada protegida:
- el MCP Server ya ve Authorization: Bearer ...;
- extrae el userId del token;
- filtra los datos por ese userId.
Gracias a esto podemos:
- separar los datos de diferentes usuarios;
- mostrar de forma segura el historial de pedidos, la lista de favoritos;
- implementar funciones de comercio (más adelante en el curso).
Arquitectura del backend: middleware + handlers de herramientas
En la práctica, en código Node/Next.js esto suele verse como una cadena: «middleware de autenticación → handler de negocio de la herramienta». En la lección sobre la implementación de los tool‑handlers ya subrayamos que hay que pasarles el contexto: user_id, tokens, ajustes.
El fragmento de código puede ser así:
// auth-context.ts
export type AuthContext = {
userId: string | null; // null para llamadas anónimas
scopes: string[];
};
Middleware que se aplica a todos los endpoints MCP:
// mcp-auth-middleware.ts
export async function buildAuthContext(req: Request): Promise<AuthContext> {
const header = req.headers.authorization || "";
const token = header.replace(/^Bearer\s+/i, "");
if (!token) return { userId: null, scopes: [] }; // usuario anónimo
const claims = await verifyAndDecodeToken(token); // verificación del token
const userId = await mapClaimsToUserId(claims);
const scopes = (claims.scope || "").split(" ");
return { userId, scopes };
}
Y el propio handler de la herramienta recibe este contexto:
// tools/getSavedGiftLists.ts
export async function getSavedGiftLists(_args: {}, ctx: AuthContext) {
if (!ctx.userId) throw new Error("User must be authenticated");
return db.giftList.findMany({
where: { ownerId: ctx.userId }
});
}
La idea es que el tool‑handler no sabe nada ni de OAuth ni de PKCE. Simplemente trabaja con el «obvio» userId. Toda la magia de OAuth está antes: en el MCP Client y en el Auth‑middleware.
7. Esquemas visuales: cómo conviven Client, Server y Auth
Ya desglosamos paso a paso el flow en el apartado 3 con texto. A veces es más fácil verlo una vez que explicarlo siete veces, así que ahora mostramos las mismas interacciones en dos diagramas.
Esqueleto de interacción (The Triangle of Trust)
flowchart TD U[User] -->|1. Login / Consent| A[MCP Auth Server] U -->|2. Chatea| C["MCP Client (ChatGPT)"] C -->|3. OAuth Flow| A C -->|4. Bearer Token| S[MCP Server] S -->|5. Data| C
La figura se lee así.
Primero, el usuario inicia sesión a través del Auth Server, que en esencia confirma su identidad y emite un token. El MCP Client gestiona este proceso y luego usa el token para dirigirse al servidor MCP. El servidor MCP no ve el login/contraseña, solo ve el token y decide qué está permitido.
Flujo desde la petición hasta la respuesta
sequenceDiagram participant User participant ChatGPT as MCP Client participant Auth as Auth Server participant MCP as MCP Server User->>ChatGPT: "Muéstrame mis listas de regalos" ChatGPT->>MCP: callTool(getSavedGiftLists) (sin token) MCP-->>ChatGPT: 401 + WWW-Authenticate (resource_metadata) ChatGPT->>Auth: /authorize + PKCE User->>Auth: Introduce login/contraseña y da el consentimiento Auth-->>ChatGPT: redirect + code ChatGPT->>Auth: /token + code_verifier Auth-->>ChatGPT: access_token (JWT) ChatGPT->>MCP: callTool(getSavedGiftLists) + Authorization: Bearer ... MCP-->>ChatGPT: JSON con listas personales ChatGPT-->>User: Lista renderizada en el widget
Este diagrama es lo que deberías poder «explicar con los ojos cerrados» al final del módulo.
8. Un poco más: varios recursos, varios clientes, DCR
Lo bueno de esta arquitectura es que escala.
Primero, puedes tener varios servidores MCP (por ejemplo, uno de regalos, otro de pedidos) y un único Auth Server que emite tokens con distintos aud/resource. Cada servidor de recursos debe comprobar que el token está realmente destinado a él; de lo contrario aparece el problema clásico del «confused deputy», cuando un token para un servicio es aceptado por otro.
Segundo, puedes tener muchos clientes:
- la ChatGPT App;
- tu propio frontend;
- la aplicación móvil;
- la integración de un partner a través de MCP Gateway.
Todos ellos van a:
- leer /.well-known/oauth-protected-resource;
- descubrir dónde está el Auth Server;
- pasar el flow de OAuth 2.1;
- obtener tokens e invocar el servidor MCP.
Tercero, los Auth Servers modernos cada vez más soportan Dynamic Client Registration (DCR) — la posibilidad de registrar clientes dinámicamente vía API. La especificación MCP presupone precisamente esta posibilidad: el cliente (ChatGPT/Jam) puede registrarse automáticamente en el Auth Server mediante su registration_endpoint.
En este módulo es importante entender que:
- el MCP Client, el MCP Server y el Auth Server se comunican a través de documentos de discovery estandarizados y tokens;
- no necesitas «codificar en duro» todos los clientes en el backend;
- puedes ampliar el ecosistema sin romper el modelo de autorización existente.
9. Errores típicos al entender la arquitectura de autorización MCP
Error n.º 1: «El servidor MCP debe autenticar al usuario por sí mismo».
A veces los desarrolladores intentan incrustar un formulario de login directamente en el servidor MCP y luego enviar el login/contraseña a través de herramientas. Esto rompe la idea misma de OAuth. El servidor MCP no debe ver la contraseña bajo ninguna circunstancia. El login y el consentimiento son responsabilidad del Auth Server. El servidor MCP trabaja solo con tokens y sus claims.
Error n.º 2: Confusión entre MCP Client y MCP Server.
Ocurre que se percibe a ChatGPT como «parte de mi backend» e intentan, por ejemplo, almacenar en él secretos o esperan que valide los permisos de acceso. En realidad, el MCP Client solo inicia OAuth y adjunta tokens. La validación del token y de los permisos es tarea del servidor MCP, no de ChatGPT.
Error n.º 3: «Una API key en .env en lugar de OAuth».
Antipatrón clásico: crear un gran SERVICE_API_KEY, ponerlo en el .env del servidor MCP y dar por resuelto el problema. En ese enfoque no hay separación de permisos por usuarios, no se pueden mostrar datos personales de forma segura ni realizar compras; todo se hace «en nombre del servicio», no del usuario. Esto contradice por completo los objetivos de la autorización en ChatGPT Apps.
Error n.º 4: Ignorar audience y resource.
Si el servidor MCP acepta cualquier JWT válido con una firma correcta y no revisa aud/resource, entonces cualquier token emitido para otro servicio por el mismo Auth Server puede utilizarse para invocar tus herramientas. Es una violación directa del modelo de seguridad de OAuth. El servidor debe comprobar que el token está emitido para su resource.
Error n.º 5: Mezclar lógica de auth y lógica de negocio.
A veces se empieza a pasar todo el análisis del token, verificación de firma, trabajo con JWK, etc., dentro de los tool‑handlers. Al final el código se vuelve frágil y difícil de mantener. Es mucho más correcto separar la capa de «validación del token, mapeo a userId» (middleware) de la capa de «lógica de la herramienta», que recibe ya un AuthContext claro.
Error n.º 6: Esperar que ChatGPT «lo haga todo solo» sin .well-known.
Sin el endpoint correcto /.well-known/oauth-protected-resource el cliente MCP simplemente no sabe dónde está tu Auth Server ni qué scopes necesita. El resultado: el chat «no sabe autenticarse» en silencio, y el desarrollador mira logs vacíos. El camino correcto: el servidor MCP declara claramente sus requisitos de autorización mediante .well-known, el cliente los lee y construye el flow.
Error n.º 7: Olvidar al usuario en la lógica de negocio.
A veces, incluso configurando correctamente OAuth y el mapeo del token a userId, los desarrolladores no usan esto en las consultas a la base de datos: por ejemplo, olvidan filtrar por ownerId = userId. Entonces cualquier usuario autenticado puede ver datos ajenos. La existencia del token es solo el primer paso; el segundo paso siempre es el uso correcto de userId y scope en el código de negocio.
GO TO FULL VERSION