MCP Authentication & Authorization, Explained
How to secure a Model Context Protocol integration with OAuth and per-user delegated tokens, and why the trap answer hands the server a shared secret or a broad token.
The short answer
Secure MCP integrations by authenticating remote (HTTP) servers with OAuth 2.1, so the client obtains a per-user delegated access token instead of a shared secret. Scope every token tightly and bind it to the specific server as its audience. Never put credentials in prompts or tool arguments, and enforce authorization at the data layer where the server acts as the real user, not in the model's reasoning. The trap is passing one broad, shared token that sees everything.
Model Context Protocol makes it easy to plug a data source or action into Claude, and that ease is exactly where security goes wrong. It is trivial to stand up a remote MCP server, hand it one API key that can read everyone's data, and let every user's requests flow through that single all-seeing identity. It works in the demo and it is a breach waiting to happen, because the server now acts with more authority than any individual user should have, and nothing at the data layer knows who is really asking.
This article explains how to authenticate and authorize MCP integrations properly. We cover the trust difference between local stdio servers and remote HTTP servers, the OAuth 2.1 flow the MCP spec defines for remote servers, tight token scoping and per-user delegation, correct secrets handling, agent-to-agent authentication, and why authorization must be enforced at the data layer rather than in the prompt. The trap to unlearn throughout: a shared secret or a broad token that turns your server into a confused deputy.
The answer, and the trap
Securing an MCP integration comes down to answering, on every request, 'who is really asking, and are they allowed?' The correct pattern is delegated authentication: a remote MCP server sits behind OAuth 2.1, the client obtains an access token on behalf of the signed-in user, and the server acts as that user against the underlying system. Each request carries the user's own bounded authority, and the data layer, not the model, decides what that user may see or do.
The trap, and the wrong answer a question will dangle in front of you, is a shared credential: one long-lived API key or a broad service-account token that the server uses for everybody. That single identity can read and write everything, it cannot be attributed to a real user, and if it leaks, the attacker inherits all of it. Worse, the server becomes a confused deputy that will happily exercise its broad power on behalf of whoever, or whatever injected instruction, is driving the current turn.
Local (stdio) vs remote (HTTP) trust
MCP's authentication model depends entirely on how the server is reached, and the two transports sit on opposite sides of a trust boundary. Getting this distinction right is the foundation for everything else, because the safe pattern for one is dangerous when copied to the other.
| Aspect | Local (stdio) | Remote (HTTP / SSE) |
|---|---|---|
| How it runs | Host launches server as a local subprocess | Shared service reached over the network |
| Trust source | Runs as the local user; inherits their machine privileges | Must authenticate every caller explicitly |
| Credentials | Read from the local environment (env vars, config) | OAuth 2.1 access token per request |
| Who can reach it | Only processes on that machine | Anyone who can route to the endpoint |
The MCP authorization specification makes this explicit: stdio transports should not use the OAuth flow at all and instead retrieve credentials from the environment, while HTTP-based transports should implement the OAuth authorization spec. A local server trusts the user because it literally runs as them on their own machine. A remote server has no such implicit trust, it is exposed to the network and serves many users, so it must authenticate and authorize every single request.
OAuth 2.1 for remote MCP servers
The MCP authorization spec is built on OAuth 2.1 and assigns clear roles. The MCP server is an OAuth 2.1 resource server: it accepts and validates access tokens on protected requests. The MCP client is the OAuth client, making requests on behalf of the resource owner (the user). A separate authorization server interacts with the user, if needed, and issues the tokens. The server advertises its authorization server through Protected Resource Metadata, so the client can discover where to authenticate.
- The client makes an MCP request without a token; the server replies 401 Unauthorized with a WWW-Authenticate header pointing to its resource metadata.
- The client discovers the authorization server, registers or identifies itself, and runs the OAuth flow with PKCE to obtain an access token for the signed-in user.
- Crucially, the client includes a resource parameter (RFC 8707) naming the specific MCP server the token is for, so the token is minted for that audience and no other.
- The client then sends every subsequent request with an Authorization: Bearer header; the token is never placed in the URL query string.
- The server validates the token on every request, including that the token was issued specifically for it as the intended audience, and rejects anything else with a 401.
Token scoping and per-user delegation
Authenticating the caller is only half the job; the other half is making sure the token that results carries the least authority that still gets the work done. Two levers do this: per-user delegation (whose authority the token represents) and scope minimization (how narrow that authority is).
Per-user delegation means the token represents the signed-in user, so the downstream system automatically bounds the request to what that user is permitted to see. There is no shared super-identity to leak and no need for the server to re-implement access control, because the user's own permissions apply. Scope minimization means requesting only the specific scopes the operation needs; the MCP spec has the server advertise required scopes and encourages clients to request the minimum, escalating incrementally through a step-up flow only when a later operation genuinely needs more.
| Anti-pattern | Least-privilege pattern |
|---|---|
| One shared API key for all users | Per-user OAuth token, acting as that user |
| Broad token: read+write everything | Narrow scopes: only what this operation needs |
| Long-lived standing credential | Short-lived token with refresh, rotated |
| Same token reused across servers | Audience-bound token, one server per token |
Secrets handling and agent-to-agent auth
Wherever credentials live, they must never live in the model's context. A prompt, a tool argument, a resource's text, and the conversation history are all things the model reads, can echo, and an injection can try to exfiltrate. The context window is not a vault. Authentication belongs in the transport layer, an Authorization header the model never sees, not in text the model handles.
- Never embed API keys, passwords, or long-lived secrets in prompts, tool arguments, system messages, or resource content.
- Carry credentials in the transport (bearer tokens on the HTTP request), out of band from the model's reasoning entirely.
- Store secrets in a secrets manager or environment, inject them at the server, and keep them off any path the model can read or emit.
- Prefer short-lived, rotatable tokens so a leaked credential expires quickly and unlocks little.
Agent-to-agent and server-to-server calls follow the same discipline rather than inventing a shortcut. When one agent or MCP server calls another on a user's behalf, it should carry a token scoped to that specific downstream service and audience, obtained through the proper flow, not forward whatever broad token it happens to hold. Forwarding a token minted for server A to server B is precisely the confused-deputy / token-passthrough mistake the audience-binding rules exist to prevent. Each hop authenticates as itself with a token intended for the next hop.
Key takeaways
- →Secure remote MCP servers with OAuth 2.1 and per-user delegated tokens; the trap answer is a shared secret or one broad token that sees everything.
- →Trust tracks transport: stdio servers run as the local user and read credentials from the environment; remote HTTP servers must authenticate and authorize every request.
- →In the MCP OAuth model the server is a resource server, the client is the OAuth client, and a separate authorization server issues tokens after user consent.
- →Bind tokens to a specific server as their audience (RFC 8707) and validate that audience on every request, so a server never forwards or accepts a token meant for someone else.
- →Scope tokens to the minimum the operation needs and step up only when required; a read-only server should hold no write scopes.
- →Never put secrets in prompts, tool arguments, or resource text; carry them in the transport, and apply the same discipline to agent-to-agent calls rather than forwarding a broad token.
- →Enforce authorization at the data layer with the authenticated user's identity, never as a prompt instruction the model can be injected into ignoring.
Now practice it
Reading builds recognition; practice builds judgment. Try these on the P3 material.
Frequently asked
How should a remote MCP server authenticate users?
Through OAuth 2.1, as the MCP authorization spec defines. The server acts as an OAuth resource server, the client obtains a per-user access token from a separate authorization server after the user consents, and every request carries that token in an Authorization: Bearer header. The server validates the token, including that it was issued specifically for this server, on each request.
Why is a shared API key for an MCP server a problem?
Because one shared credential means the server acts with the same broad authority for every user, cannot attribute actions to a real person, and hands an attacker everything if it leaks. It also makes the server a confused deputy that exercises its full power for whoever, or whatever injected instruction, is driving the turn. Per-user delegated tokens bound each request to that user's own permissions instead.
Do local stdio MCP servers need OAuth?
No. The MCP authorization spec says stdio transports should not use the OAuth flow and should instead retrieve credentials from the local environment. A stdio server runs as a subprocess of the host under the local user, so it inherits that user's trust. OAuth is for HTTP-based remote servers that are exposed to the network and serve many callers who must be authenticated explicitly.
Where should MCP authorization be enforced?
At the data layer, in deterministic server-side code that uses the authenticated user's identity and cannot be overridden by the model. A system-prompt rule like 'only show users their own data' is not authorization; it is a suggestion an injection can ignore. Filter queries by the user's identity so the model never receives data the user is not entitled to.
How should one MCP server or agent call another securely?
It should obtain a token scoped to the specific downstream service and audience through the proper flow, not forward the broad token it already holds. Forwarding a token minted for one server to a different server is the token-passthrough confused-deputy mistake that audience binding (RFC 8707) exists to prevent. Each hop authenticates as itself with a credential intended for the next hop.
Independent, unofficial study material from Claude Cert Prep. Not affiliated with Anthropic.