Model Context Protocol · OAuth · Security

OAuth for Remote MCP Servers: Discovery, Scopes, and Token Validation

Implement the MCP 2025-11-25 authorization boundary for an HTTP server without token passthrough, audience confusion, or excessive scopes.

Published
Updated
Code examples
Illustrative
Reading time
7 minutes

A protected remote MCP server is an OAuth resource server. Its client discovers authorization metadata, obtains an audience-bound access token, sends that token in the Authorization header on every HTTP request, and receives only the scopes needed for the requested capabilities. The MCP server validates issuer, audience, signature, expiry, and authorization before executing any tool.

This guide covers the authorization model in MCP specification version 2025-11-25. It does not show how to build an authorization server; the MCP specification intentionally leaves that implementation out of scope. It also does not apply the HTTP OAuth flow to stdio, for which the specification recommends credentials from the environment instead (MCP authorization scope).

Separate the three OAuth roles

MCP client ── authorization request ──► authorization server
     │                                      │
     │◄──────── audience-bound token ───────┘
     │
     └── Authorization: Bearer … ──────► MCP server
                                          resource server

The MCP client acts as an OAuth client on behalf of a resource owner. The MCP server acts as a protected resource. The authorization server authenticates the user when necessary, obtains consent, and issues tokens.

This is distinct from a tool needing access to a third-party service. If an MCP server later needs the user's GitHub authorization, for example, that server becomes an OAuth client to GitHub and should use a separate URL-mode elicitation flow. The MCP client's token must not be forwarded to GitHub, and GitHub's token must not pass through the MCP client.

Publish protected-resource metadata

MCP servers must implement OAuth 2.0 Protected Resource Metadata as defined by RFC 9728. A metadata document identifies the canonical resource and at least one authorization server:

{
  "resource": "https://mcp.example.com/mcp",
  "authorization_servers": ["https://login.example.com"],
  "scopes_supported": [
    "invoices:read",
    "refunds:preview",
    "refunds:submit"
  ]
}

Serve it at the path-specific or root well-known location required by RFC 9728. A 401 response can point to it directly:

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

The MCP client must support both the WWW-Authenticate discovery path and construction of the well-known URI. Once it has the authorization-server issuer, it discovers endpoints through OAuth Authorization Server Metadata or OpenID Connect Discovery. The MCP specification requires authorization servers to provide at least one and clients to support both discovery mechanisms (authorization discovery requirements).

Do not duplicate endpoint configuration in client code if standards-based discovery is available. Validate fetched metadata and apply a trust policy before sending a user to an authorization endpoint.

Bind authorization to the canonical MCP resource

The client includes a resource parameter in authorization and token requests:

resource=https%3A%2F%2Fmcp.example.com%2Fmcp

That value must be the canonical absolute URI for the MCP server and must not contain a fragment. The client sends it even when the authorization server does not advertise resource-indicator support. The resulting access token must be valid specifically for that MCP resource, following RFC 8707 resource indicators.

Audience binding prevents a dangerous confused-deputy pattern: a token issued for service A being accepted by service B. The MCP server must reject tokens not issued for itself. It must also reject a token issued by an unrelated authorization server even if the token is structurally valid.

Use the authorization code flow safely

Public MCP clients cannot keep a client secret. Use Authorization Code with PKCE, an exact registered redirect URI, and a high-entropy state value. Confidential clients should authenticate using a method appropriate to their deployment in addition to PKCE where required by the authorization server.

The 2025-11-25 MCP authorization specification supports several client-registration approaches:

  • OAuth Client ID Metadata Documents are the recommended interoperable approach where supported.
  • Pre-registration is appropriate when an administrator establishes a client ahead of time.
  • Dynamic Client Registration is optional, not something every authorization server must expose.

Do not silently fall back to an arbitrary client identity. Record which method was used and constrain redirect URIs. The MCP specification is based on an OAuth 2.1 draft plus stable supporting RFCs, so pin the specification date and revisit draft-dependent behavior on upgrade.

Design scopes around outcomes

Avoid a single mcp:all scope. For a billing server, a reasonable initial surface might be:

