Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Chapter 18: Authentication Patterns

Authentication becomes a real engineering problem the moment an MCP server stops being a private local helper and starts protecting data or actions that matter. A local stdio server running inside a developer’s shell usually inherits trust from the machine, the user session, and the process boundary. A remote MCP server does not. Once requests cross HTTP, you need an explicit answer to four questions: who is calling, on whose behalf, what resource is the token for, and what the caller is allowed to do after they are authenticated.

MCP is helpful here because it does not invent a custom identity stack. The HTTP authorization model in the current revision (2026-07-28) is built on familiar pieces: OAuth 2.1-style flows, OAuth authorization server metadata (RFC 8414), protected resource metadata (RFC 9728), resource indicators (RFC 8707), authorization server issuer identification (RFC 9207), and OAuth Client ID Metadata Documents for client registration. That is good news for operators because it means the hard parts are known problems with existing libraries, existing failure modes, and existing review practices. It is also bad news for anyone hoping they can ship production auth by stapling a bearer token check onto /mcp and calling it done.

The rule for this chapter: choose the grant by naming the principal that must appear in the audit log. Part VI carries one server throughout, the tickets server at https://mcp.example.com/mcp, with its authorization server at auth.example.com and scopes tickets:read, tickets:write, deployments:read and deployments:write. Two kinds of caller use it: engineers working through an AI client, and a deploy bot that runs on a schedule. This chapter decides how each one authenticates. Chapter 19 implements it.

Start With the Trust Boundary, Not the Credential Format

A common mistake is to start with the credential someone already has. “We have API keys.” “Our platform already uses JWTs.” “The gateway can attach a token.” Those are implementation facts, not design principles.

The first real question is whether your MCP server is local or remote.

For a local stdio MCP server, the trust boundary is usually the local process environment. The client launches the server as a subprocess, sends JSON-RPC over stdin, and receives responses on stdout. The authorization spec explicitly says stdio servers should not follow the HTTP authorization flow and should retrieve credentials from the environment instead. That does not mean local servers are magically safe. It means their security depends on local machine trust, filesystem permissions, environment handling, and process isolation rather than on OAuth redirects and access tokens.

For a remote MCP server, the trust boundary moves to the network. The current Streamable HTTP transport expects one HTTP endpoint and explicitly warns operators to validate Origin, bind locally when appropriate, and implement proper authentication. That warning matters because a remote MCP server is a network service boundary, not “stdio but over HTTP.” Once you expose an MCP server over the network, you must assume requests can come from public clients, semi-trusted first-party apps, internal services, or browsers that should never have been able to reach the server in the first place.

That is why the right starting question is “What kind of caller are we authorizing?” rather than “Do we use API keys or OAuth?”

Name the Principal You Will Audit, Then Pick the Grant

Every deployment falls into one of four rows. The rest of the chapter expands them; Appendix B has each flow as ordered steps.

PrincipalGrantWhat the audit log showsWhen it breaks
The local user, on their own machine (stdio)None over HTTP; credentials from the environmentThe OS user who launched the clientWhen the server is exposed over HTTP “just for a demo” and keeps its local assumptions
A person, through a public or user-facing clientAuthorization Code with PKCE, plus resourcesub = the person, client_id = the app they usedWhen one shared token stands in for many users
A service acting as itselfClient Credentialssub = the serviceWhen the service is quietly acting for a person the log never names
A single first-party client with low riskScoped API keyThe key’s label, nothing finerWhen a second client, a second tenant, or delegated consent arrives

If you are torn between two patterns, ask which principal you need to audit. If the answer is a person, use a user-delegated flow. If the answer is a service, use a service flow. If you cannot answer clearly, your identity model is not ready yet. The same choice is a tree in Appendix L.

The Token Says Who Acted

On the tickets server, an engineer who signs in through an AI client and the deploy bot both present valid tokens for the same audience. Decoded, they tell different stories:

{
  "iss": "https://auth.example.com",
  "sub": "user_8f2c41",
  "client_id": "https://app.example.com/oauth/client-metadata.json",
  "aud": "https://mcp.example.com/mcp",
  "scope": "tickets:read tickets:write",
  "tenant_id": "acme",
  "roles": ["engineer"],
  "exp": 1790596800
}
{
  "iss": "https://auth.example.com",
  "sub": "deploy-bot",
  "client_id": "deploy-bot",
  "aud": "https://mcp.example.com/mcp",
  "scope": "deployments:read deployments:write",
  "tenant_id": "acme",
  "exp": 1790596800
}

The first token came from Authorization Code with PKCE: sub is a person and client_id is the app that person used, so a tickets:write audit line names both. The second came from Client Credentials: sub and client_id are the same service. If the deploy bot ever deletes a deployment because an engineer asked it to, the log will say “deploy-bot did this,” and nothing in the token can tell you who asked. That is the argument of this chapter, in two payloads.

Pattern 1: Authorization Code With PKCE When a Person Acts

If your MCP client is acting on behalf of a human user, Authorization Code with PKCE should be your default answer.

This is the standard fit for remote MCP clients such as chat apps, desktop applications, web apps, editor integrations, and other public clients that cannot safely keep a client secret. The user signs in through the authorization server, approves access, and the client exchanges the authorization code for an access token using a PKCE verifier. MCP then adds one important requirement on top: the client must include a resource parameter so the authorization server knows which protected resource the token is meant for.

