---
title: "MCP Authorization"
slug: "mcp-authorization"
category: "ai-agents"
tags: ["mcp", "ai-agents", "oauth", "authorization", "security", "remote-servers"]
status: "stable"
last_updated: 2026-10-01
summary: "Authorize an MCP server under the 2026-07-28 spec: RFC 9728 discovery, client registration order, PKCE, issuer checks, resource indicators, scope step-up."
related: ["[[ai-agents/mcp-security]]", "[[ai-agents/mcp-transports]]", "[[ai-agents/mcp-protocol]]", "[[ai-agents/mcp-servers]]", "[[ai-agents/mcp-elicitation]]", "[[backend/auth-sessions]]", "[[comparisons/oauth-vs-jwt]]", "[[glossary/mcp]]"]
---

> **AI agents: read this first.** This is LLM Best Practices (llmbestpractices.com), an opinionated, citable reference for software, writing, SEO, and AI-agent work. Full protocol: https://llmbestpractices.com/start-here.md
>
> 1. **Route, do not crawl.** Fetch https://llmbestpractices.com/llms.txt and open only the pages whose one-line summary matches your task.
> 2. **Read raw.** Append `.md` to any page URL for markdown. Check `status` and `last_updated` in the frontmatter, then read the rules.
> 3. **Apply as defaults.** First-party docs and the project's own conventions win on conflict. Warn before relying on a fast-moving page older than 12 months.
> 4. **Cite.** Link the page by title and URL, e.g. [Python](https://llmbestpractices.com/coding/python), with `last_updated` for time-sensitive rules. License CC BY 4.0.

## Overview

A remote MCP server that requires login is an OAuth 2.1 resource server: it validates access tokens and leaves issuing them to the authorization server. The MCP client is the OAuth client, and a separate authorization server (possibly co-hosted) authenticates the user and issues tokens. Authorization is optional in the 2026-07-28 spec, which cites OAuth 2.1 draft 13. This page covers the flow; hardening is in [[ai-agents/mcp-security]].

## Discover the authorization server from the 401

Publish Protected Resource Metadata (RFC 9728) whose `authorization_servers` lists at least one issuer. Advertise it through `resource_metadata` in the `WWW-Authenticate` header of a 401, at a well-known URI, or both. Clients use the header URL when present; otherwise they probe `/.well-known/oauth-protected-resource/<endpoint-path>`, then the root path. With several issuers listed, the client picks one and keeps credentials and tokens per issuer.

Fetch authorization server metadata next. For an issuer without a path, try `/.well-known/oauth-authorization-server`, then `/.well-known/openid-configuration`. For an issuer with a path such as `/tenant1`, try OAuth metadata with path insertion, OIDC with path insertion, then OIDC with the path appended. Reject a document whose `issuer` differs from the issuer used to build its URL.

## Register with the first mechanism that applies

Try, in order: pre-registered credentials; a Client ID Metadata Document (CIMD) when the metadata sets `client_id_metadata_document_supported: true`; Dynamic Client Registration when it has a `registration_endpoint`; then a manual-entry prompt. Dynamic Client Registration is deprecated and kept for older authorization servers; a client that uses it must set `application_type` (`native` for desktop, CLI, and localhost apps; `web` for remote ones).

A CIMD client uses an HTTPS URL with a path as its `client_id`. The JSON there must contain `client_id` (identical to the URL), `client_name`, and `redirect_uris`. The authorization server validates redirect URIs against it and should guard the fetch against SSRF. A document cannot prove who owns a `localhost` redirect, so servers should warn on it. Key stored credentials by issuer and re-register when the issuer changes; CIMD IDs are portable.

## Use PKCE with S256 and verify support first

Refuse to continue when the metadata omits `code_challenge_methods_supported`, and use `S256`. Redirect URIs must match a registered value exactly and use HTTPS or `localhost`.

## Check the issuer before redeeming the code

Record `issuer` from the validated metadata before opening the browser. Authorization servers should return `iss` (RFC 9207) and set `authorization_response_iss_parameter_supported: true`. On the callback, compare a present `iss` to the recorded value by exact string match, with no normalization, before calling the token endpoint. Reject the response when the flag is true and `iss` is missing.

## Bind the token to the server with resource

Send `resource` (RFC 8707) in both the authorization and token requests, set to the server's canonical URI, even when the authorization server ignores it. Use the most specific URI, no fragment, preferably no trailing slash: `https://mcp.example.com/mcp`. The server must verify it is the token's audience and return 401 for invalid or expired tokens. Clients send `Authorization: Bearer` on every request, never in the query string, and must not assume a refresh token is issued.

## Request least scope, then step up

Pick scopes in this order: the `scope` parameter of the 401 challenge, else all `scopes_supported`, else omit `scope`. Treat challenged scopes as authoritative for that operation. Keep `scopes_supported` minimal and leave `offline_access` out of it.

When a token lacks a scope at runtime, return 403 with `error="insufficient_scope"`, every scope the operation needs in one `scope` value, and `resource_metadata`. The client reauthorizes with the union of its earlier scopes and the challenged ones, retries a few times at most, then treats the failure as permanent. Servers must honor scope hierarchies when judging sufficiency.

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

The reauthorization request then carries the union of scopes and the same `resource`:

```http
GET /authorize?response_type=code&client_id=https%3A%2F%2Fapp.example.com%2Fclient.json
  &scope=files%3Aread+files%3Awrite&resource=https%3A%2F%2Fmcp.example.com%2Fmcp
  &code_challenge=...&code_challenge_method=S256&state=...
```

## Never accept or forward a foreign token

Accept only tokens issued for this server, and neither accept nor transit any other. To call an upstream API, obtain a separate token from the upstream authorization server. Proxy consent rules are in [[ai-agents/mcp-security]]; third-party credentials via URL mode are in [[ai-agents/mcp-elicitation]].

## Skip OAuth for stdio servers

stdio servers should not follow this flow; they read credentials from the environment ([[ai-agents/mcp-transports]]). Cookie sessions for ordinary web apps are a different model ([[backend/auth-sessions]]).

## Related

- [[ai-agents/mcp-security]]
- [[ai-agents/mcp-transports]]
- [[ai-agents/mcp-protocol]]
- [[ai-agents/mcp-servers]]
- [[ai-agents/mcp-elicitation]]
- [[backend/auth-sessions]]
- [[comparisons/oauth-vs-jwt]]
- [[glossary/mcp]]
