Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
64 changes: 64 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,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
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")
}
}
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
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
79 changes: 74 additions & 5 deletions pkg/proxy/proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
Loading