Why architecture matters here

Why does MCP need its own authorization story at all — isn't OAuth solved? Because agents break OAuth's central casting. Classic OAuth assumes a known application, registered ahead of time with each API it uses, driving a user through consent for a fixed scope set. Agent hosts invert every assumption: a user can point their agent at any MCP server discovered at runtime (no pre-registration), the 'application' is a general-purpose host acting across many servers (client identity must be established dynamically), and the actions are initiated by a model whose requests the user doesn't individually review (consent and scoping must happen up front, and be enforceable server-side). MCP's adoption of dynamic client registration, discovery metadata, and mandatory PKCE is precisely the subset of the OAuth ecosystem that makes runtime-discovered, user-delegated access workable rather than terrifying.

The security stakes are agent-specific. An MCP server aggregates capabilities — read email, query the CRM, file tickets — and the agent wielding it is susceptible to prompt injection: hostile content in a fetched document instructing it to exfiltrate. Authorization architecture is the blast-radius control: if the token presented to the email server is scoped to read-only and audience-bound to that server alone, injection can read but not send, and can't replay the token against the CRM server. Every scoping and binding decision is a wall the worst day crashes into — which is why 'just use a long-lived admin API key in an env var', the local-server habit, is the exact pattern the remote spec exists to end.

The confused-deputy hazard deserves its own paragraph because MCP servers are structurally deputies: trusted by downstream APIs, commanded by untrusted-ish agents. Token passthrough — forwarding the client's token downstream — collapses the chain's audit and revocation boundaries and lets a token minted for one context act in another; the spec's prohibition, and its push toward token exchange for downstream hops, is the difference between a chain of accountable identities and a relay of ambient authority. Teams building MCP servers get this right at design time or explain it to an incident reviewer later.

Advertisement

The architecture: every piece explained

Top row: the cast. The MCP client lives inside a host (IDE, chat app, agent runtime) and speaks MCP to servers on the user's behalf. A remote MCP server is an HTTP service (streamable HTTP transport) exposing tools/resources/prompts — and, in OAuth terms, a resource server: every request must carry a valid access token, validated for signature, expiry, and — critically — audience (this token was minted for this server). The authorization server issues those tokens; it may be embedded in the MCP server's domain or an external IdP (corporate SSO, a hosted identity provider) — MCP servers declare which via metadata. Protected resources are whatever lies behind: SaaS APIs, databases, internal services.

Middle row: the choreography. Discovery: a client hitting a server without a token receives 401 plus WWW-Authenticate pointing at protected-resource metadata (RFC 9728), which names the authorization server(s); the client then fetches the AS's own metadata (endpoints, capabilities). Dynamic client registration (RFC 7591) lets the client obtain a client ID on the spot — no human pre-registration with each of thousands of servers. The PKCE authorization-code flow is the consent moment: the host opens the user's browser to the AS; the user authenticates (their SSO, their passkeys) and sees a consent screen naming the client and requested scopes; the code returns via redirect and is exchanged — with the PKCE verifier, mandatory in OAuth 2.1 — for tokens. Access tokens come back audience-bound and scoped; refresh tokens (where granted) let the host maintain access without re-consent, subject to rotation and revocation.

Bottom rows: the two trust worlds and the far side. Local stdio servers skip all of this by design: launched by the host as a subprocess, they inherit OS-level trust, and their credentials (API keys in env/config) are the user's own — appropriate for personal tooling, and the reason the spec scopes OAuth to HTTP transports. Downstream auth: when the MCP server calls the APIs behind it, it must not passthrough the inbound token; patterns are token exchange (RFC 8693 — trade the user-context token for a downstream-audience token), the server's own service identity where user context isn't needed, or per-user linked credentials stored server-side. The ops strip carries the daily grind: minimal scope design, consent screens that humans can actually evaluate, token lifetimes and rotation policy, and audit logs that answer 'which user's agent did this, under which consent'.

MCP authorization — OAuth 2.1 between clients and remote serversagents act on behalf of users, provablyMCP client (host)agent app / IDEMCP serverOAuth resource serverAuthorization serverissues tokensProtected resourcesAPIs, data, toolsDiscoveryprotected resource metadataDynamic registrationclients without preconfigPKCE flowuser consent + code exchangeAccess tokensaudience-bound, scopedLocal serversstdio: OS trust, env credsDownstream authtoken exchange, no passthroughOps — scope minimization + consent UX + token lifecycle + auditdiscoverregisterauthorizevalidatetrust modelconsentexchangeoperateoperate
MCP authorization: clients discover the server's authorization server, register dynamically, run PKCE consent, and present audience-bound tokens per request.
Advertisement

End-to-end flow

Walk the first-contact flow as a user experiences it. In their agent app, a user adds mcp.acme-crm.com. The client connects; the server answers 401 with resource metadata; the metadata names auth.acme.com as the authorization server. The client fetches AS metadata, registers dynamically (getting a client ID), and constructs the authorization URL with PKCE challenge and the scopes the server's metadata suggests for its capabilities: crm.contacts.read crm.deals.read. The host pops the system browser: the user lands on Acme's real login (their corporate SSO — the agent never sees the password), then a consent screen: 'AgentApp wants to: read contacts, read deals. Not write. Not admin.' Approve → redirect → code exchange with verifier → access token (audience: mcp.acme-crm.com, 1-hour expiry) plus refresh token. The client reconnects with the bearer token; the session initializes; tools appear in the agent.

Now a request: the agent calls the search_contacts tool. The server validates the token (signature via the AS's JWKS, expiry, audience, scope covers the tool), then needs the actual CRM API — and here the passthrough prohibition bites: instead of forwarding the MCP token, it performs a token exchange with the AS, presenting the inbound token and requesting one scoped for api.acme-crm.internal with the same user context. The CRM API sees a properly audienced token carrying the user's identity; its audit log and the MCP server's audit log both attribute the read to this user via AgentApp under this consent. When the access token expires mid-session, the client refreshes silently; when the user later revokes AgentApp from Acme's account page, the refresh token dies and the next request 401s — the agent's access ends where the user's consent ends, provably.

The attack-day version validates the design. A prompt-injected agent tries to exfiltrate by calling a write tool: the token's scopes don't cover it — 403, logged. It tries replaying the token against a different MCP server it also talks to: audience validation rejects it. It convinces the host to request broader scopes: the user sees a new consent screen asking for write access out of nowhere — the human checkpoint the architecture refuses to remove. Damage: contained to exactly what was consented, with an audit trail at every hop.