MCP Authorization for Production Servers

Build MCP authorization with OAuth, Protected Resource Metadata, token audience checks, consent, approvals, logs, and production release gates.

Wednesday, June 10, 2026Omid Saffari
MCP Authorization for Production Servers

Ship MCP authorization only for remote HTTP servers that expose user data, administrative tools, or enterprise-controlled APIs. For production, the hard line is simple: discover auth with Protected Resource Metadata, bind every token to the MCP server audience, keep upstream API tokens separate, and add human approval before the tool executes anything irreversible.

The Production Rule

MCP authorization is a remote-server control, not a blanket requirement for every local tool adapter. The latest MCP authorization spec, version 2025-11-25, defines the authorization flow for HTTP-based transports. It says authorization is optional, HTTP transports should conform when they support authorization, and STDIO transports should not follow this HTTP authorization spec.

That split matters. A local STDIO MCP server usually runs inside a developer machine or a controlled runtime and can receive credentials through environment variables, local config, secret managers, or a client-managed credential flow. A remote MCP server is different: it is a network boundary where a general MCP client can ask for tools, resources, and prompts that may touch production data.

Use authorization when the server handles any of these:

  • User-specific data such as email, documents, tickets, files, CRM records, database rows, or account-level state.
  • Administrative actions such as creating records, changing configuration, deploying code, deleting objects, or sending messages.
  • Enterprise audit requirements where the system must answer who approved, who called, what tool ran, and what resource changed.
  • Per-user rate limits, usage tracking, billing controls, or tenant-level quotas.

Skip OAuth work for a local STDIO connector that reads a developer's local files, provided the host client already controls install, execution, and consent. Build the full HTTP authorization path when the MCP server is remote, shared, multi-tenant, or able to mutate real systems.

The Flow To Ship

A production MCP auth flow starts with discovery, not with a hard-coded authorization URL. The MCP server is an OAuth resource server. The MCP client is an OAuth client. The authorization server issues tokens that are valid for the MCP server, not for any arbitrary upstream API.

The minimum remote-server flow is:

  1. Return a useful 401

    When the client calls a protected MCP endpoint without a valid token, return 401 Unauthorized and include WWW-Authenticate: Bearer. The latest spec requires protected-resource discovery through either the resource_metadata parameter in that header or a well-known OAuth protected resource URI.

  2. Serve Protected Resource Metadata

    Expose a Protected Resource Metadata document that identifies the MCP resource and lists at least one authorization server. A typical production document names the canonical MCP endpoint, supported scopes, and the authorization server that can issue tokens for that resource.

  3. Let the client discover the authorization server

    The authorization server must provide OAuth Authorization Server Metadata or OpenID Connect Discovery 1.0. MCP clients must support both, which keeps the server compatible with OAuth-native and OIDC-native identity systems.

  4. Register or identify the client

    Use pre-registration when the MCP client and server already have a business relationship. Use Client ID Metadata Documents when the client and server do not. Dynamic Client Registration remains available as a fallback when the authorization server supports it.

  5. Request a resource-bound token

    The MCP client must include the OAuth resource parameter in both the authorization request and token request. That value identifies the target MCP server and is what lets the server reject tokens minted for some other API.

  6. Validate on every request

    Every HTTP request to the MCP server must carry Authorization: Bearer <access-token>. The server validates signature or introspection, issuer, expiry, scope, and audience before it touches tool logic.

A compact metadata shape looks like this:

JSON
{
  "resource": "https://mcp.example.com/mcp",
  "authorization_servers": ["https://auth.example.com"],
  "scopes_supported": ["tickets:read", "tickets:write"]
}

For a protected write tool, the first unauthorized response should make the next step obvious:

Http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
                         scope="tickets:read"

The scope value in the challenge is not decoration. The 2025-11-25 spec says clients must treat challenged scopes as authoritative for the current request. Use that to avoid asking for broad access during the initial connection. A client that only needs to read tickets should not request tickets:write because the server exposed it in scopes_supported.

Client Registration Changed The Build Plan

The 2025-11-25 MCP authorization spec changes the practical build plan because Client ID Metadata Documents are now the normal path for clients and servers with no prior relationship. Dynamic Client Registration is still supported, but it is a fallback, not the first production assumption.

The priority order is:

ScenarioUse ThisProduction Interpretation
First-party client, known serverPre-registrationBest for internal MCP clients, enterprise deployments, and managed tenants.
Third-party client, no prior relationshipClient ID Metadata DocumentsThe client hosts metadata at an HTTPS URL and uses that URL as client_id.
Older ecosystem supportDynamic Client RegistrationKeep it only when the authorization server supports it and abuse controls are in place.
No supported registration pathUser-entered client infoAcceptable for specialist setup flows, poor as a default product path.

