AI Agents & Skills

Securing remote MCP servers: OAuth 2.1, CIMD and fatal mistakes

TL;DR

An MCP server over Streamable HTTP is a public API for agents. The spec requires Origin validation, OAuth 2.1 with PKCE and the resource parameter, accepting only tokens issued for the server itself, and never passing tokens through to downstream APIs.

An MCP server running over stdio on your machine is fairly simple: the client launches it as a subprocess and credentials come from environment variables. But once you put a server on the internet for your team or customers (the Streamable HTTP transport), you're running a public API that AI agents call into. The MCP spec defines quite strictly how to authenticate with OAuth 2.1 and what transport security is required. Skipping these opens the door to token leaks and "confused deputy" attacks.

Transport security: three requirements first

The spec warns explicitly when implementing Streamable HTTP:

  1. Validate the Origin header on every connection to prevent DNS rebinding attacks.
  2. When running locally, bind to 127.0.0.1, not 0.0.0.0.
  3. Authenticate every connection.

Without these layers, a malicious website can use DNS rebinding to call an MCP server running on your machine.

Three roles in the OAuth flow

RoleIn MCP
Resource serverThe MCP server: receives and validates access tokens
ClientThe MCP client (Claude, an IDE, an agent): requests tokens on the user's behalf
Authorization serverWhere the user signs in, consents and gets tokens (can be your company's existing IdP)

The flow step by step

  1. The client calls the MCP server without a token → the server returns 401 with a WWW-Authenticate header pointing to metadata.
  2. The client reads the Protected Resource Metadata (RFC 9728) at /.well-known/oauth-protected-resource to find the authorization server.
  3. The client reads the authorization server's metadata (RFC 8414).
  4. The client registers (see below), creates a PKCE pair, and opens a browser for the user to sign in.
  5. The client exchanges the code for a token, always including the resource parameter (RFC 8707) set to the MCP server's canonical URI.
  6. Every subsequent request sends Authorization: Bearer …; tokens never go in the query string.

Client registration: CIMD instead of DCR

MCP clients and servers usually have no prior relationship. The 2026-07-28 spec sets a priority order:

  1. Pre-registered client information (if available).
  2. Client ID Metadata Documents (CIMD): the client_id is itself an HTTPS URL pointing to a JSON file describing the client (name, redirect URIs…). The authorization server fetches and validates it.
  3. Dynamic Client Registration (RFC 7591): now deprecated, kept only for backwards compatibility.
  4. Only then, ask the user to enter client details.
{
  "client_id": "https://app.example.com/oauth/client-metadata.json",
  "client_name": "Example MCP Client",
  "redirect_uris": ["http://127.0.0.1:3000/callback"],
  "grant_types": ["authorization_code"],
  "token_endpoint_auth_method": "none"
}

Fatal security mistakes

MistakeConsequenceWhat the spec requires
Not checking the token audienceTokens issued for other services get into your serverAccept only tokens issued specifically for this server
Token passthrough: forwarding the client's token to downstream APIsConfused deputy: the downstream API trusts it wrongly, privileges are exceededForbidden. Calls downstream must use a separate token issued by that API's authorization server
Loose redirect URIsAuthorization codes get sent to phishing sitesExact matching against registered redirect URIs; use the state parameter
Long-lived tokens stored unencryptedOne leak means permanent accessShort-lived tokens, refresh token rotation for public clients

Risks and caveats

  • Stdio doesn't use this flow: the spec says stdio servers should get credentials from the environment. Don't invent OAuth for local servers.
  • Don't write your own authorization server: use an existing IdP (Auth0, Keycloak, Entra ID, Cognito…) that supports the RFCs above.
  • OAuth only proves "who": you still need per-tool authorisation (403 when scopes are missing) and prompt injection defences at the data layer.

How to apply it

  • Dev: Use the auth support built into the MCP SDK, check audience in middleware, and test the sign-in flow with MCP Inspector before release.
  • Tech Lead: Write a review checklist for every remote MCP server: Origin, binding, audience, no passthrough, PKCE, redirect URIs, token lifetime.
  • PM/Security: Put remote MCP servers through the same security review as public APIs, even when they're "internal only".

Sources & references

  1. Transports — Model Context Protocol
  2. Authorization — Model Context Protocol
  3. Client Registration (2026-07-28) — Model Context Protocol