DEV Community

Cover image for OAuth 2.1 para MCP: cómo proteger servidores remotos sin romper los clientes
Khavel
Khavel

Posted on • Originally published at devaisemanal.com

OAuth 2.1 para MCP: cómo proteger servidores remotos sin romper los clientes

Un servidor MCP remoto que lee documentos o ejecuta tools no puede confiar en que el cliente sea conocido. OAuth 2.1 no es un botón de login: es el contrato que descubre identidad, limita scopes y permite validar cada llamada sin convertir la autorización en un prompt.

OAuth 2.1 para MCP es el mecanismo para que un cliente obtenga un token con permiso limitado y un servidor MCP remoto compruebe ese token antes de exponer tools, recursos o acciones. En MCP, el servidor protegido es un resource server; el host del agente es el OAuth client; y tu proveedor de identidad emite los access tokens.

TL;DR

La keyword principal es OAuth 2.1 MCP. La intención es de implementación: publicar Protected Resource Metadata, descubrir el authorization server, usar Authorization Code + PKCE, definir scopes pequeños y validar issuer, audience, expiración y permisos en cada tool call.

Mi postura: si tu MCP remoto puede tocar correo, documentos, datos de cliente o sistemas internos, no intentes resolver identidad con una API key compartida en una variable de entorno. Una key solo identifica una integración; no expresa quién pidió una acción ni qué alcance tenía. OAuth te da un contrato, pero sigues necesitando autorización de negocio en tu backend.

Qué cambió y por qué importa ahora

La revisión MCP 2026-07-28 endurece la autorización y elimina parte de la complejidad de sesiones del protocolo. Para OAuth, el cambio relevante es práctico: la especificación prioriza Client ID Metadata Documents (CIMD) para clientes que no tienen una relación previa con el servidor, deja Dynamic Client Registration como compatibilidad y exige discovery interoperable de metadata OAuth u OpenID Connect.

No conviertas eso en una migración cosmética. Un servidor MCP público no puede suponer que conoce de antemano todos los hosts que se conectarán. El flujo debe permitir discovery sin aceptar redirect URIs arbitrarias, tokens para otra audiencia o scopes enormes porque son más cómodos de configurar.

La propiedad evergreen es que el patrón no depende de un host concreto. Cambiarán SDKs y pantallas de consentimiento; seguirán siendo necesarios un resource identifier estable, metadata verificable, PKCE, token validation y una política de autorización que no viva dentro del modelo.

Diagrama de OAuth para MCP con cliente, servidor MCP protegido, proveedor de identidad y sistema de datos; el flujo muestra challenge, metadata, consentimiento, token, validación de permisos y auditoría

El servidor MCP valida el token antes de ejecutar una tool; el proveedor de identidad autentica y emite credenciales, pero no sustituye la política por tenant o recurso de tu aplicación.

El mapa mental correcto: cuatro roles, dos decisiones

Hay cuatro piezas. El cliente MCP (un host de agente) inicia la conexión y representa al usuario. El servidor MCP es el resource server que protege su endpoint HTTP. El authorization server autentica y emite tokens. Por último, tu API, base de datos o SaaS aguas abajo contiene el recurso real. No confundas el servidor MCP con el identity provider: uno recibe la tool call; el otro decide cómo se obtiene la identidad.

OAuth resuelve la primera decisión: ¿este cliente presenta un token válido para este recurso y con estos scopes? Tu aplicación resuelve la segunda: ¿este usuario de este tenant puede leer este documento o ejecutar esta acción ahora? El scope abre una capacidad general; la autorización de negocio revisa IDs, ownership, rol, estado y consecuencias.

Por ejemplo, tickets:read no autoriza a leer cualquier ticket. Autoriza a intentar la tool de lectura. El handler debe cargar el usuario desde claims verificadas y aplicar el filtro de tenant antes de consultar. Si pasas el user_id que propone el modelo como autoridad, has vuelto a delegar seguridad al prompt.

