mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-23 14:04:18 +02:00
Fix RFC 8414 path-aware authorization server metadata discovery (#2533)
* Fix RFC 8414 path-aware authorization server metadata discovery Override get_well_known_routes() in OAuthProvider to rewrite the authorization server metadata route to be path-aware based on issuer_url, matching how protected resource metadata already works. Closes #2527 * Update readme
This commit is contained in:
parent
ee63405262
commit
9cade6c8c8
8 changed files with 412 additions and 33 deletions
35
AGENTS.md
35
AGENTS.md
|
|
@ -20,24 +20,24 @@ uv run pytest # Run full test suite
|
|||
|
||||
## Repository Structure
|
||||
|
||||
| Path | Purpose |
|
||||
| ------------------ | --------------------------------------------------- |
|
||||
| `src/fastmcp/` | Library source code (Python ≥ 3.10) |
|
||||
| `├─server/` | Server implementation, `FastMCP`, auth, networking |
|
||||
| Path | Purpose |
|
||||
| ------------------ | ----------------------------------------------------------------------------------- |
|
||||
| `src/fastmcp/` | Library source code (Python ≥ 3.10) |
|
||||
| `├─server/` | Server implementation, `FastMCP`, auth, networking |
|
||||
| `│ ├─auth/` | Authentication providers (Google, GitHub, Azure, AWS, WorkOS, Auth0, JWT, and more) |
|
||||
| `│ └─middleware/` | Error handling, logging, rate limiting |
|
||||
| `├─client/` | High-level client SDK + transports |
|
||||
| `│ └─auth/` | Client authentication (Bearer, OAuth) |
|
||||
| `├─tools/` | Tool implementations + `ToolManager` |
|
||||
| `├─resources/` | Resources, templates + `ResourceManager` |
|
||||
| `├─prompts/` | Prompt templates + `PromptManager` |
|
||||
| `├─cli/` | FastMCP CLI commands (`run`, `dev`, `install`) |
|
||||
| `├─contrib/` | Community contributions (bulk caller, mixins) |
|
||||
| `├─experimental/` | Experimental features (sampling handlers) |
|
||||
| `└─utilities/` | Shared utilities (logging, JSON schema, HTTP) |
|
||||
| `tests/` | Comprehensive pytest suite with markers |
|
||||
| `docs/` | Mintlify documentation (published to gofastmcp.com) |
|
||||
| `examples/` | Runnable demo servers (echo, smart_home, atproto) |
|
||||
| `│ └─middleware/` | Error handling, logging, rate limiting |
|
||||
| `├─client/` | High-level client SDK + transports |
|
||||
| `│ └─auth/` | Client authentication (Bearer, OAuth) |
|
||||
| `├─tools/` | Tool implementations + `ToolManager` |
|
||||
| `├─resources/` | Resources, templates + `ResourceManager` |
|
||||
| `├─prompts/` | Prompt templates + `PromptManager` |
|
||||
| `├─cli/` | FastMCP CLI commands (`run`, `dev`, `install`) |
|
||||
| `├─contrib/` | Community contributions (bulk caller, mixins) |
|
||||
| `├─experimental/` | Experimental features (sampling handlers) |
|
||||
| `└─utilities/` | Shared utilities (logging, JSON schema, HTTP) |
|
||||
| `tests/` | Comprehensive pytest suite with markers |
|
||||
| `docs/` | Mintlify documentation (published to gofastmcp.com) |
|
||||
| `examples/` | Runnable demo servers (echo, smart_home, atproto) |
|
||||
|
||||
## Core MCP Objects
|
||||
|
||||
|
|
@ -106,6 +106,7 @@ async with Client(transport=StreamableHttpTransport(server_url)) as client:
|
|||
- Improvements = enhancements (not features) unless specified
|
||||
- **NEVER** force-push on collaborative repos
|
||||
- **ALWAYS** run prek before PRs
|
||||
- **NEVER** create a release, comment on an issue, or open a PR unless specifically instructed to do so.
|
||||
|
||||
### Commit Messages and Agent Attribution
|
||||
|
||||
|
|
|
|||
|
|
@ -328,8 +328,6 @@ OAuth specifications (RFC 8414 and RFC 9728) require discovery metadata to be ac
|
|||
# Result: /api/api/mcp (double prefix!)
|
||||
```
|
||||
|
||||
3. **Not setting issuer_url when mounting** - Without `issuer_url` set to root level, OAuth discovery will attempt path-scoped discovery first (which will 404), adding unnecessary error logs.
|
||||
|
||||
Follow the configuration instructions below to set up mounting correctly.
|
||||
</Warning>
|
||||
|
||||
|
|
@ -373,12 +371,15 @@ base_url="http://localhost:8000/api" # Includes mount prefix
|
|||
mcp_path="/mcp" # Internal MCP path, NOT the mount prefix
|
||||
```
|
||||
|
||||
**`issuer_url`** tells clients where to find discovery metadata. This should point to the root level of your server where well-known routes are mounted:
|
||||
**`issuer_url`** (optional) controls the authorization server identity for OAuth discovery. Defaults to `base_url`.
|
||||
|
||||
```python
|
||||
issuer_url="http://localhost:8000" # Root level, no prefix
|
||||
# Usually not needed - just set base_url and it works
|
||||
issuer_url="http://localhost:8000" # Only if you want root-level discovery
|
||||
```
|
||||
|
||||
When `issuer_url` has a path (either explicitly or by defaulting from `base_url`), FastMCP creates path-aware discovery routes per RFC 8414. For example, if `base_url` is `http://localhost:8000/api`, the authorization server metadata will be at `/.well-known/oauth-authorization-server/api`.
|
||||
|
||||
**Key Invariant:** `base_url + mcp_path = actual externally-accessible MCP URL`
|
||||
|
||||
Example:
|
||||
|
|
@ -404,14 +405,14 @@ MOUNT_PREFIX = "/api"
|
|||
MCP_PATH = "/mcp"
|
||||
```
|
||||
|
||||
Create the auth provider with both `issuer_url` and `base_url`:
|
||||
Create the auth provider with `base_url`:
|
||||
|
||||
```python
|
||||
auth = GitHubProvider(
|
||||
client_id="your-client-id",
|
||||
client_secret="your-client-secret",
|
||||
issuer_url=ROOT_URL, # Discovery metadata at root
|
||||
base_url=f"{ROOT_URL}{MOUNT_PREFIX}", # Operational endpoints under prefix
|
||||
# issuer_url defaults to base_url - path-aware discovery works automatically
|
||||
)
|
||||
```
|
||||
|
||||
|
|
@ -445,9 +446,11 @@ This configuration produces the following URL structure:
|
|||
- MCP endpoint: `http://localhost:8000/api/mcp`
|
||||
- OAuth authorization: `http://localhost:8000/api/authorize`
|
||||
- OAuth callback: `http://localhost:8000/api/auth/callback`
|
||||
- Authorization server metadata: `http://localhost:8000/.well-known/oauth-authorization-server`
|
||||
- Authorization server metadata: `http://localhost:8000/.well-known/oauth-authorization-server/api`
|
||||
- Protected resource metadata: `http://localhost:8000/.well-known/oauth-protected-resource/api/mcp`
|
||||
|
||||
Both discovery endpoints use path-aware URLs per RFC 8414 and RFC 9728, matching the `base_url` path.
|
||||
|
||||
### Complete Example
|
||||
|
||||
Here's a complete working example showing all the pieces together:
|
||||
|
|
@ -468,8 +471,8 @@ MCP_PATH = "/mcp"
|
|||
auth = GitHubProvider(
|
||||
client_id="your-client-id",
|
||||
client_secret="your-client-secret",
|
||||
issuer_url=ROOT_URL,
|
||||
base_url=f"{ROOT_URL}{MOUNT_PREFIX}",
|
||||
# issuer_url defaults to base_url - path-aware discovery works automatically
|
||||
)
|
||||
|
||||
# Create MCP server
|
||||
|
|
|
|||
|
|
@ -127,21 +127,24 @@ mcp = FastMCP(name="My Server", auth=auth)
|
|||
<ParamField body="issuer_url" type="AnyHttpUrl | str | None">
|
||||
Issuer URL for OAuth authorization server metadata (defaults to `base_url`).
|
||||
|
||||
When mounting your MCP server under a path prefix (e.g., `/api`), set this to your root-level URL to avoid 404 logs during OAuth discovery. MCP clients try path-scoped discovery first per RFC 8414, which will fail if your auth server metadata is at the root level.
|
||||
When `issuer_url` has a path component (either explicitly or by defaulting from `base_url`), FastMCP creates path-aware discovery routes per RFC 8414. For example, if `base_url` is `http://localhost:8000/api`, the authorization server metadata will be at `/.well-known/oauth-authorization-server/api`.
|
||||
|
||||
**Example with mounting:**
|
||||
**Default behavior (recommended for most cases):**
|
||||
```python
|
||||
auth = GitHubProvider(
|
||||
base_url="http://localhost:8000/api", # OAuth endpoints under /api
|
||||
issuer_url="http://localhost:8000" # Auth server metadata at root
|
||||
# issuer_url defaults to base_url - path-aware discovery works automatically
|
||||
)
|
||||
```
|
||||
|
||||
Without `issuer_url`, clients will attempt `/.well-known/oauth-authorization-server/api` (404) before falling back to `/.well-known/oauth-authorization-server` (success). Setting `issuer_url` to the root eliminates the 404 attempt.
|
||||
|
||||
**When to use:**
|
||||
- **Default (`None`)**: Use `base_url` as issuer - simple deployments at root path
|
||||
- **Root-level URL**: Mounting under a path prefix - avoids 404 logs
|
||||
**When to set explicitly:**
|
||||
Set `issuer_url` to root level only if you want multiple MCP servers to share a single discovery endpoint:
|
||||
```python
|
||||
auth = GitHubProvider(
|
||||
base_url="http://localhost:8000/api",
|
||||
issuer_url="http://localhost:8000" # Shared root-level discovery
|
||||
)
|
||||
```
|
||||
|
||||
See the [HTTP Deployment guide](/deployment/http#mounting-authenticated-servers) for complete mounting examples.
|
||||
</ParamField>
|
||||
|
|
|
|||
39
examples/auth/mounted/README.md
Normal file
39
examples/auth/mounted/README.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# Multi-Provider OAuth Example
|
||||
|
||||
This example demonstrates mounting multiple OAuth-protected MCP servers in a single application, each with its own OAuth provider. It showcases RFC 8414 path-aware discovery where each server has its own authorization server metadata endpoint.
|
||||
|
||||
## URL Structure
|
||||
|
||||
- **GitHub MCP**: `http://localhost:8000/api/mcp/github/mcp`
|
||||
- **Google MCP**: `http://localhost:8000/api/mcp/google/mcp`
|
||||
|
||||
Discovery endpoints (RFC 8414 path-aware):
|
||||
- **GitHub**: `http://localhost:8000/.well-known/oauth-authorization-server/api/mcp/github`
|
||||
- **Google**: `http://localhost:8000/.well-known/oauth-authorization-server/api/mcp/google`
|
||||
|
||||
## Setup
|
||||
|
||||
Set environment variables for both providers:
|
||||
|
||||
```bash
|
||||
export FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID="your-github-client-id"
|
||||
export FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET="your-github-client-secret"
|
||||
export FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_ID="your-google-client-id"
|
||||
export FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_SECRET="your-google-client-secret"
|
||||
```
|
||||
|
||||
Configure redirect URIs in each provider's developer console (note the `/api/mcp/{provider}` prefix since the servers are mounted):
|
||||
- GitHub: `http://localhost:8000/api/mcp/github/auth/callback/github`
|
||||
- Google: `http://localhost:8000/api/mcp/google/auth/callback/google`
|
||||
|
||||
## Running
|
||||
|
||||
Start the server:
|
||||
```bash
|
||||
python server.py
|
||||
```
|
||||
|
||||
Connect with the client:
|
||||
```bash
|
||||
python client.py
|
||||
```
|
||||
50
examples/auth/mounted/client.py
Normal file
50
examples/auth/mounted/client.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
"""Mounted OAuth servers client example for FastMCP.
|
||||
|
||||
This example demonstrates connecting to multiple mounted OAuth-protected MCP servers.
|
||||
|
||||
To run:
|
||||
python client.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from fastmcp.client import Client
|
||||
|
||||
GITHUB_URL = "http://127.0.0.1:8000/api/mcp/github/mcp"
|
||||
GOOGLE_URL = "http://127.0.0.1:8000/api/mcp/google/mcp"
|
||||
|
||||
|
||||
async def main():
|
||||
# Connect to GitHub server
|
||||
print("\n--- GitHub Server ---")
|
||||
try:
|
||||
async with Client(GITHUB_URL, auth="oauth") as client:
|
||||
assert await client.ping()
|
||||
print("✅ Successfully authenticated!")
|
||||
|
||||
tools = await client.list_tools()
|
||||
print(f"🔧 Available tools ({len(tools)}):")
|
||||
for tool in tools:
|
||||
print(f" - {tool.name}: {tool.description}")
|
||||
except Exception as e:
|
||||
print(f"❌ Authentication failed: {e}")
|
||||
raise
|
||||
|
||||
# Connect to Google server
|
||||
print("\n--- Google Server ---")
|
||||
try:
|
||||
async with Client(GOOGLE_URL, auth="oauth") as client:
|
||||
assert await client.ping()
|
||||
print("✅ Successfully authenticated!")
|
||||
|
||||
tools = await client.list_tools()
|
||||
print(f"🔧 Available tools ({len(tools)}):")
|
||||
for tool in tools:
|
||||
print(f" - {tool.name}: {tool.description}")
|
||||
except Exception as e:
|
||||
print(f"❌ Authentication failed: {e}")
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
121
examples/auth/mounted/server.py
Normal file
121
examples/auth/mounted/server.py
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
"""Mounted OAuth servers example for FastMCP.
|
||||
|
||||
This example demonstrates mounting multiple OAuth-protected MCP servers in a single
|
||||
application, each with its own provider. It showcases RFC 8414 path-aware discovery
|
||||
where each server has its own authorization server metadata endpoint.
|
||||
|
||||
URL structure:
|
||||
- GitHub MCP: http://localhost:8000/api/mcp/github/mcp
|
||||
- Google MCP: http://localhost:8000/api/mcp/google/mcp
|
||||
- GitHub discovery: http://localhost:8000/.well-known/oauth-authorization-server/api/mcp/github
|
||||
- Google discovery: http://localhost:8000/.well-known/oauth-authorization-server/api/mcp/google
|
||||
|
||||
Required environment variables:
|
||||
- FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID: Your GitHub OAuth app client ID
|
||||
- FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET: Your GitHub OAuth app client secret
|
||||
- FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_ID: Your Google OAuth client ID
|
||||
- FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_SECRET: Your Google OAuth client secret
|
||||
|
||||
To run:
|
||||
python server.py
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import uvicorn
|
||||
from starlette.applications import Starlette
|
||||
from starlette.routing import Mount
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth.providers.github import GitHubProvider
|
||||
from fastmcp.server.auth.providers.google import GoogleProvider
|
||||
|
||||
# Configuration
|
||||
ROOT_URL = "http://localhost:8000"
|
||||
API_PREFIX = "/api/mcp"
|
||||
|
||||
# --- GitHub OAuth Server ---
|
||||
github_auth = GitHubProvider(
|
||||
client_id=os.getenv("FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID") or "",
|
||||
client_secret=os.getenv("FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET") or "",
|
||||
base_url=f"{ROOT_URL}{API_PREFIX}/github",
|
||||
redirect_path="/auth/callback/github",
|
||||
)
|
||||
|
||||
github_mcp = FastMCP("GitHub Server", auth=github_auth)
|
||||
|
||||
|
||||
@github_mcp.tool
|
||||
def github_echo(message: str) -> str:
|
||||
"""Echo from the GitHub-authenticated server."""
|
||||
return f"[GitHub] {message}"
|
||||
|
||||
|
||||
@github_mcp.tool
|
||||
def github_info() -> str:
|
||||
"""Get info about the GitHub server."""
|
||||
return "This is the GitHub OAuth protected MCP server"
|
||||
|
||||
|
||||
# --- Google OAuth Server ---
|
||||
google_auth = GoogleProvider(
|
||||
client_id=os.getenv("FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_ID") or "",
|
||||
client_secret=os.getenv("FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_SECRET") or "",
|
||||
base_url=f"{ROOT_URL}{API_PREFIX}/google",
|
||||
redirect_path="/auth/callback/google",
|
||||
)
|
||||
|
||||
google_mcp = FastMCP("Google Server", auth=google_auth)
|
||||
|
||||
|
||||
@google_mcp.tool
|
||||
def google_echo(message: str) -> str:
|
||||
"""Echo from the Google-authenticated server."""
|
||||
return f"[Google] {message}"
|
||||
|
||||
|
||||
@google_mcp.tool
|
||||
def google_info() -> str:
|
||||
"""Get info about the Google server."""
|
||||
return "This is the Google OAuth protected MCP server"
|
||||
|
||||
|
||||
# --- Create ASGI apps ---
|
||||
github_app = github_mcp.http_app(path="/mcp")
|
||||
google_app = google_mcp.http_app(path="/mcp")
|
||||
|
||||
# Get well-known routes for each provider (path-aware per RFC 8414)
|
||||
github_well_known = github_auth.get_well_known_routes(mcp_path="/mcp")
|
||||
google_well_known = google_auth.get_well_known_routes(mcp_path="/mcp")
|
||||
|
||||
# --- Combine into single application ---
|
||||
# Note: Each provider has its own path-aware discovery endpoint:
|
||||
# - /.well-known/oauth-authorization-server/api/mcp/github
|
||||
# - /.well-known/oauth-authorization-server/api/mcp/google
|
||||
app = Starlette(
|
||||
routes=[
|
||||
# Well-known routes at root level (path-aware)
|
||||
*github_well_known,
|
||||
*google_well_known,
|
||||
# MCP servers under /api/mcp prefix
|
||||
Mount(f"{API_PREFIX}/github", app=github_app),
|
||||
Mount(f"{API_PREFIX}/google", app=google_app),
|
||||
],
|
||||
# Use one of the app lifespans (they're functionally equivalent)
|
||||
lifespan=github_app.lifespan,
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Starting mounted OAuth servers...")
|
||||
print(f" GitHub MCP: {ROOT_URL}{API_PREFIX}/github/mcp")
|
||||
print(f" Google MCP: {ROOT_URL}{API_PREFIX}/google/mcp")
|
||||
print()
|
||||
print("Discovery endpoints (RFC 8414 path-aware):")
|
||||
print(
|
||||
f" GitHub: {ROOT_URL}/.well-known/oauth-authorization-server{API_PREFIX}/github"
|
||||
)
|
||||
print(
|
||||
f" Google: {ROOT_URL}/.well-known/oauth-authorization-server{API_PREFIX}/google"
|
||||
)
|
||||
print()
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Any, cast
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from mcp.server.auth.middleware.auth_context import AuthContextMiddleware
|
||||
from mcp.server.auth.middleware.bearer_auth import BearerAuthBackend
|
||||
|
|
@ -397,3 +398,51 @@ class OAuthProvider(
|
|||
oauth_routes.extend(super().get_routes(mcp_path))
|
||||
|
||||
return oauth_routes
|
||||
|
||||
def get_well_known_routes(
|
||||
self,
|
||||
mcp_path: str | None = None,
|
||||
) -> list[Route]:
|
||||
"""Get well-known discovery routes with RFC 8414 path-aware support.
|
||||
|
||||
Overrides the base implementation to support path-aware authorization
|
||||
server metadata discovery per RFC 8414. If issuer_url has a path component,
|
||||
the authorization server metadata route is adjusted to include that path.
|
||||
|
||||
For example, if issuer_url is "http://example.com/api", the discovery
|
||||
endpoint will be at "/.well-known/oauth-authorization-server/api" instead
|
||||
of just "/.well-known/oauth-authorization-server".
|
||||
|
||||
Args:
|
||||
mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
|
||||
|
||||
Returns:
|
||||
List of well-known discovery routes
|
||||
"""
|
||||
routes = super().get_well_known_routes(mcp_path)
|
||||
|
||||
# RFC 8414: If issuer_url has a path, use path-aware discovery
|
||||
if self.issuer_url:
|
||||
parsed = urlparse(str(self.issuer_url))
|
||||
issuer_path = parsed.path.rstrip("/")
|
||||
|
||||
if issuer_path and issuer_path != "/":
|
||||
# Replace /.well-known/oauth-authorization-server with path-aware version
|
||||
new_routes = []
|
||||
for route in routes:
|
||||
if route.path == "/.well-known/oauth-authorization-server":
|
||||
new_path = (
|
||||
f"/.well-known/oauth-authorization-server{issuer_path}"
|
||||
)
|
||||
new_routes.append(
|
||||
Route(
|
||||
new_path,
|
||||
endpoint=route.endpoint,
|
||||
methods=route.methods,
|
||||
)
|
||||
)
|
||||
else:
|
||||
new_routes.append(route)
|
||||
return new_routes
|
||||
|
||||
return routes
|
||||
|
|
|
|||
|
|
@ -265,3 +265,116 @@ class TestOAuthMounting:
|
|||
"https://api.example.com/api",
|
||||
"https://api.example.com/api/",
|
||||
]
|
||||
|
||||
async def test_oauth_authorization_server_metadata_path_aware_discovery(
|
||||
self, test_tokens
|
||||
):
|
||||
"""Test RFC 8414 path-aware discovery when issuer_url has a path.
|
||||
|
||||
This validates the fix for issue #2527 where authorization server metadata
|
||||
should be exposed at a path-aware URL when issuer_url has a path component.
|
||||
|
||||
When issuer_url defaults to base_url (e.g., http://example.com/api), the
|
||||
authorization server metadata should be at:
|
||||
/.well-known/oauth-authorization-server/api
|
||||
|
||||
This is consistent with how protected resource metadata already works
|
||||
(RFC 9728) and complies with RFC 8414 path-aware discovery.
|
||||
"""
|
||||
# Create OAuth proxy where issuer_url defaults to base_url (which has a path)
|
||||
token_verifier = StaticTokenVerifier(tokens=test_tokens)
|
||||
auth_provider = OAuthProxy(
|
||||
upstream_authorization_endpoint="https://upstream.example.com/authorize",
|
||||
upstream_token_endpoint="https://upstream.example.com/token",
|
||||
upstream_client_id="test-client-id",
|
||||
upstream_client_secret="test-client-secret",
|
||||
token_verifier=token_verifier,
|
||||
base_url="https://api.example.com/api", # Has path, no explicit issuer_url
|
||||
)
|
||||
|
||||
mcp = FastMCP("test-server", auth=auth_provider)
|
||||
mcp_app = mcp.http_app(path="/mcp")
|
||||
|
||||
# Get well-known routes - should include path-aware authorization server metadata
|
||||
well_known_routes = auth_provider.get_well_known_routes(mcp_path="/mcp")
|
||||
|
||||
# Find the authorization server metadata route
|
||||
auth_server_routes = [
|
||||
r for r in well_known_routes if "oauth-authorization-server" in r.path
|
||||
]
|
||||
assert len(auth_server_routes) == 1
|
||||
|
||||
# The route should be path-aware (RFC 8414)
|
||||
assert (
|
||||
auth_server_routes[0].path == "/.well-known/oauth-authorization-server/api"
|
||||
)
|
||||
|
||||
# Find the protected resource metadata route for comparison
|
||||
protected_resource_routes = [
|
||||
r for r in well_known_routes if "oauth-protected-resource" in r.path
|
||||
]
|
||||
assert len(protected_resource_routes) == 1
|
||||
# Protected resource should also be path-aware (RFC 9728)
|
||||
assert (
|
||||
protected_resource_routes[0].path
|
||||
== "/.well-known/oauth-protected-resource/api/mcp"
|
||||
)
|
||||
|
||||
# Mount the app and verify the routes are accessible
|
||||
parent_app = Starlette(
|
||||
routes=[
|
||||
*well_known_routes,
|
||||
Mount("/api", app=mcp_app),
|
||||
],
|
||||
lifespan=mcp_app.lifespan,
|
||||
)
|
||||
|
||||
async with httpx.AsyncClient(
|
||||
transport=httpx.ASGITransport(app=parent_app),
|
||||
base_url="https://api.example.com",
|
||||
) as client:
|
||||
# Path-aware authorization server metadata should be accessible
|
||||
response = await client.get("/.well-known/oauth-authorization-server/api")
|
||||
assert response.status_code == 200
|
||||
|
||||
metadata = response.json()
|
||||
assert (
|
||||
metadata["authorization_endpoint"]
|
||||
== "https://api.example.com/api/authorize"
|
||||
)
|
||||
assert metadata["token_endpoint"] == "https://api.example.com/api/token"
|
||||
|
||||
# Path-aware protected resource metadata should also work
|
||||
response = await client.get("/.well-known/oauth-protected-resource/api/mcp")
|
||||
assert response.status_code == 200
|
||||
|
||||
async def test_oauth_authorization_server_metadata_root_issuer(self, test_tokens):
|
||||
"""Test that root-level issuer_url still uses root discovery path.
|
||||
|
||||
When issuer_url is explicitly set to root (no path), the authorization
|
||||
server metadata should remain at the root path:
|
||||
/.well-known/oauth-authorization-server
|
||||
|
||||
This maintains backwards compatibility with the documented mounting pattern.
|
||||
"""
|
||||
token_verifier = StaticTokenVerifier(tokens=test_tokens)
|
||||
auth_provider = OAuthProxy(
|
||||
upstream_authorization_endpoint="https://upstream.example.com/authorize",
|
||||
upstream_token_endpoint="https://upstream.example.com/token",
|
||||
upstream_client_id="test-client-id",
|
||||
upstream_client_secret="test-client-secret",
|
||||
token_verifier=token_verifier,
|
||||
base_url="https://api.example.com/api",
|
||||
issuer_url="https://api.example.com", # Explicitly root
|
||||
)
|
||||
|
||||
well_known_routes = auth_provider.get_well_known_routes(mcp_path="/mcp")
|
||||
|
||||
# Find the authorization server metadata route
|
||||
auth_server_routes = [
|
||||
r for r in well_known_routes if "oauth-authorization-server" in r.path
|
||||
]
|
||||
assert len(auth_server_routes) == 1
|
||||
|
||||
# Should be at root (no path suffix) when issuer_url is root
|
||||
assert auth_server_routes[0].path == "/.well-known/oauth-authorization-server"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue