Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
17 changes: 9 additions & 8 deletions docs/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,13 +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-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.
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