From 732706bd85e270472aba42249f20ef27db9f46f1 Mon Sep 17 00:00:00 2001 From: ipe4647 Date: Thu, 23 Apr 2026 10:07:35 +0000 Subject: [PATCH 1/3] feat(proxy): add forwardAuthorization flag to control token forwarding - Introduced `--proxy-forward-authorization` command line option. - Updated configuration documentation to include new option. - Modified proxy handling to conditionally forward the Authorization header based on the new flag. - Added tests to verify the behavior of the new flag. --- docs/docs/configuration.md | 5 ++- main.go | 4 ++ main_test.go | 64 ++++++++++++++++++++++++++++++ pkg/mcp-proxy/main.go | 3 +- pkg/proxy/proxy.go | 38 ++++++++++-------- pkg/proxy/proxy_test.go | 79 +++++++++++++++++++++++++++++++++++--- 6 files changed, 170 insertions(+), 23 deletions(-) diff --git a/docs/docs/configuration.md b/docs/docs/configuration.md index 8b0e567..2493477 100644 --- a/docs/docs/configuration.md +++ b/docs/docs/configuration.md @@ -178,8 +178,9 @@ openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 | Option | Environment Variable | Default | Description | | ----------------------- | --------------------- | ----------- | ----------------------------------------------------------------------------------------------------- | -| `--proxy-bearer-token` | `PROXY_BEARER_TOKEN` | - | Bearer token to add to Authorization header when proxying requests | -| `--proxy-headers` | `PROXY_HEADERS` | - | Comma-separated list of headers to add when proxying requests (format: Header1:Value1,Header2:Value2) | +| `--proxy-bearer-token` | `PROXY_BEARER_TOKEN` | - | Bearer token to add to Authorization header when proxying requests | +| `--proxy-forward-authorization` | `PROXY_FORWARD_AUTHORIZATION` | `false` | Forward the incoming Authorization bearer token to the backend after validation | +| `--proxy-headers` | `PROXY_HEADERS` | - | Comma-separated list of headers to add when proxying requests (format: Header1:Value1,Header2:Value2) | | `--header-mapping` | `HEADER_MAPPING` | - | Comma-separated mapping of JSON pointer paths to header names (e.g., `/email:X-Forwarded-Email`) | | `--header-mapping-base` | `HEADER_MAPPING_BASE` | `/userinfo` | JSON pointer base path for header mapping claims lookup (e.g., `/userinfo` or `/`) | | `--http-streaming-only` | `HTTP_STREAMING_ONLY` | `false` | Reject SSE (GET) requests and keep the backend operating in HTTP streaming-only mode | diff --git a/main.go b/main.go index fc9e434..080b5be 100644 --- a/main.go +++ b/main.go @@ -152,6 +152,7 @@ type proxyRunnerFunc func( trustedProxy []string, proxyHeaders []string, proxyBearerToken string, + forwardAuthorizationHeader bool, proxyTarget []string, httpStreamingOnly bool, headerMapping map[string]string, @@ -199,6 +200,7 @@ func newRootCommand(run proxyRunnerFunc) *cobra.Command { var password string var passwordHash string var proxyBearerToken string + var forwardAuthorizationHeader bool var proxyHeaders string var headerMapping string var headerMappingBase string @@ -322,6 +324,7 @@ func newRootCommand(run proxyRunnerFunc) *cobra.Command { trustedProxiesList, proxyHeadersList, proxyBearerToken, + forwardAuthorizationHeader, args, httpStreamingOnly, headerMappingMap, @@ -376,6 +379,7 @@ func newRootCommand(run proxyRunnerFunc) *cobra.Command { // Proxy headers configuration rootCmd.Flags().StringVar(&proxyBearerToken, "proxy-bearer-token", getEnvWithDefault("PROXY_BEARER_TOKEN", ""), "Bearer token to add to Authorization header when proxying requests") + rootCmd.Flags().BoolVar(&forwardAuthorizationHeader, "proxy-forward-authorization", getEnvBoolWithDefault("PROXY_FORWARD_AUTHORIZATION", false), "Forward the incoming Authorization bearer token to the backend after validation") rootCmd.Flags().StringVar(&trustedProxies, "trusted-proxies", getEnvWithDefault("TRUSTED_PROXIES", ""), "Comma-separated list of trusted proxies (IP addresses or CIDR ranges)") rootCmd.Flags().StringVar(&proxyHeaders, "proxy-headers", getEnvWithDefault("PROXY_HEADERS", ""), "Comma-separated list of headers to add when proxying requests (format: Header1:Value1,Header2:Value2)") rootCmd.Flags().BoolVar(&httpStreamingOnly, "http-streaming-only", getEnvBoolWithDefault("HTTP_STREAMING_ONLY", false), "Reject SSE (GET) requests and keep the backend in HTTP streaming-only mode") diff --git a/main_test.go b/main_test.go index a9f31cf..49923f2 100644 --- a/main_test.go +++ b/main_test.go @@ -400,6 +400,7 @@ func TestNewRootCommand_HTTPStreamingOnlyFlag(t *testing.T) { trustedProxy []string, proxyHeaders []string, proxyBearerToken string, + forwardAuthorizationHeader bool, proxyTarget []string, httpStreamingOnly bool, headerMapping map[string]string, @@ -465,6 +466,7 @@ func TestNewRootCommand_HTTPStreamingOnlyFromEnv(t *testing.T) { trustedProxy []string, proxyHeaders []string, proxyBearerToken string, + forwardAuthorizationHeader bool, proxyTarget []string, httpStreamingOnly bool, headerMapping map[string]string, @@ -485,3 +487,65 @@ func TestNewRootCommand_HTTPStreamingOnlyFromEnv(t *testing.T) { t.Fatalf("expected httpStreamingOnly to default to true from env var") } } + +func TestNewRootCommand_ForwardAuthorizationFlag(t *testing.T) { + t.Setenv("PROXY_FORWARD_AUTHORIZATION", "") + + var forwardAuthorization bool + runner := proxyRunnerFunc(func(listen string, + tlsListen string, + autoTLS bool, + tlsHost string, + tlsDirectoryURL string, + tlsAcceptTOS bool, + tlsCertFile string, + tlsKeyFile string, + dataPath string, + repositoryBackend string, + repositoryDSN string, + externalURL string, + googleClientID string, + googleClientSecret string, + googleAllowedUsers []string, + googleAllowedWorkspaces []string, + githubClientID string, + githubClientSecret string, + githubAllowedUsers []string, + githubAllowedOrgs []string, + oidcConfigurationURL string, + oidcClientID string, + oidcClientSecret string, + oidcScopes []string, + oidcUserIDField string, + oidcProviderName string, + oidcAllowedUsers []string, + oidcAllowedUsersGlob []string, + oidcAllowedAttributes map[string][]string, + oidcAllowedAttributesGlob map[string][]string, + noProviderAutoSelect bool, + password string, + passwordHash string, + trustedProxy []string, + proxyHeaders []string, + proxyBearerToken string, + forwardAuthorizationHeader bool, + proxyTarget []string, + httpStreamingOnly bool, + headerMapping map[string]string, + headerMappingBase string, + ) error { + forwardAuthorization = forwardAuthorizationHeader + return nil + }) + + cmd := newRootCommand(runner) + cmd.SetArgs([]string{"--proxy-forward-authorization", "http://backend"}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("expected command to succeed, got error: %v", err) + } + + if !forwardAuthorization { + t.Fatalf("expected forwardAuthorizationHeader to be true when flag is set") + } +} diff --git a/pkg/mcp-proxy/main.go b/pkg/mcp-proxy/main.go index f5b4385..bf78a72 100644 --- a/pkg/mcp-proxy/main.go +++ b/pkg/mcp-proxy/main.go @@ -74,6 +74,7 @@ func Run( trustedProxy []string, proxyHeaders []string, proxyBearerToken string, + forwardAuthorizationHeader bool, proxyTarget []string, httpStreamingOnly bool, headerMapping map[string]string, @@ -298,7 +299,7 @@ func Run( if err != nil { return fmt.Errorf("failed to create IDP router: %w", err) } - proxyRouter, err := newProxyRouter(externalURL, beHandler, &privKey.PublicKey, proxyHeadersMap, httpStreamingOnly, headerMapping, headerMappingBase) + proxyRouter, err := newProxyRouter(externalURL, beHandler, &privKey.PublicKey, proxyHeadersMap, httpStreamingOnly, forwardAuthorizationHeader, headerMapping, headerMappingBase) if err != nil { return fmt.Errorf("failed to create proxy router: %w", err) } diff --git a/pkg/proxy/proxy.go b/pkg/proxy/proxy.go index 5e552ec..95591b9 100644 --- a/pkg/proxy/proxy.go +++ b/pkg/proxy/proxy.go @@ -12,13 +12,14 @@ import ( ) type ProxyRouter struct { - externalURL string - proxy http.Handler - publicKey *rsa.PublicKey - proxyHeaders http.Header - httpStreamingOnly bool - headerMapping map[string]string - headerMappingBase string + externalURL string + proxy http.Handler + publicKey *rsa.PublicKey + proxyHeaders http.Header + httpStreamingOnly bool + forwardAuthorizationHeader bool + headerMapping map[string]string + headerMappingBase string } func NewProxyRouter( @@ -27,17 +28,19 @@ func NewProxyRouter( publicKey *rsa.PublicKey, proxyHeaders http.Header, httpStreamingOnly bool, + forwardAuthorizationHeader bool, headerMapping map[string]string, headerMappingBase string, ) (*ProxyRouter, error) { return &ProxyRouter{ - externalURL: externalURL, - proxy: proxy, - publicKey: publicKey, - proxyHeaders: proxyHeaders, - httpStreamingOnly: httpStreamingOnly, - headerMapping: headerMapping, - headerMappingBase: headerMappingBase, + externalURL: externalURL, + proxy: proxy, + publicKey: publicKey, + proxyHeaders: proxyHeaders, + httpStreamingOnly: httpStreamingOnly, + forwardAuthorizationHeader: forwardAuthorizationHeader, + headerMapping: headerMapping, + headerMappingBase: headerMappingBase, }, nil } @@ -87,8 +90,13 @@ func (p *ProxyRouter) handleProxy(c *gin.Context) { return } - c.Request.Header.Del("Authorization") + if !p.forwardAuthorizationHeader { + c.Request.Header.Del("Authorization") + } for key, values := range p.proxyHeaders { + if strings.EqualFold(key, "Authorization") { + c.Request.Header.Del("Authorization") + } for _, value := range values { c.Request.Header.Add(key, value) } diff --git a/pkg/proxy/proxy_test.go b/pkg/proxy/proxy_test.go index 77ca4f5..c013c1b 100644 --- a/pkg/proxy/proxy_test.go +++ b/pkg/proxy/proxy_test.go @@ -70,7 +70,7 @@ func TestProxyRouter_HandleProxy_ValidToken(t *testing.T) { proxyHeaders := make(http.Header) proxyHeaders.Set("X-Forwarded-By", "mcp-auth-proxy") - proxyRouter, err := NewProxyRouter("https://example.com", proxyHandler, publicKey, proxyHeaders, false, nil, "/userinfo") + proxyRouter, err := NewProxyRouter("https://example.com", proxyHandler, publicKey, proxyHeaders, false, false, nil, "/userinfo") require.NoError(t, err) gin.SetMode(gin.TestMode) @@ -163,7 +163,7 @@ func TestProxyRouter_HeaderMapping(t *testing.T) { w.WriteHeader(http.StatusOK) }) - proxyRouter, err := NewProxyRouter("https://example.com", proxyHandler, publicKey, http.Header{}, false, tt.headerMapping, "/userinfo") + proxyRouter, err := NewProxyRouter("https://example.com", proxyHandler, publicKey, http.Header{}, false, false, tt.headerMapping, "/userinfo") require.NoError(t, err) gin.SetMode(gin.TestMode) @@ -293,7 +293,7 @@ func TestProxyRouter_HeaderMappingBase(t *testing.T) { w.WriteHeader(http.StatusOK) }) - proxyRouter, err := NewProxyRouter("https://example.com", proxyHandler, publicKey, http.Header{}, false, tt.headerMapping, tt.headerMappingBase) + proxyRouter, err := NewProxyRouter("https://example.com", proxyHandler, publicKey, http.Header{}, false, false, tt.headerMapping, tt.headerMappingBase) require.NoError(t, err) gin.SetMode(gin.TestMode) @@ -321,11 +321,80 @@ func TestProxyRouter_HeaderMappingBase(t *testing.T) { } } +func TestProxyRouter_AuthorizationHeaderDefaultBehavior(t *testing.T) { + privateKey, publicKey, err := generateRSAKeyPair() + require.NoError(t, err) + + t.Run("strips authorization header by default", func(t *testing.T) { + var backendAuthorization string + proxyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + backendAuthorization = r.Header.Get("Authorization") + w.WriteHeader(http.StatusOK) + }) + + proxyRouter, err := NewProxyRouter("https://example.com", proxyHandler, publicKey, http.Header{}, false, false, nil, "/userinfo") + require.NoError(t, err) + + gin.SetMode(gin.TestMode) + router := gin.New() + proxyRouter.SetupRoutes(router) + + token, err := createJWT(privateKey, jwt.MapClaims{ + "sub": "user", + "exp": time.Now().Add(time.Hour).Unix(), + "iat": time.Now().Unix(), + }) + require.NoError(t, err) + + req, err := http.NewRequest(http.MethodGet, "/mcp", nil) + require.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+token) + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Empty(t, backendAuthorization) + }) + + t.Run("forwards authorization header when enabled", func(t *testing.T) { + var backendAuthorization string + proxyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + backendAuthorization = r.Header.Get("Authorization") + w.WriteHeader(http.StatusOK) + }) + + proxyRouter, err := NewProxyRouter("https://example.com", proxyHandler, publicKey, http.Header{}, false, true, nil, "/userinfo") + require.NoError(t, err) + + gin.SetMode(gin.TestMode) + router := gin.New() + proxyRouter.SetupRoutes(router) + + token, err := createJWT(privateKey, jwt.MapClaims{ + "sub": "user", + "exp": time.Now().Add(time.Hour).Unix(), + "iat": time.Now().Unix(), + }) + require.NoError(t, err) + + req, err := http.NewRequest(http.MethodGet, "/mcp", nil) + require.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+token) + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "Bearer "+token, backendAuthorization) + }) +} + func TestProxyRouter_ProtectedResourceTrailingSlash(t *testing.T) { _, publicKey, err := generateRSAKeyPair() require.NoError(t, err) - proxyRouter, err := NewProxyRouter("https://example.com/", http.NotFoundHandler(), publicKey, http.Header{}, false, nil, "/userinfo") + proxyRouter, err := NewProxyRouter("https://example.com/", http.NotFoundHandler(), publicKey, http.Header{}, false, false, nil, "/userinfo") require.NoError(t, err) gin.SetMode(gin.TestMode) @@ -429,7 +498,7 @@ func TestProxyRouter_HTTPStreamingOnlyRejectsSSE(t *testing.T) { w.WriteHeader(http.StatusOK) }) - proxyRouter, err := NewProxyRouter("https://example.com", proxyHandler, publicKey, http.Header{}, tt.streamingOnly, nil, "/userinfo") + proxyRouter, err := NewProxyRouter("https://example.com", proxyHandler, publicKey, http.Header{}, tt.streamingOnly, false, nil, "/userinfo") require.NoError(t, err) gin.SetMode(gin.TestMode) From e9c49bea0088954298a77eb2d120bfeb2c96327a Mon Sep 17 00:00:00 2001 From: ipe4647 Date: Thu, 23 Apr 2026 12:25:17 +0000 Subject: [PATCH 2/3] test(proxy): add tests for forwardAuthorization flag from env --- main_test.go | 62 ++++++++++++++++++++++++++++++++++++++ pkg/mcp-proxy/main_test.go | 7 +++-- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/main_test.go b/main_test.go index 49923f2..009bb05 100644 --- a/main_test.go +++ b/main_test.go @@ -549,3 +549,65 @@ func TestNewRootCommand_ForwardAuthorizationFlag(t *testing.T) { t.Fatalf("expected forwardAuthorizationHeader to be true when flag is set") } } + +func TestNewRootCommand_ForwardAuthorizationFromEnv(t *testing.T) { + t.Setenv("PROXY_FORWARD_AUTHORIZATION", "true") + + var forwardAuthorization bool + runner := proxyRunnerFunc(func(listen string, + tlsListen string, + autoTLS bool, + tlsHost string, + tlsDirectoryURL string, + tlsAcceptTOS bool, + tlsCertFile string, + tlsKeyFile string, + dataPath string, + repositoryBackend string, + repositoryDSN string, + externalURL string, + googleClientID string, + googleClientSecret string, + googleAllowedUsers []string, + googleAllowedWorkspaces []string, + githubClientID string, + githubClientSecret string, + githubAllowedUsers []string, + githubAllowedOrgs []string, + oidcConfigurationURL string, + oidcClientID string, + oidcClientSecret string, + oidcScopes []string, + oidcUserIDField string, + oidcProviderName string, + oidcAllowedUsers []string, + oidcAllowedUsersGlob []string, + oidcAllowedAttributes map[string][]string, + oidcAllowedAttributesGlob map[string][]string, + noProviderAutoSelect bool, + password string, + passwordHash string, + trustedProxy []string, + proxyHeaders []string, + proxyBearerToken string, + forwardAuthorizationHeader bool, + proxyTarget []string, + httpStreamingOnly bool, + headerMapping map[string]string, + headerMappingBase string, + ) error { + forwardAuthorization = forwardAuthorizationHeader + return nil + }) + + cmd := newRootCommand(runner) + cmd.SetArgs([]string{"http://backend"}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("expected command to succeed, got error: %v", err) + } + + if !forwardAuthorization { + t.Fatalf("expected forwardAuthorizationHeader to default to true from env var") + } +} diff --git a/pkg/mcp-proxy/main_test.go b/pkg/mcp-proxy/main_test.go index bb978c8..8ac728e 100644 --- a/pkg/mcp-proxy/main_test.go +++ b/pkg/mcp-proxy/main_test.go @@ -34,7 +34,7 @@ func TestRun_NormalizesExternalURLTrailingSlash(t *testing.T) { for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { var receivedURL string - newProxyRouter = func(externalURL string, proxyHandler http.Handler, publicKey *rsa.PublicKey, proxyHeaders http.Header, httpStreamingOnly bool, headerMapping map[string]string, headerMappingBase string) (*proxy.ProxyRouter, error) { + newProxyRouter = func(externalURL string, proxyHandler http.Handler, publicKey *rsa.PublicKey, proxyHeaders http.Header, httpStreamingOnly bool, forwardAuthorizationHeader bool, headerMapping map[string]string, headerMappingBase string) (*proxy.ProxyRouter, error) { receivedURL = externalURL return nil, errors.New("stop early") } @@ -46,7 +46,7 @@ func TestRun_NormalizesExternalURLTrailingSlash(t *testing.T) { "", "", nil, nil, "", "", nil, nil, "", "", "", nil, "", "", nil, nil, nil, nil, - false, "", "", nil, nil, "", + false, "", "", nil, nil, "", false, []string{"http://example.com"}, false, nil, "/userinfo", ) @@ -70,7 +70,7 @@ func TestRun_PassesHTTPStreamingOnlyToProxyRouter(t *testing.T) { }) var streamingOnlyReceived bool - newProxyRouter = func(externalURL string, proxyHandler http.Handler, publicKey *rsa.PublicKey, proxyHeaders http.Header, httpStreamingOnly bool, headerMapping map[string]string, headerMappingBase string) (*proxy.ProxyRouter, error) { + newProxyRouter = func(externalURL string, proxyHandler http.Handler, publicKey *rsa.PublicKey, proxyHeaders http.Header, httpStreamingOnly bool, forwardAuthorizationHeader bool, headerMapping map[string]string, headerMappingBase string) (*proxy.ProxyRouter, error) { streamingOnlyReceived = httpStreamingOnly return nil, errors.New("proxy router init failed") } @@ -112,6 +112,7 @@ func TestRun_PassesHTTPStreamingOnlyToProxyRouter(t *testing.T) { nil, nil, "", + false, []string{"http://example.com"}, true, nil, From 67217f62a749dac3b4beb66bd7687a849946fa52 Mon Sep 17 00:00:00 2001 From: ipe4647 Date: Thu, 23 Apr 2026 13:15:16 +0000 Subject: [PATCH 3/3] docs(configuration): update proxy options table formatting to adhere to linter --- docs/docs/configuration.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/docs/configuration.md b/docs/docs/configuration.md index 2493477..616ab35 100644 --- a/docs/docs/configuration.md +++ b/docs/docs/configuration.md @@ -176,14 +176,14 @@ openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 ### Proxy Options -| Option | Environment Variable | Default | Description | -| ----------------------- | --------------------- | ----------- | ----------------------------------------------------------------------------------------------------- | -| `--proxy-bearer-token` | `PROXY_BEARER_TOKEN` | - | Bearer token to add to Authorization header when proxying requests | -| `--proxy-forward-authorization` | `PROXY_FORWARD_AUTHORIZATION` | `false` | Forward the incoming Authorization bearer token to the backend after validation | -| `--proxy-headers` | `PROXY_HEADERS` | - | Comma-separated list of headers to add when proxying requests (format: Header1:Value1,Header2:Value2) | -| `--header-mapping` | `HEADER_MAPPING` | - | Comma-separated mapping of JSON pointer paths to header names (e.g., `/email:X-Forwarded-Email`) | -| `--header-mapping-base` | `HEADER_MAPPING_BASE` | `/userinfo` | JSON pointer base path for header mapping claims lookup (e.g., `/userinfo` or `/`) | -| `--http-streaming-only` | `HTTP_STREAMING_ONLY` | `false` | Reject SSE (GET) requests and keep the backend operating in HTTP streaming-only mode | -| `--trusted-proxies` | `TRUSTED_PROXIES` | - | Comma-separated list of trusted proxies (IP addresses or CIDR ranges) | +| Option | Environment Variable | Default | Description | +| ------------------------------- | ----------------------------- | ----------- | ----------------------------------------------------------------------------------------------------- | +| `--proxy-bearer-token` | `PROXY_BEARER_TOKEN` | - | Bearer token to add to Authorization header when proxying requests | +| `--proxy-forward-authorization` | `PROXY_FORWARD_AUTHORIZATION` | `false` | Forward the incoming Authorization bearer token to the backend after validation | +| `--proxy-headers` | `PROXY_HEADERS` | - | Comma-separated list of headers to add when proxying requests (format: Header1:Value1,Header2:Value2) | +| `--header-mapping` | `HEADER_MAPPING` | - | Comma-separated mapping of JSON pointer paths to header names (e.g., `/email:X-Forwarded-Email`) | +| `--header-mapping-base` | `HEADER_MAPPING_BASE` | `/userinfo` | JSON pointer base path for header mapping claims lookup (e.g., `/userinfo` or `/`) | +| `--http-streaming-only` | `HTTP_STREAMING_ONLY` | `false` | Reject SSE (GET) requests and keep the backend operating in HTTP streaming-only mode | +| `--trusted-proxies` | `TRUSTED_PROXIES` | - | Comma-separated list of trusted proxies (IP addresses or CIDR ranges) | For practical configuration examples including environment variables, Docker Compose, and Kubernetes deployments, see the [Configuration Examples](./examples.md) page.