An AI agent at a Fortune 500 recently started reading employee emails. Not because it was designed to — because an MCP server it connected to accepted a token issued for a completely different service. The token’s audience claim said “internal-calendar-api.” The MCP server never checked.

This is the token passthrough problem. And it sits at the center of a security architecture that every team building MCP servers needs to understand.

The Authorization Architecture

MCP does not invent a new auth system. It builds on OAuth 2.1 and a stack of RFC-based standards that have been battle-tested across the identity ecosystem:

Standard What It Does
OAuth 2.1 (draft) Core authorization framework
RFC 8414 Authorization Server Metadata discovery
RFC 7591 Dynamic Client Registration
RFC 9728 Protected Resource Metadata
RFC 8707 Resource Indicators

Authorization is optional for MCP servers. It is not optional if your server touches user data, needs audit trails, exposes APIs that require consent, or operates in enterprise environments. Which is to say: it is not optional for production.

The one exception is local MCP servers using STDIO transport. Those run on the user’s machine and can use environment-based credentials, embedded libraries, or filesystem access patterns. OAuth flows are designed for HTTP-based transports where the server is remotely hosted.

The Six-Step Flow

Every authorized MCP connection follows the same dance:

Step 1: The 401. Client connects. Server responds with 401 Unauthorized and a WWW-Authenticate header pointing to its Protected Resource Metadata (PRM) document.

Step 2: PRM Discovery. Client fetches the PRM document to learn which authorization server protects this resource, what scopes are supported, and other metadata.

Step 3: Authorization Server Discovery. Client fetches the authorization server’s metadata — its authorization endpoint, token endpoint, registration endpoint, and issuer URL. This uses OIDC Discovery or OAuth 2.0 Auth Server Metadata.

Step 4: Client Registration. The client either has pre-registered credentials embedded, or it uses Dynamic Client Registration (DCR) to register itself with the authorization server. If neither is available, the client developer must provide a manual credential entry affordance.

Step 5: User Authorization. Client opens a browser to the authorization endpoint. The user authenticates, consents to the requested scopes, and the authorization server redirects back with an authorization code. The client exchanges this code for an access token and refresh token, following standard OAuth 2.1 authorization code with PKCE.

Step 6: Authenticated Requests. Every subsequent request includes the access token in the Authorization: Bearer header. The MCP server validates the token — typically via introspection against the authorization server — and processes the request if the token is valid and carries the required scopes.

┌──────────────────────────────────────────────────────────────┐
│                    MCP AUTHORIZATION FLOW                     │
│                                                              │
│  MCP Client                                                  │
│      │                                                       │
│      ├──1──> MCP Server                                      │
│      │       └── 401 + resource_metadata URL                 │
│      │                                                       │
│      ├──2──> Fetch PRM Document                              │
│      │       └── authorization_servers, scopes_supported     │
│      │                                                       │
│      ├──3──> Fetch Auth Server Metadata                      │
│      │       └── authorize, token, registration endpoints    │
│      │                                                       │
│      ├──4──> Client Registration (DCR or pre-registered)     │
│      │       └── client_id, redirect_uri                     │
│      │                                                       │
│      ├──5──> User Authorizes (browser + PKCE)                │
│      │       └── access_token, refresh_token                 │
│      │                                                       │
│      └──6──> Authenticated MCP Requests                      │
│              └── Authorization: Bearer <token>               │
└──────────────────────────────────────────────────────────────┘

Token Validation: The Critical Gate

The MCP server does not blindly trust the token. It must validate it. There are two primary approaches:

Token Introspection (RFC 7662): The server calls the authorization server’s introspection endpoint with the token. The authorization server responds with the token’s current status — active or inactive — plus its associated client ID, scopes, audience, expiration, and subject. This is the approach used in the official MCP examples. It requires a network call per token but guarantees current state.

JWT Validation: If the token is a self-contained JWT, the server can validate it locally by checking the signature, issuer, audience, expiration, and scopes. Faster (no network call) but cannot detect revoked tokens until they expire.

