fix(auth): add /.well-known/openid-configuration alias for OAuth server metadata (#4167)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Shigechika AIKAWA 2026-05-20 23:36:01 +09:00 committed by GitHub
commit 2a262438fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 145 additions and 22 deletions

View file

@ -821,28 +821,52 @@ class OAuthProvider(
"""
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 not self.issuer_url:
return routes
parsed = urlparse(str(self.issuer_url))
issuer_path = parsed.path.rstrip("/")
new_routes = []
for route in routes:
if route.path != "/.well-known/oauth-authorization-server":
new_routes.append(route)
continue
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
# RFC 8414: replace base path with path-aware authorization server metadata
new_routes.append(
Route(
f"/.well-known/oauth-authorization-server{issuer_path}",
endpoint=route.endpoint,
methods=route.methods,
)
)
# RFC 8414 §5: path-aware OIDC discovery alias
new_routes.append(
Route(
f"/.well-known/openid-configuration{issuer_path}",
endpoint=route.endpoint,
methods=route.methods,
)
)
else:
# Root deployment: keep standard RFC 8414 path
new_routes.append(route)
return routes
# Always register /.well-known/openid-configuration regardless of path depth.
# Two reasons:
# 1. Root deployments: direct OIDC discovery alias (RFC 8414 §5 / OIDC 1.0).
# 2. Path-prefix reverse proxy deployments: the proxy strips the path prefix
# before forwarding, so a client request for
# /{prefix}/.well-known/openid-configuration arrives here as
# /.well-known/openid-configuration.
new_routes.append(
Route(
"/.well-known/openid-configuration",
endpoint=route.endpoint,
methods=route.methods,
)
)
return new_routes

View file

@ -382,3 +382,102 @@ class TestOAuthMounting:
# Should be at root (no path suffix) when issuer_url is root
assert auth_server_routes[0].path == "/.well-known/oauth-authorization-server"
async def test_oidc_discovery_alias_path_aware(self, test_tokens):
"""Test that /.well-known/openid-configuration aliases are added for path-aware issuers.
RFC 8414 §5 allows servers to expose OAuth metadata at the OIDC discovery path.
The MCP SDK client probes both paths; fastmcp must respond to either.
For a path-aware issuer (e.g. https://api.example.com/api) two OIDC paths
are registered:
- /.well-known/openid-configuration/api (RFC 8414 §5, path-aware)
- /.well-known/openid-configuration (OIDC 1.0 / reverse-proxy compat)
"""
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",
client_storage=MemoryStore(),
)
well_known_routes = auth_provider.get_well_known_routes(mcp_path="/mcp")
oidc_routes = [r for r in well_known_routes if "openid-configuration" in r.path]
oidc_paths = {r.path for r in oidc_routes}
assert "/.well-known/openid-configuration/api" in oidc_paths # RFC 8414 §5
assert (
"/.well-known/openid-configuration" in oidc_paths
) # OIDC 1.0 / proxy compat
async def test_oidc_discovery_alias_root(self, test_tokens):
"""Test that /.well-known/openid-configuration alias is added for root issuers."""
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",
client_storage=MemoryStore(),
)
well_known_routes = auth_provider.get_well_known_routes(mcp_path="/mcp")
oidc_routes = [r for r in well_known_routes if "openid-configuration" in r.path]
assert len(oidc_routes) == 1
assert oidc_routes[0].path == "/.well-known/openid-configuration"
async def test_oidc_discovery_returns_auth_server_metadata(self, test_tokens):
"""Test that /.well-known/openid-configuration returns valid OAuth server metadata.
The response must include the core RFC 8414 fields so that OIDC-aware
clients (e.g. Claude Desktop backend) can discover the authorization endpoint.
"""
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",
client_storage=MemoryStore(),
)
mcp = FastMCP("test-server", auth=auth_provider)
mcp_app = mcp.http_app(path="/mcp")
well_known_routes = auth_provider.get_well_known_routes(mcp_path="/mcp")
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 OIDC discovery (RFC 8414 §5)
response = await client.get("/.well-known/openid-configuration/api")
assert response.status_code == 200
metadata = response.json()
assert "authorization_endpoint" in metadata
assert "token_endpoint" in metadata
# Root OIDC discovery (OIDC 1.0 / reverse-proxy compat)
response = await client.get("/.well-known/openid-configuration")
assert response.status_code == 200
metadata = response.json()
assert "authorization_endpoint" in metadata
assert "token_endpoint" in metadata