With Client ID Metadata Documents, the client hosts JSON metadata at an HTTPS URL with a path component. The metadata must include client_id, client_name, and redirect_uris, and the client_id value must match the metadata URL exactly. The authorization server fetches that document, validates it, checks redirect URIs against it, and caches it according to HTTP cache headers.

That gives production teams a cleaner line than open DCR endpoints. The client identity is stable, inspectable, and tied to an HTTPS origin. It also avoids turning every protected MCP server into a public registration surface.

For an internal MCP server, still prefer pre-registration. A product engineering org connecting its own ChatGPT app, Claude desktop policy, or internal agent runner to mcp.company.com does not need general dynamic onboarding. Register the approved clients, pin redirect URIs, assign scopes, and alert when an unregistered client tries to connect.

For a public SaaS MCP server, support Client ID Metadata Documents and keep DCR behind explicit policy. If DCR is enabled, log every registration, apply rate limits, expire unused registrations, and review redirect URI patterns. The registration path is part of the attack surface.

Token Boundaries Are The Security Boundary

The most important MCP authorization rule is that the token presented to the MCP server must be for the MCP server. Do not accept a token minted for GitHub, Google, Slack, Salesforce, or an internal API and pass it downstream.

The latest authorization spec requires MCP clients to include resource in authorization and token requests, and it requires MCP servers to validate that access tokens were issued specifically for them. The security best practices go further: token passthrough is forbidden because it breaks accountability, bypasses controls, and creates confused-deputy risk.

Build the MCP server as a resource boundary:

TypeScript
type VerifiedMcpToken = {
  subject: string;
  clientId: string;
  tenantId: string;
  scopes: string[];
  audience: string[];
  expiresAt: number;
};

function assertMcpAudience(token: VerifiedMcpToken, mcpResource: string) {
  if (!token.audience.includes(mcpResource)) {
    throw new AuthError(401, "token_audience_mismatch");
  }
}

function assertScope(token: VerifiedMcpToken, required: string) {
  if (!token.scopes.includes(required)) {
    throw new AuthError(403, "insufficient_scope", { scope: required });
  }
}

Then separate downstream access. If the MCP tool calls a ticketing API, the MCP server should use its own upstream OAuth client, token exchange, delegated service credential, or stored per-user grant. The inbound MCP token proves the client can call the MCP server. It is not the credential that should be forwarded to the ticketing API.

The same applies to transport sessions. Session IDs are useful for stream resumability and request correlation, but the session must not become the auth check. Store session state under a key that includes internal user identity, for example user_123:session_abc, and re-check the access token before each tool call. If a session ID leaks, the bearer token still gates the request.

OAuth consent decides whether a client may access the MCP server. Human approval decides whether a specific high-risk tool call should execute. Production systems need both.

A consent screen should show the requesting client, redirect URI, requested scopes, resource, tenant, and duration. Store consent per user and per client_id, not as a loose "user has consented" flag. The MCP security guidance requires proxy servers to maintain approved client IDs per user and check that registry before forwarding to third-party authorization.

Approval is a runtime control. Add it to tools that send email, spend money, change production config, delete data, export sensitive records, merge code, or mutate customer-visible state. The approval payload should include:

  • Actor: user subject, client ID, tenant, and originating IP or device context when available.
  • Action: tool name, target resource, parameters after schema validation, and requested scope.
  • Risk: data sensitivity, environment, estimated blast radius, and whether the tool is reversible.
  • Evidence: model trace ID, prompt or instruction hash, retrieved documents if relevant, and policy decision.
  • Decision: approver, decision time, reason, expiry, and replay protection token.

A practical policy looks like this:

TypeScript
const toolPolicy = {
  "tickets.search": { scope: "tickets:read", approval: "none" },
  "tickets.update_status": { scope: "tickets:write", approval: "low_risk_write" },
  "tickets.bulk_close": { scope: "tickets:admin", approval: "human_required" },
  "users.export": { scope: "users:export", approval: "human_required" }
} as const;

Consent can be long-lived. Approval should be short-lived and action-specific. If the agent changes the target record, parameters, or tool after approval, invalidate the approval and ask again. If the model asks for a broader scope at runtime, return 403 Forbidden with error="insufficient_scope" and the minimum required scope so the client can run a step-up authorization flow.

OAuth Proxies Are Architecture, Not Glue

An OAuth proxy can be the right bridge when an existing identity provider does not support the MCP client's expected flow, but treat it as an authorization service, not a thin adapter. Speakeasy's May 22, 2026 analysis of MCP authentication calls out the mismatch directly: many OAuth providers need significant implementation changes to satisfy MCP's authorization expectations.

