In this article
January 7, 2026
January 7, 2026

How MCP Clients Find Your Auth Server (Without You Telling Them)

The spec evolution that made zero-config MCP connections possible—and closed a category of security holes

The original problem

Early MCP had no standard for authorization. You'd spin up a server, share a URL, and hope whoever connected had the right credentials configured manually.

When the spec added OAuth in March 2025, it created a new problem: how does an MCP client know where to authenticate? Your MCP server lives at mcp.yourapp.com. Your auth server lives at auth.yourapp.com. The client needs to discover the second from the first.

The DCR era (and why it was a security nightmare)

The initial answer was Dynamic Client Registration (RFC 7591). Clients would hit your MCP server, get redirected to register themselves with your auth server, receive credentials, then authenticate.

This worked for demos. It was a security liability in production.

The attack surface DCR creates

Registration endpoint abuse. An open /register endpoint is an invitation. Attackers can register thousands of malicious clients, exhausting database resources or preparing for token-based attacks. Rate limiting helps but doesn't eliminate the problem—you're still accepting registrations from strangers.

SSRF by design. The DCR spec allows clients to provide URLs for logo_uri, jwks_uri, sector_identifier_uri, and others. Your auth server fetches these URLs to validate client metadata. PortSwigger researchers demonstrated these SSRF vectors in ForgeRock OpenAM and MITREid Connect, calling the pattern "SSRF by design."

Once attackers can make your server issue arbitrary outbound requests, the standard SSRF playbook applies: probing internal services, pivoting through your infrastructure, or targeting cloud metadata endpoints.

Client impersonation. Without strong verification, anyone can register a client claiming to be "Claude Desktop" or "Cursor." Users see familiar names in consent screens and approve access. The credential sprawl from dynamic registration makes tracking legitimate vs. malicious clients nearly impossible.

Resource exhaustion. Every registered client creates database records, potentially allocates secrets, and consumes storage. A coordinated registration flood is a trivial denial-of-service vector.

Why enterprise IdPs refused to enable it

Most enterprise identity providers—Okta, Azure AD, Ping—disable DCR by default. Their security teams understand the tradeoffs. Accepting arbitrary client registrations violates the principle of least privilege and creates audit nightmares.

The workaround was pre-registering clients manually. But you can't pre-register every MCP client that might want to connect. The whole point was interoperability.

The metadata discovery fix

The June 2025 spec revision introduced Protected Resource Metadata (RFC 9728). Instead of clients registering with your MCP server, your server simply declares which authorization server handles auth.

Your MCP server implements one metadata endpoint:

app.get('/.well-known/oauth-protected-resource', (req, res) =>
  res.json({
    resource: 'https://mcp.yourapp.com',
    authorization_servers: ['https://your-project.authkit.app'],
    bearer_methods_supported: ['header'],
  }),
);

When a client hits your server without a valid token, it gets a 401 Unauthorized with a WWW-Authenticate header pointing to this metadata. The client fetches /.well-known/oauth-protected-resource, discovers AuthKit as the authorization server, and initiates a standard OAuth flow there.

Your token verification middleware tells clients where to find the metadata:

import { jwtVerify, createRemoteJWKSet } from 'jose';

const JWKS = createRemoteJWKSet(
  new URL('https://your-project.authkit.app/oauth2/jwks')
);

const WWW_AUTHENTICATE_HEADER = [
  'Bearer error="unauthorized"',
  'error_description="Authorization needed"',
  'resource_metadata="https://mcp.yourapp.com/.well-known/oauth-protected-resource"',
].join(', ');

const bearerTokenMiddleware = async (req, res, next) => {
  const token = req.headers.authorization?.match(/^Bearer (.+)$/)?.[1];
  if (!token) {
    return res
      .set('WWW-Authenticate', WWW_AUTHENTICATE_HEADER)
      .status(401)
      .json({ error: 'No token provided.' });
  }

  try {
    const { payload } = await jwtVerify(token, JWKS, {
      issuer: 'https://your-project.authkit.app',
    });
    req.userId = payload.sub;
    next();
  } catch (err) {
    return res
      .set('WWW-Authenticate', WWW_AUTHENTICATE_HEADER)
      .status(401)
      .json({ error: 'Invalid bearer token.' });
  }
};

Your MCP server never handles registration. It just validates tokens.

November 2025: CIMD replaces DCR

The November 2025 spec revision introduced Client ID Metadata Document (CIMD), which flips the trust model entirely.

Instead of clients registering dynamically, they publish their own metadata at a URL they control. The client_id is the URL. The auth server fetches the client's metadata document to verify identity.

No registration endpoint. No credential exchange. No SSRF vectors. The client proves identity by hosting a document at a URL matching their client_id.

This solves the core problem: enterprise IdPs can validate clients without opening registration endpoints to the internet.

Key takeaways

If you're building an MCP server in 2026:

  1. Enable CIMD in your WorkOS Dashboard (under Connect → Configuration)
  2. Implement the /.well-known/oauth-protected-resource endpoint pointing to AuthKit
  3. Use the WWW-Authenticate header with resource_metadata to enable discovery
  4. Keep DCR enabled only for backward compatibility with older clients

The security model is now sane: clients assert identity, auth servers verify it, your MCP server just checks tokens.

Set up MCP Auth with AuthKit →

This site uses cookies to improve your experience. Please accept the use of cookies on this site. You can review our cookie policy here and our privacy policy here. If you choose to refuse, functionality of this site will be limited.