From 6cd9c140b3c2a5480ef230363de0cf06b3528356 Mon Sep 17 00:00:00 2001 From: Trey Date: Thu, 2 Apr 2026 07:21:23 -0700 Subject: [PATCH 1/3] Wire jwksUrl and introspectionUrl from vMCP inline OIDC config to runtime Implements changes for issue #4485: - Add JwksUrl and IntrospectionUrl fields to vmcpconfig.OIDCConfig - Map resolved JWKSURL/IntrospectionURL in CRD-to-vMCP converter - Pass JWKSURL/IntrospectionURL to auth.TokenValidatorConfig in auth factory - Add converter and YAML loader tests for the new fields - Fix pre-existing goconst lint issue in crd_cli_roundtrip_test.go --- cmd/thv-operator/pkg/vmcpconfig/converter.go | 2 + .../pkg/vmcpconfig/converter_test.go | 40 ++++++++++++++++-- pkg/vmcp/auth/factory/incoming.go | 2 + pkg/vmcp/config/config.go | 11 +++++ pkg/vmcp/config/crd_cli_roundtrip_test.go | 6 +-- pkg/vmcp/config/yaml_loader_test.go | 42 +++++++++++++++++++ 6 files changed, 96 insertions(+), 7 deletions(-) diff --git a/cmd/thv-operator/pkg/vmcpconfig/converter.go b/cmd/thv-operator/pkg/vmcpconfig/converter.go index e4f7ebf775..efbd9faa1d 100644 --- a/cmd/thv-operator/pkg/vmcpconfig/converter.go +++ b/cmd/thv-operator/pkg/vmcpconfig/converter.go @@ -237,6 +237,8 @@ func mapResolvedOIDCToVmcpConfig( ClientID: resolved.ClientID, Audience: resolved.Audience, Resource: resolved.ResourceURL, + JwksUrl: resolved.JWKSURL, + IntrospectionUrl: resolved.IntrospectionURL, ProtectedResourceAllowPrivateIP: resolved.JWKSAllowPrivateIP, InsecureAllowHTTP: resolved.InsecureAllowHTTP, Scopes: resolved.Scopes, diff --git a/cmd/thv-operator/pkg/vmcpconfig/converter_test.go b/cmd/thv-operator/pkg/vmcpconfig/converter_test.go index 1e8a44abe1..c7bae0493d 100644 --- a/cmd/thv-operator/pkg/vmcpconfig/converter_test.go +++ b/cmd/thv-operator/pkg/vmcpconfig/converter_test.go @@ -89,6 +89,7 @@ func TestConverter_OIDCResolution(t *testing.T) { mockReturn: &oidc.OIDCConfig{ Issuer: "https://issuer.example.com", Audience: "my-audience", ResourceURL: "https://resource.example.com", JWKSAllowPrivateIP: true, + JWKSURL: "https://issuer.example.com/jwks", IntrospectionURL: "https://issuer.example.com/introspect", }, validate: func(t *testing.T, config *vmcpconfig.Config, err error) { t.Helper() @@ -97,6 +98,8 @@ func TestConverter_OIDCResolution(t *testing.T) { assert.Equal(t, "https://issuer.example.com", config.IncomingAuth.OIDC.Issuer) assert.Equal(t, "my-audience", config.IncomingAuth.OIDC.Audience) assert.Equal(t, "https://resource.example.com", config.IncomingAuth.OIDC.Resource) + assert.Equal(t, "https://issuer.example.com/jwks", config.IncomingAuth.OIDC.JwksUrl) + assert.Equal(t, "https://issuer.example.com/introspect", config.IncomingAuth.OIDC.IntrospectionUrl) assert.True(t, config.IncomingAuth.OIDC.ProtectedResourceAllowPrivateIP) }, }, @@ -310,6 +313,31 @@ func TestConverter_IncomingAuthRequired(t *testing.T) { }, description: "Should correctly convert OIDC auth config with scopes", }, + { + name: "oidc auth with jwksUrl and introspectionUrl", + incomingAuth: &mcpv1alpha1.IncomingAuthConfig{ + Type: "oidc", + OIDCConfig: &mcpv1alpha1.OIDCConfigRef{ + Type: "inline", + Inline: &mcpv1alpha1.InlineOIDCConfig{ + Issuer: "https://auth.example.com", + ClientID: "test-client", + Audience: "test-audience", + JWKSURL: "https://auth.example.com/custom/jwks", + IntrospectionURL: "https://auth.example.com/custom/introspect", + }, + }, + }, + expectedAuthType: "oidc", + expectedOIDCConfig: &vmcpconfig.OIDCConfig{ + Issuer: "https://auth.example.com", + ClientID: "test-client", + Audience: "test-audience", + JwksUrl: "https://auth.example.com/custom/jwks", + IntrospectionUrl: "https://auth.example.com/custom/introspect", + }, + description: "Should correctly convert OIDC auth config with jwksUrl and introspectionUrl", + }, } for _, tt := range tests { @@ -334,10 +362,12 @@ func TestConverter_IncomingAuthRequired(t *testing.T) { // Configure mock to return expected OIDC config if tt.expectedOIDCConfig != nil { mockResolver.EXPECT().Resolve(gomock.Any(), gomock.Any()).Return(&oidc.OIDCConfig{ - Issuer: tt.expectedOIDCConfig.Issuer, - ClientID: tt.expectedOIDCConfig.ClientID, - Audience: tt.expectedOIDCConfig.Audience, - Scopes: tt.expectedOIDCConfig.Scopes, + Issuer: tt.expectedOIDCConfig.Issuer, + ClientID: tt.expectedOIDCConfig.ClientID, + Audience: tt.expectedOIDCConfig.Audience, + JWKSURL: tt.expectedOIDCConfig.JwksUrl, + IntrospectionURL: tt.expectedOIDCConfig.IntrospectionUrl, + Scopes: tt.expectedOIDCConfig.Scopes, }, nil) } else { mockResolver.EXPECT().Resolve(gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes() @@ -361,6 +391,8 @@ func TestConverter_IncomingAuthRequired(t *testing.T) { assert.Equal(t, tt.expectedOIDCConfig.Issuer, config.IncomingAuth.OIDC.Issuer, tt.description) assert.Equal(t, tt.expectedOIDCConfig.ClientID, config.IncomingAuth.OIDC.ClientID, tt.description) assert.Equal(t, tt.expectedOIDCConfig.Audience, config.IncomingAuth.OIDC.Audience, tt.description) + assert.Equal(t, tt.expectedOIDCConfig.JwksUrl, config.IncomingAuth.OIDC.JwksUrl, tt.description) + assert.Equal(t, tt.expectedOIDCConfig.IntrospectionUrl, config.IncomingAuth.OIDC.IntrospectionUrl, tt.description) assert.Equal(t, tt.expectedOIDCConfig.Scopes, config.IncomingAuth.OIDC.Scopes, tt.description) } else { assert.Nil(t, config.IncomingAuth.OIDC, tt.description) diff --git a/pkg/vmcp/auth/factory/incoming.go b/pkg/vmcp/auth/factory/incoming.go index 823582caff..20d317ee8e 100644 --- a/pkg/vmcp/auth/factory/incoming.go +++ b/pkg/vmcp/auth/factory/incoming.go @@ -161,6 +161,8 @@ func newOIDCAuthMiddleware( ClientID: oidcCfg.ClientID, Audience: oidcCfg.Audience, ResourceURL: oidcCfg.Resource, + JWKSURL: oidcCfg.JwksUrl, + IntrospectionURL: oidcCfg.IntrospectionUrl, AllowPrivateIP: oidcCfg.ProtectedResourceAllowPrivateIP || oidcCfg.JwksAllowPrivateIP, InsecureAllowHTTP: oidcCfg.InsecureAllowHTTP, Scopes: oidcCfg.Scopes, diff --git a/pkg/vmcp/config/config.go b/pkg/vmcp/config/config.go index 390bd9660f..ea89ec749c 100644 --- a/pkg/vmcp/config/config.go +++ b/pkg/vmcp/config/config.go @@ -218,6 +218,17 @@ type OIDCConfig struct { // If not specified, defaults to Audience. Resource string `json:"resource,omitempty" yaml:"resource,omitempty"` + // JwksUrl is the explicit JWKS endpoint URL. + // When set, skips OIDC discovery and fetches the JWKS directly from this URL. + // This is useful when the OIDC issuer does not serve a /.well-known/openid-configuration. + // +optional + JwksUrl string `json:"jwksUrl,omitempty" yaml:"jwksUrl,omitempty"` + + // IntrospectionUrl is the token introspection endpoint URL (RFC 7662). + // When set, enables token introspection for opaque (non-JWT) tokens. + // +optional + IntrospectionUrl string `json:"introspectionUrl,omitempty" yaml:"introspectionUrl,omitempty"` + // Scopes are the required OAuth scopes. Scopes []string `json:"scopes,omitempty" yaml:"scopes,omitempty"` diff --git a/pkg/vmcp/config/crd_cli_roundtrip_test.go b/pkg/vmcp/config/crd_cli_roundtrip_test.go index 1ad76d3064..6237e75acb 100644 --- a/pkg/vmcp/config/crd_cli_roundtrip_test.go +++ b/pkg/vmcp/config/crd_cli_roundtrip_test.go @@ -447,7 +447,7 @@ func TestConfigRoundtrip(t *testing.T) { Name: "test-server", Group: "test-group", IncomingAuth: &IncomingAuthConfig{ - Type: "oidc", + Type: IncomingAuthTypeOIDC, OIDC: &OIDCConfig{ Issuer: "https://issuer.example.com", ClientID: "client-123", @@ -517,8 +517,8 @@ func TestConfigRoundtrip(t *testing.T) { if parsedConfig.IncomingAuth == nil { t.Fatal("IncomingAuth is nil") } - if parsedConfig.IncomingAuth.Type != "oidc" { - t.Errorf("IncomingAuth.Type = %q, want %q", parsedConfig.IncomingAuth.Type, "oidc") + if parsedConfig.IncomingAuth.Type != IncomingAuthTypeOIDC { + t.Errorf("IncomingAuth.Type = %q, want %q", parsedConfig.IncomingAuth.Type, IncomingAuthTypeOIDC) } if len(parsedConfig.CompositeTools) != 1 { t.Fatalf("CompositeTools length = %d, want 1", len(parsedConfig.CompositeTools)) diff --git a/pkg/vmcp/config/yaml_loader_test.go b/pkg/vmcp/config/yaml_loader_test.go index 4fe04ed3b0..231b38bf73 100644 --- a/pkg/vmcp/config/yaml_loader_test.go +++ b/pkg/vmcp/config/yaml_loader_test.go @@ -130,6 +130,48 @@ aggregation: }, wantErr: false, }, + { + name: "valid OIDC configuration with jwksUrl and introspectionUrl", + yaml: ` +name: test-vmcp +groupRef: test-group + +incomingAuth: + type: oidc + oidc: + issuer: https://auth.example.com + clientId: test-client + audience: vmcp + jwksUrl: https://auth.example.com/custom/jwks + introspectionUrl: https://auth.example.com/custom/introspect + +outgoingAuth: + source: inline + default: + type: unauthenticated + +aggregation: + conflictResolution: prefix + conflictResolutionConfig: + prefixFormat: "{workload}_" +`, + want: func(t *testing.T, cfg *Config) { + t.Helper() + if cfg.IncomingAuth.Type != "oidc" { + t.Errorf("IncomingAuth.Type = %v, want oidc", cfg.IncomingAuth.Type) + } + if cfg.IncomingAuth.OIDC == nil { + t.Fatal("IncomingAuth.OIDC is nil") + } + if cfg.IncomingAuth.OIDC.JwksUrl != "https://auth.example.com/custom/jwks" { + t.Errorf("OIDC.JwksUrl = %v, want https://auth.example.com/custom/jwks", cfg.IncomingAuth.OIDC.JwksUrl) + } + if cfg.IncomingAuth.OIDC.IntrospectionUrl != "https://auth.example.com/custom/introspect" { + t.Errorf("OIDC.IntrospectionUrl = %v, want https://auth.example.com/custom/introspect", cfg.IncomingAuth.OIDC.IntrospectionUrl) + } + }, + wantErr: false, + }, { name: "partial operational config gets defaults for missing fields", yaml: ` From 6fda674e5cccb1e71b8c76d5967c415c15ebda6b Mon Sep 17 00:00:00 2001 From: Trey Date: Thu, 2 Apr 2026 07:32:49 -0700 Subject: [PATCH 2/3] Address code review feedback Fixed issues from code review: - MEDIUM: Rename Go struct fields JwksUrl -> JWKSURL and IntrospectionUrl -> IntrospectionURL to follow codebase acronym conventions (JSON tags unchanged) - MEDIUM: Revert unrelated crd_cli_roundtrip_test.go cleanup that replaced "oidc" string literals with IncomingAuthTypeOIDC constant --- cmd/thv-operator/pkg/vmcpconfig/converter.go | 4 ++-- .../pkg/vmcpconfig/converter_test.go | 16 ++++++++-------- pkg/vmcp/auth/factory/incoming.go | 4 ++-- pkg/vmcp/config/config.go | 8 ++++---- pkg/vmcp/config/crd_cli_roundtrip_test.go | 6 +++--- pkg/vmcp/config/yaml_loader_test.go | 11 ++++------- 6 files changed, 23 insertions(+), 26 deletions(-) diff --git a/cmd/thv-operator/pkg/vmcpconfig/converter.go b/cmd/thv-operator/pkg/vmcpconfig/converter.go index efbd9faa1d..940a5ef5a8 100644 --- a/cmd/thv-operator/pkg/vmcpconfig/converter.go +++ b/cmd/thv-operator/pkg/vmcpconfig/converter.go @@ -237,8 +237,8 @@ func mapResolvedOIDCToVmcpConfig( ClientID: resolved.ClientID, Audience: resolved.Audience, Resource: resolved.ResourceURL, - JwksUrl: resolved.JWKSURL, - IntrospectionUrl: resolved.IntrospectionURL, + JWKSURL: resolved.JWKSURL, + IntrospectionURL: resolved.IntrospectionURL, ProtectedResourceAllowPrivateIP: resolved.JWKSAllowPrivateIP, InsecureAllowHTTP: resolved.InsecureAllowHTTP, Scopes: resolved.Scopes, diff --git a/cmd/thv-operator/pkg/vmcpconfig/converter_test.go b/cmd/thv-operator/pkg/vmcpconfig/converter_test.go index c7bae0493d..aff80f5368 100644 --- a/cmd/thv-operator/pkg/vmcpconfig/converter_test.go +++ b/cmd/thv-operator/pkg/vmcpconfig/converter_test.go @@ -98,8 +98,8 @@ func TestConverter_OIDCResolution(t *testing.T) { assert.Equal(t, "https://issuer.example.com", config.IncomingAuth.OIDC.Issuer) assert.Equal(t, "my-audience", config.IncomingAuth.OIDC.Audience) assert.Equal(t, "https://resource.example.com", config.IncomingAuth.OIDC.Resource) - assert.Equal(t, "https://issuer.example.com/jwks", config.IncomingAuth.OIDC.JwksUrl) - assert.Equal(t, "https://issuer.example.com/introspect", config.IncomingAuth.OIDC.IntrospectionUrl) + assert.Equal(t, "https://issuer.example.com/jwks", config.IncomingAuth.OIDC.JWKSURL) + assert.Equal(t, "https://issuer.example.com/introspect", config.IncomingAuth.OIDC.IntrospectionURL) assert.True(t, config.IncomingAuth.OIDC.ProtectedResourceAllowPrivateIP) }, }, @@ -333,8 +333,8 @@ func TestConverter_IncomingAuthRequired(t *testing.T) { Issuer: "https://auth.example.com", ClientID: "test-client", Audience: "test-audience", - JwksUrl: "https://auth.example.com/custom/jwks", - IntrospectionUrl: "https://auth.example.com/custom/introspect", + JWKSURL: "https://auth.example.com/custom/jwks", + IntrospectionURL: "https://auth.example.com/custom/introspect", }, description: "Should correctly convert OIDC auth config with jwksUrl and introspectionUrl", }, @@ -365,8 +365,8 @@ func TestConverter_IncomingAuthRequired(t *testing.T) { Issuer: tt.expectedOIDCConfig.Issuer, ClientID: tt.expectedOIDCConfig.ClientID, Audience: tt.expectedOIDCConfig.Audience, - JWKSURL: tt.expectedOIDCConfig.JwksUrl, - IntrospectionURL: tt.expectedOIDCConfig.IntrospectionUrl, + JWKSURL: tt.expectedOIDCConfig.JWKSURL, + IntrospectionURL: tt.expectedOIDCConfig.IntrospectionURL, Scopes: tt.expectedOIDCConfig.Scopes, }, nil) } else { @@ -391,8 +391,8 @@ func TestConverter_IncomingAuthRequired(t *testing.T) { assert.Equal(t, tt.expectedOIDCConfig.Issuer, config.IncomingAuth.OIDC.Issuer, tt.description) assert.Equal(t, tt.expectedOIDCConfig.ClientID, config.IncomingAuth.OIDC.ClientID, tt.description) assert.Equal(t, tt.expectedOIDCConfig.Audience, config.IncomingAuth.OIDC.Audience, tt.description) - assert.Equal(t, tt.expectedOIDCConfig.JwksUrl, config.IncomingAuth.OIDC.JwksUrl, tt.description) - assert.Equal(t, tt.expectedOIDCConfig.IntrospectionUrl, config.IncomingAuth.OIDC.IntrospectionUrl, tt.description) + assert.Equal(t, tt.expectedOIDCConfig.JWKSURL, config.IncomingAuth.OIDC.JWKSURL, tt.description) + assert.Equal(t, tt.expectedOIDCConfig.IntrospectionURL, config.IncomingAuth.OIDC.IntrospectionURL, tt.description) assert.Equal(t, tt.expectedOIDCConfig.Scopes, config.IncomingAuth.OIDC.Scopes, tt.description) } else { assert.Nil(t, config.IncomingAuth.OIDC, tt.description) diff --git a/pkg/vmcp/auth/factory/incoming.go b/pkg/vmcp/auth/factory/incoming.go index 20d317ee8e..5b899894f1 100644 --- a/pkg/vmcp/auth/factory/incoming.go +++ b/pkg/vmcp/auth/factory/incoming.go @@ -161,8 +161,8 @@ func newOIDCAuthMiddleware( ClientID: oidcCfg.ClientID, Audience: oidcCfg.Audience, ResourceURL: oidcCfg.Resource, - JWKSURL: oidcCfg.JwksUrl, - IntrospectionURL: oidcCfg.IntrospectionUrl, + JWKSURL: oidcCfg.JWKSURL, + IntrospectionURL: oidcCfg.IntrospectionURL, AllowPrivateIP: oidcCfg.ProtectedResourceAllowPrivateIP || oidcCfg.JwksAllowPrivateIP, InsecureAllowHTTP: oidcCfg.InsecureAllowHTTP, Scopes: oidcCfg.Scopes, diff --git a/pkg/vmcp/config/config.go b/pkg/vmcp/config/config.go index ea89ec749c..ef98861794 100644 --- a/pkg/vmcp/config/config.go +++ b/pkg/vmcp/config/config.go @@ -218,16 +218,16 @@ type OIDCConfig struct { // If not specified, defaults to Audience. Resource string `json:"resource,omitempty" yaml:"resource,omitempty"` - // JwksUrl is the explicit JWKS endpoint URL. + // JWKSURL is the explicit JWKS endpoint URL. // When set, skips OIDC discovery and fetches the JWKS directly from this URL. // This is useful when the OIDC issuer does not serve a /.well-known/openid-configuration. // +optional - JwksUrl string `json:"jwksUrl,omitempty" yaml:"jwksUrl,omitempty"` + JWKSURL string `json:"jwksUrl,omitempty" yaml:"jwksUrl,omitempty"` - // IntrospectionUrl is the token introspection endpoint URL (RFC 7662). + // IntrospectionURL is the token introspection endpoint URL (RFC 7662). // When set, enables token introspection for opaque (non-JWT) tokens. // +optional - IntrospectionUrl string `json:"introspectionUrl,omitempty" yaml:"introspectionUrl,omitempty"` + IntrospectionURL string `json:"introspectionUrl,omitempty" yaml:"introspectionUrl,omitempty"` // Scopes are the required OAuth scopes. Scopes []string `json:"scopes,omitempty" yaml:"scopes,omitempty"` diff --git a/pkg/vmcp/config/crd_cli_roundtrip_test.go b/pkg/vmcp/config/crd_cli_roundtrip_test.go index 6237e75acb..1ad76d3064 100644 --- a/pkg/vmcp/config/crd_cli_roundtrip_test.go +++ b/pkg/vmcp/config/crd_cli_roundtrip_test.go @@ -447,7 +447,7 @@ func TestConfigRoundtrip(t *testing.T) { Name: "test-server", Group: "test-group", IncomingAuth: &IncomingAuthConfig{ - Type: IncomingAuthTypeOIDC, + Type: "oidc", OIDC: &OIDCConfig{ Issuer: "https://issuer.example.com", ClientID: "client-123", @@ -517,8 +517,8 @@ func TestConfigRoundtrip(t *testing.T) { if parsedConfig.IncomingAuth == nil { t.Fatal("IncomingAuth is nil") } - if parsedConfig.IncomingAuth.Type != IncomingAuthTypeOIDC { - t.Errorf("IncomingAuth.Type = %q, want %q", parsedConfig.IncomingAuth.Type, IncomingAuthTypeOIDC) + if parsedConfig.IncomingAuth.Type != "oidc" { + t.Errorf("IncomingAuth.Type = %q, want %q", parsedConfig.IncomingAuth.Type, "oidc") } if len(parsedConfig.CompositeTools) != 1 { t.Fatalf("CompositeTools length = %d, want 1", len(parsedConfig.CompositeTools)) diff --git a/pkg/vmcp/config/yaml_loader_test.go b/pkg/vmcp/config/yaml_loader_test.go index 231b38bf73..ac6976492e 100644 --- a/pkg/vmcp/config/yaml_loader_test.go +++ b/pkg/vmcp/config/yaml_loader_test.go @@ -157,17 +157,14 @@ aggregation: `, want: func(t *testing.T, cfg *Config) { t.Helper() - if cfg.IncomingAuth.Type != "oidc" { - t.Errorf("IncomingAuth.Type = %v, want oidc", cfg.IncomingAuth.Type) - } if cfg.IncomingAuth.OIDC == nil { t.Fatal("IncomingAuth.OIDC is nil") } - if cfg.IncomingAuth.OIDC.JwksUrl != "https://auth.example.com/custom/jwks" { - t.Errorf("OIDC.JwksUrl = %v, want https://auth.example.com/custom/jwks", cfg.IncomingAuth.OIDC.JwksUrl) + if cfg.IncomingAuth.OIDC.JWKSURL != "https://auth.example.com/custom/jwks" { + t.Errorf("OIDC.JWKSURL = %v, want https://auth.example.com/custom/jwks", cfg.IncomingAuth.OIDC.JWKSURL) } - if cfg.IncomingAuth.OIDC.IntrospectionUrl != "https://auth.example.com/custom/introspect" { - t.Errorf("OIDC.IntrospectionUrl = %v, want https://auth.example.com/custom/introspect", cfg.IncomingAuth.OIDC.IntrospectionUrl) + if cfg.IncomingAuth.OIDC.IntrospectionURL != "https://auth.example.com/custom/introspect" { + t.Errorf("OIDC.IntrospectionURL = %v, want https://auth.example.com/custom/introspect", cfg.IncomingAuth.OIDC.IntrospectionURL) } }, wantErr: false, From 6f7d14a2f743c501052c95274c4322903e21f403 Mon Sep 17 00:00:00 2001 From: Trey Date: Thu, 2 Apr 2026 08:10:24 -0700 Subject: [PATCH 3/3] Run `task operator-manifests` and `task crdref-gen` --- .../crds/toolhive.stacklok.dev_virtualmcpservers.yaml | 11 +++++++++++ .../toolhive.stacklok.dev_virtualmcpservers.yaml | 11 +++++++++++ docs/operator/crd-api.md | 2 ++ 3 files changed, 24 insertions(+) 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 fe998a5076..aced9163fb 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 @@ -1196,6 +1196,11 @@ spec: InsecureAllowHTTP allows HTTP (non-HTTPS) OIDC issuers for development/testing WARNING: This is insecure and should NEVER be used in production type: boolean + introspectionUrl: + description: |- + IntrospectionURL is the token introspection endpoint URL (RFC 7662). + When set, enables token introspection for opaque (non-JWT) tokens. + type: string issuer: description: Issuer is the OIDC issuer URL. pattern: ^https?:// @@ -1207,6 +1212,12 @@ spec: the OIDC middleware needs to fetch its JWKS from that address. Use with caution - only enable for trusted internal IDPs or testing. type: boolean + jwksUrl: + description: |- + JWKSURL is the explicit JWKS endpoint URL. + When set, skips OIDC discovery and fetches the JWKS directly from this URL. + This is useful when the OIDC issuer does not serve a /.well-known/openid-configuration. + type: string protectedResourceAllowPrivateIp: description: |- ProtectedResourceAllowPrivateIP allows protected resource endpoint on private IP addresses 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 f6fee39825..07b4a3a2ee 100644 --- a/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_virtualmcpservers.yaml +++ b/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_virtualmcpservers.yaml @@ -1199,6 +1199,11 @@ spec: InsecureAllowHTTP allows HTTP (non-HTTPS) OIDC issuers for development/testing WARNING: This is insecure and should NEVER be used in production type: boolean + introspectionUrl: + description: |- + IntrospectionURL is the token introspection endpoint URL (RFC 7662). + When set, enables token introspection for opaque (non-JWT) tokens. + type: string issuer: description: Issuer is the OIDC issuer URL. pattern: ^https?:// @@ -1210,6 +1215,12 @@ spec: the OIDC middleware needs to fetch its JWKS from that address. Use with caution - only enable for trusted internal IDPs or testing. type: boolean + jwksUrl: + description: |- + JWKSURL is the explicit JWKS endpoint URL. + When set, skips OIDC discovery and fetches the JWKS directly from this URL. + This is useful when the OIDC issuer does not serve a /.well-known/openid-configuration. + type: string protectedResourceAllowPrivateIp: description: |- ProtectedResourceAllowPrivateIP allows protected resource endpoint on private IP addresses diff --git a/docs/operator/crd-api.md b/docs/operator/crd-api.md index 758140f336..a38af578c3 100644 --- a/docs/operator/crd-api.md +++ b/docs/operator/crd-api.md @@ -381,6 +381,8 @@ _Appears in:_ | `clientSecretEnv` _string_ | ClientSecretEnv is the name of the environment variable containing the client secret.
This is the secure way to reference secrets - the actual secret value is never stored
in configuration files, only the environment variable name.
The secret value will be resolved from this environment variable at runtime. | | | | `audience` _string_ | Audience is the required token audience. | | | | `resource` _string_ | Resource is the OAuth 2.0 resource indicator (RFC 8707).
Used in WWW-Authenticate header and OAuth discovery metadata (RFC 9728).
If not specified, defaults to Audience. | | | +| `jwksUrl` _string_ | JWKSURL is the explicit JWKS endpoint URL.
When set, skips OIDC discovery and fetches the JWKS directly from this URL.
This is useful when the OIDC issuer does not serve a /.well-known/openid-configuration. | | Optional: \{\}
| +| `introspectionUrl` _string_ | IntrospectionURL is the token introspection endpoint URL (RFC 7662).
When set, enables token introspection for opaque (non-JWT) tokens. | | Optional: \{\}
| | `scopes` _string array_ | Scopes are the required OAuth scopes. | | | | `protectedResourceAllowPrivateIp` _boolean_ | ProtectedResourceAllowPrivateIP allows protected resource endpoint on private IP addresses
Use with caution - only enable for trusted internal IDPs or testing | | | | `jwksAllowPrivateIp` _boolean_ | JwksAllowPrivateIP allows OIDC discovery and JWKS fetches to private IP addresses.
Enable when the embedded auth server runs on a loopback address and
the OIDC middleware needs to fetch its JWKS from that address.
Use with caution - only enable for trusted internal IDPs or testing. | | |