CClaude Cert Prep
ExplainerCCAR-P · P39 min read

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.

AspectLocal (stdio)Remote (HTTP / SSE)
How it runsHost launches server as a local subprocessShared service reached over the network
Trust sourceRuns as the local user; inherits their machine privilegesMust authenticate every caller explicitly
CredentialsRead from the local environment (env vars, config)OAuth 2.1 access token per request
Who can reach itOnly processes on that machineAnyone 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-patternLeast-privilege pattern
One shared API key for all usersPer-user OAuth token, acting as that user
Broad token: read+write everythingNarrow scopes: only what this operation needs
Long-lived standing credentialShort-lived token with refresh, rotated
Same token reused across serversAudience-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.

Authorization at the data layer, not the prompt

The single most important principle, and the one questions test hardest, is where the access-control decision is made. It must be made at the data layer, by code, using the authenticated user's identity, and it must be impossible for the model to override. A system prompt that says 'only show users their own records' is not authorization; it is a suggestion to a component that can be wrong, confused, or injected. Real authorization is a filter in the query and a permission check in the server that the model cannot talk its way past.

code
# Anti-pattern: authorization as a prompt instruction
system = "You are a support agent. Only reveal data belonging\n         to the current user. Never show other accounts."
# A prompt injection or model error can ignore this entirely.

# Correct: authorization enforced at the data layer
user = verify_access_token(request)      # who is really asking
rows = db.query(
    "SELECT * FROM tickets WHERE account_id = %s",
    user.account_id,                     # scoped by the DB, not the model
)
# The server acts AS the user; the model never sees other accounts'
# data, so it cannot leak what it was never given.

The reasoning is the same one behind least privilege everywhere: the model is the component you cannot fully trust under adversarial input, so it must never be the thing enforcing a must-hold access rule. When the MCP server acts as the authenticated user and the database filters by that user's identity, the model simply never receives data the user is not entitled to, and there is nothing for an injection to extract.

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.

All explainers

Independent, unofficial study material from Claude Cert Prep. Not affiliated with Anthropic.