From 39a9a1e3679444fba01c930af042a0238f65b536 Mon Sep 17 00:00:00 2001 From: Jakub Hrozek Date: Tue, 24 Mar 2026 15:49:28 +0000 Subject: [PATCH 1/7] Add upstream_inject strategy and auth server validation Add StrategyTypeUpstreamInject and UpstreamInjectConfig to backend auth strategy types, enabling vMCP backends to inject upstream IDP tokens obtained by the embedded authorization server. Add ValidateAuthServerIntegration with cross-cutting validation rules between the embedded auth server config and backend auth strategies: - V-01: upstream_inject requires auth server - V-02: upstream_inject providerName must exist in upstreams - V-04: auth server issuer must match incomingAuth OIDC issuer - V-05: auth server requires issuer and at least one upstream - V-07: incomingAuth audience must be in allowed audiences - V-09: auth server requires OIDC incoming auth - V-10: warn on duplicate upstream_inject provider names - V-11: warn when token_exchange targets the auth server - V-13: AllowedAudiences required for MCP compliance Refs: #4142 --- ...olhive.stacklok.dev_virtualmcpservers.yaml | 30 +- ...olhive.stacklok.dev_virtualmcpservers.yaml | 30 +- docs/operator/crd-api.md | 21 +- pkg/authserver/config.go | 16 +- pkg/vmcp/auth/types/types.go | 22 +- pkg/vmcp/auth/types/zz_generated.deepcopy.go | 20 + pkg/vmcp/config/validator.go | 221 ++++++++- pkg/vmcp/config/validator_test.go | 419 +++++++++++++++++- 8 files changed, 738 insertions(+), 41 deletions(-) diff --git a/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_virtualmcpservers.yaml b/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_virtualmcpservers.yaml index 25031670ec..7c966ccd0f 100644 --- a/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_virtualmcpservers.yaml +++ b/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_virtualmcpservers.yaml @@ -1422,8 +1422,21 @@ spec: type: object type: description: 'Type is the auth strategy: "unauthenticated", - "header_injection", "token_exchange"' + "header_injection", "token_exchange", "upstream_inject"' type: string + upstreamInject: + description: |- + UpstreamInject contains configuration for upstream inject auth strategy. + Used when Type = "upstream_inject". + properties: + providerName: + description: |- + ProviderName is the name of the upstream provider configured in the + embedded authorization server. Must match an entry in AuthServer.Upstreams. + type: string + required: + - providerName + type: object required: - type type: object @@ -1498,8 +1511,21 @@ spec: type: object type: description: 'Type is the auth strategy: "unauthenticated", - "header_injection", "token_exchange"' + "header_injection", "token_exchange", "upstream_inject"' type: string + upstreamInject: + description: |- + UpstreamInject contains configuration for upstream inject auth strategy. + Used when Type = "upstream_inject". + properties: + providerName: + description: |- + ProviderName is the name of the upstream provider configured in the + embedded authorization server. Must match an entry in AuthServer.Upstreams. + type: string + required: + - providerName + type: object required: - type type: object diff --git a/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_virtualmcpservers.yaml b/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_virtualmcpservers.yaml index 18ce40d09e..fa894ac0a0 100644 --- a/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_virtualmcpservers.yaml +++ b/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_virtualmcpservers.yaml @@ -1425,8 +1425,21 @@ spec: type: object type: description: 'Type is the auth strategy: "unauthenticated", - "header_injection", "token_exchange"' + "header_injection", "token_exchange", "upstream_inject"' type: string + upstreamInject: + description: |- + UpstreamInject contains configuration for upstream inject auth strategy. + Used when Type = "upstream_inject". + properties: + providerName: + description: |- + ProviderName is the name of the upstream provider configured in the + embedded authorization server. Must match an entry in AuthServer.Upstreams. + type: string + required: + - providerName + type: object required: - type type: object @@ -1501,8 +1514,21 @@ spec: type: object type: description: 'Type is the auth strategy: "unauthenticated", - "header_injection", "token_exchange"' + "header_injection", "token_exchange", "upstream_inject"' type: string + upstreamInject: + description: |- + UpstreamInject contains configuration for upstream inject auth strategy. + Used when Type = "upstream_inject". + properties: + providerName: + description: |- + ProviderName is the name of the upstream provider configured in the + embedded authorization server. Must match an entry in AuthServer.Upstreams. + type: string + required: + - providerName + type: object required: - type type: object diff --git a/docs/operator/crd-api.md b/docs/operator/crd-api.md index e36e9381d1..829f3e86db 100644 --- a/docs/operator/crd-api.md +++ b/docs/operator/crd-api.md @@ -70,9 +70,10 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `type` _string_ | Type is the auth strategy: "unauthenticated", "header_injection", "token_exchange" | | | +| `type` _string_ | Type is the auth strategy: "unauthenticated", "header_injection", "token_exchange", "upstream_inject" | | | | `headerInjection` _[auth.types.HeaderInjectionConfig](#authtypesheaderinjectionconfig)_ | HeaderInjection contains configuration for header injection auth strategy.
Used when Type = "header_injection". | | | | `tokenExchange` _[auth.types.TokenExchangeConfig](#authtypestokenexchangeconfig)_ | TokenExchange contains configuration for token exchange auth strategy.
Used when Type = "token_exchange". | | | +| `upstreamInject` _[auth.types.UpstreamInjectConfig](#authtypesupstreaminjectconfig)_ | UpstreamInject contains configuration for upstream inject auth strategy.
Used when Type = "upstream_inject". | | | #### auth.types.HeaderInjectionConfig @@ -117,6 +118,24 @@ _Appears in:_ | `subjectTokenType` _string_ | SubjectTokenType is the token type of the incoming subject token.
Defaults to "urn:ietf:params:oauth:token-type:access_token" if not specified. | | | +#### auth.types.UpstreamInjectConfig + + + +UpstreamInjectConfig configures the upstream inject auth strategy. +This strategy uses the embedded authorization server to obtain and inject +upstream IDP tokens into backend requests. + + + +_Appears in:_ +- [auth.types.BackendAuthStrategy](#authtypesbackendauthstrategy) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `providerName` _string_ | ProviderName is the name of the upstream provider configured in the
embedded authorization server. Must match an entry in AuthServer.Upstreams. | | | + + ## toolhive.stacklok.dev/config diff --git a/pkg/authserver/config.go b/pkg/authserver/config.go index 5b772c7c16..93daaaaa19 100644 --- a/pkg/authserver/config.go +++ b/pkg/authserver/config.go @@ -119,6 +119,18 @@ const ( UpstreamProviderTypeOAuth2 UpstreamProviderType = "oauth2" ) +// DefaultUpstreamName is the name assigned to a single unnamed upstream. +const DefaultUpstreamName = "default" + +// ResolveUpstreamName returns the canonical name for an upstream. +// An empty name is resolved to DefaultUpstreamName ("default"). +func ResolveUpstreamName(name string) string { + if name == "" { + return DefaultUpstreamName + } + return name +} + // upstreamNameRegex validates upstream provider names. // Names must be DNS-label-like to prevent delimiter injection in storage keys. var upstreamNameRegex = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]*[a-z0-9])?$`) @@ -414,14 +426,14 @@ func (c *Config) validateUpstreams() error { func (c *Config) validateUpstreamName(i int, up *UpstreamConfig) error { if len(c.Upstreams) == 1 { if up.Name == "" { - up.Name = "default" + up.Name = DefaultUpstreamName } } else { if up.Name == "" { return fmt.Errorf( "upstream[%d]: name must be explicitly set when multiple upstreams are configured", i) } - if up.Name == "default" { + if up.Name == DefaultUpstreamName { return fmt.Errorf( "upstream[%d]: name %q is reserved for single-upstream configs; use a descriptive name", i, up.Name) diff --git a/pkg/vmcp/auth/types/types.go b/pkg/vmcp/auth/types/types.go index a2e76b663d..36ae549ef5 100644 --- a/pkg/vmcp/auth/types/types.go +++ b/pkg/vmcp/auth/types/types.go @@ -27,6 +27,11 @@ const ( // This strategy exchanges an incoming token for a new token to use // when authenticating to the backend service. StrategyTypeTokenExchange = "token_exchange" + + // StrategyTypeUpstreamInject identifies the upstream inject strategy. + // This strategy injects an upstream IDP token obtained by the embedded + // authorization server into requests to the backend service. + StrategyTypeUpstreamInject = "upstream_inject" ) // BackendAuthStrategy defines how to authenticate to a specific backend. @@ -36,7 +41,7 @@ const ( // +kubebuilder:object:generate=true // +gendoc type BackendAuthStrategy struct { - // Type is the auth strategy: "unauthenticated", "header_injection", "token_exchange" + // Type is the auth strategy: "unauthenticated", "header_injection", "token_exchange", "upstream_inject" Type string `json:"type" yaml:"type"` // HeaderInjection contains configuration for header injection auth strategy. @@ -46,6 +51,10 @@ type BackendAuthStrategy struct { // TokenExchange contains configuration for token exchange auth strategy. // Used when Type = "token_exchange". TokenExchange *TokenExchangeConfig `json:"tokenExchange,omitempty" yaml:"tokenExchange,omitempty"` + + // UpstreamInject contains configuration for upstream inject auth strategy. + // Used when Type = "upstream_inject". + UpstreamInject *UpstreamInjectConfig `json:"upstreamInject,omitempty" yaml:"upstreamInject,omitempty"` } // HeaderInjectionConfig configures the header injection auth strategy. @@ -95,3 +104,14 @@ type TokenExchangeConfig struct { // Defaults to "urn:ietf:params:oauth:token-type:access_token" if not specified. SubjectTokenType string `json:"subjectTokenType,omitempty" yaml:"subjectTokenType,omitempty"` } + +// UpstreamInjectConfig configures the upstream inject auth strategy. +// This strategy uses the embedded authorization server to obtain and inject +// upstream IDP tokens into backend requests. +// +kubebuilder:object:generate=true +// +gendoc +type UpstreamInjectConfig struct { + // ProviderName is the name of the upstream provider configured in the + // embedded authorization server. Must match an entry in AuthServer.Upstreams. + ProviderName string `json:"providerName" yaml:"providerName"` +} diff --git a/pkg/vmcp/auth/types/zz_generated.deepcopy.go b/pkg/vmcp/auth/types/zz_generated.deepcopy.go index a8b619499a..bcb6a0b716 100644 --- a/pkg/vmcp/auth/types/zz_generated.deepcopy.go +++ b/pkg/vmcp/auth/types/zz_generated.deepcopy.go @@ -35,6 +35,11 @@ func (in *BackendAuthStrategy) DeepCopyInto(out *BackendAuthStrategy) { *out = new(TokenExchangeConfig) (*in).DeepCopyInto(*out) } + if in.UpstreamInject != nil { + in, out := &in.UpstreamInject, &out.UpstreamInject + *out = new(UpstreamInjectConfig) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackendAuthStrategy. @@ -81,3 +86,18 @@ func (in *TokenExchangeConfig) DeepCopy() *TokenExchangeConfig { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UpstreamInjectConfig) DeepCopyInto(out *UpstreamInjectConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpstreamInjectConfig. +func (in *UpstreamInjectConfig) DeepCopy() *UpstreamInjectConfig { + if in == nil { + return nil + } + out := new(UpstreamInjectConfig) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/vmcp/config/validator.go b/pkg/vmcp/config/validator.go index 83b9421654..9882e2085a 100644 --- a/pkg/vmcp/config/validator.go +++ b/pkg/vmcp/config/validator.go @@ -5,9 +5,12 @@ package config import ( "fmt" + "log/slog" + "slices" "strings" "time" + "github.com/stacklok/toolhive/pkg/authserver" "github.com/stacklok/toolhive/pkg/vmcp" authtypes "github.com/stacklok/toolhive/pkg/vmcp/auth/types" ) @@ -99,7 +102,7 @@ func (v *DefaultValidator) validateIncomingAuth(auth *IncomingAuthConfig) error // Validate auth type validTypes := []string{IncomingAuthTypeOIDC, IncomingAuthTypeLocal, IncomingAuthTypeAnonymous} - if !contains(validTypes, auth.Type) { + if !slices.Contains(validTypes, auth.Type) { return fmt.Errorf("incomingAuth.type must be one of: %s", strings.Join(validTypes, ", ")) } @@ -140,7 +143,7 @@ func (v *DefaultValidator) validateIncomingAuth(auth *IncomingAuthConfig) error func (*DefaultValidator) validateAuthz(authz *AuthzConfig) error { validTypes := []string{"cedar", "none"} - if !contains(validTypes, authz.Type) { + if !slices.Contains(validTypes, authz.Type) { return fmt.Errorf("type must be one of: %s", strings.Join(validTypes, ", ")) } @@ -158,7 +161,7 @@ func (v *DefaultValidator) validateOutgoingAuth(auth *OutgoingAuthConfig) error // Validate source validSources := []string{"inline", "discovered"} - if !contains(validSources, auth.Source) { + if !slices.Contains(validSources, auth.Source) { return fmt.Errorf("outgoingAuth.source must be one of: %s", strings.Join(validSources, ", ")) } @@ -188,10 +191,9 @@ func (*DefaultValidator) validateBackendAuthStrategy(_ string, strategy *authtyp authtypes.StrategyTypeUnauthenticated, authtypes.StrategyTypeHeaderInjection, authtypes.StrategyTypeTokenExchange, - // TODO: Add more as strategies are implemented: - // "pass_through", "client_credentials", "oauth_proxy", + authtypes.StrategyTypeUpstreamInject, } - if !contains(validTypes, strategy.Type) { + if !slices.Contains(validTypes, strategy.Type) { return fmt.Errorf("type must be one of: %s", strings.Join(validTypes, ", ")) } @@ -217,6 +219,13 @@ func (*DefaultValidator) validateBackendAuthStrategy(_ string, strategy *authtyp if strategy.HeaderInjection.HeaderValue == "" { return fmt.Errorf("headerInjection requires headerValue field") } + + case authtypes.StrategyTypeUpstreamInject: + if strategy.UpstreamInject == nil { + return fmt.Errorf("upstream_inject requires UpstreamInject configuration") + } + // Note: empty ProviderName is allowed here; ValidateAuthServerIntegration + // handles provider name resolution including the empty→"default" mapping. } return nil @@ -233,7 +242,7 @@ func (v *DefaultValidator) validateAggregation(agg *AggregationConfig) error { vmcp.ConflictStrategyPriority, vmcp.ConflictStrategyManual, } - if !containsStrategy(validStrategies, agg.ConflictResolution) { + if !slices.Contains(validStrategies, agg.ConflictResolution) { return fmt.Errorf("conflictResolution must be one of: prefix, priority, manual") } @@ -360,7 +369,7 @@ func (*DefaultValidator) validateFailureHandling(fh *FailureHandlingConfig) erro } validModes := []string{"fail", "bestEffort"} - if !contains(validModes, fh.PartialFailureMode) { + if !slices.Contains(validModes, fh.PartialFailureMode) { return fmt.Errorf("partialFailureMode must be one of: %s", strings.Join(validModes, ", ")) } @@ -436,22 +445,204 @@ func (*DefaultValidator) validateCompositeToolRefs(refs []CompositeToolRef) erro // Note: Workflow step validation is now handled by the shared ValidateWorkflowSteps function // in composite_validation.go, which is called by ValidateCompositeToolConfig. -// Helper functions +// ValidateAuthServerIntegration validates cross-cutting rules between the +// embedded auth server configuration and backend auth strategies. +// This is called separately from Validate() because it needs the runtime-only +// auth server RunConfig that is not part of the serializable Config. +func ValidateAuthServerIntegration(cfg *Config, rc *authserver.RunConfig) error { + strategies := collectAllBackendStrategies(cfg) + hasUpstreamInject := hasStrategyType(strategies, authtypes.StrategyTypeUpstreamInject) + + // Guard clause: nothing to validate if no auth server and no upstream_inject backends. + if rc == nil && !hasUpstreamInject { + return nil + } + + // upstream_inject requires an auth server to obtain upstream tokens. + if hasUpstreamInject && rc == nil { + return fmt.Errorf("upstream_inject requires an embedded auth server (authServer must be configured)") + } + + // Structural validation of the auth server RunConfig. + if err := validateAuthServerRunConfig(rc); err != nil { + return err + } + + // Auth server requires OIDC incoming auth to validate issued tokens. + if err := validateAuthServerRequiresOIDC(cfg); err != nil { + return err + } + + // Every upstream_inject providerName must reference an existing upstream. + if err := validateUpstreamInjectProviders(rc, strategies); err != nil { + return err + } + + // Issuer and audience consistency between auth server and incoming auth. + if err := validateAuthServerIncomingAuthConsistency(cfg, rc); err != nil { + return err + } + + // Warn about duplicate upstream_inject provider names. + warnDuplicateUpstreamInjectProviders(strategies) + + return nil +} + +// validateAuthServerRunConfig performs lightweight structural validation of the +// auth server RunConfig (issuer, upstreams, allowed audiences). +func validateAuthServerRunConfig(rc *authserver.RunConfig) error { + if rc == nil { + return nil + } + if rc.Issuer == "" { + return fmt.Errorf("auth server issuer is required") + } + if len(rc.Upstreams) == 0 { + return fmt.Errorf("auth server requires at least one upstream") + } + // AllowedAudiences is required for MCP compliance (RFC 8707). + if len(rc.AllowedAudiences) == 0 { + return fmt.Errorf("auth server requires at least one allowed audience (MCP clients must send RFC 8707 resource parameter)") + } + return nil +} -func contains(slice []string, item string) bool { - for _, s := range slice { - if s == item { +// validateUpstreamInjectProviders checks that every upstream_inject strategy +// references a provider that exists in the auth server upstreams. +func validateUpstreamInjectProviders( + rc *authserver.RunConfig, + strategies map[string]*authtypes.BackendAuthStrategy, +) error { + if rc == nil { + return nil + } + for name, strategy := range strategies { + if strategy.Type != authtypes.StrategyTypeUpstreamInject || strategy.UpstreamInject == nil { + continue + } + if !upstreamExists(rc, strategy.UpstreamInject.ProviderName) { + return fmt.Errorf( + "backend %q: upstream_inject providerName %q not found in auth server upstreams", + name, strategy.UpstreamInject.ProviderName, + ) + } + } + return nil +} + +// validateAuthServerIncomingAuthConsistency checks issuer and audience consistency +// between the auth server and incoming OIDC auth. +func validateAuthServerIncomingAuthConsistency(cfg *Config, rc *authserver.RunConfig) error { + if !hasAuthServerWithOIDCIncoming(cfg, rc) { + return nil + } + oidc := cfg.IncomingAuth.OIDC + + // Issuer mismatch. + if rc.Issuer != oidc.Issuer { + return fmt.Errorf( + "auth server issuer mismatch: auth server issuer %q != incomingAuth.oidc.issuer %q", + rc.Issuer, oidc.Issuer, + ) + } + + // The embedded AS uses the RFC 8707 resource parameter value as the + // token's aud claim (identity mapping). AllowedAudiences gates which resource + // values the AS accepts. If incomingAuth expects an audience not in that list, + // the AS will never issue a matching token. + // Note: oidc.Audience is required when incomingAuth.type is "oidc" (enforced + // by validateIncomingAuth), so the empty check is defensive for callers that + // invoke ValidateAuthServerIntegration independently. + if oidc.Audience != "" && !slices.Contains(rc.AllowedAudiences, oidc.Audience) { + return fmt.Errorf( + "incomingAuth.oidc.audience %q not in auth server's allowed audiences %v", + oidc.Audience, rc.AllowedAudiences, + ) + } + + return nil +} + +// validateAuthServerRequiresOIDC checks that when the auth server is configured, +// incomingAuth is OIDC. The AS issues tokens that the OIDC middleware +// validates; without OIDC incoming auth the entire OAuth flow is pointless. +func validateAuthServerRequiresOIDC(cfg *Config) error { + if cfg.IncomingAuth == nil || cfg.IncomingAuth.Type != IncomingAuthTypeOIDC || cfg.IncomingAuth.OIDC == nil { + return fmt.Errorf("embedded auth server requires OIDC incoming auth") + } + return nil +} + +// warnDuplicateUpstreamInjectProviders warns when multiple upstream_inject +// backends reference the same provider name. +func warnDuplicateUpstreamInjectProviders(strategies map[string]*authtypes.BackendAuthStrategy) { + seen := make(map[string]string) // providerName -> first backend name + for name, strategy := range strategies { + if strategy.Type != authtypes.StrategyTypeUpstreamInject || strategy.UpstreamInject == nil { + continue + } + pn := authserver.ResolveUpstreamName(strategy.UpstreamInject.ProviderName) + if first, ok := seen[pn]; ok { + slog.Warn("multiple upstream_inject backends reference the same provider; likely a copy-paste error", + "providerName", pn, + "backend1", first, + "backend2", name, + ) + } else { + seen[pn] = name + } + } +} + + +// hasAuthServerWithOIDCIncoming returns true when both the auth server and +// incoming OIDC auth are configured, enabling cross-cutting validation. +func hasAuthServerWithOIDCIncoming(cfg *Config, rc *authserver.RunConfig) bool { + return rc != nil && + cfg.IncomingAuth != nil && + cfg.IncomingAuth.Type == IncomingAuthTypeOIDC && + cfg.IncomingAuth.OIDC != nil +} + +// collectAllBackendStrategies returns all backend auth strategies from the config. +func collectAllBackendStrategies(cfg *Config) map[string]*authtypes.BackendAuthStrategy { + result := make(map[string]*authtypes.BackendAuthStrategy) + if cfg.OutgoingAuth == nil { + return result + } + if cfg.OutgoingAuth.Default != nil { + result["(default)"] = cfg.OutgoingAuth.Default + } + for name, strategy := range cfg.OutgoingAuth.Backends { + result[name] = strategy + } + return result +} + +// hasStrategyType checks if any strategy in the map uses the given type. +func hasStrategyType(strategies map[string]*authtypes.BackendAuthStrategy, strategyType string) bool { + for _, s := range strategies { + if s.Type == strategyType { return true } } return false } -func containsStrategy(slice []vmcp.ConflictResolutionStrategy, item vmcp.ConflictResolutionStrategy) bool { - for _, s := range slice { - if s == item { +// upstreamExists checks if a provider name exists in the RunConfig's upstreams. +// Provider names and upstream names are resolved via authserver.ResolveUpstreamName +// before comparison to ensure consistent empty→"default" normalization. +func upstreamExists(rc *authserver.RunConfig, providerName string) bool { + if rc == nil { + return false + } + resolved := authserver.ResolveUpstreamName(providerName) + for i := range rc.Upstreams { + if authserver.ResolveUpstreamName(rc.Upstreams[i].Name) == resolved { return true } } return false } + diff --git a/pkg/vmcp/config/validator_test.go b/pkg/vmcp/config/validator_test.go index dd59f1e1b5..3b1c0924d7 100644 --- a/pkg/vmcp/config/validator_test.go +++ b/pkg/vmcp/config/validator_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/stacklok/toolhive/pkg/authserver" thvjson "github.com/stacklok/toolhive/pkg/json" "github.com/stacklok/toolhive/pkg/vmcp" authtypes "github.com/stacklok/toolhive/pkg/vmcp/auth/types" @@ -279,24 +280,50 @@ func TestValidator_ValidateOutgoingAuth(t *testing.T) { wantErr: true, errMsg: "type must be one of", }, - // TODO: Uncomment when token_exchange strategy is implemented - // { - // name: "token_exchange missing required metadata", - // auth: &OutgoingAuthConfig{ - // Source: "inline", - // Backends: map[string]*authtypes.BackendAuthStrategy{ - // "github": { - // Type: "token_exchange", - // Metadata: map[string]any{ - // "client_id": "test-client", - // // Missing token_url and audience - // }, - // }, - // }, - // }, - // wantErr: true, - // errMsg: "token_exchange requires metadata field", - // }, + { + name: "valid upstream_inject backend", + auth: &OutgoingAuthConfig{ + Source: "inline", + Backends: map[string]*authtypes.BackendAuthStrategy{ + "github": { + Type: authtypes.StrategyTypeUpstreamInject, + UpstreamInject: &authtypes.UpstreamInjectConfig{ + ProviderName: "github", + }, + }, + }, + }, + wantErr: false, + }, + { + name: "upstream_inject nil config", + auth: &OutgoingAuthConfig{ + Source: "inline", + Backends: map[string]*authtypes.BackendAuthStrategy{ + "github": { + Type: authtypes.StrategyTypeUpstreamInject, + UpstreamInject: nil, + }, + }, + }, + wantErr: true, + errMsg: "upstream_inject requires UpstreamInject configuration", + }, + { + name: "upstream_inject empty providerName allowed", + auth: &OutgoingAuthConfig{ + Source: "inline", + Backends: map[string]*authtypes.BackendAuthStrategy{ + "github": { + Type: authtypes.StrategyTypeUpstreamInject, + UpstreamInject: &authtypes.UpstreamInjectConfig{ + ProviderName: "", + }, + }, + }, + }, + wantErr: false, // V-02 handles provider name resolution + }, } for _, tt := range tests { @@ -813,3 +840,359 @@ func TestValidator_ValidateFailureHandling(t *testing.T) { }) } } + +func TestValidateAuthServerIntegration(t *testing.T) { + t.Parallel() + + // Helper to build a minimal valid auth server RunConfig. + validASRunConfig := func(issuer string, upstreamName string) *authserver.RunConfig { + return &authserver.RunConfig{ + Issuer: issuer, + Upstreams: []authserver.UpstreamRunConfig{ + {Name: upstreamName, Type: authserver.UpstreamProviderTypeOIDC}, + }, + AllowedAudiences: []string{"https://my-vmcp"}, + } + } + + tests := []struct { + name string + cfg *Config + rc *authserver.RunConfig + wantErr bool + errMsg string + }{ + { + name: "mode_a_no_auth_server_passes", + cfg: &Config{ + OutgoingAuth: &OutgoingAuthConfig{ + Source: "inline", + Default: &authtypes.BackendAuthStrategy{ + Type: authtypes.StrategyTypeUnauthenticated, + }, + }, + }, + rc: nil, + wantErr: false, + }, + { + name: "v01_upstream_inject_without_auth_server", + cfg: &Config{ + OutgoingAuth: &OutgoingAuthConfig{ + Source: "inline", + Backends: map[string]*authtypes.BackendAuthStrategy{ + "github-tools": { + Type: authtypes.StrategyTypeUpstreamInject, + UpstreamInject: &authtypes.UpstreamInjectConfig{ + ProviderName: "github", + }, + }, + }, + }, + }, + rc: nil, + wantErr: true, + errMsg: "upstream_inject requires an embedded auth server", + }, + { + name: "v02_provider_not_in_upstreams", + cfg: &Config{ + IncomingAuth: &IncomingAuthConfig{ + Type: IncomingAuthTypeOIDC, + OIDC: &OIDCConfig{ + Issuer: "http://localhost:9090", + Audience: "https://my-vmcp", + }, + }, + OutgoingAuth: &OutgoingAuthConfig{ + Source: "inline", + Backends: map[string]*authtypes.BackendAuthStrategy{ + "github-tools": { + Type: authtypes.StrategyTypeUpstreamInject, + UpstreamInject: &authtypes.UpstreamInjectConfig{ + ProviderName: "github", + }, + }, + }, + }, + }, + rc: &authserver.RunConfig{ + Issuer: "http://localhost:9090", + Upstreams: []authserver.UpstreamRunConfig{ + {Name: "entra", Type: authserver.UpstreamProviderTypeOIDC}, + }, + AllowedAudiences: []string{"https://my-vmcp"}, + }, + wantErr: true, + errMsg: "not found in auth server upstreams", + }, + { + name: "v04_issuer_mismatch", + cfg: &Config{ + IncomingAuth: &IncomingAuthConfig{ + Type: IncomingAuthTypeOIDC, + OIDC: &OIDCConfig{ + Issuer: "http://localhost:8080", + Audience: "https://my-vmcp", + }, + }, + OutgoingAuth: &OutgoingAuthConfig{ + Source: "inline", + }, + }, + rc: validASRunConfig("http://localhost:9090", "default"), + wantErr: true, + errMsg: "issuer mismatch", + }, + { + name: "v05_empty_issuer", + cfg: &Config{ + OutgoingAuth: &OutgoingAuthConfig{ + Source: "inline", + }, + }, + rc: &authserver.RunConfig{ + Issuer: "", + Upstreams: []authserver.UpstreamRunConfig{ + {Name: "default", Type: authserver.UpstreamProviderTypeOIDC}, + }, + }, + wantErr: true, + errMsg: "issuer is required", + }, + { + name: "v05_no_upstreams", + cfg: &Config{ + OutgoingAuth: &OutgoingAuthConfig{ + Source: "inline", + }, + }, + rc: &authserver.RunConfig{ + Issuer: "http://localhost:9090", + Upstreams: nil, + }, + wantErr: true, + errMsg: "at least one upstream", + }, + { + name: "v07_audience_not_in_allowed", + cfg: &Config{ + IncomingAuth: &IncomingAuthConfig{ + Type: IncomingAuthTypeOIDC, + OIDC: &OIDCConfig{ + Issuer: "http://localhost:9090", + Audience: "https://my-app", + }, + }, + OutgoingAuth: &OutgoingAuthConfig{ + Source: "inline", + }, + }, + rc: &authserver.RunConfig{ + Issuer: "http://localhost:9090", + Upstreams: []authserver.UpstreamRunConfig{ + {Name: "default", Type: authserver.UpstreamProviderTypeOIDC}, + }, + AllowedAudiences: []string{"https://other"}, + }, + wantErr: true, + errMsg: "not in auth server's allowed audiences", + }, + { + name: "v09_auth_server_requires_oidc_incoming", + cfg: &Config{ + IncomingAuth: &IncomingAuthConfig{ + Type: IncomingAuthTypeAnonymous, + }, + OutgoingAuth: &OutgoingAuthConfig{ + Source: "inline", + }, + }, + rc: validASRunConfig("http://localhost:9090", "default"), + wantErr: true, + errMsg: "embedded auth server requires OIDC incoming auth", + }, + { + name: "v10_duplicate_upstream_inject_providers_warns", + cfg: &Config{ + IncomingAuth: &IncomingAuthConfig{ + Type: IncomingAuthTypeOIDC, + OIDC: &OIDCConfig{ + Issuer: "http://localhost:9090", + Audience: "https://my-vmcp", + }, + }, + OutgoingAuth: &OutgoingAuthConfig{ + Source: "inline", + Backends: map[string]*authtypes.BackendAuthStrategy{ + "backend-a": { + Type: authtypes.StrategyTypeUpstreamInject, + UpstreamInject: &authtypes.UpstreamInjectConfig{ + ProviderName: "github", + }, + }, + "backend-b": { + Type: authtypes.StrategyTypeUpstreamInject, + UpstreamInject: &authtypes.UpstreamInjectConfig{ + ProviderName: "github", + }, + }, + }, + }, + }, + rc: validASRunConfig("http://localhost:9090", "github"), + wantErr: false, // V-10 is warning-only + }, + { + name: "v13_empty_allowed_audiences", + cfg: &Config{ + IncomingAuth: &IncomingAuthConfig{ + Type: IncomingAuthTypeOIDC, + OIDC: &OIDCConfig{ + Issuer: "http://localhost:9090", + Audience: "https://my-vmcp", + }, + }, + OutgoingAuth: &OutgoingAuthConfig{ + Source: "inline", + }, + }, + rc: &authserver.RunConfig{ + Issuer: "http://localhost:9090", + Upstreams: []authserver.UpstreamRunConfig{ + {Name: "default", Type: authserver.UpstreamProviderTypeOIDC}, + }, + AllowedAudiences: nil, + }, + wantErr: true, + errMsg: "at least one allowed audience", + }, + { + name: "v02_empty_upstream_name_matches_default", + cfg: &Config{ + IncomingAuth: &IncomingAuthConfig{ + Type: IncomingAuthTypeOIDC, + OIDC: &OIDCConfig{ + Issuer: "http://localhost:9090", + Audience: "https://my-vmcp", + }, + }, + OutgoingAuth: &OutgoingAuthConfig{ + Source: "inline", + Backends: map[string]*authtypes.BackendAuthStrategy{ + "my-backend": { + Type: authtypes.StrategyTypeUpstreamInject, + UpstreamInject: &authtypes.UpstreamInjectConfig{ + ProviderName: "default", + }, + }, + }, + }, + }, + rc: &authserver.RunConfig{ + Issuer: "http://localhost:9090", + Upstreams: []authserver.UpstreamRunConfig{ + {Name: "", Type: authserver.UpstreamProviderTypeOIDC}, // empty name → "default" + }, + AllowedAudiences: []string{"https://my-vmcp"}, + }, + wantErr: false, + }, + { + name: "upstream_inject_as_default_strategy", + cfg: &Config{ + IncomingAuth: &IncomingAuthConfig{ + Type: IncomingAuthTypeOIDC, + OIDC: &OIDCConfig{ + Issuer: "http://localhost:9090", + Audience: "https://my-vmcp", + }, + }, + OutgoingAuth: &OutgoingAuthConfig{ + Source: "inline", + Default: &authtypes.BackendAuthStrategy{ + Type: authtypes.StrategyTypeUpstreamInject, + UpstreamInject: &authtypes.UpstreamInjectConfig{ + ProviderName: "github", + }, + }, + }, + }, + rc: validASRunConfig("http://localhost:9090", "github"), + wantErr: false, + }, + { + name: "upstream_inject_default_provider_not_found", + cfg: &Config{ + IncomingAuth: &IncomingAuthConfig{ + Type: IncomingAuthTypeOIDC, + OIDC: &OIDCConfig{ + Issuer: "http://localhost:9090", + Audience: "https://my-vmcp", + }, + }, + OutgoingAuth: &OutgoingAuthConfig{ + Source: "inline", + Default: &authtypes.BackendAuthStrategy{ + Type: authtypes.StrategyTypeUpstreamInject, + UpstreamInject: &authtypes.UpstreamInjectConfig{ + ProviderName: "nonexistent", + }, + }, + }, + }, + rc: validASRunConfig("http://localhost:9090", "github"), + wantErr: true, + errMsg: "not found in auth server upstreams", + }, + { + name: "valid_mode_b_config", + cfg: &Config{ + IncomingAuth: &IncomingAuthConfig{ + Type: IncomingAuthTypeOIDC, + OIDC: &OIDCConfig{ + Issuer: "http://localhost:9090", + Audience: "https://my-vmcp", + }, + }, + OutgoingAuth: &OutgoingAuthConfig{ + Source: "inline", + Backends: map[string]*authtypes.BackendAuthStrategy{ + "github-tools": { + Type: authtypes.StrategyTypeUpstreamInject, + UpstreamInject: &authtypes.UpstreamInjectConfig{ + ProviderName: "github", + }, + }, + }, + }, + }, + rc: &authserver.RunConfig{ + Issuer: "http://localhost:9090", + Upstreams: []authserver.UpstreamRunConfig{ + {Name: "github", Type: authserver.UpstreamProviderTypeOIDC}, + }, + AllowedAudiences: []string{"https://my-vmcp"}, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + err := ValidateAuthServerIntegration(tt.cfg, tt.rc) + + if (err != nil) != tt.wantErr { + t.Errorf("ValidateAuthServerIntegration() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if tt.wantErr && err != nil && tt.errMsg != "" { + if !strings.Contains(err.Error(), tt.errMsg) { + t.Errorf("ValidateAuthServerIntegration() error message = %v, want to contain %v", err.Error(), tt.errMsg) + } + } + }) + } +} From 777eec5763ffb7151b390f09822d155cc61055cf Mon Sep 17 00:00:00 2001 From: Jakub Hrozek Date: Tue, 24 Mar 2026 21:39:58 +0000 Subject: [PATCH 2/7] Fix gci lint whitespace in validator.go Remove extra blank line and trailing newline that caused the gci linter to fail in CI. Co-Authored-By: Claude Opus 4.6 (1M context) --- pkg/vmcp/config/validator.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/vmcp/config/validator.go b/pkg/vmcp/config/validator.go index 9882e2085a..73bf4d545a 100644 --- a/pkg/vmcp/config/validator.go +++ b/pkg/vmcp/config/validator.go @@ -595,7 +595,6 @@ func warnDuplicateUpstreamInjectProviders(strategies map[string]*authtypes.Backe } } - // hasAuthServerWithOIDCIncoming returns true when both the auth server and // incoming OIDC auth are configured, enabling cross-cutting validation. func hasAuthServerWithOIDCIncoming(cfg *Config, rc *authserver.RunConfig) bool { @@ -645,4 +644,3 @@ func upstreamExists(rc *authserver.RunConfig, providerName string) bool { } return false } - From a98ccedaa55b5c558848f819bba7f8259f3f96a1 Mon Sep 17 00:00:00 2001 From: Jakub Hrozek Date: Tue, 24 Mar 2026 22:53:34 +0000 Subject: [PATCH 3/7] Clarify that AS-OIDC consistency check is strategy-agnostic Expand the doc comment on validateAuthServerIncomingAuthConsistency to explain that it applies regardless of outgoing backend strategy (upstream_inject, token_exchange, etc.) and why the issuer/audience match matters: the OIDC middleware rejects every token whose iss claim differs from what it expects. Addresses PR review feedback from @tgrunnagle. Co-Authored-By: Claude Opus 4.6 (1M context) --- pkg/vmcp/config/validator.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/pkg/vmcp/config/validator.go b/pkg/vmcp/config/validator.go index 73bf4d545a..293f7826df 100644 --- a/pkg/vmcp/config/validator.go +++ b/pkg/vmcp/config/validator.go @@ -531,15 +531,24 @@ func validateUpstreamInjectProviders( return nil } -// validateAuthServerIncomingAuthConsistency checks issuer and audience consistency -// between the auth server and incoming OIDC auth. +// validateAuthServerIncomingAuthConsistency checks that the embedded auth server +// and the incoming OIDC middleware agree on issuer and audience. +// +// This is a general consistency check that applies whenever both the embedded AS +// and OIDC incoming auth are configured, regardless of which outgoing backend +// strategies (upstream_inject, token_exchange, etc.) are in use. +// +// The embedded AS issues tokens that the OIDC incoming auth middleware validates. +// If these two components disagree on issuer or audience, the middleware will +// reject every token the AS issues, and no authenticated request will succeed. func validateAuthServerIncomingAuthConsistency(cfg *Config, rc *authserver.RunConfig) error { if !hasAuthServerWithOIDCIncoming(cfg, rc) { return nil } oidc := cfg.IncomingAuth.OIDC - // Issuer mismatch. + // The OIDC middleware validates the "iss" claim against incomingAuth.oidc.issuer. + // If the AS uses a different issuer, every token it issues will fail validation. if rc.Issuer != oidc.Issuer { return fmt.Errorf( "auth server issuer mismatch: auth server issuer %q != incomingAuth.oidc.issuer %q", From 5f14225b7b4a6b3c0b2874bf2dc891640ded3df5 Mon Sep 17 00:00:00 2001 From: Jakub Hrozek Date: Tue, 24 Mar 2026 22:55:44 +0000 Subject: [PATCH 4/7] Remove duplicate upstream_inject provider warning Multiple backends referencing the same upstream provider is a valid and expected configuration (e.g., two GitHub-related backends both needing the "github" upstream token). The "likely a copy-paste error" warning was a false positive for legitimate configs. Addresses PR review feedback from @tgrunnagle. Co-Authored-By: Claude Opus 4.6 (1M context) --- pkg/vmcp/config/validator.go | 25 ------------------------- pkg/vmcp/config/validator_test.go | 31 ------------------------------- 2 files changed, 56 deletions(-) diff --git a/pkg/vmcp/config/validator.go b/pkg/vmcp/config/validator.go index 293f7826df..851b8b25fc 100644 --- a/pkg/vmcp/config/validator.go +++ b/pkg/vmcp/config/validator.go @@ -5,7 +5,6 @@ package config import ( "fmt" - "log/slog" "slices" "strings" "time" @@ -483,9 +482,6 @@ func ValidateAuthServerIntegration(cfg *Config, rc *authserver.RunConfig) error return err } - // Warn about duplicate upstream_inject provider names. - warnDuplicateUpstreamInjectProviders(strategies) - return nil } @@ -583,27 +579,6 @@ func validateAuthServerRequiresOIDC(cfg *Config) error { return nil } -// warnDuplicateUpstreamInjectProviders warns when multiple upstream_inject -// backends reference the same provider name. -func warnDuplicateUpstreamInjectProviders(strategies map[string]*authtypes.BackendAuthStrategy) { - seen := make(map[string]string) // providerName -> first backend name - for name, strategy := range strategies { - if strategy.Type != authtypes.StrategyTypeUpstreamInject || strategy.UpstreamInject == nil { - continue - } - pn := authserver.ResolveUpstreamName(strategy.UpstreamInject.ProviderName) - if first, ok := seen[pn]; ok { - slog.Warn("multiple upstream_inject backends reference the same provider; likely a copy-paste error", - "providerName", pn, - "backend1", first, - "backend2", name, - ) - } else { - seen[pn] = name - } - } -} - // hasAuthServerWithOIDCIncoming returns true when both the auth server and // incoming OIDC auth are configured, enabling cross-cutting validation. func hasAuthServerWithOIDCIncoming(cfg *Config, rc *authserver.RunConfig) bool { diff --git a/pkg/vmcp/config/validator_test.go b/pkg/vmcp/config/validator_test.go index 3b1c0924d7..ac355b33f9 100644 --- a/pkg/vmcp/config/validator_test.go +++ b/pkg/vmcp/config/validator_test.go @@ -1012,37 +1012,6 @@ func TestValidateAuthServerIntegration(t *testing.T) { wantErr: true, errMsg: "embedded auth server requires OIDC incoming auth", }, - { - name: "v10_duplicate_upstream_inject_providers_warns", - cfg: &Config{ - IncomingAuth: &IncomingAuthConfig{ - Type: IncomingAuthTypeOIDC, - OIDC: &OIDCConfig{ - Issuer: "http://localhost:9090", - Audience: "https://my-vmcp", - }, - }, - OutgoingAuth: &OutgoingAuthConfig{ - Source: "inline", - Backends: map[string]*authtypes.BackendAuthStrategy{ - "backend-a": { - Type: authtypes.StrategyTypeUpstreamInject, - UpstreamInject: &authtypes.UpstreamInjectConfig{ - ProviderName: "github", - }, - }, - "backend-b": { - Type: authtypes.StrategyTypeUpstreamInject, - UpstreamInject: &authtypes.UpstreamInjectConfig{ - ProviderName: "github", - }, - }, - }, - }, - }, - rc: validASRunConfig("http://localhost:9090", "github"), - wantErr: false, // V-10 is warning-only - }, { name: "v13_empty_allowed_audiences", cfg: &Config{ From b3a7c46c483e21508fc66a0657e92f9a39849d13 Mon Sep 17 00:00:00 2001 From: Jakub Hrozek Date: Tue, 24 Mar 2026 22:58:52 +0000 Subject: [PATCH 5/7] Replace "(default)" magic string with defaultStrategyKey constant Extract the synthetic map key for the default outgoing auth strategy into a named constant. Use "" instead of "(default)" to clearly distinguish it from authserver.DefaultUpstreamName and prevent key collisions with user-defined backend names. Addresses PR review feedback from @tgrunnagle. Co-Authored-By: Claude Opus 4.6 (1M context) --- pkg/vmcp/config/validator.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pkg/vmcp/config/validator.go b/pkg/vmcp/config/validator.go index 851b8b25fc..876c744568 100644 --- a/pkg/vmcp/config/validator.go +++ b/pkg/vmcp/config/validator.go @@ -21,6 +21,12 @@ const ( IncomingAuthTypeAnonymous = "anonymous" ) +// defaultStrategyKey is the synthetic map key used for the default outgoing auth +// strategy in collectAllBackendStrategies. It is deliberately different from +// authserver.DefaultUpstreamName ("default") to avoid confusion with upstream +// provider names and to prevent key collisions with user-defined backend names. +const defaultStrategyKey = "" + // DefaultValidator implements comprehensive configuration validation. type DefaultValidator struct{} @@ -595,7 +601,7 @@ func collectAllBackendStrategies(cfg *Config) map[string]*authtypes.BackendAuthS return result } if cfg.OutgoingAuth.Default != nil { - result["(default)"] = cfg.OutgoingAuth.Default + result[defaultStrategyKey] = cfg.OutgoingAuth.Default } for name, strategy := range cfg.OutgoingAuth.Backends { result[name] = strategy From 01d6fd37e31253173441f598b8771b803322e892 Mon Sep 17 00:00:00 2001 From: Jakub Hrozek Date: Tue, 24 Mar 2026 23:18:41 +0000 Subject: [PATCH 6/7] Extract hasOIDCIncoming predicate from duplicated checks Both validateAuthServerRequiresOIDC and hasAuthServerWithOIDCIncoming checked the same three-field condition. Extract a shared hasOIDCIncoming predicate so the logic lives in one place. Addresses PR review feedback from @tgrunnagle. Co-Authored-By: Claude Opus 4.6 (1M context) --- pkg/vmcp/config/validator.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pkg/vmcp/config/validator.go b/pkg/vmcp/config/validator.go index 876c744568..e16d19c471 100644 --- a/pkg/vmcp/config/validator.go +++ b/pkg/vmcp/config/validator.go @@ -575,11 +575,18 @@ func validateAuthServerIncomingAuthConsistency(cfg *Config, rc *authserver.RunCo return nil } +// hasOIDCIncoming reports whether the config has OIDC incoming auth fully configured. +func hasOIDCIncoming(cfg *Config) bool { + return cfg.IncomingAuth != nil && + cfg.IncomingAuth.Type == IncomingAuthTypeOIDC && + cfg.IncomingAuth.OIDC != nil +} + // validateAuthServerRequiresOIDC checks that when the auth server is configured, // incomingAuth is OIDC. The AS issues tokens that the OIDC middleware // validates; without OIDC incoming auth the entire OAuth flow is pointless. func validateAuthServerRequiresOIDC(cfg *Config) error { - if cfg.IncomingAuth == nil || cfg.IncomingAuth.Type != IncomingAuthTypeOIDC || cfg.IncomingAuth.OIDC == nil { + if !hasOIDCIncoming(cfg) { return fmt.Errorf("embedded auth server requires OIDC incoming auth") } return nil @@ -588,10 +595,7 @@ func validateAuthServerRequiresOIDC(cfg *Config) error { // hasAuthServerWithOIDCIncoming returns true when both the auth server and // incoming OIDC auth are configured, enabling cross-cutting validation. func hasAuthServerWithOIDCIncoming(cfg *Config, rc *authserver.RunConfig) bool { - return rc != nil && - cfg.IncomingAuth != nil && - cfg.IncomingAuth.Type == IncomingAuthTypeOIDC && - cfg.IncomingAuth.OIDC != nil + return rc != nil && hasOIDCIncoming(cfg) } // collectAllBackendStrategies returns all backend auth strategies from the config. From 4e5376c45d0301c858eaec35f3bdb5caa213c267 Mon Sep 17 00:00:00 2001 From: Jakub Hrozek Date: Wed, 25 Mar 2026 10:55:50 +0000 Subject: [PATCH 7/7] Fix revive lint: return validateAuthServerIncomingAuthConsistency directly Co-Authored-By: Claude Opus 4.6 (1M context) --- pkg/vmcp/config/validator.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pkg/vmcp/config/validator.go b/pkg/vmcp/config/validator.go index e16d19c471..525f363335 100644 --- a/pkg/vmcp/config/validator.go +++ b/pkg/vmcp/config/validator.go @@ -484,11 +484,7 @@ func ValidateAuthServerIntegration(cfg *Config, rc *authserver.RunConfig) error } // Issuer and audience consistency between auth server and incoming auth. - if err := validateAuthServerIncomingAuthConsistency(cfg, rc); err != nil { - return err - } - - return nil + return validateAuthServerIncomingAuthConsistency(cfg, rc) } // validateAuthServerRunConfig performs lightweight structural validation of the