Skip to content

Commit 4ba2dcf

Browse files
aron-muonclaude
andcommitted
Add disableUpstreamTokenInjection to embedded auth server config
The embedded auth server always injected upstream IdP tokens into requests forwarded to backend MCP servers. This made it impossible to use the embedded auth server for client-facing OAuth flows when the upstream MCP server is public and doesn't require authentication — the injected token caused 401 rejections from the upstream. Add a `disableUpstreamTokenInjection` field to EmbeddedAuthServerConfig that skips the upstream swap middleware while keeping the embedded auth server running for client authentication. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0de510d commit 4ba2dcf

File tree

9 files changed

+102
-2
lines changed

9 files changed

+102
-2
lines changed

cmd/thv-operator/api/v1alpha1/mcpexternalauthconfig_types.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,16 @@ type EmbeddedAuthServerConfig struct {
192192
// +optional
193193
Storage *AuthServerStorageConfig `json:"storage,omitempty"`
194194

195+
// DisableUpstreamTokenInjection prevents the embedded auth server from injecting
196+
// upstream IdP tokens into requests forwarded to the backend MCP server.
197+
// When true, the embedded auth server still handles OAuth flows for clients
198+
// but does not swap ToolHive JWTs for upstream tokens on outgoing requests.
199+
// This is useful when the backend MCP server does not require authentication
200+
// (e.g., public documentation servers) but you still want client authentication.
201+
// +kubebuilder:default=false
202+
// +optional
203+
DisableUpstreamTokenInjection bool `json:"disableUpstreamTokenInjection,omitempty"`
204+
195205
// AllowedAudiences is the list of valid resource URIs that tokens can be issued for.
196206
// For an embedded auth server, this can be determined by the servers (MCP or vMCP) it serves.
197207

cmd/thv-operator/pkg/controllerutil/authserver.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,9 @@ func buildEmbeddedAuthServerRunnerConfig(
444444
}
445445
config.Storage = storageCfg
446446

447+
// Wire through upstream token injection flag
448+
config.DisableUpstreamTokenInjection = authConfig.DisableUpstreamTokenInjection
449+
447450
return config, nil
448451
}
449452

cmd/thv-operator/pkg/controllerutil/authserver_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -875,6 +875,43 @@ func TestBuildEmbeddedAuthServerRunnerConfig(t *testing.T) {
875875
assert.Equal(t, []string{"openid", "profile", "email", "custom:scope"}, config.ScopesSupported)
876876
},
877877
},
878+
{
879+
name: "DisableUpstreamTokenInjection is wired through",
880+
authConfig: &mcpv1alpha1.EmbeddedAuthServerConfig{
881+
Issuer: "https://auth.example.com",
882+
SigningKeySecretRefs: []mcpv1alpha1.SecretKeyRef{
883+
{Name: "signing-key", Key: "private.pem"},
884+
},
885+
HMACSecretRefs: []mcpv1alpha1.SecretKeyRef{
886+
{Name: "hmac-secret", Key: "hmac"},
887+
},
888+
DisableUpstreamTokenInjection: true,
889+
},
890+
oidcConfig: defaultOIDCConfig,
891+
checkFunc: func(t *testing.T, config *authserver.RunConfig) {
892+
t.Helper()
893+
assert.True(t, config.DisableUpstreamTokenInjection,
894+
"DisableUpstreamTokenInjection should be wired from CRD to RunConfig")
895+
},
896+
},
897+
{
898+
name: "DisableUpstreamTokenInjection defaults to false",
899+
authConfig: &mcpv1alpha1.EmbeddedAuthServerConfig{
900+
Issuer: "https://auth.example.com",
901+
SigningKeySecretRefs: []mcpv1alpha1.SecretKeyRef{
902+
{Name: "signing-key", Key: "private.pem"},
903+
},
904+
HMACSecretRefs: []mcpv1alpha1.SecretKeyRef{
905+
{Name: "hmac-secret", Key: "hmac"},
906+
},
907+
},
908+
oidcConfig: defaultOIDCConfig,
909+
checkFunc: func(t *testing.T, config *authserver.RunConfig) {
910+
t.Helper()
911+
assert.False(t, config.DisableUpstreamTokenInjection,
912+
"DisableUpstreamTokenInjection should default to false")
913+
},
914+
},
878915
}
879916

