Skip to content

Commit d96922d

Browse files
tgrunnagleclaude
andauthored
Validate audience matches resourceUrl for embedded auth server (#4904)
* Validate audience matches resourceUrl when embedded auth server is active (#4860) The embedded auth server mints tokens with aud set to the ResourceURL (the RFC 8707 resource parameter), but the token validator checks aud against the user-specified OIDCConfigRef.Audience. When these diverge, every authenticated request fails silently. Add reconciler-time validation requiring audience == resourceUrl when an embedded auth server is configured, with a clear error message guiding operators to fix the mismatch. This mirrors the existing validation in the vMCP inline config path (ValidateAuthServerIntegration). Fixes #4860 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Extract audience validation into shared helper and improve error messages Consolidate the duplicated audience/resourceUrl validation from AddEmbeddedAuthServerConfigOptions and AddAuthServerRefOptions into validateOIDCConfigForEmbeddedAuthServer. Add a distinct error for empty audience (missing field) vs mismatched audience (wrong value) to help operators identify the root cause faster. Document the rationale for validation-based enforcement (Option D) over silent override (Option A): operators see exactly what values are in play and control both sides explicitly, consistent with the existing vMCP inline config validation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix integration tests to set matching audience and resourceUrl Integration test fixtures set OIDCConfigRef.Audience but not ResourceURL, so the resolver auto-computed a different ResourceURL from the proxy/server name. The new audience validation correctly rejects this mismatch. Set ResourceURL to match Audience in all embedded auth server integration test fixtures so the audience consistency check passes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 03f5025 commit d96922d

5 files changed

Lines changed: 111 additions & 22 deletions

File tree

cmd/thv-operator/controllers/mcpserver_externalauth_runconfig_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ func TestAddExternalAuthConfigOptions(t *testing.T) {
250250
},
251251
},
252252
oidcConfig: &oidc.OIDCConfig{
253+
Audience: "http://test-server.default.svc.cluster.local:8080",
253254
ResourceURL: "http://test-server.default.svc.cluster.local:8080",
254255
Scopes: []string{"openid", "offline_access"},
255256
},
@@ -754,7 +755,7 @@ func TestCreateRunConfigFromMCPServer_WithExternalAuth(t *testing.T) {
754755
},
755756
OIDCConfigRef: &mcpv1alpha1.MCPOIDCConfigReference{
756757
Name: "embedded-oidc",
757-
Audience: "toolhive",
758+
Audience: "http://embedded-auth-server.default.svc.cluster.local:8080",
758759
Scopes: []string{"openid", "offline_access", "mcp:tools"},
759760
},
760761
},

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

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -395,12 +395,8 @@ func AddEmbeddedAuthServerConfigOptions(
395395
return fmt.Errorf("embedded auth server configuration is nil for type embeddedAuthServer")
396396
}
397397