That resource parameter sounds small, but it is operationally important. If you skip it, you make it easier to issue tokens that are too broadly usable or ambiguously scoped. RFC 8707 exists because “token for some backend somewhere” is not a good security boundary. An MCP client should request a token for the canonical URI of the MCP server it is trying to reach, such as https://mcp.example.com/mcp.

This pattern is the right fit when the user’s identity actually matters. If your MCP server exposes a user’s files, tickets, documents, billing workspace, repositories, or deployment controls, the server should not be pretending that a shared backend credential is equivalent to that user. Authorization Code with PKCE gives you a clean chain from user authentication to delegated access.

Both official SDKs ship the client and server auth helpers. Use them. Hand-rolled PKCE and metadata parsing is where interop bugs come from.

The main tradeoff is complexity. Browser redirects, callback handling, refresh token storage, session expiration, and consent UX all become part of your product. That is still the right tradeoff when the alternative is flattening all users into one service principal.

Pattern 2: Client Credentials When the Service Is the Actor

Client Credentials is the right pattern when no end user is involved and the calling service itself is the principal.

This pattern works well for internal automation, scheduled jobs, service meshes, backend orchestration, and platform-controlled agents that act as infrastructure rather than as users. The tickets server’s deploy bot, which runs environment audits and rollout previews on a schedule, is a good Client Credentials candidate.

What makes this pattern safe is the honesty of the principal model, not the grant type alone. If the actor is truly a service, then a service token is appropriate. If the actor is secretly standing in for a human, then Client Credentials is often a shortcut that erases the distinction between “the system did this” and “Alice did this through the system.” That distinction matters for audit trails, tenant isolation, and least privilege.

Client Credentials is easy to automate (no browser, no user interaction, no refresh-token UX), which makes it tempting to issue one machine token to a client that serves many users and then reconstruct user identity from headers or request payloads. That is an application-specific impersonation scheme, not delegated authorization.

Client Credentials is strongest when the scopes are narrow, the audience is explicit, and the service identity is meaningful on its own. A token that says “deploy-bot can read service inventory and trigger rollout previews” is defensible. A token that says “web-app can do whatever any signed-in user can do” is usually a design smell.

Pattern 3: API Keys Are a Transitional Convenience

API keys remain useful, but only in narrow situations.

If you control both sides of the integration, the capability surface is small, the risk is low, and you do not need per-user delegation, an API key may be a reasonable first step. For example, a single-tenant internal MCP server that exposes read-only operational data to one known first-party client can start with a scoped API key. It has outgrown the key the day a second client, a second tenant, or a write tool arrives.

API keys are weak in exactly the places MCP servers tend to become interesting:

  • they do not model user delegation well,
  • they are easy to over-share across environments,
  • rotation tends to be manual and painful,
  • and attribution is usually at the client level rather than the user level.

If you use them, put real guardrails around them: narrow permissions, separate keys per environment, clear rotation procedures, and strong logging. Once you need multi-user access, delegated consent, third-party clients, or durable auditability, move to OAuth.

Discovery Is Part of the Security Model

A modern MCP server does not make the client guess how auth works; the server advertises how it wants to be accessed. That matters for security in three ways. It removes brittle manual configuration: clients should not carry hard-coded knowledge of every authorization server and token endpoint they might meet. It reduces mix-ups between the resource server and the authorization server, because the client learns which authorization server is authoritative from the resource itself. And it leaves room for more than one authorization server, which enterprise deployments need during migrations and across identity estates. For registration, the 2026-07-28 revision prefers Client ID Metadata Documents over Dynamic Client Registration. If your security team reviews every redirect URI, pre-register and skip CIMD. Zero-touch onboarding is not worth fighting governance over. Chapter 19 walks the discovery chain, the issuer checks and the registration order request by request.

Failure Modes

Shared service identity pretending to be multi-user auth

One backend token is used for every request, and the application passes the user identity separately in headers or request payloads. That may appear to work until you need reliable tenant isolation, revocation, or auditability. If the authorization server only knows one principal, your logs and controls only know one principal too.

Wrong or inconsistent resource URI

MCP requires clients to use the resource parameter, and tokens should be bound to the correct canonical resource. If your deployment sometimes treats https://mcp.example.com and https://mcp.example.com/mcp as equivalent and sometimes does not, you will create confusing token acquisition and validation failures. Pick a canonical form and use it consistently.

Browser-capable endpoints without origin discipline

The Streamable HTTP transport documentation explicitly warns about Origin validation and local binding. If you expose a local or semi-local MCP endpoint and do not validate Origin, you make browser-based attacks easier than they need to be. Remote auth does not rescue a sloppy transport boundary.

Implementation Checklist

  • Write down, for each caller, the principal the audit log must name.
  • Decide whether the MCP server is local-trust, remote-trust, or mixed.
  • For remote user-facing access, default to Authorization Code with PKCE.
  • For remote service-to-service access, use Client Credentials only when the service is truly the actor.
  • Use API keys only for constrained first-party cases with narrow permissions and a rotation plan.
  • Publish protected resource metadata and return WWW-Authenticate guidance on 401 responses.
  • Separate token validation from tool-level and tenant-level authorization decisions.
  • Test unhappy paths in the MCP Inspector and client SDKs, as well as successful login.

What Comes Next

You now know which principal each caller of the tickets server is, and which grant proves it: engineers through Authorization Code with PKCE, the deploy bot through Client Credentials. Choosing the grant by the principal in the audit log is the easy half. Chapter 19 builds it, in the order the requests cross the wire, from the first 401 to the step-up that asks for deployments:write.