880917
for _, tt := range tests {

deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,16 @@ spec:
181181
EmbeddedAuthServer configures an embedded OAuth2/OIDC authorization server
182182
Only used when Type is "embeddedAuthServer"
183183
properties:
184+
disableUpstreamTokenInjection:
185+
default: false
186+
description: |-
187+
DisableUpstreamTokenInjection prevents the embedded auth server from injecting
188+
upstream IdP tokens into requests forwarded to the backend MCP server.
189+
When true, the embedded auth server still handles OAuth flows for clients
190+
but does not swap ToolHive JWTs for upstream tokens on outgoing requests.
191+
This is useful when the backend MCP server does not require authentication
192+
(e.g., public documentation servers) but you still want client authentication.
193+
type: boolean
184194
hmacSecretRefs:
185195
description: |-
186196
HMACSecretRefs references Kubernetes Secrets containing symmetric secrets for signing

deploy/charts/operator-crds/templates/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,16 @@ spec:
184184
EmbeddedAuthServer configures an embedded OAuth2/OIDC authorization server
185185
Only used when Type is "embeddedAuthServer"
186186
properties:
187+
disableUpstreamTokenInjection:
188+
default: false
189+
description: |-
190+
DisableUpstreamTokenInjection prevents the embedded auth server from injecting
191+
upstream IdP tokens into requests forwarded to the backend MCP server.
192+
When true, the embedded auth server still handles OAuth flows for clients
193+
but does not swap ToolHive JWTs for upstream tokens on outgoing requests.
194+
This is useful when the backend MCP server does not require authentication
195+
(e.g., public documentation servers) but you still want client authentication.
196+
type: boolean
187197
hmacSecretRefs:
188198
description: |-
189199
HMACSecretRefs references Kubernetes Secrets containing symmetric secrets for signing

docs/operator/crd-api.md

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/authserver/config.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ type RunConfig struct {
7171
// Storage configures the storage backend for the auth server.
7272
// If nil, defaults to in-memory storage.
7373
Storage *storage.RunConfig `json:"storage,omitempty" yaml:"storage,omitempty"`
74+
75+
// DisableUpstreamTokenInjection prevents the upstream swap middleware from being added.
76+
// When true, the embedded auth server handles OAuth flows for clients but does not
77+
// inject upstream IdP tokens into requests forwarded to the backend MCP server.
78+
DisableUpstreamTokenInjection bool `json:"disable_upstream_token_injection,omitempty" yaml:"disable_upstream_token_injection,omitempty"`
7479
}
7580

7681
// SigningKeyRunConfig configures where to load signing keys from.

pkg/runner/middleware.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,8 +251,9 @@ func addUsageMetricsMiddleware(middlewares []types.MiddlewareConfig, configDisab
251251

252252
// addUpstreamSwapMiddleware adds upstream swap middleware if the embedded auth server is configured.
253253
// This middleware exchanges ToolHive JWTs for upstream IdP tokens.
254-
// The middleware is only added when EmbeddedAuthServerConfig is set; if UpstreamSwapConfig
255-
// is nil, default configuration values are used.
254+
// The middleware is only added when EmbeddedAuthServerConfig is set and
255+
// DisableUpstreamTokenInjection is false. If UpstreamSwapConfig is nil,
256+
// default configuration values are used.
256257
func addUpstreamSwapMiddleware(
257258
middlewares []types.MiddlewareConfig,
258259
config *RunConfig,
@@ -262,6 +263,11 @@ func addUpstreamSwapMiddleware(
262263
return middlewares, nil
263264
}
264265

266+
// Skip upstream token injection if explicitly disabled
267+
if config.EmbeddedAuthServerConfig.DisableUpstreamTokenInjection {
268+
return middlewares, nil
269+
}
270+
265271
// Use provided config or defaults
266272
upstreamSwapConfig := config.UpstreamSwapConfig
267273
if upstreamSwapConfig == nil {

pkg/runner/middleware_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,15 @@ func TestAddUpstreamSwapMiddleware(t *testing.T) {
270270
},
271271
wantAppended: true,
272272
},
273+
{
274+
name: "EmbeddedAuthServerConfig with DisableUpstreamTokenInjection skips middleware",
275+
config: func() *RunConfig {
276+
cfg := createMinimalAuthServerConfig()
277+
cfg.DisableUpstreamTokenInjection = true
278+
return &RunConfig{EmbeddedAuthServerConfig: cfg}
279+
}(),
280+
wantAppended: false,
281+
},
273282
{
274283
name: "EmbeddedAuthServerConfig set with explicit UpstreamSwapConfig uses provided config",
275284
config: &RunConfig{
@@ -347,6 +356,15 @@ func TestPopulateMiddlewareConfigs_UpstreamSwap(t *testing.T) {
347356
config: &RunConfig{EmbeddedAuthServerConfig: nil},
348357
wantUpstreamSwap: false,
349358
},
359+
{
360+
name: "DisableUpstreamTokenInjection omits upstream-swap",
361+
config: func() *RunConfig {
362+
cfg := createMinimalAuthServerConfig()
363+
cfg.DisableUpstreamTokenInjection = true
364+
return &RunConfig{EmbeddedAuthServerConfig: cfg}
365+
}(),
366+
wantUpstreamSwap: false,
367+
},
350368
{
351369
name: "explicit UpstreamSwapConfig is used",
352370
config: &RunConfig{

0 commit comments

Comments
 (0)