Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
4 changes: 4 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ type proxyRunnerFunc func(
trustedProxy []string,
proxyHeaders []string,
proxyBearerToken string,
forwardAuthorizationHeader bool,
proxyTarget []string,
httpStreamingOnly bool,
headerMapping map[string]string,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -322,6 +324,7 @@ func newRootCommand(run proxyRunnerFunc) *cobra.Command {
trustedProxiesList,
proxyHeadersList,
proxyBearerToken,
forwardAuthorizationHeader,
args,
httpStreamingOnly,
headerMappingMap,
Expand Down Expand Up @@ -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")
Expand Down
126 changes: 126 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -485,3 +487,127 @@ 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
Comment on lines +491 to +494
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new --proxy-forward-authorization flag is tested, but there’s no test that verifies the PROXY_FORWARD_AUTHORIZATION environment variable is correctly wired into newRootCommand (e.g., catching a typo in the env var name). Consider adding a TestNewRootCommand_ForwardAuthorizationFromEnv similar to TestNewRootCommand_HTTPStreamingOnlyFromEnv.

Copilot uses AI. Check for mistakes.
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")
}
}

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")
}
}
3 changes: 2 additions & 1 deletion pkg/mcp-proxy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ func Run(
trustedProxy []string,
proxyHeaders []string,
proxyBearerToken string,
forwardAuthorizationHeader bool,
proxyTarget []string,
httpStreamingOnly bool,
headerMapping map[string]string,
Expand Down Expand Up @@ -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)
}
Expand Down
7 changes: 4 additions & 3 deletions pkg/mcp-proxy/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand All @@ -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",
)

Expand All @@ -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")
}
Expand Down Expand Up @@ -112,6 +112,7 @@ func TestRun_PassesHTTPStreamingOnlyToProxyRouter(t *testing.T) {
nil,
nil,
"",
false,
[]string{"http://example.com"},
true,
nil,
Expand Down
38 changes: 23 additions & 15 deletions pkg/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
}

Expand Down Expand Up @@ -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)
}
Expand Down
Loading
Loading