1. Qué es un Auth Server en la práctica y por qué elegimos Keycloak
Empecemos con un breve recordatorio: un Auth Server (IdP) es un servicio que:
- muestra al usuario la pantalla de inicio de sesión/registro y el consentimiento;
- emite tokens OAuth/OIDC (access_token, id_token, refresh_token);
- publica el documento de discovery y las claves JWKS para que los servidores de recursos puedan validar esos tokens.
En nuestro stack:
- ChatGPT / MCP Jam actúa como cliente OAuth (public client);
- tu servidor MCP — como Resource Server;
- Keycloak — como Auth Server.
Por qué Keycloak es cómodo para el curso y la vida real:
- es open source, fácil de levantar localmente/en Docker;
- tiene un modelo de entidades bastante transparente: realm, clients, users, roles;
- en esencia, cualquier configuración que domines en Keycloak se traslada casi 1:1 a Auth0/Okta/Cognito: allí están las mismas ideas — client, scopes, redirect URIs, PKCE.
Idea importante: configuramos no «Keycloak para todo el proyecto», sino un Realm concreto para nuestra ChatGPT App. Es una especie de «sandbox» de autenticación específicamente para clientes MCP.
En resumen, al final de la lección tendrás:
- tu propio realm en Keycloak para la aplicación de ChatGPT;
- un public‑client configurado con Authorization Code + PKCE;
- un conjunto mínimo de scopes y claims;
- entendimiento de cómo vive ese token en tu servidor MCP de Node.
2. Entidades básicas de Keycloak a través del prisma de MCP
Para no perdernos luego en el panel de administración, desglosaremos las entidades.
Realm: espacio de configuración y usuarios
Un realm en Keycloak es un espacio aislado con sus propios usuarios, clientes y políticas. Una analogía útil — «una oficina alquilada en un centro de negocios»: cada uno tiene su propio local, su lista de empleados y sus reglas de acceso.
Para el curso y para tu primera app real, tiene sentido crear un realm separado, por ejemplo giftgenius-mcp o mcp-course. Esto permite:
- no tocar el master realm, para no romper accidentalmente el panel de administración;
- reutilizar configuraciones y usuarios entre distintos entornos (dev / staging / prod) mediante exportación/importación del realm.
Client: registro de una aplicación (ChatGPT / MCP Jam)
Client en Keycloak no es un «usuario», sino una aplicación que acude al Auth Server por tokens. En nuestro caso esto no es tu backend de Next.js, sino el cliente MCP: ChatGPT, MCP Jam, y quizá aparte tu widget, si haces el flujo OAuth manual en la UI.
Campos clave del cliente:
- client_id — identificador de cadena;
- tipo (public / confidential / bearer-only);
- flujos OAuth habilitados (Standard Flow, Client Credentials, etc.);
- lista de redirect URI permitidos;
- lista de scopes y protocol mappers (claims en el token).
Para ChatGPT/MCP Jam necesitamos un public client, porque:
- ChatGPT como cliente no puede almacenar de forma segura el client_secret;
- MCP Jam, como herramienta de escritorio/navegador, también se ejecuta en un entorno no confiable.
User: una persona real
User es un usuario «real»: tiene username, contraseña, email, atributos, grupos, roles. Cuando alguien inicia sesión mediante Keycloak, su sub y otros datos entran en el token, que luego validas en el MCP‑servidor y mapeas a tus accountId / tenantId.
Para nuestra demo basta con:
- uno o dos usuarios de prueba (por ejemplo, alice@example.com, bob@example.com);
- quizá un par de atributos como tenant o plan, si queremos mostrar cómo los claims del token influyen en el comportamiento de las tools.
3. Elección del tipo de cliente: public, PKCE y por qué sin secreto
Ahora al grano: cómo configurar exactamente el cliente en Keycloak para ChatGPT/MCP.
Public vs Confidential: por qué no client_secret
En una aplicación web clásica haces un backend, pones allí el client_secret, y es el servidor quien habla con el IdP para obtener tokens. Ese es un confidential client: puede guardar el secreto.
En el mundo de ChatGPT es al revés:
- el cliente OAuth es la propia plataforma ChatGPT o una utilidad como MCP Jam;
- no controlas su código ni su entorno;
- cualquier client_secret que le des a ChatGPT debe considerarse comprometido al instante.
Por eso ChatGPT/Jam funcionan como public clients, es decir, sin client_secret, y compensan esto con PKCE — Proof Key for Code Exchange.
Qué hace PKCE en lenguaje llano
PKCE es un «secreto por sesión» de un solo uso. Su objetivo es evitar que alguien intercepte el authorization code y lo canjee por un token desde otro lugar. El esquema es así:
- El cliente genera una cadena aleatoria code_verifier.
- La hashea (normalmente SHA-256) y obtiene code_challenge.
- Al redirigir a /authorize envía code_challenge y code_challenge_method=S256.
- Tras el login, el usuario vuelve con code al redirect URI.
- El cliente hace POST /token, enviando code y el code_verifier original.
- Keycloak hashea el verifier, lo compara con el challenge y, si todo está bien, emite el token.
Importante para nosotros: de todo esto se encargan ChatGPT/MCP Jam. Solo debemos en el cliente de Keycloak activar la compatibilidad con Authorization Code + PKCE (S256) y no exigir client_secret.
4. Configuración paso a paso de Keycloak para el escenario MCP
Ya lo tenemos claro: para ChatGPT/MCP Jam necesitamos un public client con Authorization Code Flow y PKCE (S256), sin client_secret. Ahora veamos cómo se ve esta configuración en Keycloak.
Supongamos que ya tienes Keycloak funcionando (contenedor Docker, instalación local — da igual). Ahora nos interesa la lógica de la configuración, no dónde hacer clic exactamente.
Creamos un nuevo realm para la app
Creemos el realm giftgenius-mcp: es un área separada donde habrá:
- usuarios específicos para la aplicación de ChatGPT;
- clientes a través de los cuales ChatGPT/MCP Jam pasará por OAuth;
- sus propias políticas de contraseñas y tokens.
Consejo práctico: no mezcles el realm con el que autorizas a los empleados del panel de administración con el realm para los clientes de ChatGPT. Es más seguro y lógicamente más simple.
Añadimos un usuario de prueba
Creamos un usuario, por ejemplo alice:
- username: alice;
- email: alice@example.com;
- definimos una contraseña (para simplificar — sin políticas complejas);
- si queremos, añadimos el atributo tenantId=demo-tenant o el rol ROLE_PREMIUM.
Más adelante, en el MCP‑servidor, podrás decodificar el token, extraer sub, email, tenantId y vincularlos con tu modelo de usuario.
Creamos un public client para MCP Jam / ChatGPT
Ahora lo más interesante — el Client.
A nivel conceptual los parámetros deberían verse así:
- Client ID: giftgenius-mcp-client (el nombre que prefieras);
- Tipo: public / Client Authentication off;
- Standard Flow habilitado (Authorization Code);
- PKCE habilitado con el método S256;
- redirect URI configurados;
- scopes necesarios configurados (openid + tu personalizado, por ejemplo, mcp:tools).
Activamos Standard Flow y PKCE
A nivel conceptual:
- activamos Authorization Code Flow (a menudo llamado «Standard Flow Enabled»);
- en la sección PKCE indicamos pkceRequired=true y, con frecuencia, explícitamente code_challenge_method=S256.
Por qué S256: en la documentación moderna de OAuth 2.1 y en las recomendaciones de OpenAI/Model Context Protocol precisamente S256 se admite como método seguro; el PKCE «plain» se considera inseguro.
Redirect URI — el punto más frágil
Los redirect URI deben coincidir letra por letra con lo que usará el cliente. De lo contrario obtendrás el error invalid_redirect_uri en la fase de autorización.
En nuestro curso hay dos clientes típicos:
- MCP Jam/Inspector para depuración. Suelen funcionar en http://localhost:PORT/.... Para el escenario local es lógico permitir un redirect del tipo:
- http://localhost:5173/* o la ruta concreta que use Jam.
- ChatGPT / Apps SDK en producción. Aquí el redirect URI lo determina la propia plataforma. En una integración real consultarás la documentación actual de OpenAI y pondrás la URL que ChatGPT utilizará como callback.
En el marco de la lección lo importante es entender: ChatGPT no puede usar cualquier redirect; debe coincidir con el registrado en el Auth Server. Por lo tanto:
- nunca pongas * y «vale cualquier URL»;
- para desarrollo local se admiten comodines dentro de localhost, pero no en producción.
Scopes: lo mínimo suficiente
Los scopes son una mini‑lista de permisos que solicita el cliente.
Para nuestro escenario MCP normalmente necesitamos:
- openid — para habilitar OpenID Connect y recibir id_token con el campo sub, a veces email;
- un scope personalizado, por ejemplo mcp:tools, que indique «se permite el acceso a las herramientas MCP».
En Keycloak se puede hacer mediante Client Scopes:
- dejar openid;
- desactivar por defecto scopes sobrantes como profile y email si no los necesitas;
- añadir un nuevo scope mcp:tools, con el que luego limitarás el acceso a tools en el Resource Server.
Esto es importante por dos razones:
- Sin openid no recibirás id_token ni parte de los campos estándar de OIDC.
- Sin un scope personalizado aparte no podrás, en el lado del MCP‑servidor, afirmar con claridad: «este token se puede usar para invocar mis herramientas».
5. Configuración de tokens: tiempo de vida, firma y claims
Ahora veamos qué tokens emitirá Keycloak y cómo configurarlos para el escenario MCP.
Tiempo de vida del access token
En la configuración del realm de Keycloak hay una sección Tokens, donde puedes definir:
- Access Token Lifespan;
- Refresh Token Lifespan y otros timeouts.
Para una ChatGPT App son importantes los access tokens de corta duración:
- unos pocos minutos u horas son valores razonables;
- si el token caduca, el MCP‑servidor responde 401, ChatGPT reinicia el flujo OAuth y el usuario, si es necesario, vuelve a iniciar sesión.
La idea es la misma que en la documentación de OpenAI sobre el Apps SDK: TTL corto + renovación de tokens y la posibilidad de «cerrar sesión» con bastante rapidez revocando el token en el IdP.
Los refresh tokens para el cliente de ChatGPT, por lo general, o bien no son tan críticos, o bien se emiten con un periodo corto para no mantener sesiones perpetuas.
Qué claims queremos ver en el token
Mínimamente necesitamos:
- sub — identificador único del usuario en Keycloak;
- iss — quién emitió el token (issuer);
- aud — para qué recurso es el token (se usa más adelante en el MCP‑servidor);
- exp — momento de expiración;
- scope — lista de scopes.
Adicionalmente, a menudo son útiles:
- email — si quieres ver la dirección del usuario;
- tenantId u otro claim similar — para escenarios multi‑tenant;
- roles — para autorización adicional.
En Keycloak esto se configura mediante Protocol Mappers:
- mappers estándar para email, preferred_username, etc.;
- mappers personalizados para atributos de usuario (user.attribute → claim.name).
Ejemplo: un mapper que añade el email como claim al token, indicando user.attribute=email, claim.name=email.
En el lado del MCP‑servidor podrás tomar esos claims del JWT parseado y:
- vincular sub con tu accountId;
- usar tenantId para seleccionar solo los datos pertenecientes a ese tenant;
- usar roles para delimitar permisos más «finos».
Firma del token y JWKS
De forma predeterminada, Keycloak firma los access/id tokens con un algoritmo asimétrico (normalmente RS256) y publica las claves públicas mediante el endpoint JWKS desde el documento de OpenID Discovery.
Para nosotros es importante porque el MCP‑servidor podrá:
- tomar issuer del token;
- a partir de /.well-known/openid-configuration encontrar el endpoint JWKS;
- obtener la clave pública y verificar la firma del token localmente.
Esta parte la veremos con más detalle en la lección sobre el MCP‑servidor como recurso protegido, pero ya ahora es útil entender por qué Keycloak proporciona esos metadatos.
6. Dynamic Client Registration (DCR): cuándo hace falta realmente
Esta sección es más bien avanzada. Hasta ahora hemos configurado el cliente «a mano» en el panel, y con eso basta para poner la app en marcha. Pero el protocolo OAuth permite que los clientes se registren dinámicamente mediante un endpoint específico.
En el contexto de ChatGPT y MCP, OpenAI dice explícitamente que la plataforma puede usar Dynamic Client Registration. Es decir, ChatGPT se registra en el Auth Server «al vuelo», mediante el registration_endpoint del documento de discovery.
En Keycloak esto se ve así:
- se habilita DCR a nivel de realm;
- configuras la política: quién puede registrar nuevos clientes y con qué grant types/scopes.
Ejemplo de JSON para registrar un public client con Authorization Code + PKCE y el scope openid mcp:tools podría ser así:
{
"clientName": "My ChatGPT App",
"redirectUris": ["https://jam.proxy.mcpapps.com/callback"],
"grantTypes": ["authorization_code"],
"responseTypes": ["code"],
"scope": "openid mcp:tools",
"tokenEndpointAuthMethod": "none"
}
Donde tokenEndpointAuthMethod: "none" indica precisamente un public client sin client_secret.
Para el curso basta con saber que:
- DCR es útil si hay muchos clientes o son de vida corta;
- ChatGPT potencialmente puede registrarse por sí mismo en tu IdP;
- pero al principio se puede apañar con un cliente estático creado por UI.
7. Cómo se relaciona esto con nuestra aplicación didáctica
Recordemos que tenemos un MCP‑servidor didáctico (por ejemplo, GiftGenius) que sabe:
- ofrecer una lista de posibles regalos;
- guardar algunas listas de deseos del usuario;
- más adelante — acudir a la parte de comercio, tramitar pedidos, etc.
Mientras el MCP‑servidor esté abierto, no sabe quién le golpea:
- una petición desde ChatGPT puede ser lógicamente «de Alicia» o «de Bob», pero el MCP‑servidor no lo distingue;
- no puedes mostrar el historial privado de regalos;
- no puedes cargar con seguridad el cargo en la cuenta correcta.
Tras configurar Keycloak como Auth Server, la situación cambia:
- ChatGPT entiende por .well-known de tu recurso MCP que está protegido y requiere token.
- ChatGPT envía al usuario a Keycloak mediante Authorization Code + PKCE.
- El usuario inicia sesión (nuestra alice).
- ChatGPT obtiene un access token, en el que están sub, email, mcp:tools y otros claims.
- ChatGPT llama a la herramienta GiftGenius ya con Authorization: Bearer <token>.
- El MCP‑servidor, al verificar el token, entiende: «Ajá, es Alice con sub=... y tenantId=demo-tenant» — y responde en consecuencia.
Este encaje se completará en la siguiente lección, donde haremos que el MCP‑servidor sea un resource server «de verdad»: implementaremos el endpoint de metadatos, la verificación del token y la vinculación al usuario.
8. Pequeños ejemplos prácticos (nuestro stack: TypeScript + Node)
Todo lo de abajo no es «la única forma correcta», sino una referencia de cómo puede verse en un stack típico de Node/TypeScript. Si ahora estás más centrado en hacer clic en Keycloak, puedes ojear esta sección y volver cuando conectes el MCP‑servidor.
Aunque la configuración de Keycloak se hace principalmente «clicando» en el UI o mediante su Admin REST API, es útil mostrar un par de trozos de código alrededor, para entender cómo usarás todo esto en el lado del MCP‑servidor.
Supongamos que ya tenemos un MCP‑servidor en Node.js (TypeScript) basado en el SDK oficial.
Config de autorización (issuer y audience)
Creemos un pequeño módulo authConfig.ts:
// authConfig.ts
export const authConfig = {
issuer: 'https://auth.my-company.com/realms/giftgenius-mcp',
audience: 'https://mcp.my-company.com', // URL de tu servidor MCP
requiredScopes: ['mcp:tools'], // mínimo que esperamos en el token
};
Aquí issuer es la URL del realm de Keycloak, y audience es el identificador del recurso (lo usaremos también en la configuración del token y en MCP).
Verificación básica de JWT por JWKS
En la vida real probablemente usarás una biblioteca como jsonwebtoken + jwks-rsa o utilidades listas del MCP SDK. Un esqueleto muy simple puede verse así:
// verifyToken.ts
import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
import { authConfig } from './authConfig';
const client = jwksClient({
jwksUri: `${authConfig.issuer}/protocol/openid-connect/certs`,
});
function getKey(header: any, callback: any) {
client.getSigningKey(header.kid, (err, key) => {
const signingKey = key?.getPublicKey();
callback(err, signingKey);
});
}
export function verifyAccessToken(token: string): Promise<any> {
return new Promise((resolve, reject) => {
jwt.verify(
token,
getKey,
{
audience: authConfig.audience,
issuer: authConfig.issuer,
},
(err, decoded) => (err ? reject(err) : resolve(decoded)),
);
});
}
Por supuesto, el manejo de errores y la caché de claves deberían ser más cuidadosos, pero se ve la idea: Keycloak publica las claves JWKS, las obtenemos y verificamos la firma.
Comprobación del scope y extracción de la identidad
En un middleware para las MCP‑tools podrás hacer algo como:
// authMiddleware.ts
import { verifyAccessToken } from './verifyToken';
import { authConfig } from './authConfig';
export async function requireAuth(bearerToken: string) {
const token = bearerToken.replace(/^Bearer\s+/i, '');
const decoded: any = await verifyAccessToken(token);
const scopes = (decoded.scope as string).split(' ');
const hasScope = authConfig.requiredScopes.every(s => scopes.includes(s));
if (!hasScope) {
throw new Error('Insufficient scope');
}
return {
userId: decoded.sub,
email: decoded.email,
tenantId: decoded.tenantId,
};
}
Y luego, ya en los handlers de las herramientas MCP, usarás userId y tenantId, para cargar las listas de regalos del usuario. Las propias herramientas ya las implementamos en módulos anteriores; ahora solo importa ver cómo el token de Keycloak se convierte en una identidad comprensible para tu backend.
9. Errores típicos al configurar Keycloak como MCP Auth Server
Error n.º 1: Se usa un confidential client con client_secret.
A veces, por costumbre, se crea un cliente de tipo confidential y se intenta poner el client_secret en la configuración de MCP/ChatGPT. En el ecosistema de ChatGPT App esto no debería funcionar y no será seguro: ChatGPT es un public client, no puede guardar un secreto. La vía correcta — public client + PKCE.
Error n.º 2: Scopes demasiado amplios por defecto.
Dejar activados profile, email y un montón de scopes estándar — y luego emitir esos tokens para cada chat — no es buena idea. Mejor minimizar: openid y un mcp:tools concreto (o un par de scopes de negocio) — suficiente para las primeras versiones. Esto reduce el riesgo de fuga de datos innecesarios y hace el comportamiento más predecible.
Error n.º 3: Redirect URI incorrecto.
Clásico: en Keycloak está indicado http://localhost:5173/callback, pero MCP Jam usa http://localhost:5173/. O al revés. Como resultado — invalid_redirect_uri y una depuración desesperante. Comprueba siempre el valor exacto del redirect URI en la documentación de Jam/ChatGPT y escríbelo letra por letra.
Error n.º 4: PKCE no está activado o lo está con el método equivocado.
Algunas versiones de Keycloak requieren activar por separado «PKCE required» e indicar el método S256. Si no lo haces, ChatGPT/Jam, que espera PKCE, puede obtener un error invalid_request quejándose de code_challenge. Comprueba obligatoriamente la configuración de PKCE para los public‑clients.
Error n.º 5: Claims incorrectos o ausentes en el token.
A veces el token no tiene sub o email, porque el scope o el protocol mapper correspondiente no está configurado. Como resultado, en el MCP‑servidor ves el token, pero no puedes mapearlo a un usuario real. Solución: asegúrate de que los campos necesarios (como mínimo sub, y mejor también email/tenantId) estén mapeados en los access/id tokens.
Error n.º 6: TTL demasiado largo para los access tokens.
Desde el punto de vista de seguridad, emitir access tokens para un día/semana es mala idea. Si el token se filtra, el atacante obtendrá acceso prolongado al recurso MCP. Mejor hacer que los access tokens sean de corta duración (minutos u horas) y usar la reautorización cuando sea necesario.
Error n.º 7: Confusión con el realm y uso de master.
A veces lo primero que se hace es crear clientes y usuarios directamente en el realm master. Luego se conectan allí un par de proyectos más — y al final nada está claro. Mejor crear enseguida un realm separado para cada aplicación/curso. Te simplificará la vida a ti y a tu DevOps.
GO TO FULL VERSION