All field notes

MCP Authentication: A Complete Guide

How auth actually works in MCP. The four real patterns (none, shared secret, per-user token, OAuth), what each is good for, and the security mistakes that keep showing up.

On this page8 sections
  1. 01The four auth patterns in the wild
  2. 02Pattern 1: no auth (local stdio)
  3. 03Pattern 2: shared service token
  4. 04Pattern 3: per-user token
  5. 05Pattern 4: OAuth 2.1 + PKCE
  6. 06Multi-tenant gotchas
  7. 07Don't trust the model with secrets
  8. 08Test your auth before you ship

The MCP spec doesn't mandate an auth model. It defines the wire protocol and leaves auth to you. That sounds reasonable until you ship and realize you have to make four decisions you didn't know you had.

This is a guide to those four decisions, the real-world patterns most teams pick, and the security mistakes I keep seeing in production MCP servers.

The four auth patterns in the wild

Almost every production MCP server fits into one of these:

Pattern Server type Best for
No auth Local stdio, single user Personal tools, dev rigs
Shared service token Local stdio or hosted Internal tools where every user has the same access
Per-user token Hosted HTTP/SSE SaaS products, multi-tenant tools
OAuth 2.1 + PKCE Hosted HTTP/SSE Public servers consumers connect to from any client

Pick wrong here and you'll either be building unnecessary plumbing or shipping a security hole.

Pattern 1: no auth (local stdio)

The MCP server runs as a child process of the model client. The OS process boundary is your auth: if you can run the server, you can use it.

This is fine for:

  • Personal scripts ("read my Obsidian vault")
  • Dev tools you alone use
  • Anything that only touches your own data on your own machine

It is not fine for:

  • Anything that calls a third-party API with a stored token (you've turned the model into an unauthenticated proxy to that API)
  • Anything other people on your machine could exploit (think shared Macs)

Mistake to avoid. Storing API keys in your claude_desktop_config.json. That file has been screenshotted in support tickets, copied into Slack, and committed to git more times than I can count.

Pattern 2: shared service token

The MCP server holds one token (env var, secrets manager) and acts as a single principal. Every user of the server gets the same access.

Right for:

  • Internal company tools where any employee can use the action
  • Read-only public-data servers (a server that calls the GitHub public API on your behalf)

Wrong for:

  • SaaS where customer A must not see customer B's data
  • Anything per-user audited

Implementation tip. Don't put the token in the MCP config. Read it from your OS keychain or a secrets manager. The MCP server config should be free of secrets so it can be checked into git.

import { exec } from "node:child_process";
const token = await new Promise<string>((res, rej) =>
  exec("security find-generic-password -s preman -w", (e, out) =>
    e ? rej(e) : res(out.trim())
  )
);

Pattern 3: per-user token

The hosted MCP server requires every request to carry a token that identifies the calling user. Conceptually identical to Bearer auth on a REST API.

Two ways the token gets to the server:

A. Header passthrough. The MCP client (Claude Desktop, Cursor) holds the token and adds it to every request. The user pastes the token into client config once.

B. Cookie / session. The user authenticates in a browser flow once, gets a session cookie, and the client sends it.

Right for:

  • SaaS products where each user has their own data
  • Hosted servers exposed over HTTPS

Wrong for:

  • Local stdio (overkill)
  • Public servers where you want zero-config UX (use OAuth instead)

Mistake to avoid. Letting tools accept a user_id parameter from the model. The model will fill it in, the model can be tricked into filling in someone else's id, and now your authorization is "trust the LLM." Always derive the user from the auth header server-side, never from a tool argument.

Pattern 4: OAuth 2.1 + PKCE

The official MCP spec recommends OAuth 2.1 for hosted servers. The flow:

  1. User clicks "Connect" in their MCP client (e.g. Cursor)
  2. Client opens your authorization URL in the browser
  3. User logs in and consents
  4. Authorization server redirects back with a code
  5. Client exchanges code + PKCE verifier for an access token
  6. Client stores the token and sends it as Authorization: Bearer <token> on every MCP request

Right for:

  • Public hosted MCP servers
  • Anything where you want users to connect once, in a browser, without copying tokens

Wrong for:

  • Local stdio servers (no browser, no callback URL)
  • Internal tools where shared service token is good enough

Implementation tips.

  • Support PKCE, not just confidential clients. MCP clients are public clients.
  • Use refresh tokens with rotation. Access tokens should be short-lived (1 hour is reasonable).
  • Scope tokens. A read-only MCP scope is much easier to defend than "all access."
  • Document the metadata endpoint (/.well-known/oauth-authorization-server). MCP clients use it for discovery.

Multi-tenant gotchas

Once you have per-user auth, three things tend to bite:

1. Tool results leak across users. A tool returns "the user's recent orders." If you cached the response by tool name + arguments, two users with the same arguments see each other's data. Cache key must include user id.

2. Logs leak tokens. MCP servers love to log the full request. Strip Authorization headers from logs at the framework level, not at each call site.

3. Rate limits leak existence. A 429 with the message "user X exceeded their quota" tells an attacker user X exists. Use generic 429 bodies.

Don't trust the model with secrets

A pattern I keep seeing: a tool returns the user's API key in its response, the model includes it in a chat message, the user pastes the chat into a public Slack. Game over.

Two rules:

  • Tools should never return raw credentials in their output.
  • If a tool legitimately needs to surface a token (e.g. a "rotate API key" tool), make the response minimal and the description clear: "Returns a new API key. Do not include the key in your response to the user; instruct the user to retrieve it from their dashboard."

The model will follow that instruction more often than you'd expect. Not always. So also redact server-side as a fallback.

Test your auth before you ship

Auth bugs are the worst kind to find in production. Run these tests against any MCP server before shipping:

  1. Call every tool with no token. Expect 401 across the board.
  2. Call every tool with an expired token. Expect 401, not 500.
  3. Call every tool with a token from user A while passing arguments that reference user B's data. Expect 403 or empty results, never user B's data.
  4. Replay a captured request 24 hours later. Expect token expiry to kick in.
  5. Run the OAuth flow in three different MCP clients. Confirm token format and refresh both work.

PreMan can run all five as part of a saved test suite. Set the auth header to a known-bad value and watch every tool fail uniformly.

→ Test your MCP auth flow in PreMan

Bring the loop to your API

Catch the regression. Open a verified fix.

Join the waitlist to see which users a release may affect, monitor endpoints in production, and prepare a reviewable fix PR when something breaks.