Skip to content

Commit 9578bb7

Browse files
authored
Add root-level OAuth discovery endpoint to prevent request hangs (#673)
OAuth discovery requests to `/.well-known/oauth-authorization-server` hang instead of returning 404, causing Codex rmcp client to timeout after 15s per server. Gateway only registers handler at `/mcp/.well-known/oauth-authorization-server`. ## Changes - **Route registration**: Add handler at `/.well-known/oauth-authorization-server` in both unified (`transport.go`) and routed (`routed.go`) modes - **Handler reuse**: Both paths now use existing `handleOAuthDiscovery()` that returns 404 - **Test coverage**: Added test cases for root-level and MCP-prefixed paths (GET/POST methods) ## Implementation ```go // OAuth discovery endpoints - return 404 since we don't use OAuth // Standard path for OAuth discovery (per RFC 8414) mux.Handle("/.well-known/oauth-authorization-server", withResponseLogging(handleOAuthDiscovery())) // MCP-prefixed path for backward compatibility mux.Handle("/mcp/.well-known/oauth-authorization-server", withResponseLogging(handleOAuthDiscovery())) ``` Result: OAuth discovery now fails fast with immediate 404 instead of timing out. > [!WARNING] > > <details> > <summary>Firewall rules blocked me from connecting to one or more addresses (expand for details)</summary> > > #### I tried to connect to the following addresses, but was blocked by firewall rules: > > - `example.com` > - Triggering command: `/tmp/go-build3835057396/b270/launcher.test /tmp/go-build3835057396/b270/launcher.test -test.testlogfile=/tmp/go-build3835057396/b270/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true g_.a dler at /.well-known/oauth-authorization-server for both unified and routed modes - Prevents req--norc x_amd64/vet` (dns block) > - `invalid-host-that-does-not-exist-12345.com` > - Triggering command: `/tmp/go-build3835057396/b258/config.test /tmp/go-build3835057396/b258/config.test -test.testlogfile=/tmp/go-build3835057396/b258/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true ache/go/1.25.6/x64/src/runtime/cgo` (dns block) > - `nonexistent.local` > - Triggering command: `/tmp/go-build3835057396/b270/launcher.test /tmp/go-build3835057396/b270/launcher.test -test.testlogfile=/tmp/go-build3835057396/b270/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true g_.a dler at /.well-known/oauth-authorization-server for both unified and routed modes - Prevents req--norc x_amd64/vet` (dns block) > - `slow.example.com` > - Triggering command: `/tmp/go-build3835057396/b270/launcher.test /tmp/go-build3835057396/b270/launcher.test -test.testlogfile=/tmp/go-build3835057396/b270/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true g_.a dler at /.well-known/oauth-authorization-server for both unified and routed modes - Prevents req--norc x_amd64/vet` (dns block) > - `this-host-does-not-exist-12345.com` > - Triggering command: `/tmp/go-build3835057396/b279/mcp.test /tmp/go-build3835057396/b279/mcp.test -test.testlogfile=/tmp/go-build3835057396/b279/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true g_.a 64/src/crypto/internal/boring/bbig/big.go df11d1ebad5122b89293a9cf3b8353f09e1d0c3b3841ebf6471/log.json -g&#34; &#34;-lresolv&#34; ernal/sys -I 64/pkg/tool/linux_amd64/vet` (dns block) > > If you need me to access, download, or install something from one of these locations, you can either: > > - Configure [Actions setup steps](https://gh.io/copilot/actions-setup-steps) to set up my environment, which run before the firewall is enabled > - Add the appropriate URLs or hosts to the custom allowlist in this repository's [Copilot coding agent settings](https://github.com/github/gh-aw-mcpg/settings/copilot/coding_agent) (admins only) > > </details> <!-- START COPILOT ORIGINAL PROMPT --> <details> <summary>Original prompt</summary> > > ---- > > *This section details on the original issue you should resolve* > > <issue_title>OAuth discovery requests hang instead of returning 404</issue_title> > <issue_description>## Summary > > OAuth discovery requests to `/.well-known/oauth-authorization-server` hang indefinitely instead of returning a quick 404 response. This causes Codex MCP connections to timeout after 15 seconds per server. > > ## Problem > > The Codex rmcp client (Rust MCP client) attempts OAuth discovery at the **standard path**: > ``` > GET http://host.docker.internal/.well-known/oauth-authorization-server > ``` > > But the MCP Gateway registers the OAuth handler at: > ``` > /mcp/.well-known/oauth-authorization-server > ``` > > Since there's no handler at `/.well-known/...`, the request has no matching route and hangs instead of returning 404. > > ## Evidence > > ### Smoke-codex workflow run: https://github.com/github/gh-aw/actions/runs/21688558782 > > **Error pattern:** > ``` > DEBUG session_init: codex_rmcp_client::auth_status: OAuth discovery requests failed for > http://host.docker.internal:80/mcp/playwright: error sending request for url > (http://host.docker.internal/.well-known/oauth-authorization-server) > > Caused by: > operation timed out > ``` > > **Result:** > - OAuth discovery times out (15 seconds per server) > - 4 of 6 MCP servers fail to connect > - Only tavily and safeoutputs succeed (race condition - they complete before timeout exhausts resources) > > **Firewall logs confirm traffic reaches gateway:** > ``` > ▼ 11 requests | 11 allowed | 0 blocked | 1 unique domain > | Domain | Allowed | Denied | > |---------------------|---------|--------| > | host.docker.internal | 11 | 0 | > ``` > > ### Comparison with working run > > In run 21653900083 (before chroot mode), using IP address 172.30.0.1, OAuth discovery was NOT attempted and all 6 MCP servers connected successfully: > ``` > ready: ["safeoutputs", "safeinputs", "github", "playwright", "tavily", "serena"] > ``` > > ## Current Route Registration > > From `internal/server/transport.go`: > > | Route | Handler | > |-------|---------| > | `/mcp/.well-known/oauth-authorization-server` | OAuth handler (returns 404) | > | `/mcp/` and `/mcp` | StreamableHTTPHandler | > | `/health` | Health check | > | `/close` | Graceful shutdown | > | `/.well-known/*` | **NO HANDLER** ← causes hang | > > ## Proposed Fix > > Add a handler for OAuth discovery at the standard path (without `/mcp/` prefix): > > ```go > // In internal/server/transport.go, add alongside existing routes: > mux.HandleFunc("/.well-known/oauth-authorization-server", func(w http.ResponseWriter, r *http.Request) { > http.NotFound(w, r) > }) > ``` > > This ensures OAuth discovery requests get an immediate 404 response instead of hanging. > > ## Impact > > This fix would: > 1. Make OAuth discovery fail fast (instant 404 instead of 15s timeout) > 2. Allow all 6 MCP servers to connect successfully in Codex workflows > 3. Fix smoke-codex CI failures > > ## Related > > - gh-aw PR github/gh-aw-mcpg#13792: Removed hardcoded IP from Codex config (firewall fix - separate issue)</issue_description> > > ## Comments on the Issue (you are @copilot in this section) > > <comments> > </comments> > </details> <!-- START COPILOT CODING AGENT SUFFIX --> - Fixes #672 <!-- START COPILOT CODING AGENT TIPS --> --- ✨ Let Copilot coding agent [set things up for you](https://github.com/github/gh-aw-mcpg/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot) — coding agent works faster and does higher quality work when set up for your repo.
2 parents d51afb5 + 2b0b0a3 commit 9578bb7

4 files changed

Lines changed: 80 additions & 4 deletions

File tree

internal/server/routed.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,10 @@ func CreateHTTPServerForRoutedMode(addr string, unifiedServer *UnifiedServer, ap
8282
logRouted.Printf("Creating HTTP server for routed mode: addr=%s", addr)
8383
mux := http.NewServeMux()
8484

85-
// OAuth discovery endpoint - return 404 since we don't use OAuth
85+
// OAuth discovery endpoints - return 404 since we don't use OAuth
86+
// Standard path for OAuth discovery (per RFC 8414)
87+
mux.Handle("/.well-known/oauth-authorization-server", withResponseLogging(handleOAuthDiscovery()))
88+
// MCP-prefixed path for backward compatibility
8689
mux.Handle("/mcp/.well-known/oauth-authorization-server", withResponseLogging(handleOAuthDiscovery()))
8790

8891
// Create routes for all backends, plus sys only if DIFC is enabled

internal/server/routed_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,3 +553,61 @@ func TestCreateFilteredServer_EdgeCases(t *testing.T) {
553553
assert.Equal(t, "tool2", backend2Tools[0].Name)
554554
})
555555
}
556+
557+
// TestCreateHTTPServerForRoutedMode_OAuth tests OAuth discovery endpoint in routed mode
558+
func TestCreateHTTPServerForRoutedMode_OAuth(t *testing.T) {
559+
tests := []struct {
560+
name string
561+
path string
562+
method string
563+
wantStatusCode int
564+
}{
565+
{
566+
name: "OAuthDiscovery_GET_MCPPath",
567+
path: "/mcp/.well-known/oauth-authorization-server",
568+
method: "GET",
569+
wantStatusCode: http.StatusNotFound,
570+
},
571+
{
572+
name: "OAuthDiscovery_POST_MCPPath",
573+
path: "/mcp/.well-known/oauth-authorization-server",
574+
method: "POST",
575+
wantStatusCode: http.StatusNotFound,
576+
},
577+
{
578+
name: "OAuthDiscovery_GET_RootPath",
579+
path: "/.well-known/oauth-authorization-server",
580+
method: "GET",
581+
wantStatusCode: http.StatusNotFound,
582+
},
583+
{
584+
name: "OAuthDiscovery_POST_RootPath",
585+
path: "/.well-known/oauth-authorization-server",
586+
method: "POST",
587+
wantStatusCode: http.StatusNotFound,
588+
},
589+
}
590+
591+
// Create minimal server for routed mode testing
592+
ctx := context.Background()
593+
cfg := &config.Config{
594+
Servers: map[string]*config.ServerConfig{},
595+
}
596+
us, err := NewUnified(ctx, cfg)
597+
require.NoError(t, err)
598+
defer us.Close()
599+
600+
// Create HTTP server in routed mode without API key
601+
httpServer := CreateHTTPServerForRoutedMode(":0", us, "")
602+
603+
for _, tt := range tests {
604+
t.Run(tt.name, func(t *testing.T) {
605+
req := httptest.NewRequest(tt.method, tt.path, nil)
606+
w := httptest.NewRecorder()
607+
608+
httpServer.Handler.ServeHTTP(w, req)
609+
610+
assert.Equal(t, tt.wantStatusCode, w.Code, "Should return 404 for OAuth discovery")
611+
})
612+
}
613+
}

