Skip to content

Commit 074326e

Browse files
jhrozekclaude
andauthored
Enable multi-upstream for MCPServer, MCPRemoteProxy, and proxy runner (#4322)
* Add explicit-name validation and proxy runner multi-upstream guard Move multi-upstream restrictions from the authserver library to consumer layers. The library now accepts multi-upstream configs but enforces name semantics: single-upstream defaults empty names to "default", while multi-upstream requires explicit non-"default" names with distinct error messages for empty vs reserved names. Validate upstream names against a DNS-label regex (no leading/trailing hyphens, lowercase alphanumeric only) to prevent delimiter injection in storage keys. Add test coverage for invalid name formats (uppercase, underscores, leading/trailing hyphens). Remove the GetUpstream() convenience method (no callers remain after Phase 2). Add a cardinality guard in the proxy runner's Run() that rejects len(Upstreams) > 1 with an actionable error pointing to VirtualMCPServer. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Move multi-upstream restriction from CRD to controller layers Remove the len > 1 guard from MCPExternalAuthConfig.validateEmbeddedAuthServer() so the CRD accepts multi-upstream configs. Add multi-upstream rejection to MCPServer and MCPRemoteProxy controllers in handleExternalAuthConfig(), setting a ConditionFalse status with reason MultiUpstreamNotSupported and an actionable error directing users to VirtualMCPServer. Add duplicate upstream name validation in the CRD webhook so conflicts are caught at admission time rather than Pod startup. Tighten the Name field pattern to disallow trailing hyphens and add MaxLength=63 for RFC 1123 compliance. VirtualMCPServer remains unrestricted as the intended multi-upstream consumer. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Enable multi-upstream for MCPServer, MCPRemoteProxy, and proxy runner The embedded auth server has supported sequential multi-upstream chains since the Phase 2 work, but every consumer layer (MCPServer controller, MCPRemoteProxy controller, proxy runner) blocked configs with more than one upstream provider. This commit lifts all those restrictions and fixes the two bugs that would have broken multi-upstream even without the guards. Guard removal: - MCPServer controller: remove len > 1 check in handleExternalAuthConfig - MCPRemoteProxy controller: same removal - Proxy runner Run(): remove len > 1 early-return - Remove associated condition type/reason constants and rejection tests Converter fix (authserver.go): - GenerateAuthServerEnvVars now iterates all providers and emits name-keyed env vars (TOOLHIVE_UPSTREAM_CLIENT_SECRET_OKTA, _GITHUB, …) derived from each provider's Name field, instead of only reading UpstreamProviders[0] into a single unindexed var. Name-keyed bindings are position-independent, so reordering providers in the CRD does not change the secret-to-provider mapping. - buildEmbeddedAuthServerRunnerConfig now builds Upstreams from all providers instead of only the first; buildUpstreamRunConfig gains an envVarName parameter to match the env var naming Middleware fix (middleware.go): - Restore ProviderName auto-derivation in addUpstreamSwapMiddleware, defaulting to the first upstream's name for single-upstream configs. Multi-upstream ProviderName selection will be addressed when a CRD field is added in a follow-up. Observability (upstreamswap/middleware.go): - Add provider field to all upstreamswap log lines (injection success, empty-token warning, get-tokens error) to make confused-deputy debugging tractable when multiple providers are in play. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix lint: reduce cyclomatic complexity and update stale comments Extract validateUpstreamName and validateUpstreamType from validateUpstreams to bring cyclomatic complexity under the limit. Break long fmt.Errorf line that exceeded 130 characters. Update stale godoc comments in controllerutil/authserver.go that still referenced the old index-based env var naming scheme. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Downgrade client-attributable token errors to Debug log level Client-attributable errors (session not found, no refresh token, refresh failed, invalid binding) are expected conditions that trigger re-auth, not server issues an operator needs to investigate. Log these at Debug instead of Warn; keep Warn for unexpected server-side failures. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Re-add multi-upstream guards for MCPServer and MCPRemoteProxy The upstream swap middleware only injects one provider's token, and there is no CRD field to control which provider. Until tokenInjection- Provider is added and Cedar upstream claims extraction is implemented, multi-upstream configs would silently inject only the first upstream's token while the second is stored but unused. Re-add controller-level guards (MCPServer, MCPRemoteProxy, proxy runner) that reject len > 1 with actionable errors directing users to VirtualMCPServer. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * regen docs --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f64d807 commit 074326e

20 files changed

Lines changed: 482 additions & 144 deletions

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ type EmbeddedAuthServerConfig struct {
182182

183183
// UpstreamProviders configures connections to upstream Identity Providers.
184184
// The embedded auth server delegates authentication to these providers.
185-
// Currently only a single upstream provider is supported (validated at runtime).
185+
// MCPServer and MCPRemoteProxy support a single upstream; VirtualMCPServer supports multiple.
186186
// +kubebuilder:validation:Required
187187
// +kubebuilder:validation:MinItems=1
188188
UpstreamProviders []UpstreamProviderConfig `json:"upstreamProviders"`
@@ -238,8 +238,11 @@ const (
238238
type UpstreamProviderConfig struct {
239239
// Name uniquely identifies this upstream provider.
240240
// Used for routing decisions and session binding in multi-upstream scenarios.
241+
// Must be lowercase alphanumeric with hyphens (DNS-label-like).
241242
// +kubebuilder:validation:Required
242243
// +kubebuilder:validation:MinLength=1
244+
// +kubebuilder:validation:MaxLength=63
245+
// +kubebuilder:validation:Pattern=`^[a-z0-9]([a-z0-9-]*[a-z0-9])?$`
243246
Name string `json:"name"`
244247

245248
// Type specifies the provider type: "oidc" or "oauth2"
@@ -783,12 +786,17 @@ func (r *MCPExternalAuthConfig) validateEmbeddedAuthServer() error {
783786
if len(cfg.UpstreamProviders) == 0 {
784787
return fmt.Errorf("at least one upstream provider is required")
785788
}
786-
// Note: we add runtime validation for 'max items = 1' here since multi-provider support is not yet implemented.
787-
if len(cfg.UpstreamProviders) > 1 {
788-
return fmt.Errorf("currently only one upstream provider is supported (found %d)", len(cfg.UpstreamProviders))
789-
}
789+
// Note: multi-upstream is accepted at the CRD level. Consumer controllers
790+
// (MCPServer, MCPRemoteProxy) enforce single-upstream restrictions;
791+
// VirtualMCPServer allows multiple upstreams.
790792

793+
seen := make(map[string]bool, len(cfg.UpstreamProviders))
791794
for i, provider := range cfg.UpstreamProviders {
795+
if seen[provider.Name] {
796+
return fmt.Errorf("upstreamProviders[%d]: duplicate name %q", i, provider.Name)
797+
}
798+
seen[provider.Name] = true
799+
792800
if err := r.validateUpstreamProvider(i, &provider); err != nil {
793801
return err
794802
}

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

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ func TestMCPExternalAuthConfig_Validate(t *testing.T) {
127127
expectErr: false,
128128
},
129129
{
130-
name: "invalid embeddedAuthServer with multiple providers",
130+
name: "embeddedAuthServer with multiple providers - valid at CRD level",
131131
config: &MCPExternalAuthConfig{
132132
ObjectMeta: metav1.ObjectMeta{
133133
Name: "test-embedded-multi",
@@ -152,8 +152,7 @@ func TestMCPExternalAuthConfig_Validate(t *testing.T) {
152152
},
153153
},
154154
},
155-
expectErr: true,
156-
errMsg: "currently only one upstream provider is supported (found 2)",
155+
expectErr: false,
157156
},
158157
{
159158
name: "invalid embeddedAuthServer with no providers",
@@ -312,7 +311,7 @@ func TestMCPExternalAuthConfig_validateEmbeddedAuthServer(t *testing.T) {
312311
expectErr: false,
313312
},
314313
{
315-
name: "multiple providers - invalid",
314+
name: "multiple providers - valid at CRD level",
316315
config: &MCPExternalAuthConfig{
317316
Spec: MCPExternalAuthConfigSpec{
318317
Type: ExternalAuthTypeEmbeddedAuthServer,
@@ -343,8 +342,7 @@ func TestMCPExternalAuthConfig_validateEmbeddedAuthServer(t *testing.T) {
343342
},
344343
},
345344
},
346-
expectErr: true,
347-
errMsg: "currently only one upstream provider is supported (found 3)",
345+
expectErr: false,
348346
},
349347
{
350348
name: "empty providers array - invalid",

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,10 @@ const (
260260
// ConditionReasonMCPRemoteProxyExternalAuthConfigFetchError indicates an error occurred fetching the MCPExternalAuthConfig
261261
ConditionReasonMCPRemoteProxyExternalAuthConfigFetchError = "ExternalAuthConfigFetchError"
262262

263+
// ConditionReasonMCPRemoteProxyExternalAuthConfigMultiUpstream indicates multi-upstream is not supported
264+
// for MCPRemoteProxy (use VirtualMCPServer for multi-upstream).
265+
ConditionReasonMCPRemoteProxyExternalAuthConfigMultiUpstream = "MultiUpstreamNotSupported"
266+
263267
// ConditionReasonConfigurationValid indicates all configuration validations passed
264268
ConditionReasonConfigurationValid = "ConfigurationValid"
265269

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,17 @@ const (
6868
ConditionReasonCABundleRefInvalid = "CABundleRefInvalid"
6969
)
7070

71+
const (
72+
// ConditionTypeExternalAuthConfigValidated indicates whether the ExternalAuthConfig is valid
73+
ConditionTypeExternalAuthConfigValidated = "ExternalAuthConfigValidated"
74+
)
75+
76+
const (
77+
// ConditionReasonExternalAuthConfigMultiUpstream indicates the ExternalAuthConfig has multiple upstreams,
78+
// which is not supported for MCPServer (use VirtualMCPServer for multi-upstream).
79+
ConditionReasonExternalAuthConfigMultiUpstream = "MultiUpstreamNotSupported"
80+
)
81+
7182
// MCPServerSpec defines the desired state of MCPServer
7283
type MCPServerSpec struct {
7384
// Image is the container image for the MCP server

cmd/thv-operator/controllers/mcpremoteproxy_controller.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,23 @@ func (r *MCPRemoteProxyReconciler) handleExternalAuthConfig(ctx context.Context,
652652
return fmt.Errorf("failed to fetch MCPExternalAuthConfig: %w", err)
653653
}
654654

655+
// MCPRemoteProxy supports only single-upstream embedded auth server configs.
656+
// Multi-upstream requires VirtualMCPServer.
657+
if embeddedCfg := externalAuthConfig.Spec.EmbeddedAuthServer; embeddedCfg != nil && len(embeddedCfg.UpstreamProviders) > 1 {
658+
meta.SetStatusCondition(&proxy.Status.Conditions, metav1.Condition{
659+
Type: mcpv1alpha1.ConditionTypeMCPRemoteProxyExternalAuthConfigValidated,
660+
Status: metav1.ConditionFalse,
661+
Reason: mcpv1alpha1.ConditionReasonMCPRemoteProxyExternalAuthConfigMultiUpstream,
662+
Message: fmt.Sprintf(
663+
"MCPRemoteProxy supports only one upstream provider (found %d); "+
664+
"use VirtualMCPServer for multi-upstream",
665+
len(embeddedCfg.UpstreamProviders)),
666+
ObservedGeneration: proxy.Generation,
667+
})
668+
return fmt.Errorf("MCPRemoteProxy %s/%s: embedded auth server has %d upstream providers, but only 1 is supported",
669+
proxy.Namespace, proxy.Name, len(embeddedCfg.UpstreamProviders))
670+
}
671+
655672
// ExternalAuthConfig found and valid
656673
meta.SetStatusCondition(&proxy.Status.Conditions, metav1.Condition{
657674
Type: mcpv1alpha1.ConditionTypeMCPRemoteProxyExternalAuthConfigValidated,

cmd/thv-operator/controllers/mcpremoteproxy_controller_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,43 @@ func TestHandleExternalAuthConfig(t *testing.T) {
699699
expectedCondStatus: metav1.ConditionFalse,
700700
expectedCondReason: mcpv1alpha1.ConditionReasonMCPRemoteProxyExternalAuthConfigFetchError,
701701
},
702+
{
703+
name: "embedded auth server with multiple upstreams rejected",
704+
proxy: &mcpv1alpha1.MCPRemoteProxy{
705+
ObjectMeta: metav1.ObjectMeta{
706+
Name: "multi-upstream-proxy",
707+
Namespace: "default",
708+
},
709+
Spec: mcpv1alpha1.MCPRemoteProxySpec{
710+
RemoteURL: "https://mcp.example.com",
711+
ExternalAuthConfigRef: &mcpv1alpha1.ExternalAuthConfigRef{
712+
Name: "multi-upstream-config",
713+
},
714+
},
715+
},
716+
externalAuth: &mcpv1alpha1.MCPExternalAuthConfig{
717+
ObjectMeta: metav1.ObjectMeta{
718+
Name: "multi-upstream-config",
719+
Namespace: "default",
720+
},
721+
Spec: mcpv1alpha1.MCPExternalAuthConfigSpec{
722+
Type: mcpv1alpha1.ExternalAuthTypeEmbeddedAuthServer,
723+
EmbeddedAuthServer: &mcpv1alpha1.EmbeddedAuthServerConfig{
724+
Issuer: "https://auth.example.com",
725+
UpstreamProviders: []mcpv1alpha1.UpstreamProviderConfig{
726+
{Name: "github", Type: mcpv1alpha1.UpstreamProviderTypeOIDC, OIDCConfig: &mcpv1alpha1.OIDCUpstreamConfig{IssuerURL: "https://github.com", ClientID: "id1"}},
727+
{Name: "google", Type: mcpv1alpha1.UpstreamProviderTypeOIDC, OIDCConfig: &mcpv1alpha1.OIDCUpstreamConfig{IssuerURL: "https://accounts.google.com", ClientID: "id2"}},
728+
},
729+
},
730+
},
731+
Status: mcpv1alpha1.MCPExternalAuthConfigStatus{ConfigHash: "multi-hash"},
732+
},
733+
expectError: true,
734+
errContains: "only 1 is supported",
735+
expectCondition: true,
736+
expectedCondStatus: metav1.ConditionFalse,
737+
expectedCondReason: mcpv1alpha1.ConditionReasonMCPRemoteProxyExternalAuthConfigMultiUpstream,
738+
},
702739
}
703740

704741
for _, tt := range tests {

cmd/thv-operator/controllers/mcpserver_controller.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1827,6 +1827,25 @@ func (r *MCPServerReconciler) handleExternalAuthConfig(ctx context.Context, m *m
18271827
return fmt.Errorf("MCPExternalAuthConfig %s not found", m.Spec.ExternalAuthConfigRef.Name)
18281828
}
18291829

1830+
// MCPServer supports only single-upstream embedded auth server configs.
1831+
// Multi-upstream requires VirtualMCPServer.
1832+
if embeddedCfg := externalAuthConfig.Spec.EmbeddedAuthServer; embeddedCfg != nil && len(embeddedCfg.UpstreamProviders) > 1 {
1833+
meta.SetStatusCondition(&m.Status.Conditions, metav1.Condition{
1834+
Type: mcpv1alpha1.ConditionTypeExternalAuthConfigValidated,
1835+
Status: metav1.ConditionFalse,
1836+
Reason: mcpv1alpha1.ConditionReasonExternalAuthConfigMultiUpstream,
1837+
Message: fmt.Sprintf(
1838+
"MCPServer supports only one upstream provider (found %d); "+
1839+
"use VirtualMCPServer for multi-upstream",
1840+
len(embeddedCfg.UpstreamProviders)),
1841+
ObservedGeneration: m.Generation,
1842+
})
1843+
return fmt.Errorf(
1844+
"MCPServer %s/%s: embedded auth server has %d upstream providers, "+
1845+
"but only 1 is supported; use VirtualMCPServer",
1846+
m.Namespace, m.Name, len(embeddedCfg.UpstreamProviders))
1847+
}
1848+
18301849
// Check if the MCPExternalAuthConfig hash has changed
18311850
if m.Status.ExternalAuthConfigHash != externalAuthConfig.Status.ConfigHash {
18321851
ctxLogger.Info("MCPExternalAuthConfig has changed, updating MCPServer",

cmd/thv-operator/controllers/mcpserver_externalauth_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,40 @@ func TestMCPServerReconciler_handleExternalAuthConfig(t *testing.T) {
175175
expectHash: "",
176176
expectHashCleared: true,
177177
},
178+
{
179+
name: "embedded auth server with multiple upstreams rejected",
180+
mcpServer: &mcpv1alpha1.MCPServer{
181+
ObjectMeta: metav1.ObjectMeta{
182+
Name: "test-server",
183+
Namespace: "default",
184+
},
185+
Spec: mcpv1alpha1.MCPServerSpec{
186+
Image: "test-image",
187+
ExternalAuthConfigRef: &mcpv1alpha1.ExternalAuthConfigRef{
188+
Name: "multi-upstream-config",
189+
},
190+
},
191+
Status: mcpv1alpha1.MCPServerStatus{},
192+
},
193+
externalAuthConfig: &mcpv1alpha1.MCPExternalAuthConfig{
194+
ObjectMeta: metav1.ObjectMeta{
195+
Name: "multi-upstream-config",
196+
Namespace: "default",
197+
},
198+
Spec: mcpv1alpha1.MCPExternalAuthConfigSpec{
199+
Type: mcpv1alpha1.ExternalAuthTypeEmbeddedAuthServer,
200+
EmbeddedAuthServer: &mcpv1alpha1.EmbeddedAuthServerConfig{
201+
Issuer: "https://auth.example.com",
202+
UpstreamProviders: []mcpv1alpha1.UpstreamProviderConfig{
203+
{Name: "github", Type: mcpv1alpha1.UpstreamProviderTypeOIDC, OIDCConfig: &mcpv1alpha1.OIDCUpstreamConfig{IssuerURL: "https://github.com", ClientID: "id1"}},
204+
{Name: "google", Type: mcpv1alpha1.UpstreamProviderTypeOIDC, OIDCConfig: &mcpv1alpha1.OIDCUpstreamConfig{IssuerURL: "https://accounts.google.com", ClientID: "id2"}},
205+
},
206+
},
207+
},
208+
Status: mcpv1alpha1.MCPExternalAuthConfigStatus{ConfigHash: "multi-hash"},
209+
},
210+
expectError: true,
211+
},
178212
}
179213

180214
for _, tt := range tests {

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

Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package controllerutil
66
import (
77
"context"
88
"fmt"
9+
"strings"
910

1011
corev1 "k8s.io/api/core/v1"
1112
k8sptr "k8s.io/utils/ptr"
@@ -51,14 +52,43 @@ const (
5152
// AuthServerHMACFilePattern is the pattern for HMAC secret filenames
5253
AuthServerHMACFilePattern = "hmac-%d"
5354

54-
// UpstreamClientSecretEnvVar is the environment variable name for the upstream client secret
55+
// UpstreamClientSecretEnvVar is the prefix for upstream client secret environment variables.
56+
// Actual names are TOOLHIVE_UPSTREAM_CLIENT_SECRET_<PROVIDER> where PROVIDER is the
57+
// upstream name uppercased with hyphens replaced by underscores.
5558
// #nosec G101 -- This is an environment variable name, not a hardcoded credential
5659
UpstreamClientSecretEnvVar = "TOOLHIVE_UPSTREAM_CLIENT_SECRET"
5760

5861
// DefaultSentinelPort is the default Redis Sentinel port
5962
DefaultSentinelPort = 26379
6063
)
6164

65+
// upstreamSecretBinding binds an upstream provider to its env var name for the
66+
// client secret. Both GenerateAuthServerEnvVars (Pod env) and
67+
// buildUpstreamRunConfig (runtime config) MUST use these bindings so the
68+
// env var names stay consistent.
69+
type upstreamSecretBinding struct {
70+
Provider *mcpv1alpha1.UpstreamProviderConfig
71+
EnvVarName string
72+
}
73+
74+
// buildUpstreamSecretBindings computes the canonical env var name for each
75+
// upstream provider's client secret. The env var name is derived from the
76+
// provider's Name field (uppercased, hyphens replaced with underscores) to
77+
// keep bindings stable across provider reordering in the CRD.
78+
func buildUpstreamSecretBindings(
79+
providers []mcpv1alpha1.UpstreamProviderConfig,
80+
) []upstreamSecretBinding {
81+
bindings := make([]upstreamSecretBinding, len(providers))
82+
for i := range providers {
83+
suffix := strings.ToUpper(strings.ReplaceAll(providers[i].Name, "-", "_"))
84+
bindings[i] = upstreamSecretBinding{
85+
Provider: &providers[i],
86+
EnvVarName: fmt.Sprintf("%s_%s", UpstreamClientSecretEnvVar, suffix),
87+
}
88+
}
89+
return bindings
90+
}
91+
6292
// GenerateAuthServerConfig generates volumes, volume mounts, and environment variables
6393
// for the embedded auth server if the external auth config is of type embeddedAuthServer.
6494
//
@@ -228,13 +258,11 @@ func GenerateAuthServerVolumes(
228258
}
229259

230260
// GenerateAuthServerEnvVars creates environment variables for embedded auth server.
231-
// Currently generates TOOLHIVE_UPSTREAM_CLIENT_SECRET from the upstream provider's
232-
// client secret reference.
261+
// Generates TOOLHIVE_UPSTREAM_CLIENT_SECRET_<PROVIDER> env vars for each upstream
262+
// provider that has a client secret reference configured, where PROVIDER is the
263+
// provider name uppercased with hyphens replaced by underscores.
233264
//
234-
// The function looks at the first upstream provider (currently only one is supported)
235-
// and generates an environment variable for its client secret if one is configured.
236-
//
237-
// Returns nil slice if authConfig is nil or if no client secret is configured.
265+
// Returns nil slice if authConfig is nil or if no client secrets are configured.
238266
func GenerateAuthServerEnvVars(
239267
authConfig *mcpv1alpha1.EmbeddedAuthServerConfig,
240268
) []corev1.EnvVar {
@@ -244,27 +272,25 @@ func GenerateAuthServerEnvVars(
244272

245273
var envVars []corev1.EnvVar
246274

247-
// Generate env var for upstream client secret if provided
248-
if len(authConfig.UpstreamProviders) > 0 {
249-
provider := authConfig.UpstreamProviders[0]
250-
275+
// Generate env vars for upstream client secrets using shared bindings
276+
for _, b := range buildUpstreamSecretBindings(authConfig.UpstreamProviders) {
251277
// Extract client secret reference based on provider type
252278
var clientSecretRef *mcpv1alpha1.SecretKeyRef
253279

254-
switch provider.Type {
280+
switch b.Provider.Type {
255281
case mcpv1alpha1.UpstreamProviderTypeOIDC:
256-
if provider.OIDCConfig != nil {
257-
clientSecretRef = provider.OIDCConfig.ClientSecretRef
282+
if b.Provider.OIDCConfig != nil {
283+
clientSecretRef = b.Provider.OIDCConfig.ClientSecretRef
258284
}
259285
case mcpv1alpha1.UpstreamProviderTypeOAuth2:
260-
if provider.OAuth2Config != nil {
261-
clientSecretRef = provider.OAuth2Config.ClientSecretRef
286+
if b.Provider.OAuth2Config != nil {
287+
clientSecretRef = b.Provider.OAuth2Config.ClientSecretRef
262288
}
263289
}
264290

265291
if clientSecretRef != nil {
266292
envVars = append(envVars, corev1.EnvVar{
267-
Name: UpstreamClientSecretEnvVar,
293+
Name: b.EnvVarName,
268294
ValueFrom: &corev1.EnvVarSource{
269295
SecretKeyRef: &corev1.SecretKeySelector{
270296
LocalObjectReference: corev1.LocalObjectReference{
@@ -431,10 +457,11 @@ func buildEmbeddedAuthServerRunnerConfig(
431457
}
432458
}
433459

434-
// Build upstream provider config (currently only one supported)
435-
if len(authConfig.UpstreamProviders) > 0 {
436-
provider := authConfig.UpstreamProviders[0]
437-
config.Upstreams = []authserver.UpstreamRunConfig{*buildUpstreamRunConfig(&provider)}
460+
// Build upstream provider configs using shared bindings
461+
bindings := buildUpstreamSecretBindings(authConfig.UpstreamProviders)
462+
config.Upstreams = make([]authserver.UpstreamRunConfig, 0, len(bindings))
463+
for _, b := range bindings {
464+
config.Upstreams = append(config.Upstreams, *buildUpstreamRunConfig(b.Provider, b.EnvVarName))
438465
}
439466

440467
// Build storage configuration
@@ -561,9 +588,11 @@ func resolveSentinelAddrs(
561588
}
562589

563590
// buildUpstreamRunConfig converts CRD UpstreamProviderConfig to authserver.UpstreamRunConfig.
564-
// Client secrets are passed via environment variable reference (UpstreamClientSecretEnvVar).
591+
// The envVarName is computed by buildUpstreamSecretBindings to keep Pod env
592+
// and runtime config in sync.
565593
func buildUpstreamRunConfig(
566594
provider *mcpv1alpha1.UpstreamProviderConfig,
595+
envVarName string,
567596
) *authserver.UpstreamRunConfig {
568597
config := &authserver.UpstreamRunConfig{
569598
Name: provider.Name,
@@ -581,7 +610,7 @@ func buildUpstreamRunConfig(
581610
}
582611
// If client secret is configured, reference it via env var
583612
if provider.OIDCConfig.ClientSecretRef != nil {
584-
config.OIDCConfig.ClientSecretEnvVar = UpstreamClientSecretEnvVar
613+
config.OIDCConfig.ClientSecretEnvVar = envVarName
585614
}
586615
if provider.OIDCConfig.UserInfoOverride != nil {
587616
config.OIDCConfig.UserInfoOverride = buildUserInfoRunConfig(provider.OIDCConfig.UserInfoOverride)
@@ -598,7 +627,7 @@ func buildUpstreamRunConfig(
598627
}
599628
// If client secret is configured, reference it via env var
600629
if provider.OAuth2Config.ClientSecretRef != nil {
601-
config.OAuth2Config.ClientSecretEnvVar = UpstreamClientSecretEnvVar
630+
config.OAuth2Config.ClientSecretEnvVar = envVarName
602631
}
603632
if provider.OAuth2Config.UserInfo != nil {
604633
config.OAuth2Config.UserInfo = buildUserInfoRunConfig(provider.OAuth2Config.UserInfo)

0 commit comments

Comments
 (0)