From 59f6c13f30690ce09e3455a0877b1a0c0f1d0f61 Mon Sep 17 00:00:00 2001 From: Khanh Dinh Nguyen <77538987+beiryu@users.noreply.github.com> Date: Fri, 13 Nov 2026 23:52:36 +0700 Subject: [PATCH] auth: drop query and fragment from server URL for resource metadata Fixes #1519 --- auth/authorization_code.go | 5 +++++ auth/authorization_code_test.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/auth/authorization_code.go b/auth/authorization_code.go index 747b038f..f3ec053e 100644 --- a/auth/authorization_code.go +++ b/auth/authorization_code.go @@ -474,6 +474,11 @@ func errorFromChallenges(cs []oauthex.Challenge) string { // If no metadata was found or the fetched metadata fails security checks, // it returns an error. func (h *AuthorizationCodeHandler) getProtectedResourceMetadata(ctx context.Context, wwwChallenges []oauthex.Challenge, mcpServerURL string) (*oauthex.ProtectedResourceMetadata, error) { + // RFC 8707 section 2: a resource indicator SHOULD NOT include a query component. + if u, err := url.Parse(mcpServerURL); err == nil { + u.RawQuery, u.ForceQuery, u.Fragment = "", false, "" + mcpServerURL = u.String() + } // Use MCP server URL as the resource URI per // https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#canonical-server-uri. for _, url := range protectedResourceMetadataURLs(resourceMetadataURLFromChallenges(wwwChallenges), mcpServerURL) { diff --git a/auth/authorization_code_test.go b/auth/authorization_code_test.go index ef30e93d..d9df4723 100644 --- a/auth/authorization_code_test.go +++ b/auth/authorization_code_test.go @@ -806,6 +806,38 @@ func TestGetProtectedResourceMetadata_Success(t *testing.T) { } } +// TestGetProtectedResourceMetadata_QueryInServerURL verifies that a query in +// the MCP server URL does not prevent matching metadata whose resource has no +// query (RFC 8707 section 2). +func TestGetProtectedResourceMetadata_QueryInServerURL(t *testing.T) { + handler, err := NewAuthorizationCodeHandler(validConfig()) + if err != nil { + t.Fatalf("NewAuthorizationCodeHandler() error = %v", err) + } + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + metadata := &oauthex.ProtectedResourceMetadata{ + Resource: server.URL + "/mcp", + AuthorizationServers: []string{"https://oauth.example.com"}, + } + mux.Handle("/.well-known/oauth-protected-resource/mcp", ProtectedResourceMetadataHandler(metadata)) + challenges := []oauthex.Challenge{{ + Scheme: "Bearer", + Params: map[string]string{ + "resource_metadata": server.URL + "/.well-known/oauth-protected-resource/mcp", + }, + }} + + got, err := handler.getProtectedResourceMetadata(t.Context(), challenges, server.URL+"/mcp?features=flags") + if err != nil { + t.Fatalf("getProtectedResourceMetadata() error = %v", err) + } + if diff := cmp.Diff(metadata, got); diff != "" { + t.Errorf("getProtectedResourceMetadata() metadata mismatch (-want +got):\n%s", diff) + } +} + func TestGetProtectedResourceMetadata_Backcompat(t *testing.T) { handler, err := NewAuthorizationCodeHandler(validConfig()) if err != nil {