Most published “MCP auth” guides have a problem: they were written against an older spec revision. The scoped tool access story - the enforcement flow that actually separates what an agent can read from what it can write or delete - did not exist in those older versions. It landed in the 2025-11-25 MCP specification revision.

This guide covers the complete authorization model as it stands today, anchored on the current stable spec. If your reference material cites 2025-06-18 or earlier, you are missing the machinery the spec provides to enforce least privilege at runtime.

Why “add a bearer token” is not MCP authorization

The most common pattern in tutorials goes like this: generate a token, add Authorization: Bearer <token> to every request, done. This approach authenticates the request but does not authorize the operation.

Authentication answers who the caller is. Authorization answers what the caller is allowed to do.

When a single bearer token covers every operation on the server - reading documents, writing files, deleting records, calling administrative functions - you have authentication without authorization. Any component that holds the token can do everything the server exposes.

The one thing that breaks first: a read-only agent that can still write

Consider an AI agent that reads calendar events to summarize a user’s week. It holds a token. That token lets it call the MCP server. If the server does not enforce scope at the operation level, the same token also lets the agent write events, delete events, and accept meeting invitations on the user’s behalf.

This is the over-permissioning failure mode the MCP Security Best Practices document names under Scope Minimization: “treating claimed scopes in token as sufficient without server-side authorization logic.” The token might carry calendar:read in its scope claim, but if the server never checks whether calendar:write is required before allowing a write operation, the scope claim is decorative.

The rest of this guide explains the spec-mandated flow that closes this gap.

The roles: MCP server as OAuth 2.1 resource server

The MCP 2025-11-25 authorization spec assigns OAuth 2.1 roles directly. The MCP server “acts as an OAuth 2.1 resource server.” The MCP client “acts as an OAuth 2.1 client.” The authorization server that issues tokens “may be hosted with the resource server or a separate entity.”

This is the distinction most tutorials miss. The MCP server is not where users log in. It validates tokens that were minted somewhere else. It checks whether a presented credential is valid, unexpired, intended for it specifically, and scoped to allow the requested operation.

Authorization is OPTIONAL for MCP. If an HTTP-transport server implements authorization, the spec says it SHOULD conform to this model. STDIO-transport servers should not use this flow - they should read credentials from the environment, because interactive OAuth flows do not make sense in a subprocess model.

The spec-mandated flow, end to end

How does MCP authorization discovery work?

The client does not hardcode where to authenticate. The flow is discovery-driven.

The MCP server MUST implement OAuth 2.0 Protected Resource Metadata (RFC 9728). Its metadata document MUST include an authorization_servers field naming at least one authorization server.

When a client makes an unauthenticated request, the server returns 401 Unauthorized. The client locates the protected resource metadata from one of two sources: the WWW-Authenticate header (which carries resource_metadata="<url>"), or the well-known URI /.well-known/oauth-protected-resource. Clients MUST support both.

Once the client has the protected resource metadata and knows which authorization server to use, it discovers that server’s capabilities through OAuth 2.0 Authorization Server Metadata (RFC 8414) or OpenID Connect Discovery 1.0. Clients MUST support both discovery mechanisms here too.

PKCE with S256, and why the client must refuse to proceed without it

OAuth 2.1 requires PKCE for all authorization-code clients. The MCP spec adds a client-side verification requirement on top of that: clients MUST check code_challenge_methods_supported in the authorization server metadata before starting an authorization flow.

If S256 is not listed in that field, or if the field is absent entirely, the client MUST refuse to proceed. Not silently fall back to a no-PKCE flow. Refuse entirely.

The reason: silent fallback reintroduces authorization-code interception. An attacker who intercepts the code in transit can exchange it for a token without the code verifier that PKCE requires. Many MCP client implementations skip this check and proceed anyway - which is exactly the implementation mistake the spec is trying to prohibit.

The resource parameter and canonical server URIs

The resource parameter (RFC 8707) is the mechanism that binds a token to one specific MCP server. Clients MUST include it in both the authorization request and the token request. It identifies the MCP server by its canonical URI.