Flujo OAuth 2.1 de un servidor MCP remoto

  1. El cliente llama al endpoint MCP sin token o con token insuficiente. El servidor devuelve 401 Unauthorized y un WWW-Authenticate: Bearer que apunta a su Protected Resource Metadata (PRM).

  2. El cliente descarga el documento PRM. Ahí descubre el resource que debe aparecer como audiencia, el authorization server permitido y los scopes que el recurso entiende. Si el resource del JSON no coincide exactamente con el recurso pedido, debe rechazarlo.

  3. El cliente descubre los endpoints OAuth u OpenID Connect del issuer, registra su identidad por pre-registro o CIMD cuando esté disponible y abre Authorization Code con PKCE. PKCE evita que otro proceso intercepte y canjee el código de autorización.

  4. Tras consentimiento, el authorization server emite un access token dirigido a tu resource identifier. El cliente repite la llamada MCP con Authorization: Bearer …. El servidor valida firma o introspección, iss, aud, exp, scopes y cualquier claim de tenant antes de despachar una tool.

  5. Cada tool ejecuta autorización propia, registra actor, cliente, tool, recurso y resultado, y devuelve un error de autorización seguro cuando corresponda. No guardes el access token en trazas, mensajes de error ni contenido de tool.

¿Te está sirviendo? Hay una dosis cada semana

Te resumo herramientas de IA para devs, agentes, MCP, seguridad y workflows en un email de 5 minutos. En español y sin ruido.

Suscribirme gratis

Protected Resource Metadata: la pieza que suele faltar

Protected Resource Metadata es un JSON servido por el resource server, no por el proveedor de login. Según RFC 9728 y MCP, publica el identificador del recurso y los authorization servers autorizados. En una ruta MCP como https://mcp.acme.test/remote, el cliente puede buscar https://mcp.acme.test/.well-known/oauth-protected-resource/remote o seguir el resource_metadata del challenge, que debe tener prioridad.

Lo que conviene comprobar

Evita meter aquí una lista fantasiosa de permisos. scopes_supported documenta lo que el servidor puede pedir; el challenge de una request concreta puede exigir un conjunto más preciso y el cliente debe tratar ese challenge como autoridad para ese intento. Es una forma de pedir consentimiento incremental sin entregar admin:* al primer clic.

Un ejemplo mínimo y explícito podría ser el siguiente. Los nombres de scopes son tuyos: diseña verbos y dominios que alguien de seguridad pueda revisar, no copias de los nombres de tools.

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

{
  "resource": "https://mcp.example.com/mcp",
  "authorization_servers": ["https://login.example.com"],
  "scopes_supported": [
    "issues:read",
    "issues:write",
    "deployments:read"
  ],
  "resource_name": "Example engineering MCP"
}
Enter fullscreen mode Exit fullscreen mode

Implementación Node: challenge, metadata y guard de token

El SDK MCP puede encargarse del transporte y del registro de tools, pero el borde HTTP debe seguir devolver metadata y rechazar tokens inválidos antes de llegar al modelo o a los sistemas internos. Este ejemplo usa Express y jose para mostrar el contrato; adapta los endpoints y claims a tu proveedor de identidad. En producción, cachea JWKS respetando sus cabeceras y mantén las URLs de issuer y audiencia en configuración revisada, no en input del usuario.

auth-boundary.mjs

import express from "express";
import { createRemoteJWKSet, jwtVerify } from "jose";

const app = express();
const resource = "https://mcp.example.com/mcp";
const issuer = "https://login.example.com";
const jwks = createRemoteJWKSet(new URL(`${issuer}/.well-known/jwks.json`));

app.get("/.well-known/oauth-protected-resource/mcp", (_req, res) => {
  res.type("application/json").send({
    resource,
    authorization_servers: [issuer],
    scopes_supported: ["issues:read", "issues:write"],
  });
});

