Skip to content

Commit 21dbeb0

Browse files
docs: complete concepts, authorization, and low-level server pages
1 parent 66aaf93 commit 21dbeb0

File tree

3 files changed

+1057
-13
lines changed

3 files changed

+1057
-13
lines changed

docs/authorization.md

Lines changed: 360 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,363 @@
11
# Authorization
22

3-
!!! warning "Under Construction"
3+
This page covers how the MCP Python SDK implements OAuth 2.1 authorization for protecting MCP servers and authenticating clients.
44

5-
This page is currently being written. Check back soon for complete documentation.
5+
## OAuth 2.1 Overview
6+
7+
MCP uses [OAuth 2.1](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization) to authorize access to protected servers. The architecture follows a separation between Authorization Servers and Resource Servers:
8+
9+
- **Authorization Server (AS)**: Handles OAuth flows, user authentication, and token issuance.
10+
- **Resource Server (RS)**: Your MCP server, which validates tokens issued by the AS and serves protected resources.
11+
- **Client**: Discovers the AS through [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) (Protected Resource Metadata), obtains tokens, and uses them with the MCP server.
12+
13+
The typical flow is:
14+
15+
1. The client connects to the MCP server and receives a `401 Unauthorized` response.
16+
2. The client discovers the Authorization Server via Protected Resource Metadata.
17+
3. The client performs an OAuth flow (authorization code with PKCE, or client credentials) to obtain an access token.
18+
4. The client includes the access token in subsequent requests to the MCP server.
19+
5. The MCP server validates the token and serves the request.
20+
21+
## Server-Side Authorization
22+
23+
### TokenVerifier
24+
25+
The simplest way to add authentication to a FastMCP server is by providing a `TokenVerifier` implementation. The server acts as a Resource Server that validates bearer tokens issued by a separate Authorization Server:
26+
27+
```python
28+
from pydantic import AnyHttpUrl
29+
30+
from mcp.server.auth.provider import AccessToken, TokenVerifier
31+
from mcp.server.auth.settings import AuthSettings
32+
from mcp.server.fastmcp import FastMCP
33+
34+
35+
class MyTokenVerifier(TokenVerifier):
36+
"""Verify tokens issued by your Authorization Server."""
37+
38+
async def verify_token(self, token: str) -> AccessToken | None:
39+
# Validate the token (e.g., verify JWT signature, check expiry,
40+
# call an introspection endpoint, etc.)
41+
# Return an AccessToken if valid, or None to reject.
42+
if is_valid(token):
43+
return AccessToken(
44+
token=token,
45+
client_id="client-123",
46+
scopes=["user"],
47+
)
48+
return None
49+
50+
51+
mcp = FastMCP(
52+
"Protected Server",
53+
token_verifier=MyTokenVerifier(),
54+
auth=AuthSettings(
55+
issuer_url=AnyHttpUrl("https://auth.example.com"),
56+
resource_server_url=AnyHttpUrl("http://localhost:8000"),
57+
required_scopes=["user"],
58+
),
59+
)
60+
61+
62+
@mcp.tool()
63+
async def get_secret_data() -> str:
64+
"""This tool requires authentication."""
65+
return "secret data"
66+
67+
68+
if __name__ == "__main__":
69+
mcp.run(transport="streamable-http")
70+
```
71+
72+
The `TokenVerifier` protocol requires a single method:
73+
74+
- `verify_token(token: str) -> AccessToken | None`: Receives the raw bearer token string. Return an `AccessToken` with the token's metadata if valid, or `None` to reject the request.
75+
76+
### AuthSettings
77+
78+
`AuthSettings` configures the server's Protected Resource Metadata (RFC 9728), which tells clients how to discover the Authorization Server:
79+
80+
```python
81+
from mcp.server.auth.settings import AuthSettings, ClientRegistrationOptions
82+
83+
auth = AuthSettings(
84+
# The OAuth Authorization Server that issues tokens for this server
85+
issuer_url=AnyHttpUrl("https://auth.example.com"),
86+
# This server's URL (used as the resource identifier)
87+
resource_server_url=AnyHttpUrl("https://api.example.com"),
88+
# Scopes required for accessing this server
89+
required_scopes=["read", "write"],
90+
# Optional: documentation URL
91+
service_documentation_url=AnyHttpUrl("https://docs.example.com"),
92+
)
93+
```
94+
95+
Key settings:
96+
97+
| Setting | Description |
98+
|---------|-------------|
99+
| `issuer_url` | The Authorization Server URL that issues tokens |
100+
| `resource_server_url` | This MCP server's URL, used as the resource identifier |
101+
| `required_scopes` | Scopes that must be present in the access token |
102+
| `service_documentation_url` | Optional URL to documentation for the API |
103+
| `client_registration_options` | Optional settings for dynamic client registration |
104+
| `revocation_options` | Optional settings for token revocation |
105+
106+
### OAuthAuthorizationServerProvider
107+
108+
For servers that need to act as both the Authorization Server and the Resource Server, implement the `OAuthAuthorizationServerProvider` protocol. This is more complex than the `TokenVerifier` approach and is typically only needed when you want a self-contained auth solution without an external AS:
109+
110+
```python
111+
from mcp.server.auth.provider import (
112+
AccessToken,
113+
AuthorizationCode,
114+
AuthorizationParams,
115+
OAuthAuthorizationServerProvider,
116+
RefreshToken,
117+
)
118+
from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
119+
120+
121+
class MyAuthProvider(OAuthAuthorizationServerProvider):
122+
async def get_client(self, client_id: str) -> OAuthClientInformationFull | None:
123+
"""Look up a registered client by ID."""
124+
...
125+
126+
async def register_client(self, client_info: OAuthClientInformationFull) -> None:
127+
"""Register a new OAuth client (dynamic client registration)."""
128+
...
129+
130+
async def authorize(
131+
self, client: OAuthClientInformationFull, params: AuthorizationParams
132+
) -> str:
133+
"""Handle authorization and return a redirect URL."""
134+
...
135+
136+
async def load_authorization_code(
137+
self, client: OAuthClientInformationFull, authorization_code: str
138+
) -> AuthorizationCode | None:
139+
"""Load a previously issued authorization code."""
140+
...
141+
142+
async def exchange_authorization_code(
143+
self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode
144+
) -> OAuthToken:
145+
"""Exchange an authorization code for tokens."""
146+
...
147+
148+
async def load_access_token(self, token: str) -> AccessToken | None:
149+
"""Validate and load an access token."""
150+
...
151+
152+
async def load_refresh_token(
153+
self, client: OAuthClientInformationFull, refresh_token: str
154+
) -> RefreshToken | None:
155+
"""Load a refresh token."""
156+
...
157+
158+
async def exchange_refresh_token(
159+
self, client: OAuthClientInformationFull, refresh_token: RefreshToken
160+
) -> OAuthToken:
161+
"""Exchange a refresh token for new tokens."""
162+
...
163+
164+
async def revoke_token(
165+
self, client: OAuthClientInformationFull, token: str, token_type_hint: str | None = None
166+
) -> None:
167+
"""Revoke an access or refresh token."""
168+
...
169+
```
170+
171+
## Client-Side Authorization
172+
173+
### OAuthClientProvider
174+
175+
`OAuthClientProvider` is an `httpx.Auth` subclass that handles the full OAuth flow for MCP clients. It manages Protected Resource Metadata discovery, client registration, authorization code exchange with PKCE, token storage, and automatic token refresh:
176+
177+
```python
178+
import asyncio
179+
from urllib.parse import parse_qs, urlparse
180+
181+
import httpx
182+
from pydantic import AnyUrl
183+
184+
from mcp import ClientSession
185+
from mcp.client.auth import OAuthClientProvider, TokenStorage
186+
from mcp.client.streamable_http import streamable_http_client
187+
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken
188+
189+
190+
class InMemoryTokenStorage(TokenStorage):
191+
"""Simple in-memory token storage."""
192+
193+
def __init__(self):
194+
self.tokens: OAuthToken | None = None
195+
self.client_info: OAuthClientInformationFull | None = None
196+
197+
async def get_tokens(self) -> OAuthToken | None:
198+
return self.tokens
199+
200+
async def set_tokens(self, tokens: OAuthToken) -> None:
201+
self.tokens = tokens
202+
203+
async def get_client_info(self) -> OAuthClientInformationFull | None:
204+
return self.client_info
205+
206+
async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
207+
self.client_info = client_info
208+
209+
210+
async def handle_redirect(auth_url: str) -> None:
211+
"""Handle the authorization redirect (e.g., open browser)."""
212+
print(f"Visit this URL to authorize: {auth_url}")
213+
214+
215+
async def handle_callback() -> tuple[str, str | None]:
216+
"""Handle the callback after authorization."""
217+
callback_url = input("Paste the callback URL: ")
218+
params = parse_qs(urlparse(callback_url).query)
219+
return params["code"][0], params.get("state", [None])[0]
220+
221+
222+
async def main():
223+
oauth_auth = OAuthClientProvider(
224+
server_url="http://localhost:8000",
225+
client_metadata=OAuthClientMetadata(
226+
client_name="My MCP Client",
227+
redirect_uris=[AnyUrl("http://localhost:3000/callback")],
228+
grant_types=["authorization_code", "refresh_token"],
229+
response_types=["code"],
230+
scope="user",
231+
),
232+
storage=InMemoryTokenStorage(),
233+
redirect_handler=handle_redirect,
234+
callback_handler=handle_callback,
235+
)
236+
237+
async with httpx.AsyncClient(auth=oauth_auth, follow_redirects=True) as http_client:
238+
async with streamable_http_client(
239+
"http://localhost:8000/mcp", http_client=http_client
240+
) as (read, write, _):
241+
async with ClientSession(read, write) as session:
242+
await session.initialize()
243+
tools = await session.list_tools()
244+
print(f"Tools: {[t.name for t in tools.tools]}")
245+
246+
247+
asyncio.run(main())
248+
```
249+
250+
The `OAuthClientProvider` constructor takes these parameters:
251+
252+
| Parameter | Description |
253+
|-----------|-------------|
254+
| `server_url` | The MCP server URL |
255+
| `client_metadata` | OAuth client metadata (name, redirect URIs, grant types, scopes) |
256+
| `storage` | A `TokenStorage` implementation for persisting tokens and client info |
257+
| `redirect_handler` | Async callback invoked with the authorization URL (open a browser) |
258+
| `callback_handler` | Async callback that returns the authorization code from the callback URL |
259+
| `timeout` | Timeout for the OAuth flow in seconds (default: 300) |
260+
261+
### TokenStorage
262+
263+
The `TokenStorage` protocol defines how tokens and client registration info are persisted. Implement this to store tokens in a database, file system, or any other backend:
264+
265+
```python
266+
from mcp.client.auth import TokenStorage
267+
from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
268+
269+
270+
class FileTokenStorage(TokenStorage):
271+
async def get_tokens(self) -> OAuthToken | None:
272+
"""Load tokens from disk."""
273+
...
274+
275+
async def set_tokens(self, tokens: OAuthToken) -> None:
276+
"""Save tokens to disk."""
277+
...
278+
279+
async def get_client_info(self) -> OAuthClientInformationFull | None:
280+
"""Load client registration info from disk."""
281+
...
282+
283+
async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
284+
"""Save client registration info to disk."""
285+
...
286+
```
287+
288+
The storage is called during the OAuth flow to persist and retrieve tokens and client information, enabling token reuse across sessions.
289+
290+
## Client Credentials Extension
291+
292+
For service-to-service authentication where no user interaction is needed, use `ClientCredentialsOAuthProvider`. This implements the OAuth 2.1 client credentials grant:
293+
294+
```python
295+
from mcp.client.auth import TokenStorage
296+
from mcp.client.auth.extensions.client_credentials import ClientCredentialsOAuthProvider
297+
298+
299+
provider = ClientCredentialsOAuthProvider(
300+
server_url="https://api.example.com",
301+
storage=my_token_storage,
302+
client_id="my-service-client-id",
303+
client_secret="my-service-client-secret",
304+
scopes="read write",
305+
)
306+
```
307+
308+
This provider skips dynamic client registration and the authorization code flow entirely. It directly exchanges the client credentials for an access token at the token endpoint.
309+
310+
### PrivateKeyJWTOAuthProvider
311+
312+
For client credentials authentication using JWT assertions (RFC 7523), use `PrivateKeyJWTOAuthProvider`. This is common in workload identity federation scenarios:
313+
314+
```python
315+
from mcp.client.auth.extensions.client_credentials import (
316+
PrivateKeyJWTOAuthProvider,
317+
SignedJWTParameters,
318+
)
319+
320+
# Option 1: SDK-signed JWT (for testing or simple setups)
321+
jwt_params = SignedJWTParameters(
322+
issuer="my-client-id",
323+
subject="my-client-id",
324+
signing_key=private_key_pem,
325+
)
326+
327+
provider = PrivateKeyJWTOAuthProvider(
328+
server_url="https://api.example.com",
329+
storage=my_token_storage,
330+
client_id="my-client-id",
331+
assertion_provider=jwt_params.create_assertion_provider(),
332+
)
333+
```
334+
335+
```python
336+
# Option 2: Workload identity federation (production)
337+
async def get_workload_identity_token(audience: str) -> str:
338+
# Fetch JWT from your identity provider (GCP, AWS IAM, Azure AD)
339+
return await fetch_token_from_identity_provider(audience=audience)
340+
341+
provider = PrivateKeyJWTOAuthProvider(
342+
server_url="https://api.example.com",
343+
storage=my_token_storage,
344+
client_id="my-client-id",
345+
assertion_provider=get_workload_identity_token,
346+
)
347+
```
348+
349+
Both `ClientCredentialsOAuthProvider` and `PrivateKeyJWTOAuthProvider` are used the same way as `OAuthClientProvider` -- pass them as the `auth` parameter to an `httpx.AsyncClient`:
350+
351+
```python
352+
import httpx
353+
from mcp import ClientSession
354+
from mcp.client.streamable_http import streamable_http_client
355+
356+
async with httpx.AsyncClient(auth=provider, follow_redirects=True) as http_client:
357+
async with streamable_http_client(
358+
"https://api.example.com/mcp", http_client=http_client
359+
) as (read, write, _):
360+
async with ClientSession(read, write) as session:
361+
await session.initialize()
362+
# Use the session normally
363+
```

0 commit comments

Comments
 (0)