Canonical URI requirements: include the scheme (https://), omit the fragment. Valid: https://mcp.example.com/mcp. Invalid: mcp.example.com (no scheme), https://mcp.example.com#mcp (fragment present).

URL-encoded form in a request:

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

The spec requires this parameter regardless of whether the authorization server advertises RFC 8707 support. The client sends it unconditionally.

sequenceDiagram
    participant C as MCP Client
    participant RS as MCP Server (Resource Server)
    participant AS as Authorization Server

    C->>RS: Request (no token)
    RS-->>C: 401 Unauthorized<br/>WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource"

    C->>RS: GET /.well-known/oauth-protected-resource
    RS-->>C: {"authorization_servers": ["https://auth.example.com"]}

    C->>AS: GET /.well-known/oauth-authorization-server
    AS-->>C: {"code_challenge_methods_supported": ["S256"], ...}

    Note over C: Generate PKCE code_verifier + code_challenge (S256)

    C->>AS: Authorization request<br/>code_challenge=... + resource=https://mcp.example.com/mcp
    AS-->>C: Authorization code

    C->>AS: Token request<br/>code_verifier=... + resource=https://mcp.example.com/mcp
    AS-->>C: Access token (aud: https://mcp.example.com/mcp)

    C->>RS: Request + Bearer token
    RS-->>C: 200 OK

The complete spec-mandated flow: the 401 response begins discovery, the client verifies PKCE support before proceeding, and the resource parameter appears in both the authorization and token requests. The resulting token is audience-bound to this specific MCP server.

Audience validation and the token passthrough prohibition

Audience validation is the server-side mirror of the resource parameter. The server receives a bearer token and MUST verify that it was issued specifically for this server as the intended audience. A token that names a different resource MUST be rejected.

The spec under “Access Token Privilege Restriction” describes two distinct failure modes:

Accepting a wrong-audience token breaks a fundamental OAuth security boundary. A token minted for https://calendar.example.com should not grant access to https://files.example.com. Without audience validation, token replay across services requires no special skill from an attacker.

Token passthrough is separately and explicitly forbidden. The MCP Security Best Practices document states: “MCP servers MUST NOT accept any tokens that were not explicitly issued for the MCP server.” If the MCP server calls an upstream API as part of handling a request, it MUST obtain a separate token as its own OAuth client. Forwarding the client’s bearer token to a downstream service is a security violation.

For JWT access tokens, audience validation maps to the aud claim per the JWT access token profile (RFC 9068).

Scoped tool access: how least privilege is actually enforced

Advertising required scope in the WWW-Authenticate challenge

The 2025-11-25 revision added Scope Selection Strategy and Scope Challenge Handling sections that make least-privilege authorization practical at runtime.

When the server returns 401 Unauthorized, it SHOULD include a scope parameter in the WWW-Authenticate header naming the scope the client needs to proceed:

WWW-Authenticate: Bearer realm="mcp", scope="files:read", resource_metadata="..."

The client SHOULD use this challenged scope in its authorization request rather than requesting all of scopes_supported. The spec describes scopes_supported as “the minimal set of scopes necessary for basic functionality” - not an invitation to request every scope the server can issue.

insufficient_scope and step-up authorization

When a valid but insufficiently-scoped token calls an operation that requires a higher scope, the server SHOULD respond 403 Forbidden. The WWW-Authenticate header on that 403 carries the signal:

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

The response code matters. 403 with insufficient_scope tells the client exactly what happened and what to do next. A generic 401 or a 200 loses that signal entirely.

The client SHOULD parse the required scopes from the 403, run a new authorization request for only the additional scope needed, obtain a new token, and retry the original operation. Retry logic must be bounded - after a configured number of step-up attempts, treat the failure as permanent.

sequenceDiagram
    participant C as MCP Client
    participant RS as MCP Server

    Note over C: Token scope: files:read

    C->>RS: call write_file (Bearer: files:read token)
    RS->>RS: Check scope: write_file requires files:write
    RS-->>C: 403 Forbidden<br/>WWW-Authenticate: Bearer error="insufficient_scope",<br/>scope="files:read files:write"

    Note over C: Parse required scopes from 403 response

    C->>C: Step-up authorization<br/>(request scope: "files:read files:write")

    Note over C: New token: scope=files:read files:write<br/>aud=https://mcp.example.com/mcp

    C->>RS: call write_file (Bearer: new token)
    RS->>RS: Check scope: files:write present
    RS-->>C: 200 OK

Step-up authorization: a files:read token calls a write operation, receives a scoped 403 insufficient_scope, runs a targeted re-authorization for the minimum additional scope required, and retries. The server checks scope per operation rather than trusting the token blindly.

The over-permissioning anti-pattern

The MCP Security Best Practices document names this attack class and lists the Common Mistakes under Scope Minimization:

  • Publishing all possible scopes in scopes_supported
  • Using wildcard or omnibus scopes (*, all, full-access)
  • Bundling unrelated privileges to avoid future prompts
  • Treating claimed scopes in the token as sufficient without checking them server-side per operation

An agent holding a token with files:* db:* admin:* has unlimited blast radius. A leaked token with that scope can read, write, and delete across every tool the server exposes. The step-up model contains this: the agent starts with a minimal baseline, and each privileged operation triggers a targeted challenge and a user-visible re-authorization for only the scope that specific operation requires.

A note on scope naming: strings like files:read, mcp:tools-basic, and files:write are illustrative examples from the spec and its referenced documentation. MCP core does not standardize a per-tool scope vocabulary. The enforcement flow is mandated; the scope taxonomy is the implementer’s responsibility. That gap is exactly why over-permissioning happens - teams reach for a few broad scope strings because they have not defined granular operation-level scopes.

What changed in 2025-11-25 (and why old tutorials are wrong)

Content that does not cite the 2025-11-25 spec revision is missing at least three things:

Scope Challenge Handling. The WWW-Authenticate: insufficient_scope pattern and the step-up authorization flow did not exist in 2025-06-18. Posts written against that revision describe OAuth token issuance but have no answer for how a server enforces least privilege at the operation level after the initial token is issued.

Client Registration changes. Dynamic Client Registration (RFC 7591) was a SHOULD in 2025-06-18. In 2025-11-25 it was demoted to MAY, kept for backwards compatibility. The preferred mechanism is now OAuth Client ID Metadata Documents (draft-ietf-oauth-client-id-metadata-document-00), where the client_id itself is an HTTPS URL that resolves to a JSON metadata document describing the client. The priority order: pre-registered credentials first, then Client ID Metadata Documents if the AS advertises support, then DCR as fallback, then prompt the user.

Scope Selection Strategy. The 2025-11-25 revision added explicit guidance that scopes_supported represents a minimal baseline, not a menu of everything the server can issue. This changes how a correctly-implemented client requests scope on first contact.

A 2026-07-28 revision was in release candidate status as of this writing. The stable revision carrying the full scoped-tool-access machinery is 2025-11-25. All spec links in this post point to that exact revision.

On OAuth 2.1 itself: it is not a finished RFC. It is an IETF working-group draft. The MCP 2025-11-25 spec references draft-ietf-oauth-v2-1-13. The current working-group draft is draft-ietf-oauth-v2-1-15, published March 2026. The consolidation of security requirements that OAuth 2.1 represents - mandatory PKCE, exact redirect URI matching, removal of the implicit flow - is the clear direction, but the document has not completed the RFC process.

Verify it: a scope-enforcement checklist

These checks verify whether a running MCP server actually enforces the spec requirements rather than just passing a linter.

  1. Unauthenticated request returns 401 with discovery info. curl -i https://your-mcp-server/mcp with no token should return 401 and a WWW-Authenticate header carrying resource_metadata.

  2. Protected resource metadata resolves and names an auth server. GET /.well-known/oauth-protected-resource returns JSON with an authorization_servers array.

  3. Wrong-audience token is rejected. Present a valid token minted with a different resource value and confirm 401. If the server accepts it, audience validation is absent.

  4. Insufficient-scope operation returns 403, not 200 or a generic 401. Call a write or delete operation with a read-only token. The expected response is 403 with error="insufficient_scope" and a scope= hint. A 200 response here is the over-permissioning bug this post is about.

  5. PKCE is enforced. Confirm the authorization server metadata lists S256 in code_challenge_methods_supported. Confirm an authorization request sent without a code challenge is rejected.

  6. No token passthrough to upstream APIs. If the server calls an upstream API, confirm it uses a server-obtained token rather than the client’s bearer token.

Steps 3, 4, and 5 require a running authorization server to test against. The expected HTTP responses listed above come from the spec; do not fabricate example tool output.

Frequently asked questions

Does MCP require OAuth?

No. Authorization is OPTIONAL in MCP. If an HTTP-transport server implements authorization, it SHOULD follow the MCP authorization spec, which is built on OAuth 2.1 (draft-ietf-oauth-v2-1-13 as referenced by the current spec). STDIO-transport servers should not use this flow; they should read credentials from the environment because interactive OAuth flows do not apply in a subprocess model.

What stops an MCP agent authorized to read from also writing or deleting?

Scoped access enforced server-side per operation. The server challenges with the required scope on 401, and when a read-only token calls a write operation the server returns 403 with error="insufficient_scope", forcing a step-up authorization for the higher scope. If the server does not check scope per operation and instead trusts the token claim without verification, the read token can write. That check-skipping is what the Scope Minimization section of the MCP Security Best Practices identifies as a listed Common Mistake.

Why does my MCP server have to validate the token audience?

To stop token reuse across services. MCP servers MUST verify a token was issued specifically for them as the intended audience (RFC 8707 Section 2) and MUST reject tokens that name a different resource. Without this check, a token minted for one service can be replayed against another, which the spec calls breaking “a fundamental OAuth security boundary.”

Can an MCP server forward the client’s token to a downstream API?

No. The MCP Security Best Practices document explicitly forbids token passthrough. If the MCP server calls an upstream API, it must act as its own OAuth client and obtain a separate token for that call. The client’s bearer token MUST NOT be forwarded to any downstream service.

Is OAuth 2.1 a finished standard?

Not yet. OAuth 2.1 is an in-progress IETF working-group draft - draft-ietf-oauth-v2-1-15 as of March 2026, expiring September 2026. The MCP 2025-11-25 spec references draft-ietf-oauth-v2-1-13. The key changes OAuth 2.1 codifies that already apply to MCP implementations: PKCE is mandatory for all authorization-code flows, and redirect URIs must match by exact string comparison rather than prefix or pattern matching.