internal/server/transport.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,10 @@ func CreateHTTPServerForMCP(addr string, unifiedServer *UnifiedServer, apiKey st
6060
logTransport.Printf("Creating HTTP server for MCP: addr=%s, auth_enabled=%v", addr, apiKey != "")
6161
mux := http.NewServeMux()
6262

63-
// OAuth discovery endpoint - return 404 since we don't use OAuth
63+
// OAuth discovery endpoints - return 404 since we don't use OAuth
64+
// Standard path for OAuth discovery (per RFC 8414)
65+
mux.Handle("/.well-known/oauth-authorization-server", withResponseLogging(handleOAuthDiscovery()))
66+
// MCP-prefixed path for backward compatibility
6467
mux.Handle("/mcp/.well-known/oauth-authorization-server", withResponseLogging(handleOAuthDiscovery()))
6568

6669
logTransport.Print("Registering streamable HTTP handler for MCP protocol")

internal/server/transport_test.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,17 +233,29 @@ func TestCreateHTTPServerForMCP_OAuth(t *testing.T) {
233233
wantStatusCode int
234234
}{
235235
{
236-
name: "OAuthDiscovery_GET",
236+
name: "OAuthDiscovery_GET_MCPPath",
237237
path: "/mcp/.well-known/oauth-authorization-server",
238238
method: "GET",
239239
wantStatusCode: http.StatusNotFound,
240240
},
241241
{
242-
name: "OAuthDiscovery_POST",
242+
name: "OAuthDiscovery_POST_MCPPath",
243243
path: "/mcp/.well-known/oauth-authorization-server",
244244
method: "POST",
245245
wantStatusCode: http.StatusNotFound,
246246
},
247+
{
248+
name: "OAuthDiscovery_GET_RootPath",
249+
path: "/.well-known/oauth-authorization-server",
250+
method: "GET",
251+
wantStatusCode: http.StatusNotFound,
252+
},
253+
{
254+
name: "OAuthDiscovery_POST_RootPath",
255+
path: "/.well-known/oauth-authorization-server",
256+
method: "POST",
257+
wantStatusCode: http.StatusNotFound,
258+
},
247259
}
248260

249261
// Create minimal unified server

0 commit comments

Comments
 (0)