The proxy pattern exposes MCP-compatible endpoints such as authorization server metadata, registration or client identification, authorization, and token endpoints. It then maps those requests to the real identity provider. That means the proxy owns authorization codes, token mapping state, refresh behavior, revocation, audit events, and security headers.

Use a proxy only when the operational owner accepts these responsibilities:

  • Token lifecycle: store, rotate, revoke, expire, and garbage-collect proxy grants.
  • Client isolation: keep consent and grants separate per user and per client identity.
  • Redirect safety: exact-match redirect URIs, no wildcards, no silent changes after registration.
  • State safety: cryptographically random, single-use state values with short expiry. The MCP security guide gives 10 minutes as an example short expiry.
  • Auditability: one log line must tie together MCP client, user, grant, upstream account, scope, and tool run.
  • Incident response: revoking a client must revoke grants and prevent old refresh tokens from creating new access.

Cloudflare's workers-oauth-provider repository is an active implementation surface, with the scraped GitHub page showing a Jun 9, 2026 latest commit. That is useful signal, but not a reason to skip design review. When an OAuth proxy stores token mappings and issues tokens to MCP clients, it has become part of the trust boundary.

The Release Gate

Do not ship a protected MCP server until auth, logs, approvals, and failure behavior pass together. A server that can authenticate but cannot explain what happened after a bad tool call is not production-ready.

Use this release gate:

GatePass Condition
Discovery401 includes resource_metadata, and the well-known Protected Resource Metadata URL works.
RegistrationApproved clients use pre-registration or Client ID Metadata Documents. DCR, if enabled, has rate limits and audit logs.
Resource bindingAuthorization and token requests include resource, and the server rejects wrong-audience tokens.
Scope handlingMissing token returns 401; insufficient scope returns 403 with insufficient_scope and the minimum scope.
Token handlingTokens are never logged, never accepted from query strings, and never passed through to upstream APIs.
ConsentConsent is stored per user and client, with exact redirect URI validation.
ApprovalRisky tools pause before execution and record approver, parameters, reason, and expiry.
SessionsSession IDs are random, bound to user identity in storage, and never used as authentication.
SSRF defenseOAuth discovery URLs require HTTPS in production, block private or reserved IP ranges, and validate redirects.
ObservabilityEvery tool run has trace ID, user, client ID, tenant, scopes, resource, tool, decision, latency, and outcome.

The production failure mode to test is not "can a happy-path client connect." Test the bad cases:

  1. Send a token for the wrong resource

    The MCP server should reject it before tool routing. The log should show audience mismatch without storing the token.

  2. Request a write tool with a read-only token

    The server should return 403 Forbidden with insufficient_scope, resource_metadata, and the minimum write scope.

  3. Change the redirect URI after consent

    The authorization path should reject the request until the client identity is re-registered or re-approved.

  4. Trigger a destructive tool through an approved client

    The tool should pause, generate an approval request, and execute only after a human approves the exact action payload.

  5. Point metadata discovery at an internal IP

    A server-side MCP client should block the request rather than fetching cloud metadata, localhost services, or private network URLs.

The durable pattern is a small set of narrow MCP scopes, an explicit token boundary, an approval queue for irreversible tools, and logs that let an engineer reconstruct the full run. That is the difference between exposing internal APIs to agents and putting production systems behind a protocol-shaped hole.

Does every MCP server need OAuth?

No. MCP authorization is optional, and the HTTP authorization spec is not the right pattern for local STDIO servers. Use it for remote MCP servers that expose user data, administrative tools, shared APIs, enterprise audit requirements, or per-user usage controls.

What changed in the latest MCP authorization spec?

The latest spec version scraped this run is 2025-11-25. The practical change is that Client ID Metadata Documents are now the preferred no-prior-relationship client identification path, while Dynamic Client Registration is a fallback.

Can an MCP server pass the user's upstream API token through?

No. The server must validate a token issued for the MCP server itself and must not accept or transit tokens intended for other resources. Use a separate upstream credential, token exchange, or stored delegated grant for downstream APIs.

What status codes should a protected MCP server return?

Use 401 when authorization is required or the token is invalid, 403 when the token is valid but lacks required scope, and 400 for malformed authorization requests.

For the adjacent security checklist, read MCP Security Best Practices for Production Servers. For the architecture boundary before you decide whether MCP is worth building at all, read MCP vs Function Calling: The Production Decision Rule.

Last Updated

Jun 10, 2026

CategoryMCP
Newsletter

One letter, every week. Working systems — not hot takes.

Build logs, agentic engineering decisions, agent failures, evals, and what survives real users. Sent weekly, never more.

Weekly. No spam. Unsubscribe anytime.