Skip to content

Commit 732706b

Browse files
committed
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.
1 parent 6f6d916 commit 732706b

6 files changed

Lines changed: 170 additions & 23 deletions

File tree

docs/docs/configuration.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,9 @@ openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048
178178

179179
| Option | Environment Variable | Default | Description |
180180
| ----------------------- | --------------------- | ----------- | ----------------------------------------------------------------------------------------------------- |
181-
| `--proxy-bearer-token` | `PROXY_BEARER_TOKEN` | - | Bearer token to add to Authorization header when proxying requests |
182-
| `--proxy-headers` | `PROXY_HEADERS` | - | Comma-separated list of headers to add when proxying requests (format: Header1:Value1,Header2:Value2) |
181+
| `--proxy-bearer-token` | `PROXY_BEARER_TOKEN` | - | Bearer token to add to Authorization header when proxying requests |
182+
| `--proxy-forward-authorization` | `PROXY_FORWARD_AUTHORIZATION` | `false` | Forward the incoming Authorization bearer token to the backend after validation |
183+
| `--proxy-headers` | `PROXY_HEADERS` | - | Comma-separated list of headers to add when proxying requests (format: Header1:Value1,Header2:Value2) |
183184
| `--header-mapping` | `HEADER_MAPPING` | - | Comma-separated mapping of JSON pointer paths to header names (e.g., `/email:X-Forwarded-Email`) |
184185
| `--header-mapping-base` | `HEADER_MAPPING_BASE` | `/userinfo` | JSON pointer base path for header mapping claims lookup (e.g., `/userinfo` or `/`) |
185186
| `--http-streaming-only` | `HTTP_STREAMING_ONLY` | `false` | Reject SSE (GET) requests and keep the backend operating in HTTP streaming-only mode |