ScopePermitted operations
invoices:readRead bounded invoice data
refunds:previewCompute a non-binding refund preview
refunds:submitSubmit an already approved refund

Tool discovery does not grant scope, and possession of a scope does not remove application-level authorization. refunds:submit means the token may request that class of action; the server must still verify tenant membership, invoice ownership, refund limits, approval evidence, and idempotency.

Start with the scopes required by the initial challenge rather than requesting every value in scopes_supported. The MCP specification says the challenge's scope value is authoritative for that request and need not have a strict subset relationship with advertised scopes.

When a valid token lacks permission, return 403 with an insufficient_scope challenge:

HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope", scope="invoices:read refunds:preview", resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp"

A client acting for a user may run step-up authorization and retry, but it should cap attempts so a configuration error cannot create an authorization loop (MCP scope challenge guidance).

Validate every request at the resource server

Send the access token only in the header:

POST /mcp HTTP/1.1
Host: mcp.example.com
Authorization: Bearer eyJ…
MCP-Protocol-Version: 2025-11-25
Content-Type: application/json
Accept: application/json, text/event-stream

The MCP authorization specification forbids access tokens in URI query strings and requires authorization on every client-to-server HTTP request, even when requests share an MCP session.

A JWT validator generally checks:

  1. the issuer exactly matches a trusted authorization server;
  2. the signature uses an allowed algorithm and current trusted key;
  3. the audience contains this MCP resource;
  4. expiry and not-before claims pass with a small documented clock tolerance;
  5. required scopes are present; and
  6. the subject or client identity maps to an active local principal.

An opaque-token deployment uses introspection or another authorization-server contract instead of local signature validation, but it still needs issuer trust, audience, lifetime, and scope checks. Never choose validation solely by whether a token contains periods.

Return 401 for a missing, invalid, or expired token. Return 403 when a valid token lacks permission. Avoid putting token details or sensitive policy information in the response body.

Enforce identity and tenant boundaries inside tools

Create a request context only after token validation:

type Principal = Readonly<{
  subject: string;
  clientId: string;
  tenantId: string;
  scopes: ReadonlySet<string>;
}>;

function requireScope(principal: Principal, scope: string): void {
  if (!principal.scopes.has(scope)) {
    throw new InsufficientScope(scope);
  }
}

The type is illustrative. Do not accept tenantId or subject from tool arguments as authoritative. Derive identity from the verified token and resolve tenant membership from controlled data. Include both principal and tenant in every database query; avoid fetching by object ID and checking tenant afterward.

For machine-to-machine clients, distinguish a workload subject from a human user. Do not manufacture an end-user identity from a client-credentials token.

Prevent token passthrough and leakage

An MCP server must not accept or transit tokens intended for other resources. If a tool calls an upstream API, use server-owned credentials or a distinct user grant acquired for that upstream resource. This maintains audience boundaries and makes revocation comprehensible.

Also keep tokens out of:

  • model context and prompt history;
  • tool arguments and results;
  • MCP resources;
  • application logs, traces, and error reports;
  • browser URLs; and
  • analytics or replay systems.

Use TLS, short-lived access tokens, encrypted storage where refresh tokens are necessary, rotation, and explicit logout or revocation behavior. Treat authorization-server metadata and signing-key rotation as operational dependencies with monitoring and cache rules.

Production checklist

  • Publish RFC 9728 protected-resource metadata for the canonical MCP URI.
  • Discover the authorization server rather than hard-coding unverified endpoints.
  • Use Authorization Code with PKCE for user-facing clients.
  • Include the canonical resource in authorization and token requests.
  • Validate issuer, audience, signature or introspection, lifetime, and scope.
  • Send authorization on every HTTP request; never put tokens in URLs.
  • Separate 401 invalid-token handling from 403 insufficient-scope handling.
  • Authorize principal, tenant, object, and action inside every tool.
  • Prohibit token passthrough to upstream services.
  • Redact credentials from logs, traces, errors, resources, and model-visible data.
  • Test key rotation, expired tokens, wrong audiences, missing scopes, and cross-tenant IDs.

Combine this boundary with safe tool contracts and the Streamable HTTP transport requirements before exposing a remote server.