The critical validation in both approaches is audience checking. The server must verify that the token’s aud claim matches its own resource URL. This is the single control that prevents token passthrough — the anti-pattern where a server accepts tokens issued for other services and forwards them downstream.

# Audience validation (Python SDK pattern)
def _validate_resource(self, token_data: dict) -> bool:
    aud = token_data.get("aud")
    if isinstance(aud, list):
        return any(self._is_valid_resource(a) for a in aud)
    if isinstance(aud, str):
        return self._is_valid_resource(aud)
    return False

If the audience does not match, the token is rejected. No exceptions. This is not a suggestion — the MCP authorization specification explicitly forbids token passthrough.

The 11 Attack Vectors

The MCP security best practices specification identifies 11 distinct attack classes. Here is every one, with the actual risk and the required countermeasure.

1. Confused Deputy

The highest-severity attack in the spec. Targets MCP proxy servers that bridge MCP clients to third-party APIs.

The proxy uses a static client ID with the third-party auth server. A user authenticates once, and the third-party sets a consent cookie. An attacker then dynamically registers a malicious MCP client with an attacker-controlled redirect_uri, and sends the user a crafted link. The browser’s consent cookie is still valid — the third-party skips the consent screen — and the authorization code flows to the attacker.

Countermeasure: The MCP proxy must implement its own per-client consent screen BEFORE forwarding to the third-party authorization flow. Consent must be stored server-side per client_id, with CSRF protection, clickjacking prevention, and exact redirect URI matching. The OAuth state parameter must be set AFTER consent approval, not before.

2. Token Passthrough

An MCP server accepts tokens not issued for it and forwards them to downstream APIs. This breaks the OAuth trust boundary, circumvents security controls, corrupts audit trails, and enables lateral movement.

Countermeasure: Always validate the audience claim. Reject tokens not explicitly issued for your MCP server. This is not negotiable.

3. Server-Side Request Forgery (SSRF)

A malicious MCP server populates its OAuth metadata URLs with internal addresses — 169.254.169.254 for cloud metadata, 192.168.x.x for internal services, localhost:6379 for local Redis. The client fetches these during discovery.

Countermeasure: Enforce HTTPS in production. Block private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16). Validate redirect targets with the same restrictions. Use egress proxies for server-side deployments. Pin DNS resolution to prevent time-of-check-to-time-of-use attacks.

4. State Handle Hijacking

MCP is stateless at the protocol level. Servers that need cross-request state mint explicit handles (shopping cart IDs, workflow IDs) and receive them as tool arguments. An attacker who obtains or guesses a handle can access another user’s state.

Countermeasure: Generate handles with a CSPRNG. Bind handles server-side to the authenticated user (<user_id>:<handle>). Treat handle possession as NOT authentication. Expire handles.

5. Local MCP Server Compromise

Malicious startup commands embedded in client configuration. Malicious payloads inside server binaries. DNS rebinding against insecure localhost servers.

Countermeasure: Consent dialog before connecting any local server — show the exact command, highlight dangerous patterns. Sandbox execution. For HTTP transports on localhost, require auth tokens or use unix domain sockets.

6. OAuth Authorization URL Injection

A malicious MCP server provides a javascript: URL as its authorization endpoint. The client opens it in a browser. XSS. Or the URL contains shell metacharacters and the client uses cmd.exe or sh to open it. RCE.

Countermeasure: Allowlist http:// and https:// schemes only. Reject javascript:, data:, file:, vbscript:. Never use shell commands to open URLs. Implement CSP headers.

7. stdio Transport Privilege Escalation

In proxy architectures, XSS in the client can escalate to full system compromise. The attacker steals the proxy auth token, makes authenticated requests to the local MCP proxy, and spawns arbitrary commands via stdio.

