Phase 13 - Lesson 18

MCP Auth in Production — DCR, JWKS Rotation, Audience-Pinned Tokens on iii Primitives

This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.

Lesson 16 stood up the OAuth 2.1 state machine in memory. By 2026, every MCP server you ship to a real org sits behind production auth: dynamic client registration (RFC 7591), authorization-server metadata discovery (RFC 8414), JWKS rotation that does not break a 3 a.m. token validation, and audience-pinned tokens that refuse confused-deputy reuse. This lesson wires all of that through iii primitives — iii.registerTrigger for HTTP and cron, iii.registerFunction for auth logic, state::set/get for cached keys — so the auth surface is observable, restartable, and replayable like every other workload in the engine.

Tipo: Build Lenguajes: Python (stdlib, primitivas iii simuladas para el entorno de la lección) Prerrequisitos: Phase 13 · 16 (OAuth 2.1 state machine), Phase 13 · 17 (gateways) Tiempo: ~90 minutos

Objetivos de Aprendizaje

  • Descubrir un servidor de autorización a través de los metadados de la RFC 8414 y verificar el contrato.
  • Implementar el registro dinámico de clientes (DCR) de la RFC 7591 para que los clientes MCP se inscriban sin la intervención del administrador.
  • Almacenar en caché y rotar las claves JWKS utilizando un trigger cron para que la verificación de firma sobreviva a la rotación de claves.
  • Vincular tokens a un único recurso MCP mediante los indicadores de recursos de la RFC 8707 y rechazar la reutilización por diputado confuso.
  • Conectar cada endpoint y tarea en segundo plano como primitivas iii — triggers HTTP, triggers cron, funciones con nombre y lecturas de state::* — para que un único reinicio reconstruya la superficie de autenticación.
  • Leer una matriz de capacidades de IdP y negarse a desplegar cuando el IdP no pueda satisfacer el perfil de autenticación de MCP.

El Problema

El simulador de la Lección 16 ejecuta OAuth 2.1 en memoria. La producción tiene tres brechas operativas que un simulador solo en memoria no puede ver.

La primera brecha es la inscripción. Una organización real ejecuta cientos de servidores MCP y miles de clientes MCP. Los operadores no registran manualmente a cada usuario de Cursor como un cliente OAuth. El registro dinámico de clientes de la RFC 7591 permite a un cliente enviar un POST /register al servidor de autorización y recibir un client_id (y opcionalmente un client_secret) en el acto. El servidor publica el registration_endpoint en sus metadados de la RFC 8414; el cliente lo descubre sin necesidad de configuración fuera de banda.

La segunda brecha es la rotación de claves. La validación de JWT depende de las claves de firma del servidor de autorización, publicadas como un JSON Web Key Set (JWKS). El servidor de autorización las rota según un cronograma (a menudo cada hora, a veces más rápido bajo respuesta a incidentes). Un servidor MCP que recupera el JWKS una sola vez al arrancar valida correctamente hasta la ventana de rotación; después de eso, cada solicitud falla hasta que se reinicia. La producción conecta el JWKS como un valor en caché con una tarea de actualización que sobrescribe la caché antes de que expiren las claves anteriores, más una búsqueda de contingencia (fallback) en caso de fallo de caché (cache miss) para el caso en que llega un token firmado por una clave más nueva que la caché.

La tercera brecha es la vinculación de audiencia (audience binding). La Lección 16 introdujo los indicadores de recursos de la RFC 8707. En producción, ese indicador se convierte en una verificación estricta de la declaración (claim check) en cada solicitud. El servidor MCP compara token.aud con su propia URL de recurso canónica y rechaza las discrepancias con HTTP 401. Esta es la única defensa contra un servidor MCP ascendente (upstream) (o un cliente malicioso que tenga un token destinado a un servidor) que intente replicar ese token contra otro servidor en la misma malla de confianza.

Esta lección trata cada una de esas brechas como una primitiva iii. El documento de metadados es un trigger HTTP que devuelve la salida de una función. La rotación de JWKS es un trigger cron que llama a auth::rotate-jwks, el cual escribe en state::set("auth/jwks/<issuer>", ...). La validación de JWT es una función que otros llaman a través de iii.trigger("auth::validate-jwt", token). El propio servidor MCP es solo otro trigger HTTP que llama a la validación antes de despachar. Reinicie el motor: el registro de triggers se reconstruye; el estado sobrevive; la superficie de autenticación está operativa sin reconciliación manual.

