Skip to content

Commit aba5f54

Browse files
authored
feat(auth): implement proxied /.well-known/oauth-authorization-server (#244)
Signed-off-by: Marc Nuri <[email protected]>
1 parent 94b8599 commit aba5f54

File tree

4 files changed

+67
-2
lines changed

4 files changed

+67
-2
lines changed

pkg/http/authorization.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const (
2222
func AuthorizationMiddleware(requireOAuth bool, serverURL string, oidcProvider *oidc.Provider, mcpServer *mcp.Server) func(http.Handler) http.Handler {
2323
return func(next http.Handler) http.Handler {
2424
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
25-
if r.URL.Path == healthEndpoint || r.URL.Path == oauthProtectedResourceEndpoint {
25+
if r.URL.Path == healthEndpoint || r.URL.Path == oauthProtectedResourceEndpoint || r.URL.Path == oauthAuthorizationServerEndpoint {
2626
next.ServeHTTP(w, r)
2727
return
2828
}

pkg/http/http.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ func Serve(ctx context.Context, mcpServer *mcp.Server, staticConfig *config.Stat
4444
mux.HandleFunc(healthEndpoint, func(w http.ResponseWriter, r *http.Request) {
4545
w.WriteHeader(http.StatusOK)
4646
})
47+
mux.HandleFunc(oauthAuthorizationServerEndpoint, OAuthAuthorizationServerHandler(staticConfig))
4748
mux.HandleFunc(oauthProtectedResourceEndpoint, OAuthProtectedResourceHandler(mcpServer, staticConfig))
4849

4950
ctx, cancel := context.WithCancel(ctx)

pkg/http/http_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,36 @@ func TestHealthCheck(t *testing.T) {
286286
})
287287
}
288288

289+
func TestWellKnownOAuthAuthorizationServer(t *testing.T) {
290+
// Simple http server to mock the authorization server
291+
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
292+
if r.URL.Path != "/.well-known/oauth-authorization-server" {
293+
http.NotFound(w, r)
294+
return
295+
}
296+
w.Header().Set("Content-Type", "application/json")
297+
_, _ = w.Write([]byte(`{"issuer": "https://example.com"}`))
298+
}))
299+
t.Cleanup(testServer.Close)
300+
testCaseWithContext(t, &httpContext{StaticConfig: &config.StaticConfig{AuthorizationURL: testServer.URL, RequireOAuth: true}}, func(ctx *httpContext) {
301+
resp, err := http.Get(fmt.Sprintf("http://%s/.well-known/oauth-authorization-server", ctx.HttpAddress))
302+
t.Cleanup(func() { _ = resp.Body.Close() })
303+
t.Run("Exposes .well-known/oauth-authorization-server endpoint", func(t *testing.T) {
304+
if err != nil {
305+
t.Fatalf("Failed to get .well-known/oauth-authorization-server endpoint: %v", err)
306+
}
307+
if resp.StatusCode != http.StatusOK {
308+
t.Errorf("Expected HTTP 200 OK, got %d", resp.StatusCode)
309+
}
310+
})
311+
t.Run(".well-known/oauth-authorization-server returns application/json content type", func(t *testing.T) {
312+
if resp.Header.Get("Content-Type") != "application/json" {
313+
t.Errorf("Expected Content-Type application/json, got %s", resp.Header.Get("Content-Type"))
314+
}
315+
})
316+
})
317+
}
318+
289319
func TestWellKnownOAuthProtectedResource(t *testing.T) {
290320
testCase(t, func(ctx *httpContext) {
291321
resp, err := http.Get(fmt.Sprintf("http://%s/.well-known/oauth-protected-resource", ctx.HttpAddress))

pkg/http/wellknown.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,50 @@ package http
22

33
import (
44
"encoding/json"
5+
"io"
56
"net/http"
67

78
"github.com/containers/kubernetes-mcp-server/pkg/config"
89
"github.com/containers/kubernetes-mcp-server/pkg/mcp"
910
)
1011

1112
const (
12-
oauthProtectedResourceEndpoint = "/.well-known/oauth-protected-resource"
13+
oauthAuthorizationServerEndpoint = "/.well-known/oauth-authorization-server"
14+
oauthProtectedResourceEndpoint = "/.well-known/oauth-protected-resource"
1315
)
1416

17+
func OAuthAuthorizationServerHandler(staticConfig *config.StaticConfig) http.HandlerFunc {
18+
return func(w http.ResponseWriter, r *http.Request) {
19+
if staticConfig.AuthorizationURL == "" {
20+
http.Error(w, "Authorization URL is not configured", http.StatusNotFound)
21+
return
22+
}
23+
req, err := http.NewRequest(r.Method, staticConfig.AuthorizationURL+oauthAuthorizationServerEndpoint, nil)
24+
if err != nil {
25+
http.Error(w, "Failed to create request: "+err.Error(), http.StatusInternalServerError)
26+
return
27+
}
28+
resp, err := http.DefaultClient.Do(req.WithContext(r.Context()))
29+
if err != nil {
30+
http.Error(w, "Failed to perform request: "+err.Error(), http.StatusInternalServerError)
31+
return
32+
}
33+
defer func() { _ = resp.Body.Close() }()
34+
body, err := io.ReadAll(resp.Body)
35+
if err != nil {
36+
http.Error(w, "Failed to read response body: "+err.Error(), http.StatusInternalServerError)
37+
return
38+
}
39+
for key, values := range resp.Header {
40+
for _, value := range values {
41+
w.Header().Add(key, value)
42+
}
43+
}
44+
w.WriteHeader(resp.StatusCode)
45+
_, _ = w.Write(body)
46+
}
47+
}
48+
1549
func OAuthProtectedResourceHandler(mcpServer *mcp.Server, staticConfig *config.StaticConfig) http.HandlerFunc {
1650
return func(w http.ResponseWriter, r *http.Request) {
1751
w.Header().Set("Content-Type", "application/json")

0 commit comments

Comments
 (0)