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:
- Validate the
Originheader on every connection to prevent DNS rebinding attacks. - When running locally, bind to
127.0.0.1, not0.0.0.0. - 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
| Role | In MCP |
|---|---|
| Resource server | The MCP server: receives and validates access tokens |
| Client | The MCP client (Claude, an IDE, an agent): requests tokens on the user's behalf |
| Authorization server | Where the user signs in, consents and gets tokens (can be your company's existing IdP) |
The flow step by step
- The client calls the MCP server without a token → the server returns 401 with a
WWW-Authenticateheader pointing to metadata. - The client reads the Protected Resource Metadata (RFC 9728) at
/.well-known/oauth-protected-resourceto find the authorization server. - The client reads the authorization server's metadata (RFC 8414).
- The client registers (see below), creates a PKCE pair, and opens a browser for the user to sign in.
- The client exchanges the code for a token, always including the
resourceparameter (RFC 8707) set to the MCP server's canonical URI. - 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:
- Pre-registered client information (if available).
- Client ID Metadata Documents (CIMD): the
client_idis itself an HTTPS URL pointing to a JSON file describing the client (name, redirect URIs…). The authorization server fetches and validates it. - Dynamic Client Registration (RFC 7591): now deprecated, kept only for backwards compatibility.
- 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
| Mistake | Consequence | What the spec requires |
|---|---|---|
| Not checking the token audience | Tokens issued for other services get into your server | Accept only tokens issued specifically for this server |
| Token passthrough: forwarding the client's token to downstream APIs | Confused deputy: the downstream API trusts it wrongly, privileges are exceeded | Forbidden. Calls downstream must use a separate token issued by that API's authorization server |
| Loose redirect URIs | Authorization codes get sent to phishing sites | Exact matching against registered redirect URIs; use the state parameter |
| Long-lived tokens stored unencrypted | One leak means permanent access | Short-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
- Transports — Model Context Protocol
- Authorization — Model Context Protocol
- Client Registration (2026-07-28) — Model Context Protocol