async function requireScope(req, res, next) {
  const token = req.get("authorization")?.replace(/^Bearer\s+/i, "");
  if (!token) {
    res.set("WWW-Authenticate",
      `Bearer resource_metadata="${resource.replace("/mcp", "/.well-known/oauth-protected-resource/mcp")}", scope="issues:read"`);
    return res.sendStatus(401);
  }
  try {
    const { payload } = await jwtVerify(token, jwks, { issuer, audience: resource });
    const scopes = String(payload.scope || "").split(" ");
    if (!scopes.includes("issues:read")) return res.sendStatus(403);
    req.actor = { subject: payload.sub, tenant: payload.tenant_id };
    return next();
  } catch {
    return res.sendStatus(401);
  }
}

app.post("/mcp", requireScope, mcpHttpHandler);
Enter fullscreen mode Exit fullscreen mode

No copies este fragmento sin decidir si tu access token es JWT, opaco o ambos. Un token opaco normalmente se valida por introspección contra el authorization server; un JWT se valida contra claves públicas confiables. En ambos casos, la audiencia debe ser el resource identifier de tu MCP, no el nombre genérico de tu producto.

Scopes, audiencia y acciones sensibles

Empieza con scopes legibles y estrechos: repo:read, issues:read, issues:write, deployments:read. No concedas tools:* si solo necesitas una consulta. Los scopes no deben depender de un prompt ni de los argumentos declarados por el LLM; se comprueban en el servidor antes de ejecutar la operación.

Separa lectura de escritura. Para issues:write, añade una aprobación explícita en el host o una confirmación en tu aplicación antes de mutar. Para acciones de alto impacto —borrar, desplegar, cambiar permisos, enviar comunicación externa— utiliza scopes específicos, un segundo control contextual y logs de auditoría. OAuth reduce blast radius; no vuelve segura una tool excesivamente poderosa.

La claim aud es tu defensa contra token replay entre APIs. Un token emitido para https://api.example.com no debe servir para https://mcp.example.com/mcp solo porque comparten issuer. Valida audiencia exacta, no startsWith, no el hostname a ojo, y nunca aceptes una audiencia enviada por el cliente.

CIMD, pre-registro y por qué DCR ya no es la primera opción

El registro de cliente responde a otra pregunta: ¿qué aplicación está pidiendo el token? Si controlas cliente y servidor, el pre-registro de client ID y redirect URIs es simple y robusto. Si esperas hosts desconocidos, MCP prioriza Client ID Metadata Documents: el client ID puede ser una URL HTTPS que publica metadata verificable del cliente.

Dynamic Client Registration puede seguir existiendo por compatibilidad, pero no debería ser el camino que abra registros ilimitados y redirect URIs sin validación. La especificación actual lo coloca como fallback. Trata cada mecanismo como una superficie de seguridad: limita métodos de autenticación, exige URIs exactas y registra el client ID que obtuvo consentimiento.

No prometas soporte universal antes de probar hosts reales. Algunos clientes aún llegarán con capacidades antiguas. Publica claramente qué versiones, mecanismo de registro y scopes admites; ofrece el fallback mínimo sin bajar la validación del token por «compatibilidad».

Errores que rompen autorización MCP

  • Proteger solo la pantalla de consentimiento y dejar /mcp sin validar Authorization en cada request.
  • Aceptar cualquier issuer que aparezca en un JWT o construir el JWKS URL con una claim no confiable.
  • Comprobar firma y expiración, pero omitir audiencia, scopes, tenant y autorización del recurso concreto.
  • Usar una API key global como identidad del usuario y registrar todas las acciones como si las hiciera el servidor.
  • Devolver access tokens, authorization codes, cabeceras Bearer o datos de consentimiento en logs de trazas.
  • Entregar write al conectar el servidor aunque el usuario solo quiera explorar datos de lectura.
  • Confiar en que el modelo no invocará una tool peligrosa si el system prompt dice que tenga cuidado.

Checklist de lanzamiento

El endpoint MCP remoto rechaza sin token y expone WWW-Authenticate con resource_metadata.

La Protected Resource Metadata devuelve resource exacto, authorization server permitido y scopes revisados.

El cliente usa Authorization Code con PKCE y redirecciones registradas de forma exacta.

El servidor valida issuer, firma o introspección, audiencia, expiración y scope por request.