Countermeasure: Prevent XSS (see #6). Sandbox spawned processes. Restrict file system access. Log all stdio usage. Isolate proxy communication in a separate security context.

8. Mix-Up Attacks

An attacker controlling one authorization server tricks the client into sending it an authorization code issued by a different, honest authorization server.

Countermeasure: Authorization Response Validation — bind the response to the authorization server recorded before redirecting. PKCE alone does NOT prevent this. Resource indicators do not help either. The mitigation depends on honest servers emitting the iss parameter.

9. Localhost Redirect URI Impersonation

An attacker claims to be any legitimate client by providing the client’s metadata URL as client_id and binding to any localhost port as redirect_uri. The server sees the legitimate metadata. The user sees the legitimate name. But the code goes to the attacker.

Countermeasure: Authorization servers should display additional warnings for localhost-only redirect URIs and clearly show the redirect URI hostname during the consent screen.

10. CIMD Trust Policy Exploitation

Authorization servers accepting Client ID Metadata Documents (CIMDs) need domain-based trust policies. Without them, any domain can register as any client identity.

Countermeasure: Implement allowlists for trusted domains. Reputation checks for unknown domains. Domain age and certificate validation. Display CIMDs prominently during consent to prevent phishing.

11. Scope Inflation

Broad scopes (files:*, db:*, admin:*) granted up front. A stolen token enables lateral access across unrelated tools and resources. Revocation disrupts everything.

Countermeasure: Progressive, least-privilege scope model. Start with minimal scopes (mcp:tools-basic). Escalate via targeted WWW-Authenticate scope challenges when privileged operations are first attempted. Server emits precise scope challenges, not the full catalog. Client accumulates the union of all previously granted and newly requested scopes.

Implementation Patterns by Language

The official MCP SDKs provide built-in authorization support:

TypeScript: Use mcpAuthMetadataRouter() to serve the PRM document and OAuth metadata. Use requireBearerAuth() as middleware with a custom tokenVerifier that calls the introspection endpoint. The StreamableHTTPServerTransport handles session management.

Python: Use the MCPServer class with AuthSettings and a custom TokenVerifier implementation. The server publishes PRM automatically, returns 401 with proper headers, and hands every bearer token to your verifier.

C#: Use ASP.NET Core’s builder pattern with AddJwtBearer() for JWT validation and .AddMcp() for the MCP-specific auth metadata. MapMcp().RequireAuthorization() protects all endpoints.

All three approaches share the same architecture: metadata discovery endpoints, token verification middleware, and audience validation. The SDK handles the protocol-level details. You implement the token verifier and configure the authorization server connection.

The Security Posture Checklist

For teams deploying MCP servers in production:

  • Token validation via introspection or JWT verification — never skip this
  • Audience claim matches your server’s resource URL
  • HTTPS enforced for all OAuth endpoints in production
  • Private IP ranges blocked for SSRF prevention
  • Authorization URLs validated (scheme allowlist, no shell execution)
  • Scopes are minimal and progressive, not broad and upfront
  • Tokens stored in encrypted storage with eviction policies
  • Credentials never logged (scrub Authorization headers, tokens, secrets)
  • Proper 401 challenges with WWW-Authenticate, realm, and resource_metadata
  • DCR restricted to trusted hosts or disabled in favor of pre-registration
  • Single issuer/tenant pinned — reject tokens from other realms
  • Generic error messages to clients; detailed logs with correlation IDs internally
  • Mcp-Session-Id treated as untrusted input; never tied to authorization

The Bottom Line

MCP’s authorization model is not complex. It is OAuth 2.1 with metadata discovery. What is complex is the attack surface created by the intersection of dynamic clients, static proxy identities, multi-tenant auth servers, and LLM-driven tool invocation.

The confused deputy attack is the one that should keep you up at night. It does not require breaking cryptography or exploiting zero-days. It requires a browser cookie and a cleverly crafted link.

The fix is not more sophisticated auth. It is more disciplined consent: per-client, per-scope, with exact redirect URI matching and state parameter validation. Build the consent screen into your MCP proxy before it reaches the third-party auth flow. Validate every token’s audience. Block private IP ranges in your discovery requests. And never, ever pass tokens through without checking who they were issued for.

The spec gives you the framework. The attacks give you the motivation. The checklist gives you the path. Build accordingly.


Source: MCP Authorization Specification and Security Best Practices — MCP 2026-07-28 revision.

Research notes: [[MCP Authorization and Security Best Practices 2026]]