Skip to content

Commit c32aa3a

Browse files
authored
Merge cf8b272 into 6e18a3c
2 parents 6e18a3c + cf8b272 commit c32aa3a

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)