398-
// Validate OIDC config is provided with ResourceURL (required for embedded auth server)
399-
if oidcConfig == nil {
400-
return fmt.Errorf("OIDC config is required for embedded auth server: OIDCConfigRef must be set on the MCPServer")
401-
}
402-
if oidcConfig.ResourceURL == "" {
403-
return fmt.Errorf("OIDC config resourceUrl is required for embedded auth server: set resourceUrl in OIDCConfigRef")
398+
if err := validateOIDCConfigForEmbeddedAuthServer(oidcConfig); err != nil {
399+
return err
404400
}
405401

406402
// Build the embedded auth server config for runner
@@ -418,6 +414,42 @@ func AddEmbeddedAuthServerConfigOptions(
418414
return nil
419415
}
420416

417+
// validateOIDCConfigForEmbeddedAuthServer validates OIDC configuration
418+
// requirements when an embedded auth server is active.
419+
//
420+
// The embedded auth server mints tokens with aud = ResourceURL (the value
421+
// clients send as the RFC 8707 resource parameter via discovery). The token
422+
// validator checks aud against Audience. If these differ, every authenticated
423+
// request fails with an audience mismatch.
424+
//
425+
// We validate consistency at reconciliation time (rather than silently
426+
// overriding Audience with ResourceURL) so that operators see exactly what
427+
// values are in play and control both sides explicitly. This mirrors the
428+
// existing vMCP inline config validation (ValidateAuthServerIntegration).
429+
func validateOIDCConfigForEmbeddedAuthServer(oidcConfig *oidc.OIDCConfig) error {
430+
if oidcConfig == nil {
431+
return fmt.Errorf("OIDC config is required for embedded auth server: OIDCConfigRef must be set on the MCPServer")
432+
}
433+
if oidcConfig.ResourceURL == "" {
434+
return fmt.Errorf("OIDC config resourceUrl is required for embedded auth server: set resourceUrl in OIDCConfigRef")
435+
}
436+
if oidcConfig.Audience == "" {
437+
return fmt.Errorf(
438+
"oidcConfigRef.audience is required when an embedded auth server is active; "+
439+
"set audience to %q to match resourceUrl",
440+
oidcConfig.ResourceURL,
441+
)
442+
}
443+
if oidcConfig.Audience != oidcConfig.ResourceURL {
444+
return fmt.Errorf(
445+
"oidcConfigRef.audience %q must match resourceUrl %q when an embedded auth server is active; "+
446+
"set audience to %q or set resourceUrl to match audience",
447+
oidcConfig.Audience, oidcConfig.ResourceURL, oidcConfig.ResourceURL,
448+
)
449+
}
450+
return nil
451+
}
452+
421453
// BuildAuthServerRunConfig converts CRD EmbeddedAuthServerConfig to authserver.RunConfig.
422454
// The RunConfig is serializable and contains file paths for secrets (not the secrets themselves).
423455
//
@@ -759,12 +791,8 @@ func AddAuthServerRefOptions(
759791
return fmt.Errorf("embedded auth server configuration is nil for type embeddedAuthServer")
760792
}
761793

762-
// Validate OIDC config is provided with ResourceURL (required for embedded auth server)
763-
if oidcConfig == nil {
764-
return fmt.Errorf("OIDC config is required for embedded auth server: OIDCConfigRef must be set on the MCPServer")
765-
}
766-
if oidcConfig.ResourceURL == "" {
767-
return fmt.Errorf("OIDC config resourceUrl is required for embedded auth server: set resourceUrl in OIDCConfigRef")
794+
if err := validateOIDCConfigForEmbeddedAuthServer(oidcConfig); err != nil {
795+
return err
768796
}
769797

770798
// Build the embedded auth server config for runner

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

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,6 +1110,7 @@ func TestAddEmbeddedAuthServerConfigOptions_Validation(t *testing.T) {
11101110
{
11111111
name: "valid OIDC config succeeds",
11121112
oidcConfig: &oidc.OIDCConfig{
1113+
Audience: "http://test-server.default.svc.cluster.local:8080",
11131114
ResourceURL: "http://test-server.default.svc.cluster.local:8080",
11141115
Scopes: []string{"openid", "offline_access"},
11151116
},
@@ -1118,11 +1119,32 @@ func TestAddEmbeddedAuthServerConfigOptions_Validation(t *testing.T) {
11181119
{
11191120
name: "valid OIDC config with nil scopes succeeds",
11201121
oidcConfig: &oidc.OIDCConfig{
1122+
Audience: "http://test-server.default.svc.cluster.local:8080",
11211123
ResourceURL: "http://test-server.default.svc.cluster.local:8080",
11221124
Scopes: nil,
11231125
},
11241126
expectError: false,
11251127
},
1128+
{
1129+
name: "audience mismatch with resourceUrl returns error",
1130+
oidcConfig: &oidc.OIDCConfig{
1131+
Audience: "https://different-audience.example.com",
1132+
ResourceURL: "http://test-server.default.svc.cluster.local:8080",
1133+
Scopes: []string{"openid"},
1134+
},
1135+
expectError: true,
1136+
errContains: "must match resourceUrl",
1137+
},
1138+
{
1139+
name: "empty audience returns specific error",
1140+
oidcConfig: &oidc.OIDCConfig{
1141+
Audience: "",
1142+
ResourceURL: "http://test-server.default.svc.cluster.local:8080",
1143+
Scopes: []string{"openid"},
1144+
},
1145+
expectError: true,
1146+
errContains: "audience is required when an embedded auth server is active",
1147+
},
11261148
}
11271149

11281150
for _, tt := range tests {
@@ -1665,6 +1687,7 @@ func TestAddAuthServerRefOptions(t *testing.T) {
16651687
}
16661688

16671689
validOIDCConfig := &oidc.OIDCConfig{
1690+
Audience: "https://mcp.example.com",
16681691
ResourceURL: "https://mcp.example.com",
16691692
Scopes: []string{"openid"},
16701693
}
@@ -1738,6 +1761,36 @@ func TestAddAuthServerRefOptions(t *testing.T) {
17381761
wantErr: true,
17391762
errContains: "OIDC config is required",
17401763
},
1764+
{
1765+
name: "audience mismatch with resourceUrl returns error",
1766+
authServerRef: &mcpv1alpha1.AuthServerRef{
1767+
Kind: "MCPExternalAuthConfig",
1768+
Name: "auth-server-config",
1769+
},
1770+
oidcConfig: &oidc.OIDCConfig{
1771+
Audience: "https://wrong-audience.example.com",
1772+
ResourceURL: "https://mcp.example.com",
1773+
Scopes: []string{"openid"},
1774+
},
1775+
objects: func() []runtime.Object { return []runtime.Object{newValidEmbeddedAuthConfig()} },
1776+
wantErr: true,
1777+
errContains: "must match resourceUrl",
1778+
},
1779+
{
1780+
name: "audience matching resourceUrl succeeds",
1781+
authServerRef: &mcpv1alpha1.AuthServerRef{
1782+
Kind: "MCPExternalAuthConfig",
1783+
Name: "auth-server-config",
1784+
},
1785+
oidcConfig: &oidc.OIDCConfig{
1786+
Audience: "https://mcp.example.com",
1787+
ResourceURL: "https://mcp.example.com",
1788+
Scopes: []string{"openid"},
1789+
},
1790+
objects: func() []runtime.Object { return []runtime.Object{newValidEmbeddedAuthConfig()} },
1791+
wantErr: false,
1792+
wantOptions: 1,
1793+
},
17411794
}
17421795

17431796
for _, tt := range tests {
@@ -1813,6 +1866,7 @@ func TestValidateAndAddAuthServerRefOptions(t *testing.T) {
18131866
}
18141867

18151868
validOIDC := &oidc.OIDCConfig{
1869+
Audience: "https://mcp.example.com",
18161870
ResourceURL: "https://mcp.example.com",
18171871
Scopes: []string{"openid"},
18181872
}

cmd/thv-operator/test-integration/mcp-remote-proxy/remoteproxy_helpers.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,13 @@ func (rb *RemoteProxyBuilder) WithAuthServerRef(name string) *RemoteProxyBuilder
116116
}
117117

118118
// WithOIDCConfigRef sets the OIDCConfigRef for the proxy.
119-
func (rb *RemoteProxyBuilder) WithOIDCConfigRef(name, audience string) *RemoteProxyBuilder {
119+
// resourceURL sets both Audience and ResourceURL to the same value, which is
120+
// required when an embedded auth server is active (#4860).
121+
func (rb *RemoteProxyBuilder) WithOIDCConfigRef(name, resourceURL string) *RemoteProxyBuilder {
120122
rb.proxy.Spec.OIDCConfigRef = &mcpv1alpha1.MCPOIDCConfigReference{
121-
Name: name,
122-
Audience: audience,
123+
Name: name,
124+
Audience: resourceURL,
125+
ResourceURL: resourceURL,
123126
}
124127
return rb
125128
}

cmd/thv-operator/test-integration/mcp-server/mcpserver_authserverref_integration_test.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,9 @@ var _ = Describe("MCPServer AuthServerRef Integration Tests", func() {
8181
Name: authConfigName,
8282
},
8383
OIDCConfigRef: &mcpv1alpha1.MCPOIDCConfigReference{
84-
Name: oidcConfigName,
85-
Audience: "https://test-resource.example.com",
84+
Name: oidcConfigName,
85+
Audience: "https://test-resource.example.com",
86+
ResourceURL: "https://test-resource.example.com",
8687
},
8788
},
8889
}
@@ -197,8 +198,9 @@ var _ = Describe("MCPServer AuthServerRef Integration Tests", func() {
197198
Name: authConfigConflict,
198199
},
199200
OIDCConfigRef: &mcpv1alpha1.MCPOIDCConfigReference{
200-
Name: oidcConfigName,
201-
Audience: "https://test-resource.example.com",
201+
Name: oidcConfigName,
202+
Audience: "https://test-resource.example.com",
203+
ResourceURL: "https://test-resource.example.com",
202204
},
203205
},
204206
}
@@ -376,8 +378,9 @@ var _ = Describe("MCPServer AuthServerRef Integration Tests", func() {
376378
Name: authConfigName,
377379
},
378380
OIDCConfigRef: &mcpv1alpha1.MCPOIDCConfigReference{
379-
Name: oidcConfigName,
380-
Audience: "https://test-resource.example.com",
381+
Name: oidcConfigName,
382+
Audience: "https://test-resource.example.com",
383+
ResourceURL: "https://test-resource.example.com",
381384
},
382385
},
383386
}

0 commit comments

Comments
 (0)