Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions adr/0001-oauth-authorization-server-out-of-scope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# 0001 — The MCP server is an OAuth Resource Server, not an Authorization Server

- Status: Accepted
- Date: 2026-06-15

## Context

OAuth 2.1 defines three distinct roles:

| Role | What it does | Status in this SDK | Scope |
|------|--------------|--------------------|-------|
| Resource Server | Validates incoming bearer tokens, serves Protected Resource Metadata (RFC 9728), emits `WWW-Authenticate` | Shipped (`AuthorizationMiddleware`, `JwtTokenValidator`, `ProtectedResourceMetadata`) | **IN scope** |
| Delegation / proxy to an upstream AS | Forwards `/authorize` and `/token` to your existing IdP | Shipped (`OAuthProxyMiddleware`) | **IN scope — delegation ONLY** |
| Authorization Server / Identity Provider (IdP) | Mints its own tokens, registers clients, runs login and consent | Absent | **OUT of scope** |

The SDK repeatedly receives pull requests that move it toward becoming a full OAuth 2.1
**authorization server** — asking the MCP server to mint its own access and refresh tokens,
register clients, and run login and consent flows, that is, to become an Identity Provider.
The most explicit example is [#373](https://github.com/modelcontextprotocol/php-sdk/pull/373)
("[Server] Add native OAuth 2.1 authorization server", ~3,400 lines across 50+ files), but the
direction has crept in incrementally rather than in one PR.

A contributing factor is that the SDK already exposes the *authorization-server endpoints in
proxy form*, and that surface has grown:
[#221](https://github.com/modelcontextprotocol/php-sdk/pull/221) added the OAuth resource-server
middleware, and [#269](https://github.com/modelcontextprotocol/php-sdk/pull/269) added Dynamic
Client Registration (RFC 7591). Today `OAuthProxyMiddleware` answers `/authorize`, `/token`, and
`/.well-known/oauth-authorization-server`, and DCR endpoints exist. The shape of that surface
invites contributors to "finish the job" by backing those endpoints with a real token issuer.
It is not scaffolding to be completed — it is a delegating proxy, and that is the whole of its
intent.

## Decision

**The MCP server is an OAuth 2.1 Resource Server that MAY delegate to an upstream
authorization server. It will NOT issue tokens or act as an Identity Provider.**

Concretely:

- The SDK validates bearer tokens, serves RFC 9728 Protected Resource Metadata, and emits
`WWW-Authenticate` challenges. This is the Resource Server role and is fully supported.
- The SDK MAY delegate `/authorize` and `/token` to an upstream authorization server via
`OAuthProxyMiddleware`. This is delegation only: the middleware redirects the browser to
the upstream `/authorize` endpoint and proxies `/token` requests to the upstream token
endpoint. It never mints, signs, stores, or rotates tokens of its own.
- The SDK will NOT implement an authorization server: no token issuance, no token signing or
key management, no login UI, no consent UI, no authorization-code or refresh-token storage,
no first-party Dynamic Client Registration acting as an issuer.

Pull requests that add authorization-server / IdP behavior are declined by reference to this
ADR.

## Rationale

- **Security liability.** Issuing tokens means owning signing-key generation, storage, and
rotation; authorization-code and refresh-token persistence; refresh-token rotation and
replay detection; and consent. A defect in any of these is a credential-issuance
vulnerability affecting every consumer of the SDK. This is precisely the surface an MCP SDK
should not own.
- **RFC footprint.** A correct authorization server must implement and keep current with
RFC 6749 (OAuth 2.0), RFC 7591 (Dynamic Client Registration), RFC 8414 (Authorization
Server Metadata), PKCE (RFC 7636), and refresh-token rotation guidance, among others. That
is an open-ended maintenance and conformance burden far outside the SDK's purpose.
- **Mature implementations already exist.** Token issuance is a solved problem.
`league/oauth2-server` provides it as a PHP library, and every production IdP — Keycloak,
Auth0, Microsoft Entra ID, Okta — provides it as a service. Re-implementing it inside an
MCP SDK adds risk without adding value.

## Boundary statement

`OAuthProxyMiddleware` **delegates** to an upstream IdP. It is **not** authorization-server
scaffolding to be completed. Backing its `/authorize` and `/token` endpoints with a
first-party token issuer is out of scope and will be declined.

## Consequences

- The supported authorization architecture is: an external authorization server (your IdP or
`league/oauth2-server` running in your own application) issues tokens; the MCP server
validates them as a Resource Server and optionally proxies the OAuth flow to that upstream.
- Contributors get a single, citable ruling for why authorization-server PRs are declined,
reducing repeated large-PR churn.
- Resource Server and proxy/delegation features remain welcome and supported.

## Alternatives / what to do instead

If you need an authorization server (token issuance, client registration, login, consent), do
**not** add it to this SDK. Instead:

- **Front the MCP server with an existing IdP** — Keycloak, Auth0, Microsoft Entra ID, or
Okta. Point `JwtTokenValidator` and `ProtectedResourceMetadata` at that issuer, and
optionally use `OAuthProxyMiddleware` to delegate `/authorize` and `/token` to it.
- **Run `league/oauth2-server` in your own application**, behind the SDK's existing proxy and
validator seams. The MCP server validates the tokens it issues; it does not issue them
itself.

See [`../docs/authorization.md`](../docs/authorization.md) for the supported Resource Server
and delegation setup.
10 changes: 10 additions & 0 deletions adr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Architecture Decision Records

This directory holds Architecture Decision Records (ADRs): short documents that capture a
significant architectural decision, the context that led to it, and its consequences. They
exist so a decision can be cited once and referenced thereafter — for example, when declining
a pull request that conflicts with an accepted decision.

## Records

- [0001 — The MCP server is an OAuth Resource Server, not an Authorization Server](0001-oauth-authorization-server-out-of-scope.md)
20 changes: 19 additions & 1 deletion docs/authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ The PHP MCP SDK provides OAuth 2.1 authorization support for HTTP transports, im

## Table of Contents

- [Scope: what this SDK does and does not do](#scope-what-this-sdk-does-and-does-not-do)
- [Overview](#overview)
- [Quick Start](#quick-start)
- [Components](#components)
Expand All @@ -14,13 +15,30 @@ The PHP MCP SDK provides OAuth 2.1 authorization support for HTTP transports, im
- [Scope-Based Access Control](#scope-based-access-control)
- [Examples](#examples)

## Scope: what this SDK does and does not do

The MCP server is an OAuth 2.1 **Resource Server**. It validates the tokens it receives and may
delegate the OAuth flow to your upstream Identity Provider. **It is not an authorization server
and it does not issue tokens.**

| Role | What it does | Status | Scope |
|------|--------------|--------|-------|
| Resource Server | Validates incoming bearer tokens, serves Protected Resource Metadata (RFC 9728), emits `WWW-Authenticate` | Supported (`AuthorizationMiddleware`, `JwtTokenValidator`, `ProtectedResourceMetadata`) | **In scope** |
| Delegation / proxy to an upstream AS | Forwards `/authorize` and `/token` to your existing IdP | Supported (`OAuthProxyMiddleware`) | **In scope — delegation only** |
| Authorization Server / Identity Provider | Mints its own tokens, registers clients, runs login and consent | Not implemented | **Out of scope: being an authorization server / issuing tokens is out of scope** |

To issue tokens, front the MCP server with an external IdP (Keycloak, Auth0, Microsoft Entra
ID, Okta) or run `league/oauth2-server` in your own application, and let the MCP server
validate those tokens as a Resource Server. See
[adr/0001-oauth-authorization-server-out-of-scope.md](../adr/0001-oauth-authorization-server-out-of-scope.md).

## Overview

Authorization in MCP is implemented at the transport level using PSR-15 middleware. The SDK provides:

- **AuthorizationMiddleware** - PSR-15 middleware that enforces bearer token authentication
- **ProtectedResourceMetadataMiddleware** - Serves RFC 9728 metadata at well-known endpoints
- **OAuthProxyMiddleware** - Proxies OAuth flows to upstream authorization servers
- **OAuthProxyMiddleware** - Delegates OAuth flows (`/authorize`, `/token`) to your upstream IdP; the SDK never issues tokens itself
- **OAuthRequestMetaMiddleware** - Bridges HTTP OAuth attributes to JSON-RPC request meta
- **JwtTokenValidator** - Validates JWT tokens using JWKS from OAuth 2.0 / OIDC providers
- **OidcDiscovery** - Discovers authorization server metadata from well-known endpoints
Expand Down
19 changes: 12 additions & 7 deletions src/Server/Transport/Http/Middleware/OAuthProxyMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,20 @@
use Psr\Http\Server\RequestHandlerInterface;

/**
* Proxies OAuth authorization requests to an upstream authorization server.
* Delegates OAuth flows to an upstream authorization server so you do NOT have to build one.
*
* This middleware implements the OAuth proxy pattern for MCP servers that
* delegate authorization to third-party OAuth providers (Microsoft, Keycloak, etc.).
* This middleware exists precisely so the MCP server does not become an authorization server:
* it forwards the OAuth endpoints to your existing upstream Identity Provider (Microsoft Entra
* ID, Keycloak, Auth0, Okta, league/oauth2-server, etc.) instead of issuing tokens itself.
*
* It handles:
* - /authorize: Redirects to the upstream authorization server
* - /token: Proxies token requests to the upstream token endpoint
* - /.well-known/oauth-authorization-server: Serves authorization server metadata
* It delegates:
* - /authorize: Redirects the user agent to the upstream authorization endpoint
* - /token: Proxies the token request to the upstream token endpoint and returns its response
* - /.well-known/oauth-authorization-server: Serves metadata pointing at this delegating proxy
*
* Non-goals: this middleware never issues, mints, signs, stores, or rotates tokens, and the SDK
* is not an OAuth authorization server / Identity Provider. See
* adr/0001-oauth-authorization-server-out-of-scope.md.
*
* @author Volodymyr Panivko <sveneld300@gmail.com>
*/
Expand Down