Skip to content

Commit df489b6

Browse files
authored
Support RFC 9728 well-known paths with resource components (#2197)
The previous implementation used exact path matching for the well-known OAuth protected resource metadata endpoint, which only accepted: /.well-known/oauth-protected-resource This broke RFC 9728 Section 3.1 compliance for resources with path components. When a resource is identified as "https://server.com/mcp", clients must fetch metadata from: /.well-known/oauth-protected-resource/mcp But the exact match would return 404, preventing proper OAuth discovery for MCP servers with path-based resource identifiers. Changed to prefix matching to support both: - /.well-known/oauth-protected-resource (base path) - /.well-known/oauth-protected-resource/mcp (with resource component) - /.well-known/oauth-protected-resource/resource1 (multi-tenant) This enables multi-tenant hosting configurations as specified in RFC 9728 and ensures OAuth discovery works correctly for MCP servers regardless of their path structure. Note: The current implementation accepts all paths under the prefix (e.g., /.well-known/oauth-protected-resource/*). In the future, this could be tightened by having NewAuthInfoHandler parse the request path, extract the resource component, and validate it matches the resourceURL path before returning metadata. This would enforce stricter path matching while maintaining RFC 9728 compliance.
1 parent 6e18a3c commit df489b6

File tree

2 files changed

+158
-3
lines changed

2 files changed

+158
-3
lines changed

pkg/transport/proxy/transparent/transparent_proxy.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -362,15 +362,18 @@ func (p *TransparentProxy) Start(ctx context.Context) error {
362362
if p.authInfoHandler != nil {
363363
// Create a handler that routes .well-known requests
364364
wellKnownHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
365-
switch r.URL.Path {
366-
case "/.well-known/oauth-protected-resource":
365+
// Per RFC 9728, match /.well-known/oauth-protected-resource and any subpaths
366+
// e.g., /.well-known/oauth-protected-resource/mcp
367+
if strings.HasPrefix(r.URL.Path, "/.well-known/oauth-protected-resource") {
367368
p.authInfoHandler.ServeHTTP(w, r)
368-
default:
369+
} else {
369370
http.NotFound(w, r)
370371
}
371372
})
372373
mux.Handle("/.well-known/", wellKnownHandler)
373374
logger.Info("Well-known discovery endpoints enabled at /.well-known/ (no middlewares)")
375+
} else {
376+
logger.Info("No auth info handler provided; skipping /.well-known/ endpoint")
374377
}
375378

376379
// Create the server

pkg/transport/proxy/transparent/transparent_test.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,3 +199,155 @@ func TestTracePropagationHeaders(t *testing.T) {
199199
// Verify the request still works normally
200200
assert.Equal(t, http.StatusOK, recorder.Code)
201201
}
202+
203+
func TestWellKnownPathPrefixMatching(t *testing.T) {
204+
t.Parallel()
205+
206+
tests := []struct {
207+
name string
208+
requestPath string
209+
expectedStatusCode int
210+
shouldCallHandler bool
211+
description string
212+
}{
213+
{
214+
name: "base path without resource component",
215+
requestPath: "/.well-known/oauth-protected-resource",
216+
expectedStatusCode: http.StatusOK,
217+
shouldCallHandler: true,
218+
description: "RFC 9728 base path should route to authInfoHandler",
219+
},
220+
{
221+
name: "path with single resource component",
222+
requestPath: "/.well-known/oauth-protected-resource/mcp",
223+
expectedStatusCode: http.StatusOK,
224+
shouldCallHandler: true,
225+
description: "Path with /mcp resource component should route to authInfoHandler",
226+
},
227+
{
228+
name: "path with multiple resource components",
229+
requestPath: "/.well-known/oauth-protected-resource/api/v1/service",
230+
expectedStatusCode: http.StatusOK,
231+
shouldCallHandler: true,
232+
description: "Path with multiple resource components should route to authInfoHandler",
233+
},
234+
{
235+
name: "path with different resource name",
236+
requestPath: "/.well-known/oauth-protected-resource/resource1",
237+
expectedStatusCode: http.StatusOK,
238+
shouldCallHandler: true,
239+
description: "Path with arbitrary resource component should route to authInfoHandler",
240+
},
241+
{
242+
name: "non-matching well-known path",
243+
requestPath: "/.well-known/other-endpoint",
244+
expectedStatusCode: http.StatusNotFound,
245+
shouldCallHandler: false,
246+
description: "Different well-known endpoint should return 404",
247+
},
248+
{
249+
name: "path without leading dot",
250+
requestPath: "/well-known/oauth-protected-resource",
251+
expectedStatusCode: http.StatusNotFound,
252+
shouldCallHandler: false,
253+
description: "Path without leading dot should return 404",
254+
},
255+
{
256+
name: "similar but non-matching path with suffix",
257+
requestPath: "/.well-known/oauth-protected-resource-other",
258+
expectedStatusCode: http.StatusOK,
259+
shouldCallHandler: true,
260+
description: "Per RFC 9728, prefix matching means this should match",
261+
},
262+
{
263+
name: "path with trailing slash",
264+
requestPath: "/.well-known/oauth-protected-resource/",
265+
expectedStatusCode: http.StatusOK,
266+
shouldCallHandler: true,
267+
description: "Path with trailing slash should route to authInfoHandler",
268+
},
269+
{
270+
name: "path with query parameters",
271+
requestPath: "/.well-known/oauth-protected-resource?param=value",
272+
expectedStatusCode: http.StatusOK,
273+
shouldCallHandler: true,
274+
description: "Path with query parameters should route to authInfoHandler",
275+
},
276+
}
277+
278+
for _, tt := range tests {
279+
tt := tt // capture range variable
280+
t.Run(tt.name, func(t *testing.T) {
281+
t.Parallel()
282+
283+
// Track whether the auth info handler was called
284+
handlerCalled := false
285+
authHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
286+
handlerCalled = true
287+
w.WriteHeader(http.StatusOK)
288+
w.Write([]byte(`{"authorized": true}`))
289+
})
290+
291+
// Create the well-known handler directly (same logic as in transparent_proxy.go)
292+
wellKnownHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
293+
// Per RFC 9728, match /.well-known/oauth-protected-resource and any subpaths
294+
// e.g., /.well-known/oauth-protected-resource/mcp
295+
if strings.HasPrefix(r.URL.Path, "/.well-known/oauth-protected-resource") {
296+
authHandler.ServeHTTP(w, r)
297+
} else {
298+
http.NotFound(w, r)
299+
}
300+
})
301+
302+
// Create a mux and register the well-known handler (same as in transparent_proxy.go)
303+
mux := http.NewServeMux()
304+
mux.Handle("/.well-known/", wellKnownHandler)
305+
306+
// Create a test request
307+
req := httptest.NewRequest("GET", tt.requestPath, nil)
308+
recorder := httptest.NewRecorder()
309+
310+
// Serve the request through the mux
311+
mux.ServeHTTP(recorder, req)
312+
313+
// Verify status code
314+
assert.Equal(t, tt.expectedStatusCode, recorder.Code,
315+
"%s: expected status %d but got %d", tt.description, tt.expectedStatusCode, recorder.Code)
316+
317+
// Verify whether handler was called
318+
assert.Equal(t, tt.shouldCallHandler, handlerCalled,
319+
"%s: handler call mismatch (expected=%v, actual=%v)", tt.description, tt.shouldCallHandler, handlerCalled)
320+
321+
// For successful cases, verify response body
322+
if tt.shouldCallHandler && recorder.Code == http.StatusOK {
323+
assert.Contains(t, recorder.Body.String(), "authorized",
324+
"%s: expected response body to contain auth info", tt.description)
325+
}
326+
})
327+
}
328+
}
329+
330+
func TestWellKnownPathWithoutAuthHandler(t *testing.T) {
331+
t.Parallel()
332+
333+
// Test that when authInfoHandler is nil, the well-known route is not registered
334+
// Create a mux without registering the well-known handler (simulating authInfoHandler == nil case)
335+
mux := http.NewServeMux()
336+
337+
// Only register a default handler that returns 404 for everything
338+
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
339+
http.NotFound(w, r)
340+
})
341+
342+
// Create a test request to well-known path
343+
req := httptest.NewRequest("GET", "/.well-known/oauth-protected-resource", nil)
344+
recorder := httptest.NewRecorder()
345+
346+
// Serve the request
347+
mux.ServeHTTP(recorder, req)
348+
349+
// When no auth handler is provided, the well-known route should not be registered
350+
// The request should fall through to the default handler which returns 404
351+
assert.Equal(t, http.StatusNotFound, recorder.Code,
352+
"Without auth handler, well-known path should return 404")
353+
}

0 commit comments

Comments
 (0)