Cada tool aplica control de tenant, rol, propiedad y estado además del scope OAuth.

Lectura y escritura tienen scopes distintos; las mutaciones de impacto tienen aprobación y auditoría.

Tokens, códigos y cabeceras de autorización están redaccionados de logs, errores y trazas.

Hay tests para token de otra audiencia, scope insuficiente, issuer falso, tenant cruzado y request repetida.

Conclusión

La autorización MCP bien hecha no se nota cuando todo va bien: el host descubre la identidad necesaria, obtiene permiso mínimo y la tool funciona. Se nota cuando alguien conecta un cliente nuevo, intenta reutilizar un token contra otro recurso o pide una acción que no le corresponde; ahí el servidor debe fallar de forma predecible y auditable.

Mi recomendación es empezar con un solo recurso remoto y dos scopes de lectura/escritura, no con un catálogo enorme de permisos. Publica PRM, valida aud e iss, aplica autorización de negocio en cada tool y escribe los tests hostiles antes de abrir el servidor a más clientes. Es menos vistoso que una demo de agente, pero es lo que evita convertir MCP en una llave maestra.

Preguntas frecuentes

¿Qué es OAuth 2.1 para MCP?

Es el patrón de autorización que permite a un cliente MCP obtener un access token limitado y a un servidor MCP remoto validarlo antes de exponer tools o recursos protegidos.

¿Necesita OAuth un servidor MCP local por STDIO?

Normalmente no. Un servidor local puede usar credenciales del entorno o de una librería local; OAuth está pensado sobre todo para transportes HTTP remotos donde cliente y servidor no comparten una frontera de confianza.

¿Qué es Protected Resource Metadata en MCP?

Es un documento JSON del servidor MCP que declara el resource identifier, los authorization servers y scopes. El cliente lo descubre desde WWW-Authenticate o una ruta /.well-known/ para iniciar OAuth correctamente.

¿Basta con validar la firma del JWT?

No. También debes validar issuer, audiencia, expiración y scopes, y luego aplicar autorización de negocio por usuario, tenant y recurso concreto en la tool.

¿Debo usar Dynamic Client Registration en MCP?

Puede ser un fallback de compatibilidad. La especificación actual prefiere pre-registro cuando existe relación previa y Client ID Metadata Documents cuando cliente y servidor no se conocen de antemano.

¿OAuth protege contra prompt injection?

No directamente. OAuth limita quién puede invocar capacidades; sigue siendo necesario tratar contenido externo como no confiable, validar argumentos y exigir aprobación para efectos sensibles.

Cómo proteger un servidor MCP remoto con OAuth 2.1

  1. Delimitar recurso. Define una URL HTTPS estable para el endpoint MCP que será la audiencia esperada del token.
  2. Diseñar scopes. Separa lectura, escritura y acciones de alto impacto; evita permisos globales basados en nombres de tools.
  3. Publicar metadata. Sirve Protected Resource Metadata con resource exacto, issuer permitido y scopes soportados.
  4. Emitir challenge. Devuelve 401 con WWW-Authenticate y resource_metadata cuando no haya token o falte scope.
  5. Configurar OAuth. Usa Authorization Code con PKCE, discovery OAuth/OIDC y redirect URIs registrados con coincidencia exacta.
  6. Validar access token. Comprueba firma o introspección, issuer, audiencia, expiración, scopes y claim de tenant en cada llamada.
  7. Autorizar tool. Evalúa usuario, tenant, rol, ID de recurso y estado de negocio antes de llamar a sistemas aguas abajo.
  8. Auditar sin secretos. Registra actor, client ID, tool, recurso y resultado; redacta token, código y cabeceras.
  9. Probar denegaciones. Añade casos de token de otra audiencia, scope insuficiente, issuer falso, tenant cruzado y mutación sin aprobación.

Fuentes y referencias

También te puede interesar

Recibe una lectura semanal de herramientas IA para devs

Cada semana te resumo herramientas de IA para devs, agentes, MCP, seguridad y workflows en un email de 5 minutos. En español y sin ruido.

Suscribirme gratis

Top comments (0)