main.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ type proxyRunnerFunc func(
152152
trustedProxy []string,
153153
proxyHeaders []string,
154154
proxyBearerToken string,
155+
forwardAuthorizationHeader bool,
155156
proxyTarget []string,
156157
httpStreamingOnly bool,
157158
headerMapping map[string]string,
@@ -199,6 +200,7 @@ func newRootCommand(run proxyRunnerFunc) *cobra.Command {
199200
var password string
200201
var passwordHash string
201202
var proxyBearerToken string
203+
var forwardAuthorizationHeader bool
202204
var proxyHeaders string
203205
var headerMapping string
204206
var headerMappingBase string
@@ -322,6 +324,7 @@ func newRootCommand(run proxyRunnerFunc) *cobra.Command {
322324
trustedProxiesList,
323325
proxyHeadersList,
324326
proxyBearerToken,
327+
forwardAuthorizationHeader,
325328
args,
326329
httpStreamingOnly,
327330
headerMappingMap,
@@ -376,6 +379,7 @@ func newRootCommand(run proxyRunnerFunc) *cobra.Command {
376379

377380
// Proxy headers configuration
378381
rootCmd.Flags().StringVar(&proxyBearerToken, "proxy-bearer-token", getEnvWithDefault("PROXY_BEARER_TOKEN", ""), "Bearer token to add to Authorization header when proxying requests")
382+
rootCmd.Flags().BoolVar(&forwardAuthorizationHeader, "proxy-forward-authorization", getEnvBoolWithDefault("PROXY_FORWARD_AUTHORIZATION", false), "Forward the incoming Authorization bearer token to the backend after validation")
379383
rootCmd.Flags().StringVar(&trustedProxies, "trusted-proxies", getEnvWithDefault("TRUSTED_PROXIES", ""), "Comma-separated list of trusted proxies (IP addresses or CIDR ranges)")
380384
rootCmd.Flags().StringVar(&proxyHeaders, "proxy-headers", getEnvWithDefault("PROXY_HEADERS", ""), "Comma-separated list of headers to add when proxying requests (format: Header1:Value1,Header2:Value2)")
381385
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")

main_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,7 @@ func TestNewRootCommand_HTTPStreamingOnlyFlag(t *testing.T) {
400400
trustedProxy []string,
401401
proxyHeaders []string,
402402
proxyBearerToken string,
403+
forwardAuthorizationHeader bool,
403404
proxyTarget []string,
404405
httpStreamingOnly bool,
405406
headerMapping map[string]string,
@@ -465,6 +466,7 @@ func TestNewRootCommand_HTTPStreamingOnlyFromEnv(t *testing.T) {
465466
trustedProxy []string,
466467
proxyHeaders []string,
467468
proxyBearerToken string,
469+
forwardAuthorizationHeader bool,
468470
proxyTarget []string,
469471
httpStreamingOnly bool,
470472
headerMapping map[string]string,
@@ -485,3 +487,65 @@ func TestNewRootCommand_HTTPStreamingOnlyFromEnv(t *testing.T) {
485487
t.Fatalf("expected httpStreamingOnly to default to true from env var")
486488
}
487489
}
490+
491+
func TestNewRootCommand_ForwardAuthorizationFlag(t *testing.T) {
492+
t.Setenv("PROXY_FORWARD_AUTHORIZATION", "")
493+
494+
var forwardAuthorization bool
495+
runner := proxyRunnerFunc(func(listen string,
496+
tlsListen string,
497+
autoTLS bool,
498+
tlsHost string,
499+
tlsDirectoryURL string,
500+
tlsAcceptTOS bool,
501+
tlsCertFile string,
502+
tlsKeyFile string,
503+
dataPath string,
504+
repositoryBackend string,
505+
repositoryDSN string,
506+
externalURL string,
507+
googleClientID string,
508+
googleClientSecret string,
509+
googleAllowedUsers []string,
510+
googleAllowedWorkspaces []string,
511+
githubClientID string,
512+
githubClientSecret string,
513+
githubAllowedUsers []string,
514+
githubAllowedOrgs []string,
515+
oidcConfigurationURL string,
516+
oidcClientID string,
517+
oidcClientSecret string,
518+
oidcScopes []string,
519+
oidcUserIDField string,
520+
oidcProviderName string,
521+
oidcAllowedUsers []string,
522+
oidcAllowedUsersGlob []string,
523+
oidcAllowedAttributes map[string][]string,
524+
oidcAllowedAttributesGlob map[string][]string,
525+
noProviderAutoSelect bool,
526+
password string,
527+
passwordHash string,
528+
trustedProxy []string,
529+
proxyHeaders []string,
530+
proxyBearerToken string,
531+
forwardAuthorizationHeader bool,
532+
proxyTarget []string,
533+
httpStreamingOnly bool,
534+
headerMapping map[string]string,
535+
headerMappingBase string,
536+
) error {
537+
forwardAuthorization = forwardAuthorizationHeader
538+
return nil
539+
})
540+
541+
cmd := newRootCommand(runner)
542+
cmd.SetArgs([]string{"--proxy-forward-authorization", "http://backend"})
543+
544+
if err := cmd.Execute(); err != nil {
545+
t.Fatalf("expected command to succeed, got error: %v", err)
546+
}
547+
548+
if !forwardAuthorization {
549+
t.Fatalf("expected forwardAuthorizationHeader to be true when flag is set")
550+
}
551+
}

pkg/mcp-proxy/main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ func Run(
7474
trustedProxy []string,
7575
proxyHeaders []string,
7676
proxyBearerToken string,
77+
forwardAuthorizationHeader bool,
7778
proxyTarget []string,
7879
httpStreamingOnly bool,
7980
headerMapping map[string]string,
@@ -298,7 +299,7 @@ func Run(
298299
if err != nil {
299300
return fmt.Errorf("failed to create IDP router: %w", err)
300301
}
301-
proxyRouter, err := newProxyRouter(externalURL, beHandler, &privKey.PublicKey, proxyHeadersMap, httpStreamingOnly, headerMapping, headerMappingBase)
302+
proxyRouter, err := newProxyRouter(externalURL, beHandler, &privKey.PublicKey, proxyHeadersMap, httpStreamingOnly, forwardAuthorizationHeader, headerMapping, headerMappingBase)
302303
if err != nil {
303304
return fmt.Errorf("failed to create proxy router: %w", err)
304305
}

pkg/proxy/proxy.go

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@ import (
1212
)
1313

1414
type ProxyRouter struct {
15-
externalURL string
16-
proxy http.Handler
17-
publicKey *rsa.PublicKey
18-
proxyHeaders http.Header
19-
httpStreamingOnly bool
20-
headerMapping map[string]string
21-
headerMappingBase string
15+
externalURL string
16+
proxy http.Handler
17+
publicKey *rsa.PublicKey
18+
proxyHeaders http.Header
19+
httpStreamingOnly bool
20+
forwardAuthorizationHeader bool
21+
headerMapping map[string]string
22+
headerMappingBase string
2223
}
2324

2425
func NewProxyRouter(
@@ -27,17 +28,19 @@ func NewProxyRouter(
2728
publicKey *rsa.PublicKey,
2829
proxyHeaders http.Header,
2930
httpStreamingOnly bool,
31+
forwardAuthorizationHeader bool,
3032
headerMapping map[string]string,
3133
headerMappingBase string,
3234
) (*ProxyRouter, error) {
3335
return &ProxyRouter{
34-
externalURL: externalURL,
35-
proxy: proxy,
36-
publicKey: publicKey,
37-
proxyHeaders: proxyHeaders,
38-
httpStreamingOnly: httpStreamingOnly,
39-
headerMapping: headerMapping,
40-
headerMappingBase: headerMappingBase,
36+
externalURL: externalURL,
37+
proxy: proxy,
38+
publicKey: publicKey,
39+
proxyHeaders: proxyHeaders,
40+
httpStreamingOnly: httpStreamingOnly,
41+
forwardAuthorizationHeader: forwardAuthorizationHeader,
42+
headerMapping: headerMapping,
43+
headerMappingBase: headerMappingBase,
4144
}, nil
4245
}
4346

@@ -87,8 +90,13 @@ func (p *ProxyRouter) handleProxy(c *gin.Context) {
8790
return
8891
}
8992

90-
c.Request.Header.Del("Authorization")
93+
if !p.forwardAuthorizationHeader {
94+
c.Request.Header.Del("Authorization")
95+
}
9196
for key, values := range p.proxyHeaders {
97+
if strings.EqualFold(key, "Authorization") {
98+
c.Request.Header.Del("Authorization")
99+
}
92100
for _, value := range values {
93101
c.Request.Header.Add(key, value)
94102
}

pkg/proxy/proxy_test.go

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ func TestProxyRouter_HandleProxy_ValidToken(t *testing.T) {
7070
proxyHeaders := make(http.Header)
7171
proxyHeaders.Set("X-Forwarded-By", "mcp-auth-proxy")
7272

73-
proxyRouter, err := NewProxyRouter("https://example.com", proxyHandler, publicKey, proxyHeaders, false, nil, "/userinfo")
73+
proxyRouter, err := NewProxyRouter("https://example.com", proxyHandler, publicKey, proxyHeaders, false, false, nil, "/userinfo")
7474
require.NoError(t, err)
7575

7676
gin.SetMode(gin.TestMode)
@@ -163,7 +163,7 @@ func TestProxyRouter_HeaderMapping(t *testing.T) {
163163
w.WriteHeader(http.StatusOK)
164164
})
165165

166-
proxyRouter, err := NewProxyRouter("https://example.com", proxyHandler, publicKey, http.Header{}, false, tt.headerMapping, "/userinfo")
166+
proxyRouter, err := NewProxyRouter("https://example.com", proxyHandler, publicKey, http.Header{}, false, false, tt.headerMapping, "/userinfo")
167167
require.NoError(t, err)
168168

169169
gin.SetMode(gin.TestMode)
@@ -293,7 +293,7 @@ func TestProxyRouter_HeaderMappingBase(t *testing.T) {
293293
w.WriteHeader(http.StatusOK)
294294
})
295295

296-
proxyRouter, err := NewProxyRouter("https://example.com", proxyHandler, publicKey, http.Header{}, false, tt.headerMapping, tt.headerMappingBase)
296+
proxyRouter, err := NewProxyRouter("https://example.com", proxyHandler, publicKey, http.Header{}, false, false, tt.headerMapping, tt.headerMappingBase)
297297
require.NoError(t, err)
298298

299299
gin.SetMode(gin.TestMode)
@@ -321,11 +321,80 @@ func TestProxyRouter_HeaderMappingBase(t *testing.T) {
321321
}
322322
}
323323

324+
func TestProxyRouter_AuthorizationHeaderDefaultBehavior(t *testing.T) {
325+
privateKey, publicKey, err := generateRSAKeyPair()
326+
require.NoError(t, err)
327+
328+
t.Run("strips authorization header by default", func(t *testing.T) {
329+
var backendAuthorization string
330+
proxyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
331+
backendAuthorization = r.Header.Get("Authorization")
332+
w.WriteHeader(http.StatusOK)
333+
})
334+
335+
proxyRouter, err := NewProxyRouter("https://example.com", proxyHandler, publicKey, http.Header{}, false, false, nil, "/userinfo")
336+
require.NoError(t, err)
337+
338+
gin.SetMode(gin.TestMode)
339+
router := gin.New()
340+
proxyRouter.SetupRoutes(router)
341+
342+
token, err := createJWT(privateKey, jwt.MapClaims{
343+
"sub": "user",
344+
"exp": time.Now().Add(time.Hour).Unix(),
345+
"iat": time.Now().Unix(),
346+
})
347+
require.NoError(t, err)
348+
349+
req, err := http.NewRequest(http.MethodGet, "/mcp", nil)
350+
require.NoError(t, err)
351+
req.Header.Set("Authorization", "Bearer "+token)
352+
353+
w := httptest.NewRecorder()
354+
router.ServeHTTP(w, req)
355+
356+
assert.Equal(t, http.StatusOK, w.Code)
357+
assert.Empty(t, backendAuthorization)
358+
})
359+
360+
t.Run("forwards authorization header when enabled", func(t *testing.T) {
361+
var backendAuthorization string
362+
proxyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
363+
backendAuthorization = r.Header.Get("Authorization")
364+
w.WriteHeader(http.StatusOK)
365+
})
366+
367+
proxyRouter, err := NewProxyRouter("https://example.com", proxyHandler, publicKey, http.Header{}, false, true, nil, "/userinfo")
368+
require.NoError(t, err)
369+
370+
gin.SetMode(gin.TestMode)
371+
router := gin.New()
372+
proxyRouter.SetupRoutes(router)
373+
374+
token, err := createJWT(privateKey, jwt.MapClaims{
375+
"sub": "user",
376+
"exp": time.Now().Add(time.Hour).Unix(),
377+
"iat": time.Now().Unix(),
378+
})
379+
require.NoError(t, err)
380+
381+
req, err := http.NewRequest(http.MethodGet, "/mcp", nil)
382+
require.NoError(t, err)
383+
req.Header.Set("Authorization", "Bearer "+token)
384+
385+
w := httptest.NewRecorder()
386+
router.ServeHTTP(w, req)
387+
388+
assert.Equal(t, http.StatusOK, w.Code)
389+
assert.Equal(t, "Bearer "+token, backendAuthorization)
390+
})
391+
}
392+
324393
func TestProxyRouter_ProtectedResourceTrailingSlash(t *testing.T) {
325394
_, publicKey, err := generateRSAKeyPair()
326395
require.NoError(t, err)
327396

328-
proxyRouter, err := NewProxyRouter("https://example.com/", http.NotFoundHandler(), publicKey, http.Header{}, false, nil, "/userinfo")
397+
proxyRouter, err := NewProxyRouter("https://example.com/", http.NotFoundHandler(), publicKey, http.Header{}, false, false, nil, "/userinfo")
329398
require.NoError(t, err)
330399

331400
gin.SetMode(gin.TestMode)
@@ -429,7 +498,7 @@ func TestProxyRouter_HTTPStreamingOnlyRejectsSSE(t *testing.T) {
429498
w.WriteHeader(http.StatusOK)
430499
})
431500

432-
proxyRouter, err := NewProxyRouter("https://example.com", proxyHandler, publicKey, http.Header{}, tt.streamingOnly, nil, "/userinfo")
501+
proxyRouter, err := NewProxyRouter("https://example.com", proxyHandler, publicKey, http.Header{}, tt.streamingOnly, false, nil, "/userinfo")
433502
require.NoError(t, err)
434503

435504
gin.SetMode(gin.TestMode)

0 commit comments

Comments
 (0)