diff --git a/fastmcp_slim/fastmcp/server/auth/auth.py b/fastmcp_slim/fastmcp/server/auth/auth.py index 2f21f4516..dc203fbf4 100644 --- a/fastmcp_slim/fastmcp/server/auth/auth.py +++ b/fastmcp_slim/fastmcp/server/auth/auth.py @@ -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 diff --git a/tests/server/auth/test_oauth_mounting.py b/tests/server/auth/test_oauth_mounting.py index 93f4a2308..295942353 100644 --- a/tests/server/auth/test_oauth_mounting.py +++ b/tests/server/auth/test_oauth_mounting.py @@ -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