El Concepto

RFC 8414 — OAuth Authorization Server Metadata

Un documento en /.well-known/oauth-authorization-server describe todo lo que un cliente necesita:

{
  "issuer": "https://auth.example.com",
  "authorization_endpoint": "https://auth.example.com/authorize",
  "token_endpoint": "https://auth.example.com/token",
  "jwks_uri": "https://auth.example.com/.well-known/jwks.json",
  "registration_endpoint": "https://auth.example.com/register",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"],
  "scopes_supported": ["mcp:tools.read", "mcp:tools.invoke"],
  "token_endpoint_auth_methods_supported": ["none", "private_key_jwt"]
}

Un cliente que recibe una URL de recurso MCP encadena el descubrimiento: oauth-protected-resource de la RFC 9728 (el documento del servidor de recursos) indica el emisor, luego oauth-authorization-server (esta RFC) indica cada endpoint. El cliente nunca codifica de forma fija una URL de autorización.

El contrato que verifica antes de confiar en un IdP para MCP:

  • code_challenge_methods_supported incluye S256 (PKCE según la RFC 7636).
  • grant_types_supported incluye authorization_code y rechaza password e implicit.
  • registration_endpoint está presente (soporte de la RFC 7591).
  • response_types_supported es exactamente ["code"] para OAuth 2.1.

Si falta alguno de estos, el servidor MCP se niega a desplegarse contra este IdP. El manifiesto de despliegue está mal, no el código.

RFC 9728 (recap) — Protected Resource Metadata

La Lección 16 cubrió la RFC 9728. La diferencia en producción: este documento es el único lugar donde un cliente busca para encontrar los servidores de autorización de confianza para este servidor MCP. Un único servidor MCP puede aceptar tokens de múltiples IdPs (uno para el personal, otro para socios). La RFC 9728 declara ese conjunto; la RFC 8414 documenta lo que soporta cada IdP.

{
  "resource": "https://notes.example.com",
  "authorization_servers": ["https://auth.example.com", "https://partners.example.com"],
  "scopes_supported": ["mcp:tools.invoke"],
  "bearer_methods_supported": ["header"],
  "resource_documentation": "https://notes.example.com/docs"
}

RFC 7591 — Dynamic Client Registration

Sin DCR, cada cliente MCP (Cursor, Claude Desktop, un agente personalizado) necesita un intercambio fuera de banda con el administrador del IdP. Con DCR, el cliente envía:

POST /register
Content-Type: application/json

