MCP authentication patterns: OAuth, tokens, and when to use each
MCP defines an OAuth authorization profile for HTTP transports. Public read-only servers can remain anonymous; protected tools need authentication, authorization and a verified principal. Private partner-token integrations also need an explicit compatibility contract with their MCP clients.
This article anchors to the MCP server development service pillar.
TL;DR
- Read-only over public data with rate limits is the only legitimate anonymous case.
- Scoped API tokens with hashed storage and short TTL cover B2B and headless flows.
- OAuth 2.1 with PKCE covers consumer-facing assistants acting on behalf of a logged-in user.
- Rate limiting attaches to the principal; mutating tools get a tighter bucket.
- Every auth event is logged: issued, used, revoked, failed.
How to choose an MCP authentication method
I run the same five-question check before picking an auth pattern:
- Does any tool mutate state? If yes, anonymous is off the table.
- Does the agent act on behalf of a specific human end user? If yes, OAuth.
- Does the agent act as a B2B integration with no end user attached? If yes, scoped API token.
- Is the data surface reachable on the public internet? If yes, rate limiting is mandatory.
- Are mutating and read-only tools mixed in the same server? If yes, scope the auth per tool, not per server.
The decision tree maps to three patterns:
| Pattern | When to use | Implementation |
|---|---|---|
| Anonymous + IP rate limit | Public catalogue, read-only, bounded load | Worker checks IP bucket only |
| Scoped API token | B2B integration, headless agent runtime, no human end user | Opaque random token, hashed storage, expiry and rotation |
| OAuth 2.1 + PKCE | Consumer agent acting for a logged-in user | Standard authorization code flow |
The patterns are not mutually exclusive. A real production server often runs all three, with each tool tagged with its required scope.
Anonymous read-only MCP access with rate limiting
Expose an allowlisted projection of public catalogue fields. The WooCommerce wc/v3 management API requires server-held credentials and must not be treated as an anonymous public upstream. Use a reviewed public endpoint or a server-side adapter that excludes private fields.
The implementation:
async function checkAnonymousRateLimit(request: Request, env: Env): Promise<boolean> {
const ip = request.headers.get("CF-Connecting-IP") ?? "unknown";
const { success } = await env.AUTH_RATE_LIMIT.limit({ key: `anonymous:${ip}` });
return success;
}60 requests per minute per IP is the default I ship. The KV-based counter is approximate (eventual consistency on KV writes); for tighter limits, Durable Objects or Cloudflare’s native Rate Limiting binding is the right tool.
What this pattern does not cover: any tool that returns user-specific data, any tool that mutates state, any tool that reveals stock for non-public products. Those need a real principal.
Scoped API tokens for B2B MCP integrations
The B2B case: a partner integration ships an agent that calls your MCP server on behalf of the partner organisation, not on behalf of a specific human end user. Examples are an inventory-sync agent at a wholesaler, a marketplace integration at an aggregator, an analytics agent that reports order trends to a BI tool.
The flow:
- An admin in the WordPress backend creates a token with a name, a set of scopes (
catalogue:read,inventory:read,orders:write), and an expiry (default 90 days). - The token is shown once to the admin, then stored as a SHA-256 hash plus the scope set plus the expiry.
- The partner configures their MCP client with the token in an
Authorization: Bearer <token>header. - The Worker hashes the incoming token and looks up the hash in KV. If the hash matches and the expiry is in the future and the requested tool’s required scope is in the token’s scope set, the call proceeds.
async function verifyApiToken(authHeader: string | null, env: Env): Promise<TokenContext | null> {
if (!authHeader?.startsWith("Bearer ")) return null;
const token = authHeader.slice(7);
const hash = await sha256(token);
const record = await env.TOKENS.get(`tok:${hash}`, "json") as TokenRecord | null;
if (!record) return null;
if (record.expiresAt < Date.now()) return null;
return { tokenId: record.id, scopes: record.scopes, principal: record.principal };
}
function requireScope(ctx: TokenContext, scope: string): void {
if (!ctx.scopes.includes(scope)) throw new Error("Insufficient scope");
}Two operational habits keep this pattern honest:
Rotation, not eternity. A token that never expires is a token that ends up in a public GitHub repo six months from now. 90-day default with a renewal flow that overlaps the old and new token by 7 days is the pattern that survives real partners.
Hashed storage, not plaintext. If your KV store is exfiltrated, the hashes are useless without the original token. If you stored plaintext tokens, every partner integration needs immediate rotation. The cost difference at issue time is one sha256 call.
OAuth 2.1 with PKCE for MCP servers
The consumer case: a user opens Claude Desktop, connects their account on your store via OAuth, and asks the agent to “show my last order.” The agent now needs to call your MCP server with credentials that say “this is acting for user 4231.”
OAuth 2.1 (draft-ietf-oauth-v2-1) is the consolidated profile. PKCE (RFC 7636) is mandatory for public clients (which includes desktop assistants without a confidential server-side component).
The flow, in five legs:
- The MCP host launches a browser to your authorization endpoint with
response_type=code,code_challenge=<S256 hash of verifier>, and the requested scopes. - The user logs into your WordPress site (or your authentication provider) and approves the requested scopes.
- Your authorization endpoint redirects back to the host with a one-time authorization code.
- The host exchanges the code plus the original
code_verifierat your token endpoint for an access token (short TTL, 1 hour) and a refresh token (longer TTL, 30 days). - The host calls the MCP server with
Authorization: Bearer <access_token>. The Worker verifies the token’s signature, expiry, and scope set against the called tool.
An OAuth access token can be opaque or a JWT. This example uses a configured issuer and the maintained jose verifier for JWT signature and claims validation:
import { createRemoteJWKSet, jwtVerify } from "jose";
const issuer = "https://identity.example.org";
const audience = "https://mcp.example.org/mcp";
const jwks = createRemoteJWKSet(new URL(issuer + "/.well-known/jwks.json"));
async function verifyJwt(token: string) {
const { payload } = await jwtVerify(token, jwks, {
issuer,
audience,
algorithms: ["RS256"],
requiredClaims: ["sub", "exp"],
});
if (typeof payload.sub !== "string" || typeof payload.scope !== "string") {
throw new Error("Invalid identity or scope claims");
}
return { subject: payload.sub, scopes: payload.scope.split(" ").filter(Boolean) };
}The scopes carried in the JWT mirror the scopes from the scoped-token pattern: catalogue:read, orders:read, orders:write. The user’s WordPress user ID rides in the sub claim, so an orders:read call returns only that user’s orders.
How to combine OAuth, tokens and anonymous access in one MCP server
A real WooCommerce MCP server typically exposes:
catalogue.listandproduct.detail: anonymous + IP rate limit.inventory.check(for partners): scoped token withinventory:read.order.status(for the logged-in user): OAuth withorders:read.order.intent(for the logged-in user): OAuth withorders:write, plus a tighter rate limit.
The Worker pre-dispatch logic walks through the patterns:
Authenticate the HTTP request before MCP dispatch. Then check the scope of the requested tool and ownership of the resource. Reject unknown tools or missing policy entries. Return HTTP bearer challenges for authentication failures rather than inventing string JSON-RPC error codes.
The TOOL_AUTH_REQUIREMENTS map is the single source of truth for which tool needs which auth mode. No tool should be added without an explicit entry.
MCP rate limits per IP, token and user
Anonymous traffic gets a per-IP bucket. Token-authenticated traffic gets a per-token bucket. OAuth traffic gets a per-user bucket. Mutating tools get a tighter ceiling than read tools regardless of principal.
The values below are illustrative configuration examples, not measurements or production defaults. Derive actual ceilings from origin capacity and observed traffic.
| Tool category | Anonymous | Token | OAuth |
|---|---|---|---|
catalogue.* (read) | 60 / minute / IP | 600 / minute / token | 120 / minute / user |
inventory.* (read) | not allowed | 300 / minute / token | not allowed |
order.status (read) | not allowed | 60 / minute / token | 60 / minute / user |
order.intent (write) | not allowed | 30 / minute / token | 10 / minute / user |
The numbers are starting points; the right values come from observing real traffic for two weeks and tuning. The structure is what matters.
Which MCP auth events to log
Every auth-relevant event lands in the log:
- Token issued. Admin user, target principal, scopes, expiry.
- Token used. Token ID, tool name, principal, latency, success/failure.
- Token revoked. Token ID, who revoked, why.
- Token failed. Reason (expired, missing scope, hash mismatch), IP, user agent.
- OAuth code exchanged. User ID, scopes granted, refresh token issued.
- OAuth refresh. User ID, new access token issued, old token superseded.
The logs go to Cloudflare Logpush into a long-term store. A dashboard query that watches “tokens used in the last 24 hours that have not been used in the previous 90 days” catches likely token theft. A query that watches “failed token verifications by IP” catches credential-stuffing attempts.
Discovery, resource identity and the HTTP challenge
The MCP authorization specification dated 2025-11-25 defines an OAuth-based authorization flow for HTTP transports. Authorization remains optional for an implementation, but a protected interoperable HTTP server needs the discovery and validation machinery described by that profile. Stdio uses a different deployment boundary and obtains credentials from its environment. Private partner tokens can serve a controlled integration, but do not assume an arbitrary MCP host can discover or obtain those custom credentials. Test the actual host and transport.
Separate the MCP resource server from the authorization server, even when both run in the same application. The resource server executes tools and validates access. The authorization server handles login, consent and token issuance. Publish protected resource metadata that identifies the canonical resource and its authorization servers. An HTTP 401 challenge can point the client to that metadata through WWW-Authenticate. Discovery links must remain correct behind a reverse proxy and under any endpoint path prefix. A browser displaying a login page does not prove the MCP host discovered the correct issuer.
The client requests an access token for the MCP resource using the resource indicator. The server validates that the token was issued for that resource. A token issued for an unrelated API is not acceptable merely because it has a valid signature from a familiar issuer. Reject unexpected audiences and issuers before tool execution. Define the canonical resource URI consistently across metadata, authorization requests and token validation. Avoid making the expected audience depend on an untrusted Host header or a query parameter.
JWT validation and downstream credentials
OAuth access tokens can be opaque or JWTs; the protocol does not require every issuer to use JWT. An opaque token needs the issuer’s supported verification mechanism. A JWT verifier must check the signature, accepted algorithm, issuer, audience and time claims, then validate the application claims it consumes. A TypeScript type assertion does none of this. Malformed JSON, a missing subject or a scope with the wrong runtime type must produce a controlled refusal. Use a maintained verification library and a configured trust source instead of writing a partial verifier with Web Crypto.
Resolve signing keys from the issuer’s configured JWKS location, not from an arbitrary URL supplied inside a token. Plan for key rotation and issuer unavailability. A transient key-fetch failure must not enable an unchecked token. A previously cached trusted key can support verification within its documented policy, but an unknown signing key still requires an explicit decision. Test expired tokens, tokens that are not yet valid, algorithm changes and tokens intended for another environment. Staging credentials should not silently work against production.
The MCP security guidance prohibits token passthrough. A bearer token addressed to your MCP server must not be forwarded as if it were a WooCommerce or ERP credential. Keep downstream access credentials server-side and authorise each requested action before using them. If the downstream service needs delegated identity, establish the supported delegation or token exchange contract for that service. Copying the incoming Authorization header is not a delegation design.
Scope checks and customer ownership
Authenticate the HTTP request, then enforce the required scopes for the dispatched operation. Do not let a successful token verification bypass tool-level checks. A tool registry should contain the required permissions for each operation, and an unknown entry should fail closed. Changes to the registry require tests that demonstrate both the allowed call and the denied call. Tool discovery also needs a policy for sensitive descriptions and inputs; exposing a tool name does not grant permission to execute it.
An orders:read scope answers which category of action is allowed. It does not establish which customer’s orders are visible. Bind the verified subject to an internal account using a trusted mapping and enforce ownership when loading the order. Never use a customer ID supplied by the agent as proof of ownership. Partners with organisation-wide access need a separate grant and a defined tenant boundary. A Polish warehouse partner importing stock and a British customer checking delivery need different principals even if both connect to the same MCP deployment.
For bearer authentication failures, use the HTTP challenge appropriate to the transport. A missing or invalid token produces an authentication refusal, while insufficient scopes require the corresponding scope challenge. An expired access token should not appear as an out-of-stock tool result. Keeping these failure classes separate lets the host refresh or request consent instead of retrying a business operation with unchanged credentials.
Revocation, quotas and release tests
Generate opaque partner tokens with sufficient cryptographic randomness. Hashing storage helps only when the original secret is not easily guessed. Keep the public token identifier separate from the secret and compare or verify through a reviewed implementation. Define revocation propagation explicitly. Workers KV is eventually consistent; deleting a record does not provide an immediate global revocation guarantee. Sensitive access may require an authoritative check or a different consistency boundary. Short expiry limits exposure but does not replace revocation.
Rate limiting also has a consistency model. The Workers rate limiting binding is useful for limiting request volume, but it is not a global transactional budget for orders or paid operations. Use a suitable authoritative coordinator for business quotas. A KV read followed by a write is not an atomic increment and can lose concurrent updates. An agent’s retry behaviour must not create more writes than the intended operation.
Before release, test metadata discovery from the deployed hostname, consent, expiry, refresh, revocation and insufficient scopes with the intended MCP host. Add a cross-customer test and a cross-tenant test. Verify that private tool results never enter a shared cache. Record token identifiers, principal, tool and outcome in the audit trail without recording token secrets or customer payloads. Keep security events separate from detailed debugging traces and define retention for each store. A successful login is only the beginning of the access-control test.
Related MCP server guides
This article covers the auth surface. For the implementation walkthrough see building an MCP server for WooCommerce. For typed tool definitions see writing typed catalogue tools with Zod for MCP. For the protocol-level decision see MCP vs REST. For the migration pathway from an existing API see migrating an existing WordPress API to MCP. The pillar is MCP server development.
Pricing is individual because the auth scope depends on which patterns your environment requires; an anonymous-only read server is a different engagement than a full OAuth-issuing surface.






