docs(adr): scope OAuth to resource server, not authorization server (#382)
Record ADR 0001: the MCP server is an OAuth 2.1 Resource Server that MAY delegate to an upstream authorization server, and will NOT issue tokens or act as an Identity Provider. Gives a single citable ruling to decline the recurring "native authorization server" PRs (e.g. #373). Also remove the surface signal that invites them: reframe the OAuthProxyMiddleware class docblock to lead with its delegation intent and state explicit non-goals (never mints/signs/stores/rotates tokens), and add a "Scope" section to docs/authorization.md describing the three OAuth roles and what the SDK does not do. No behavior change — docblock, docs, and ADR only.
This commit is contained in:
parent
df5379c047
commit
1587ac5bd2
4 changed files with 138 additions and 8 deletions
97
adr/0001-oauth-authorization-server-out-of-scope.md
Normal file
97
adr/0001-oauth-authorization-server-out-of-scope.md
Normal file
|
|
@ -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
adr/README.md
Normal file
10
adr/README.md
Normal file
|
|
@ -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)
|
||||||
|
|
@ -5,6 +5,7 @@ The PHP MCP SDK provides OAuth 2.1 authorization support for HTTP transports, im
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Scope: what this SDK does and does not do](#scope-what-this-sdk-does-and-does-not-do)
|
||||||
- [Overview](#overview)
|
- [Overview](#overview)
|
||||||
- [Quick Start](#quick-start)
|
- [Quick Start](#quick-start)
|
||||||
- [Components](#components)
|
- [Components](#components)
|
||||||
|
|
@ -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)
|
- [Scope-Based Access Control](#scope-based-access-control)
|
||||||
- [Examples](#examples)
|
- [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
|
## Overview
|
||||||
|
|
||||||
Authorization in MCP is implemented at the transport level using PSR-15 middleware. The SDK provides:
|
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
|
- **AuthorizationMiddleware** - PSR-15 middleware that enforces bearer token authentication
|
||||||
- **ProtectedResourceMetadataMiddleware** - Serves RFC 9728 metadata at well-known endpoints
|
- **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
|
- **OAuthRequestMetaMiddleware** - Bridges HTTP OAuth attributes to JSON-RPC request meta
|
||||||
- **JwtTokenValidator** - Validates JWT tokens using JWKS from OAuth 2.0 / OIDC providers
|
- **JwtTokenValidator** - Validates JWT tokens using JWKS from OAuth 2.0 / OIDC providers
|
||||||
- **OidcDiscovery** - Discovers authorization server metadata from well-known endpoints
|
- **OidcDiscovery** - Discovers authorization server metadata from well-known endpoints
|
||||||
|
|
|
||||||
|
|
@ -26,15 +26,20 @@ use Psr\Http\Server\MiddlewareInterface;
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
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
|
* This middleware exists precisely so the MCP server does not become an authorization server:
|
||||||
* delegate authorization to third-party OAuth providers (Microsoft, Keycloak, etc.).
|
* 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:
|
* It delegates:
|
||||||
* - /authorize: Redirects to the upstream authorization server
|
* - /authorize: Redirects the user agent to the upstream authorization endpoint
|
||||||
* - /token: Proxies token requests to the upstream token endpoint
|
* - /token: Proxies the token request to the upstream token endpoint and returns its response
|
||||||
* - /.well-known/oauth-authorization-server: Serves authorization server metadata
|
* - /.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>
|
* @author Volodymyr Panivko <sveneld300@gmail.com>
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue