From f60582bef77b5200a0a8ca2681f13c6168f54493 Mon Sep 17 00:00:00 2001 From: Sabith KS Date: Wed, 18 Feb 2026 18:05:17 -0800 Subject: [PATCH 1/3] changes for MCP auth --- server/handlers.go | 30 +++++++++++ server/handlers_test.go | 107 ++++++++++++++++++++++++---------------- server/server.go | 1 + 3 files changed, 96 insertions(+), 42 deletions(-) diff --git a/server/handlers.go b/server/handlers.go index d4543cc8e1..000fcf4e07 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -103,6 +103,36 @@ type discoveryOAuth2 struct { AuthMethods []string `json:"token_endpoint_auth_methods_supported,omitempty"` } +// protectedResourceMetadata represents the OAuth 2.0 Protected Resource Metadata +// defined in RFC 9728. This enables MCP clients to discover the authorization +// server(s) that protect this resource. +type protectedResourceMetadata struct { + Resource string `json:"resource"` + AuthorizationServers []string `json:"authorization_servers"` + ScopesSupported []string `json:"scopes_supported,omitempty"` + BearerMethodsSupported []string `json:"bearer_methods_supported,omitempty"` +} + +func (s *Server) handleProtectedResourceMetadata(w http.ResponseWriter, r *http.Request) { + meta := protectedResourceMetadata{ + Resource: s.issuerURL.String(), + AuthorizationServers: []string{s.issuerURL.String()}, + ScopesSupported: []string{"openid", "email", "groups", "profile", "offline_access"}, + BearerMethodsSupported: []string{"header"}, + } + + data, err := json.MarshalIndent(meta, "", " ") + if err != nil { + s.logger.ErrorContext(r.Context(), "failed to marshal protected resource metadata", "err", err) + s.renderError(r, w, http.StatusInternalServerError, "Internal server error.") + return + } + + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Content-Length", strconv.Itoa(len(data))) + w.Write(data) +} + type DiscoveryType int const ( diff --git a/server/handlers_test.go b/server/handlers_test.go index 79baedf7b8..e90a6af8f2 100644 --- a/server/handlers_test.go +++ b/server/handlers_test.go @@ -107,48 +107,71 @@ func TestHandleDiscoveryOIDC(t *testing.T) { } func TestHandleDiscoveryOAuth2(t *testing.T) { - httpServer, server := newTestServer(t, nil) - defer httpServer.Close() - - rr := httptest.NewRecorder() - server.ServeHTTP(rr, httptest.NewRequest("GET", "/.well-known/oauth-authorization-server", nil)) - - if rr.Code != http.StatusOK { - t.Errorf("expected 200 got %d", rr.Code) - } - - var res discoveryOAuth2 - err := json.NewDecoder(rr.Result().Body).Decode(&res) - require.NoError(t, err) - - require.Equal(t, discoveryOAuth2{ - Issuer: httpServer.URL, - Auth: fmt.Sprintf("%s/auth", httpServer.URL), - Token: fmt.Sprintf("%s/token", httpServer.URL), - Keys: fmt.Sprintf("%s/keys", httpServer.URL), - DeviceEndpoint: fmt.Sprintf("%s/device/code", httpServer.URL), - Introspect: fmt.Sprintf("%s/token/introspect", httpServer.URL), - GrantTypes: []string{ - "authorization_code", - "refresh_token", - "urn:ietf:params:oauth:grant-type:device_code", - "urn:ietf:params:oauth:grant-type:token-exchange", - }, - ResponseTypes: []string{ - "code", - }, - CodeChallengeAlgs: []string{ - "S256", - "plain", - }, - Scopes: []string{ - "offline_access", - }, - AuthMethods: []string{ - "client_secret_basic", - "client_secret_post", - }, - }, res) + httpServer, server := newTestServer(t, nil) + defer httpServer.Close() + + rr := httptest.NewRecorder() + server.ServeHTTP(rr, httptest.NewRequest("GET", "/.well-known/oauth-authorization-server", nil)) + + if rr.Code != http.StatusOK { + t.Errorf("expected 200 got %d", rr.Code) + } + + var res discoveryOAuth2 + err := json.NewDecoder(rr.Result().Body).Decode(&res) + require.NoError(t, err) + + require.Equal(t, discoveryOAuth2{ + Issuer: httpServer.URL, + Auth: fmt.Sprintf("%s/auth", httpServer.URL), + Token: fmt.Sprintf("%s/token", httpServer.URL), + Keys: fmt.Sprintf("%s/keys", httpServer.URL), + DeviceEndpoint: fmt.Sprintf("%s/device/code", httpServer.URL), + Introspect: fmt.Sprintf("%s/token/introspect", httpServer.URL), + GrantTypes: []string{ + "authorization_code", + "refresh_token", + "urn:ietf:params:oauth:grant-type:device_code", + "urn:ietf:params:oauth:grant-type:token-exchange", + }, + ResponseTypes: []string{ + "code", + }, + CodeChallengeAlgs: []string{ + "S256", + "plain", + }, + Scopes: []string{ + "offline_access", + }, + AuthMethods: []string{ + "client_secret_basic", + "client_secret_post", + }, + }, res) +} + +func TestHandleProtectedResourceMetadata(t *testing.T) { + httpServer, server := newTestServer(t, nil) + defer httpServer.Close() + + rr := httptest.NewRecorder() + server.ServeHTTP(rr, httptest.NewRequest("GET", "/.well-known/oauth-protected-resource", nil)) + + if rr.Code != http.StatusOK { + t.Errorf("expected 200 got %d", rr.Code) + } + + var res protectedResourceMetadata + err := json.NewDecoder(rr.Result().Body).Decode(&res) + require.NoError(t, err) + + require.Equal(t, protectedResourceMetadata{ + Resource: httpServer.URL, + AuthorizationServers: []string{httpServer.URL}, + ScopesSupported: []string{"openid", "email", "groups", "profile", "offline_access"}, + BearerMethodsSupported: []string{"header"}, + }, res) } func TestHandleHealthFailure(t *testing.T) { diff --git a/server/server.go b/server/server.go index 2a9d384a22..948aee13cb 100644 --- a/server/server.go +++ b/server/server.go @@ -458,6 +458,7 @@ func newServer(ctx context.Context, c Config) (*Server, error) { return nil, err } handleWithCORS("/.well-known/oauth-authorization-server", oauthHandler) + handleWithCORS("/.well-known/oauth-protected-resource", s.handleProtectedResourceMetadata) // Handle the root path for the better user experience. handleWithCORS("/", func(w http.ResponseWriter, r *http.Request) { _, err := fmt.Fprintf(w, ` From 72568f3f8e27469f993c270057919c87688f8123 Mon Sep 17 00:00:00 2001 From: Sabith KS Date: Wed, 18 Feb 2026 18:33:13 -0800 Subject: [PATCH 2/3] revert --- server/handlers.go | 32 +------------------------------- server/handlers_test.go | 23 ----------------------- server/server.go | 1 - 3 files changed, 1 insertion(+), 55 deletions(-) diff --git a/server/handlers.go b/server/handlers.go index 000fcf4e07..0ba3fcab37 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -103,36 +103,6 @@ type discoveryOAuth2 struct { AuthMethods []string `json:"token_endpoint_auth_methods_supported,omitempty"` } -// protectedResourceMetadata represents the OAuth 2.0 Protected Resource Metadata -// defined in RFC 9728. This enables MCP clients to discover the authorization -// server(s) that protect this resource. -type protectedResourceMetadata struct { - Resource string `json:"resource"` - AuthorizationServers []string `json:"authorization_servers"` - ScopesSupported []string `json:"scopes_supported,omitempty"` - BearerMethodsSupported []string `json:"bearer_methods_supported,omitempty"` -} - -func (s *Server) handleProtectedResourceMetadata(w http.ResponseWriter, r *http.Request) { - meta := protectedResourceMetadata{ - Resource: s.issuerURL.String(), - AuthorizationServers: []string{s.issuerURL.String()}, - ScopesSupported: []string{"openid", "email", "groups", "profile", "offline_access"}, - BearerMethodsSupported: []string{"header"}, - } - - data, err := json.MarshalIndent(meta, "", " ") - if err != nil { - s.logger.ErrorContext(r.Context(), "failed to marshal protected resource metadata", "err", err) - s.renderError(r, w, http.StatusInternalServerError, "Internal server error.") - return - } - - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Content-Length", strconv.Itoa(len(data))) - w.Write(data) -} - type DiscoveryType int const ( @@ -146,7 +116,7 @@ func (s *Server) discoveryHandler(ctx context.Context, t DiscoveryType) (http.Ha switch t { case DiscoveryOAuth2: d = s.constructDiscoveryOAuth2() - default: + case DiscoveryOIDC: d = s.constructDiscoveryOIDC(ctx) } diff --git a/server/handlers_test.go b/server/handlers_test.go index e90a6af8f2..2caaa0e674 100644 --- a/server/handlers_test.go +++ b/server/handlers_test.go @@ -151,29 +151,6 @@ func TestHandleDiscoveryOAuth2(t *testing.T) { }, res) } -func TestHandleProtectedResourceMetadata(t *testing.T) { - httpServer, server := newTestServer(t, nil) - defer httpServer.Close() - - rr := httptest.NewRecorder() - server.ServeHTTP(rr, httptest.NewRequest("GET", "/.well-known/oauth-protected-resource", nil)) - - if rr.Code != http.StatusOK { - t.Errorf("expected 200 got %d", rr.Code) - } - - var res protectedResourceMetadata - err := json.NewDecoder(rr.Result().Body).Decode(&res) - require.NoError(t, err) - - require.Equal(t, protectedResourceMetadata{ - Resource: httpServer.URL, - AuthorizationServers: []string{httpServer.URL}, - ScopesSupported: []string{"openid", "email", "groups", "profile", "offline_access"}, - BearerMethodsSupported: []string{"header"}, - }, res) -} - func TestHandleHealthFailure(t *testing.T) { httpServer, server := newTestServer(t, func(c *Config) { c.HealthChecker = gosundheit.New() diff --git a/server/server.go b/server/server.go index 948aee13cb..2a9d384a22 100644 --- a/server/server.go +++ b/server/server.go @@ -458,7 +458,6 @@ func newServer(ctx context.Context, c Config) (*Server, error) { return nil, err } handleWithCORS("/.well-known/oauth-authorization-server", oauthHandler) - handleWithCORS("/.well-known/oauth-protected-resource", s.handleProtectedResourceMetadata) // Handle the root path for the better user experience. handleWithCORS("/", func(w http.ResponseWriter, r *http.Request) { _, err := fmt.Fprintf(w, ` From 259bb5a05c784bf531b956b575438fb96590cadc Mon Sep 17 00:00:00 2001 From: Sabith KS Date: Wed, 18 Feb 2026 18:41:59 -0800 Subject: [PATCH 3/3] have a registration endpoint --- server/handlers.go | 4 ++++ server/handlers_test.go | 2 ++ 2 files changed, 6 insertions(+) diff --git a/server/handlers.go b/server/handlers.go index 0ba3fcab37..631e0cebd5 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -79,6 +79,7 @@ type discoveryOIDC struct { UserInfo string `json:"userinfo_endpoint"` DeviceEndpoint string `json:"device_authorization_endpoint"` Introspect string `json:"introspection_endpoint"` + Registration string `json:"registration_endpoint,omitempty"` GrantTypes []string `json:"grant_types_supported"` ResponseTypes []string `json:"response_types_supported"` Subjects []string `json:"subject_types_supported"` @@ -96,6 +97,7 @@ type discoveryOAuth2 struct { Keys string `json:"jwks_uri"` DeviceEndpoint string `json:"device_authorization_endpoint,omitempty"` Introspect string `json:"introspection_endpoint,omitempty"` + Registration string `json:"registration_endpoint,omitempty"` GrantTypes []string `json:"grant_types_supported"` ResponseTypes []string `json:"response_types_supported"` CodeChallengeAlgs []string `json:"code_challenge_methods_supported,omitempty"` @@ -141,6 +143,7 @@ func (s *Server) constructDiscoveryOIDC(ctx context.Context) discoveryOIDC { UserInfo: s.absURL("/userinfo"), DeviceEndpoint: s.absURL("/device/code"), Introspect: s.absURL("/token/introspect"), + Registration: s.absURL("/register"), Subjects: []string{"public"}, IDTokenAlgs: []string{string(jose.RS256)}, CodeChallengeAlgs: []string{codeChallengeMethodS256, codeChallengeMethodPlain}, @@ -177,6 +180,7 @@ func (s *Server) constructDiscoveryOAuth2() discoveryOAuth2 { Keys: s.absURL("/keys"), DeviceEndpoint: s.absURL("/device/code"), Introspect: s.absURL("/token/introspect"), + Registration: s.absURL("/register"), CodeChallengeAlgs: []string{codeChallengeMethodS256, codeChallengeMethodPlain}, Scopes: []string{"offline_access"}, AuthMethods: []string{"client_secret_basic", "client_secret_post"}, diff --git a/server/handlers_test.go b/server/handlers_test.go index 2caaa0e674..eaa9ff2aff 100644 --- a/server/handlers_test.go +++ b/server/handlers_test.go @@ -60,6 +60,7 @@ func TestHandleDiscoveryOIDC(t *testing.T) { UserInfo: fmt.Sprintf("%s/userinfo", httpServer.URL), DeviceEndpoint: fmt.Sprintf("%s/device/code", httpServer.URL), Introspect: fmt.Sprintf("%s/token/introspect", httpServer.URL), + Registration: fmt.Sprintf("%s/register", httpServer.URL), GrantTypes: []string{ "authorization_code", "refresh_token", @@ -128,6 +129,7 @@ func TestHandleDiscoveryOAuth2(t *testing.T) { Keys: fmt.Sprintf("%s/keys", httpServer.URL), DeviceEndpoint: fmt.Sprintf("%s/device/code", httpServer.URL), Introspect: fmt.Sprintf("%s/token/introspect", httpServer.URL), + Registration: fmt.Sprintf("%s/register", httpServer.URL), GrantTypes: []string{ "authorization_code", "refresh_token",