{
  "redirect_uris": ["http://127.0.0.1:7333/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none",
  "scope": "mcp:tools.invoke",
  "client_name": "Cursor",
  "software_id": "com.cursor.cursor",
  "software_version": "0.42.0"
}

El servidor responde con client_id y un registration_access_token para actualizaciones posteriores:

{
  "client_id": "c_3e7f1a",
  "client_id_issued_at": 1769472000,
  "redirect_uris": ["http://127.0.0.1:7333/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "registration_access_token": "regt_b2...",
  "registration_client_uri": "https://auth.example.com/register/c_3e7f1a"
}

token_endpoint_auth_method: none es el valor por defecto correcto para los clientes MCP que se ejecutan en el dispositivo del usuario. Obtienen solo un client_id, sin client_secret que exfiltrar. PKCE proporciona la prueba de posesión que necesitan los clientes públicos.

Tres trampas en producción:

  • El endpoint de registro debe limitar la tasa (rate-limit) por IP de origen. Sin eso, un actor hostil crearía scripts para millones de registros falsos y agotaría el espacio de nombres de client_id. El iii hace que esto sea trivial: el trigger HTTP de registro llama a una función auth::rate-limit antes de despachar al registrador.
  • Algunos IdPs empresariales requieren un software_statement (un JWT firmado que avala al cliente). El simulacro de la lección se lo salta; la producción conecta un paso de verificación que rechaza los registros no firmados de cualquier origen que no sea las URIs de redireccionamiento de localhost.
  • El registration_access_token debe almacenarse como un hash, no como texto plano. El robo de este token significa que el atacante puede reescribir las URIs de redireccionamiento del cliente.

RFC 8707 (recap) — Resource Indicators

La Lección 16 estableció la forma. La regla de producción: cada solicitud de token incluye resource=<canonical-mcp-url>, y el servidor MCP verifica que token.aud coincida con su propia URL de recurso en cada llamada. Si el servidor MCP es accesible en https://notes.example.com/mcp, la URL canónica es https://notes.example.com (se excluye el componente de ruta para que un único servidor aloje varias rutas bajo una misma audiencia).

RFC 7636 (recap) — PKCE

PKCE es obligatorio en OAuth 2.1. El flujo de código de autorización de la lección siempre lleva code_challenge and code_verifier. El servidor rechaza cualquier solicitud de token sin un verificador o con un verificador que no genere el hash del desafío almacenado.

Perfil de Autenticación de la Especificación MCP 25-11-2025

La especificación de MCP (25-11-2025) es precisa sobre lo que debe hacer la capa de autorización de un servidor MCP:

  • Publicar /.well-known/oauth-protected-resource (RFC 9728).
  • Aceptar tokens únicamente a través de Authorization: Bearer ....
  • Validar aud, iss, exp y los alcances (scopes) requeridos por solicitud.
  • Responder con WWW-Authenticate con Bearer error=... para cada 401 y 403, incluyendo los parámetros scope= y resource= cuando corresponda.
  • Rechazar tokens cuyo aud no coincida con el recurso canónico.
  • Rechazar tokens cuyo iss no esté en la lista authorization_servers de los metadados del recurso protegido.

El borrador de OAuth 2.1 es el sustrato; RFC 8414/7591/8707/9728 + RFC 7636 son la superficie; la especificación MCP es el perfil.

Matriz de capacidades de IdP

No todos los IdP soportan el perfil MCP completo. La siguiente matriz documenta las declaraciones de capacidad reales a partir de la especificación de 25-11-2025. Es una barrera de despliegue, no una recomendación.

Categoría de IdP Metadados RFC 8414 RFC 7591 DCR Recurso RFC 8707 RFC 7636 S256 PKCE Notas
Autohospedado (Keycloak) sí (desde 24.x) IdP de referencia para el perfil MCP en esta lección; soporta cada RFC de extremo a extremo.
SSO Empresarial (Microsoft Entra ID) sí (niveles premium) La disponibilidad de DCR varía según el nivel del inquilino (tenant); verifique en el inquilino de destino antes de desplegar.
SSO Empresarial (Okta) sí (Okta CIC / Auth0) DCR disponible en Auth0 (ahora Okta CIC); las organizaciones de Okta clásicas requieren un registro previo por parte del administrador.
IdPs de inicio de sesión social (genérico) varía rara vez rara vez La mayoría de los IdPs sociales tratan a los clientes como socios estáticos; no confíe en DCR. Úselo solo como fuente de identidad, superponga su propio servidor de autorización compatible con MCP.
Personalizado / propio depende depende depende depende Si distribuye el suyo propio, distribuye el perfil completo. Saltarse cualquiera de las cuatro RFC anteriores rompe el contrato de autenticación de MCP.

Regra de rechazo para el manifiesto de despliegue: si el IdP elegido no devuelve registration_endpoint y no incluye S256 en code_challenge_methods_supported, el servidor MCP se niega a arrancar. No hay modo degradado.

Patrón de rotación de JWKS con iii

El modo de fallo en producción es una caché JWKS desactualizada. Resuélvalo con un trigger cron y una caché state::*:

iii.registerTrigger(
    "cron",
    {"schedule": "0 */6 * * *", "name": "auth::jwks-refresh"},
    "auth::rotate-jwks",
)

Cada seis horas, el trigger cron llama a auth::rotate-jwks, que recupera <issuer>/.well-known/jwks.json y escribe en state::set("auth/jwks/<issuer>", {keys, fetched_at}). El validador lee de state::get. Un token cuyo kid falta en la caché activa una llamada síncrona a auth::rotate-jwks como contingencia (fallback). Esto maneja dos casos a la vez: rotación programada (cron) y ventanas de superposición de claves (fallback síncrono).

La estructura del estado:

{
  "auth/jwks/https://auth.example.com": {
    "keys": [
      {"kid": "k_2026_03", "kty": "RSA", "n": "...", "e": "AQAB", "alg": "RS256", "use": "sig"},
      {"kid": "k_2026_04", "kty": "RSA", "n": "...", "e": "AQAB", "alg": "RS256", "use": "sig"}
    ],
    "fetched_at": 1772668800
  }
}

Dos claves a la vez es el estado estable. Los servidores de autorización rotan introduciendo la siguiente clave (k_2026_04) antes de retirar la anterior (k_2026_03), de modo que los tokens emitidos con la clave anterior sigan siendo válidos hasta que expiren. La caché contiene la unión; el validador elige por kid.

Cableado de primitivas iii (la parte de la que trata realmente esta lección)

Cinco primitivas componen la superficie de autenticación:

# 1. Documento de metadados de la RFC 8414
iii.registerTrigger(
    "http",
    {"path": "/.well-known/oauth-authorization-server", "method": "GET"},
    "auth::serve-asm",
)

# 2. Registro dinámico de clientes de la RFC 7591
iii.registerTrigger(
    "http",
    {"path": "/register", "method": "POST"},
    "auth::register-client",
)

# 3. Validación de JWT como una función invocable (el servidor de recursos la activa)
iii.registerFunction("auth::validate-jwt", validate_jwt_handler)

# 4. Emisión de step-up para alcance incremental (SEP-835 de la L16)
iii.registerFunction("auth::issue-step-up", issue_step_up_handler)

# 5. Rotación de JWKS controlada por cron
iii.registerTrigger(
    "cron",
    {"schedule": "0 */6 * * *"},
    "auth::rotate-jwks",
)
iii.registerFunction("auth::rotate-jwks", rotate_jwks_handler)

El propio servidor MCP nunca llama a la validación directamente. Hace:

result = iii.trigger("auth::validate-jwt", {"token": bearer_token, "resource": self.resource})
if not result["valid"]:
    return {"status": 401, "WWW-Authenticate": result["www_authenticate"]}

Esta indirección es la apuesta de iii. Mañana puede cambiar el validador por una distribución (fanout) que consulte dos IdPs en paralelo, o añadir un emisor de span, o almacenar en caché las validaciones positivas. El servidor MCP no cambia.

Recorrido del diputado confuso con vinculación de audiencia

El Servidor A (notes.example.com) y el Servidor B (tasks.example.com) se registran en el mismo servidor de autorización. El Servidor A está comprometido. El atacante toma el token de notas de un usuario y lo replica en el Servidor B.

Validador del Servidor B:

  1. Decodifica el JWT, recupera el JWKS por kid, verifica la firma.
  2. Compara iss con la lista authorization_servers de los metadados de su recurso protegido. (Pasa — mismo IdP.)
  3. Compara aud == "https://tasks.example.com". (Falla — el aud del token es https://notes.example.com.)
  4. Devuelve 401 con WWW-Authenticate: Bearer error="invalid_token", error_description="audience mismatch".

La declaración de audiencia es la única defensa contra este ataque en la capa de protocolo. Saltársela por rendimiento es el error de producción más común; el validador debe ejecutarse en cada solicitud, no solo al inicio de la sesión.

Modos de fallo

  • JWKS desactualizado. El validador rechaza tokens válidos después de la rotación de claves. La solución es el patrón cron+fallback anterior. Nunca almacene en caché el JWKS sin una tarea de actualización.
  • Falta de declaración aud. Algunos IdPs omiten aud por defecto a menos que se incluya resource en la solicitud de token. El validador debe rechazar los tokens a los que les falte aud, no tratar su ausencia como un comodín.
  • Carrera de actualización de alcance (scope). Dos flujos de step-up concurrentes para el mismo usuario pueden tener éxito y producir dos tokens de acceso con diferentes alcances. El validador debe usar el token presentado en la solicitud, no buscar "el alcance actual del usuario" (eso crea una ventana TOCTOU).
  • Robo de token de registro. Un registration_access_token filtrado permite al atacante reescribir las URIs de redireccionamiento. Almacene el hash de estos en reposo; requiera que el cliente presente el texto plano en cada actualización; rótelos ante cualquier sospecha.
  • iss no fijado. Un validador que acepte cualquier iss permite a un atacante levantar su propio servidor de autorización, registrar un cliente para la audiencia de destino y emitir tokens. La lista authorization_servers de los metadados de recursos protegidos es la lista de permitidos; aplíquela.

Use It

code/main.py recorre todo el flujo de producción con Python estándar y un pequeño registro iii_mock que imita a iii.registerFunction, iii.registerTrigger, iii.trigger y state::set/get. El flujo:

  1. El servidor de autorización publica los metadados de la RFC 8414 en /.well-known/oauth-authorization-server.
  2. El cliente MCP llama al endpoint de metadados y descubre el endpoint de registro.
  3. El cliente MCP envía una solicitud a /register (RFC 7591) y recibe un client_id.
  4. El cliente MCP ejecuta el flujo de código de autorización protegido por PKCE (RFC 7636) con el indicador de recurso resource (RFC 8707).
  5. El cliente MCP llama a una herramienta en el servidor MCP con Authorization: Bearer ....
  6. El servidor MCP activa auth::validate-jwt, que lee el JWKS de state::get.
  7. El trigger cron dispara auth::rotate-jwks, reemplazando el JWKS en el estado.
  8. La siguiente llamada se valida contra las nuevas claves sin necesidad de reiniciar.
  9. Un intento de diputado confuso contra un recurso MCP diferente recibe 401 con discrepancia de audiencia.

El JWT simulado aquí utiliza HS256 con un secreto compartido (para que la lección se ejecute solo con bibliotecas estándar). La producción utiliza RS256 o EdDSA con el patrón JWKS anterior; la lógica de validación es idéntica en el resto de los aspectos.

Ship It

Esta lección produce outputs/skill-mcp-auth-iii.md. Dada una configuración del servidor MCP y un conjunto de capacidades del IdP, la habilidad emite las primitivas iii para registrar, el cronograma de rotación de JWKS, el mapeo de alcances y las reglas de rechazo a aplicar cuando el IdP no soporte el perfil RFC completo.

Ejercicios

  1. Ejecute code/main.py. Rastree el flujo de 9 pasos. Observe dónde state::get devuelve datos desactualizados inmediatamente antes de que auth::rotate-jwks los sobrescriba, y cómo la siguiente solicitud ahora se valida contra la nueva clave.

  2. Añada un nuevo IdP a la lista authorization_servers de los metadados del recurso protegido. Emita un token firmado por el nuevo IdP y confirme que el validador lo acepta. Emita un token firmado por un IdP que no esté en la lista y confirme que el validador lo rechaza con WWW-Authenticate: Bearer error="invalid_token", error_description="iss not allowed".

  3. Implemente auth::rate-limit como una función iii y llámela desde dentro del trigger HTTP de registro antes de que se ejecute el registrador. Utilice un token-bucket por IP de origen guardado en state::set("auth/ratelimit/<ip>", ...).

  4. Lea la RFC 7591 e identifique dos campos que el controlador /register de la lección no valida. Añada la validación. (Pista: software_statement y el esquema de URI de redirect_uris).

  5. Lea la sección de autorización de la especificación de MCP del 25-11-2025. Encuentre el único requisito normativo sobre las cabeceras WWW-Authenticate que el validador de la lección no emite actualmente. Añádalo.

Key Terms

Término Lo que dice la gente Lo que realmente significa
ASM "Documento de metadados de OAuth" JSON del documento de la RFC 8414 en /.well-known/oauth-authorization-server
DCR "Registro de clientes en autoservicio" Flujo POST /register de la RFC 7591
JWKS "Claves públicas para validación de JWT" JSON Web Key Set, recuperado de jwks_uri, indexado por kid
Indicador de recurso "Parámetro de audiencia" Parámetro resource de la RFC 8707 que vincula el token a un solo servidor
Declaración aud "Audiencia" Declaración de JWT que el validador compara con la URL de recurso canónica
Diputado confuso "Replicación de token" Ataque en el que un token emitido para el Servidor A se presenta al Servidor B
Lista de permitidos de iss "Servidores de autorización de confianza" El conjunto nombrado en authorization_servers de los metadados del recurso protegido
Rotación de claves "JWKS rotativo" Reemplazo periódico de claves de firma con ventanas de suposición
Cliente público "Cliente nativo o de navegador" Cliente OAuth sin client_secret; PKCE compensa la falta de este
WWW-Authenticate "Cabecera de respuesta 401/403" Lleva directivas Bearer error=... que guían la recuperación del cliente

Lecturas Adicionales

0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).