diff --git a/cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go b/cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go index aa3d8a5100..f0679aafad 100644 --- a/cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go +++ b/cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go @@ -293,12 +293,19 @@ type EmbeddedAuthServerConfig struct { // "openid" and "offline_access"). Adding a privileged scope here — e.g. // "admin:read" — would grant it to every DCR-registered client, including // public clients like Claude Code, Cursor, and VS Code. + // When cimd.enabled is true, every dynamically resolved CIMD client will + // also gain the ability to request these scopes, including third-party + // clients resolved from arbitrary HTTPS URLs. // +kubebuilder:validation:MaxItems=10 // +kubebuilder:validation:items:MinLength=1 // +kubebuilder:validation:items:Pattern=`^[\x21\x23-\x5B\x5D-\x7E]+$` // +listType=atomic // +optional BaselineClientScopes []string `json:"baselineClientScopes,omitempty"` + + // CIMD configures Client ID Metadata Document support. When omitted, CIMD is disabled. + // +optional + CIMD *EmbeddedAuthServerCIMDConfig `json:"cimd,omitempty"` } // TokenLifespanConfig holds configuration for token lifetimes. @@ -325,6 +332,31 @@ type TokenLifespanConfig struct { AuthCodeLifespan string `json:"authCodeLifespan,omitempty"` } +// EmbeddedAuthServerCIMDConfig configures Client ID Metadata Document (CIMD) support +// on the embedded authorization server. When enabled, the AS accepts HTTPS URLs as +// client_id values and resolves them via the CIMD protocol, allowing clients such as +// VS Code to authenticate without prior Dynamic Client Registration. +type EmbeddedAuthServerCIMDConfig struct { + // Enabled activates CIMD client lookup. When false (the default), the AS only + // accepts client_id values that were registered via DCR. + // +kubebuilder:default=false + Enabled bool `json:"enabled"` + + // CacheMaxSize is the maximum number of CIMD documents held in the LRU cache. + // Defaults to 256 when Enabled is true and this field is omitted. + // +kubebuilder:validation:Minimum=1 + // +optional + CacheMaxSize int `json:"cacheMaxSize,omitempty"` + + // CacheFallbackTTL is the fixed TTL applied to every cached CIMD document. + // Cache-Control header parsing is not yet implemented; all entries use this value. + // Format: Go duration string (e.g. "5m", "10m", "1h"). + // Defaults to 5 minutes when Enabled is true and this field is omitted. + // +kubebuilder:validation:Pattern=`^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$` + // +optional + CacheFallbackTTL string `json:"cacheFallbackTtl,omitempty"` +} + // UpstreamProviderType identifies the type of upstream Identity Provider. type UpstreamProviderType string diff --git a/cmd/thv-operator/api/v1beta1/zz_generated.deepcopy.go b/cmd/thv-operator/api/v1beta1/zz_generated.deepcopy.go index 07077737b5..cbfcf8cf8d 100644 --- a/cmd/thv-operator/api/v1beta1/zz_generated.deepcopy.go +++ b/cmd/thv-operator/api/v1beta1/zz_generated.deepcopy.go @@ -225,6 +225,21 @@ func (in *DCRUpstreamConfig) DeepCopy() *DCRUpstreamConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EmbeddedAuthServerCIMDConfig) DeepCopyInto(out *EmbeddedAuthServerCIMDConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EmbeddedAuthServerCIMDConfig. +func (in *EmbeddedAuthServerCIMDConfig) DeepCopy() *EmbeddedAuthServerCIMDConfig { + if in == nil { + return nil + } + out := new(EmbeddedAuthServerCIMDConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EmbeddedAuthServerConfig) DeepCopyInto(out *EmbeddedAuthServerConfig) { *out = *in @@ -260,6 +275,11 @@ func (in *EmbeddedAuthServerConfig) DeepCopyInto(out *EmbeddedAuthServerConfig) *out = make([]string, len(*in)) copy(*out, *in) } + if in.CIMD != nil { + in, out := &in.CIMD, &out.CIMD + *out = new(EmbeddedAuthServerCIMDConfig) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EmbeddedAuthServerConfig. diff --git a/cmd/thv-operator/pkg/controllerutil/authserver.go b/cmd/thv-operator/pkg/controllerutil/authserver.go index 346c4d87ad..84d4ed979d 100644 --- a/cmd/thv-operator/pkg/controllerutil/authserver.go +++ b/cmd/thv-operator/pkg/controllerutil/authserver.go @@ -582,6 +582,16 @@ func BuildAuthServerRunConfig( } config.Storage = storageCfg + // Build CIMD configuration. CacheFallbackTTL is passed as-is (string); + // resolveCIMDConfig in the runner parses it to time.Duration at startup. + if authConfig.CIMD != nil && authConfig.CIMD.Enabled { + config.CIMD = &authserver.CIMDRunConfig{ + Enabled: authConfig.CIMD.Enabled, + CacheMaxSize: authConfig.CIMD.CacheMaxSize, + CacheFallbackTTL: authConfig.CIMD.CacheFallbackTTL, + } + } + return config, nil } diff --git a/cmd/thv-operator/pkg/controllerutil/authserver_test.go b/cmd/thv-operator/pkg/controllerutil/authserver_test.go index fcb0bd6379..bec4c98179 100644 --- a/cmd/thv-operator/pkg/controllerutil/authserver_test.go +++ b/cmd/thv-operator/pkg/controllerutil/authserver_test.go @@ -2645,3 +2645,132 @@ func TestValidateAndAddAuthServerRefOptions(t *testing.T) { }) } } + +// TestBuildAuthServerRunConfig_CIMD verifies that BuildAuthServerRunConfig +// correctly converts the CRD EmbeddedAuthServerCIMDConfig into +// authserver.CIMDRunConfig. The four cases cover the nil path (CIMD off +// by default), explicit values (fields are mapped and TTL is parsed), zero +// optional fields (authserver applies its own defaults at startup), and an +// invalid TTL string (returns a parse error). +func TestBuildAuthServerRunConfig_CIMD(t *testing.T) { + t.Parallel() + + // baseAuthConfig returns a minimal EmbeddedAuthServerConfig that is valid + // enough for BuildAuthServerRunConfig to proceed past signing-key and + // upstream validation without requiring real secrets. + baseAuthConfig := func(cimd *mcpv1beta1.EmbeddedAuthServerCIMDConfig) *mcpv1beta1.EmbeddedAuthServerConfig { + return &mcpv1beta1.EmbeddedAuthServerConfig{ + Issuer: "https://auth.example.com", + SigningKeySecretRefs: []mcpv1beta1.SecretKeyRef{ + {Name: "signing-key", Key: "private.pem"}, + }, + HMACSecretRefs: []mcpv1beta1.SecretKeyRef{ + {Name: "hmac-secret", Key: "hmac"}, + }, + CIMD: cimd, + } + } + + defaultAudiences := []string{"https://mcp.example.com"} + defaultScopes := []string{"openid", "offline_access"} + + tests := []struct { + name string + cimd *mcpv1beta1.EmbeddedAuthServerCIMDConfig + wantCIMD bool + wantErr bool + errContains string + checkFunc func(t *testing.T, got *authserver.CIMDRunConfig) + }{ + { + name: "nil CIMD leaves config.CIMD nil", + cimd: nil, + wantCIMD: false, + }, + { + name: "CIMD disabled leaves config.CIMD nil", + cimd: &mcpv1beta1.EmbeddedAuthServerCIMDConfig{ + Enabled: false, + CacheMaxSize: 100, + CacheFallbackTTL: "10m", + }, + wantCIMD: false, + }, + { + name: "CIMD enabled with explicit values maps all fields", + cimd: &mcpv1beta1.EmbeddedAuthServerCIMDConfig{ + Enabled: true, + CacheMaxSize: 512, + CacheFallbackTTL: "10m", + }, + wantCIMD: true, + checkFunc: func(t *testing.T, got *authserver.CIMDRunConfig) { + t.Helper() + assert.True(t, got.Enabled) + assert.Equal(t, 512, got.CacheMaxSize) + assert.Equal(t, "10m", got.CacheFallbackTTL) + }, + }, + { + name: "CIMD enabled with zero optional fields leaves defaults to authserver", + cimd: &mcpv1beta1.EmbeddedAuthServerCIMDConfig{ + Enabled: true, + }, + wantCIMD: true, + checkFunc: func(t *testing.T, got *authserver.CIMDRunConfig) { + t.Helper() + assert.True(t, got.Enabled) + assert.Zero(t, got.CacheMaxSize, "zero means authserver applies its own default at startup") + assert.Zero(t, got.CacheFallbackTTL, "zero means authserver applies its own default at startup") + }, + }, + { + name: "invalid CacheFallbackTTL passes through to runner for validation", + cimd: &mcpv1beta1.EmbeddedAuthServerCIMDConfig{ + Enabled: true, + CacheFallbackTTL: "not-a-duration", + }, + wantCIMD: true, + checkFunc: func(t *testing.T, got *authserver.CIMDRunConfig) { + t.Helper() + // The converter passes the string through; parse errors are caught + // by CIMDRunConfig.Validate() or resolveCIMDConfig in the runner. + assert.Equal(t, "not-a-duration", got.CacheFallbackTTL) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + cfg, err := BuildAuthServerRunConfig( + "default", "test-server", + baseAuthConfig(tt.cimd), + defaultAudiences, defaultScopes, + "https://mcp.example.com", + ) + + if tt.wantErr { + require.Error(t, err) + if tt.errContains != "" { + assert.Contains(t, err.Error(), tt.errContains) + } + return + } + + require.NoError(t, err) + require.NotNil(t, cfg) + + if !tt.wantCIMD { + assert.Nil(t, cfg.CIMD, "expected config.CIMD to be nil") + return + } + + require.NotNil(t, cfg.CIMD, "expected config.CIMD to be set") + if tt.checkFunc != nil { + tt.checkFunc(t, cfg.CIMD) + } + }) + } +} diff --git a/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml b/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml index 6a61201718..afe0aaa085 100644 --- a/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml +++ b/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml @@ -232,6 +232,9 @@ spec: "openid" and "offline_access"). Adding a privileged scope here — e.g. "admin:read" — would grant it to every DCR-registered client, including public clients like Claude Code, Cursor, and VS Code. + When cimd.enabled is true, every dynamically resolved CIMD client will + also gain the ability to request these scopes, including third-party + clients resolved from arbitrary HTTPS URLs. items: minLength: 1 pattern: ^[\x21\x23-\x5B\x5D-\x7E]+$ @@ -239,6 +242,33 @@ spec: maxItems: 10 type: array x-kubernetes-list-type: atomic + cimd: + description: CIMD configures Client ID Metadata Document support. + When omitted, CIMD is disabled. + properties: + cacheFallbackTtl: + description: |- + CacheFallbackTTL is the fixed TTL applied to every cached CIMD document. + Cache-Control header parsing is not yet implemented; all entries use this value. + Format: Go duration string (e.g. "5m", "10m", "1h"). + Defaults to 5 minutes when Enabled is true and this field is omitted. + pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string + cacheMaxSize: + description: |- + CacheMaxSize is the maximum number of CIMD documents held in the LRU cache. + Defaults to 256 when Enabled is true and this field is omitted. + minimum: 1 + type: integer + enabled: + default: false + description: |- + Enabled activates CIMD client lookup. When false (the default), the AS only + accepts client_id values that were registered via DCR. + type: boolean + required: + - enabled + type: object hmacSecretRefs: description: |- HMACSecretRefs references Kubernetes Secrets containing symmetric secrets for signing @@ -1511,6 +1541,9 @@ spec: "openid" and "offline_access"). Adding a privileged scope here — e.g. "admin:read" — would grant it to every DCR-registered client, including public clients like Claude Code, Cursor, and VS Code. + When cimd.enabled is true, every dynamically resolved CIMD client will + also gain the ability to request these scopes, including third-party + clients resolved from arbitrary HTTPS URLs. items: minLength: 1 pattern: ^[\x21\x23-\x5B\x5D-\x7E]+$ @@ -1518,6 +1551,33 @@ spec: maxItems: 10 type: array x-kubernetes-list-type: atomic + cimd: + description: CIMD configures Client ID Metadata Document support. + When omitted, CIMD is disabled. + properties: + cacheFallbackTtl: + description: |- + CacheFallbackTTL is the fixed TTL applied to every cached CIMD document. + Cache-Control header parsing is not yet implemented; all entries use this value. + Format: Go duration string (e.g. "5m", "10m", "1h"). + Defaults to 5 minutes when Enabled is true and this field is omitted. + pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string + cacheMaxSize: + description: |- + CacheMaxSize is the maximum number of CIMD documents held in the LRU cache. + Defaults to 256 when Enabled is true and this field is omitted. + minimum: 1 + type: integer + enabled: + default: false + description: |- + Enabled activates CIMD client lookup. When false (the default), the AS only + accepts client_id values that were registered via DCR. + type: boolean + required: + - enabled + type: object hmacSecretRefs: description: |- HMACSecretRefs references Kubernetes Secrets containing symmetric secrets for signing 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 6e19f377d4..116ae86739 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 @@ -105,6 +105,9 @@ spec: "openid" and "offline_access"). Adding a privileged scope here — e.g. "admin:read" — would grant it to every DCR-registered client, including public clients like Claude Code, Cursor, and VS Code. + When cimd.enabled is true, every dynamically resolved CIMD client will + also gain the ability to request these scopes, including third-party + clients resolved from arbitrary HTTPS URLs. items: minLength: 1 pattern: ^[\x21\x23-\x5B\x5D-\x7E]+$ @@ -112,6 +115,33 @@ spec: maxItems: 10 type: array x-kubernetes-list-type: atomic + cimd: + description: CIMD configures Client ID Metadata Document support. + When omitted, CIMD is disabled. + properties: + cacheFallbackTtl: + description: |- + CacheFallbackTTL is the fixed TTL applied to every cached CIMD document. + Cache-Control header parsing is not yet implemented; all entries use this value. + Format: Go duration string (e.g. "5m", "10m", "1h"). + Defaults to 5 minutes when Enabled is true and this field is omitted. + pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string + cacheMaxSize: + description: |- + CacheMaxSize is the maximum number of CIMD documents held in the LRU cache. + Defaults to 256 when Enabled is true and this field is omitted. + minimum: 1 + type: integer + enabled: + default: false + description: |- + Enabled activates CIMD client lookup. When false (the default), the AS only + accepts client_id values that were registered via DCR. + type: boolean + required: + - enabled + type: object hmacSecretRefs: description: |- HMACSecretRefs references Kubernetes Secrets containing symmetric secrets for signing @@ -3029,6 +3059,9 @@ spec: "openid" and "offline_access"). Adding a privileged scope here — e.g. "admin:read" — would grant it to every DCR-registered client, including public clients like Claude Code, Cursor, and VS Code. + When cimd.enabled is true, every dynamically resolved CIMD client will + also gain the ability to request these scopes, including third-party + clients resolved from arbitrary HTTPS URLs. items: minLength: 1 pattern: ^[\x21\x23-\x5B\x5D-\x7E]+$ @@ -3036,6 +3069,33 @@ spec: maxItems: 10 type: array x-kubernetes-list-type: atomic + cimd: + description: CIMD configures Client ID Metadata Document support. + When omitted, CIMD is disabled. + properties: + cacheFallbackTtl: + description: |- + CacheFallbackTTL is the fixed TTL applied to every cached CIMD document. + Cache-Control header parsing is not yet implemented; all entries use this value. + Format: Go duration string (e.g. "5m", "10m", "1h"). + Defaults to 5 minutes when Enabled is true and this field is omitted. + pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string + cacheMaxSize: + description: |- + CacheMaxSize is the maximum number of CIMD documents held in the LRU cache. + Defaults to 256 when Enabled is true and this field is omitted. + minimum: 1 + type: integer + enabled: + default: false + description: |- + Enabled activates CIMD client lookup. When false (the default), the AS only + accepts client_id values that were registered via DCR. + type: boolean + required: + - enabled + type: object hmacSecretRefs: description: |- HMACSecretRefs references Kubernetes Secrets containing symmetric secrets for signing diff --git a/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml b/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml index 75c8fe5b7c..53946029af 100644 --- a/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml +++ b/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml @@ -235,6 +235,9 @@ spec: "openid" and "offline_access"). Adding a privileged scope here — e.g. "admin:read" — would grant it to every DCR-registered client, including public clients like Claude Code, Cursor, and VS Code. + When cimd.enabled is true, every dynamically resolved CIMD client will + also gain the ability to request these scopes, including third-party + clients resolved from arbitrary HTTPS URLs. items: minLength: 1 pattern: ^[\x21\x23-\x5B\x5D-\x7E]+$ @@ -242,6 +245,33 @@ spec: maxItems: 10 type: array x-kubernetes-list-type: atomic + cimd: + description: CIMD configures Client ID Metadata Document support. + When omitted, CIMD is disabled. + properties: + cacheFallbackTtl: + description: |- + CacheFallbackTTL is the fixed TTL applied to every cached CIMD document. + Cache-Control header parsing is not yet implemented; all entries use this value. + Format: Go duration string (e.g. "5m", "10m", "1h"). + Defaults to 5 minutes when Enabled is true and this field is omitted. + pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string + cacheMaxSize: + description: |- + CacheMaxSize is the maximum number of CIMD documents held in the LRU cache. + Defaults to 256 when Enabled is true and this field is omitted. + minimum: 1 + type: integer + enabled: + default: false + description: |- + Enabled activates CIMD client lookup. When false (the default), the AS only + accepts client_id values that were registered via DCR. + type: boolean + required: + - enabled + type: object hmacSecretRefs: description: |- HMACSecretRefs references Kubernetes Secrets containing symmetric secrets for signing @@ -1514,6 +1544,9 @@ spec: "openid" and "offline_access"). Adding a privileged scope here — e.g. "admin:read" — would grant it to every DCR-registered client, including public clients like Claude Code, Cursor, and VS Code. + When cimd.enabled is true, every dynamically resolved CIMD client will + also gain the ability to request these scopes, including third-party + clients resolved from arbitrary HTTPS URLs. items: minLength: 1 pattern: ^[\x21\x23-\x5B\x5D-\x7E]+$ @@ -1521,6 +1554,33 @@ spec: maxItems: 10 type: array x-kubernetes-list-type: atomic + cimd: + description: CIMD configures Client ID Metadata Document support. + When omitted, CIMD is disabled. + properties: + cacheFallbackTtl: + description: |- + CacheFallbackTTL is the fixed TTL applied to every cached CIMD document. + Cache-Control header parsing is not yet implemented; all entries use this value. + Format: Go duration string (e.g. "5m", "10m", "1h"). + Defaults to 5 minutes when Enabled is true and this field is omitted. + pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string + cacheMaxSize: + description: |- + CacheMaxSize is the maximum number of CIMD documents held in the LRU cache. + Defaults to 256 when Enabled is true and this field is omitted. + minimum: 1 + type: integer + enabled: + default: false + description: |- + Enabled activates CIMD client lookup. When false (the default), the AS only + accepts client_id values that were registered via DCR. + type: boolean + required: + - enabled + type: object hmacSecretRefs: description: |- HMACSecretRefs references Kubernetes Secrets containing symmetric secrets for signing 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 3f808265de..0b936aff74 100644 --- a/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_virtualmcpservers.yaml +++ b/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_virtualmcpservers.yaml @@ -108,6 +108,9 @@ spec: "openid" and "offline_access"). Adding a privileged scope here — e.g. "admin:read" — would grant it to every DCR-registered client, including public clients like Claude Code, Cursor, and VS Code. + When cimd.enabled is true, every dynamically resolved CIMD client will + also gain the ability to request these scopes, including third-party + clients resolved from arbitrary HTTPS URLs. items: minLength: 1 pattern: ^[\x21\x23-\x5B\x5D-\x7E]+$ @@ -115,6 +118,33 @@ spec: maxItems: 10 type: array x-kubernetes-list-type: atomic + cimd: + description: CIMD configures Client ID Metadata Document support. + When omitted, CIMD is disabled. + properties: + cacheFallbackTtl: + description: |- + CacheFallbackTTL is the fixed TTL applied to every cached CIMD document. + Cache-Control header parsing is not yet implemented; all entries use this value. + Format: Go duration string (e.g. "5m", "10m", "1h"). + Defaults to 5 minutes when Enabled is true and this field is omitted. + pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string + cacheMaxSize: + description: |- + CacheMaxSize is the maximum number of CIMD documents held in the LRU cache. + Defaults to 256 when Enabled is true and this field is omitted. + minimum: 1 + type: integer + enabled: + default: false + description: |- + Enabled activates CIMD client lookup. When false (the default), the AS only + accepts client_id values that were registered via DCR. + type: boolean + required: + - enabled + type: object hmacSecretRefs: description: |- HMACSecretRefs references Kubernetes Secrets containing symmetric secrets for signing @@ -3032,6 +3062,9 @@ spec: "openid" and "offline_access"). Adding a privileged scope here — e.g. "admin:read" — would grant it to every DCR-registered client, including public clients like Claude Code, Cursor, and VS Code. + When cimd.enabled is true, every dynamically resolved CIMD client will + also gain the ability to request these scopes, including third-party + clients resolved from arbitrary HTTPS URLs. items: minLength: 1 pattern: ^[\x21\x23-\x5B\x5D-\x7E]+$ @@ -3039,6 +3072,33 @@ spec: maxItems: 10 type: array x-kubernetes-list-type: atomic + cimd: + description: CIMD configures Client ID Metadata Document support. + When omitted, CIMD is disabled. + properties: + cacheFallbackTtl: + description: |- + CacheFallbackTTL is the fixed TTL applied to every cached CIMD document. + Cache-Control header parsing is not yet implemented; all entries use this value. + Format: Go duration string (e.g. "5m", "10m", "1h"). + Defaults to 5 minutes when Enabled is true and this field is omitted. + pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string + cacheMaxSize: + description: |- + CacheMaxSize is the maximum number of CIMD documents held in the LRU cache. + Defaults to 256 when Enabled is true and this field is omitted. + minimum: 1 + type: integer + enabled: + default: false + description: |- + Enabled activates CIMD client lookup. When false (the default), the AS only + accepts client_id values that were registered via DCR. + type: boolean + required: + - enabled + type: object hmacSecretRefs: description: |- HMACSecretRefs references Kubernetes Secrets containing symmetric secrets for signing diff --git a/docs/operator/crd-api.md b/docs/operator/crd-api.md index b8f04ff351..b27fd00c1d 100644 --- a/docs/operator/crd-api.md +++ b/docs/operator/crd-api.md @@ -1184,6 +1184,27 @@ _Appears in:_ +#### api.v1beta1.EmbeddedAuthServerCIMDConfig + + + +EmbeddedAuthServerCIMDConfig configures Client ID Metadata Document (CIMD) support +on the embedded authorization server. When enabled, the AS accepts HTTPS URLs as +client_id values and resolves them via the CIMD protocol, allowing clients such as +VS Code to authenticate without prior Dynamic Client Registration. + + + +_Appears in:_ +- [api.v1beta1.EmbeddedAuthServerConfig](#apiv1beta1embeddedauthserverconfig) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `enabled` _boolean_ | Enabled activates CIMD client lookup. When false (the default), the AS only
accepts client_id values that were registered via DCR. | false | | +| `cacheMaxSize` _integer_ | CacheMaxSize is the maximum number of CIMD documents held in the LRU cache.
Defaults to 256 when Enabled is true and this field is omitted. | | Minimum: 1
Optional: \{\}
| +| `cacheFallbackTtl` _string_ | CacheFallbackTTL is the fixed TTL applied to every cached CIMD document.
Cache-Control header parsing is not yet implemented; all entries use this value.
Format: Go duration string (e.g. "5m", "10m", "1h").
Defaults to 5 minutes when Enabled is true and this field is omitted. | | Pattern: `^([0-9]+(\.[0-9]+)?(ns\|us\|µs\|ms\|s\|m\|h))+$`
Optional: \{\}
| + + #### api.v1beta1.EmbeddedAuthServerConfig @@ -1207,7 +1228,8 @@ _Appears in:_ | `upstreamProviders` _[api.v1beta1.UpstreamProviderConfig](#apiv1beta1upstreamproviderconfig) array_ | UpstreamProviders configures connections to upstream Identity Providers.
The embedded auth server delegates authentication to these providers.
MCPServer and MCPRemoteProxy support a single upstream; VirtualMCPServer supports multiple. | | MinItems: 1
Required: \{\}
| | `primaryUpstreamProvider` _string_ | PrimaryUpstreamProvider names the upstream IDP whose access token Cedar
should read claims from when authorising a request. Must match the name
of one of the entries in UpstreamProviders. When empty, the controller
auto-selects the first entry of UpstreamProviders.
Only meaningful on VirtualMCPServer, where multiple upstream providers
can be configured and Cedar needs to pick which token's claims to
evaluate. The VirtualMCPServer controller validates this field against
UpstreamProviders at admission and rejects unresolvable values.
On MCPServer and MCPRemoteProxy this field is structurally present (the
EmbeddedAuthServerConfig struct is shared) but has no runtime effect:
those CRDs are restricted to a single upstream so there is no choice to
make. Setting it on those CRDs is silently ignored. | | MaxLength: 63
MinLength: 1
Pattern: `^[a-z0-9]([a-z0-9-]*[a-z0-9])?$`
Optional: \{\}
| | `storage` _[api.v1beta1.AuthServerStorageConfig](#apiv1beta1authserverstorageconfig)_ | Storage configures the storage backend for the embedded auth server.
If not specified, defaults to in-memory storage. | | Optional: \{\}
| -| `baselineClientScopes` _string array_ | BaselineClientScopes is a baseline set of OAuth 2.0 scopes guaranteed to be
included in every client registration. The embedded auth server unions these
scopes into the registered set returned by RFC 7591 Dynamic Client
Registration, so a client that narrows the `scope` field at /oauth/register
can still request the baseline scopes at /oauth/authorize. All values must
be present in the upstream-derived scopesSupported set; the auth server
fails to start if any value is missing.
Security: every client registered via /oauth/register will gain the
ability to request these scopes at /oauth/authorize, regardless of what
the client itself requested. Keep the baseline narrow (typically
"openid" and "offline_access"). Adding a privileged scope here — e.g.
"admin:read" — would grant it to every DCR-registered client, including
public clients like Claude Code, Cursor, and VS Code. | | MaxItems: 10
items:MinLength: 1
items:Pattern: `^[\x21\x23-\x5B\x5D-\x7E]+$`
Optional: \{\}
| +| `baselineClientScopes` _string array_ | BaselineClientScopes is a baseline set of OAuth 2.0 scopes guaranteed to be
included in every client registration. The embedded auth server unions these
scopes into the registered set returned by RFC 7591 Dynamic Client
Registration, so a client that narrows the `scope` field at /oauth/register
can still request the baseline scopes at /oauth/authorize. All values must
be present in the upstream-derived scopesSupported set; the auth server
fails to start if any value is missing.
Security: every client registered via /oauth/register will gain the
ability to request these scopes at /oauth/authorize, regardless of what
the client itself requested. Keep the baseline narrow (typically
"openid" and "offline_access"). Adding a privileged scope here — e.g.
"admin:read" — would grant it to every DCR-registered client, including
public clients like Claude Code, Cursor, and VS Code.
When cimd.enabled is true, every dynamically resolved CIMD client will
also gain the ability to request these scopes, including third-party
clients resolved from arbitrary HTTPS URLs. | | MaxItems: 10
items:MinLength: 1
items:Pattern: `^[\x21\x23-\x5B\x5D-\x7E]+$`
Optional: \{\}
| +| `cimd` _[api.v1beta1.EmbeddedAuthServerCIMDConfig](#apiv1beta1embeddedauthservercimdconfig)_ | CIMD configures Client ID Metadata Document support. When omitted, CIMD is disabled. | | Optional: \{\}
| #### api.v1beta1.EmbeddingResourceOverrides diff --git a/pkg/authserver/config.go b/pkg/authserver/config.go index 43bc2c2116..b5b3bc8634 100644 --- a/pkg/authserver/config.go +++ b/pkg/authserver/config.go @@ -129,10 +129,6 @@ func (c *RunConfig) validateBaselineClientScopes() error { } // CIMDRunConfig controls client_id metadata document (CIMD) support. -// -// TODO(cimd): expose these fields in the MCPExternalAuthConfig CRD so Kubernetes -// operators can configure CIMD through the normal CRD workflow instead of -// writing RunConfig YAML directly. type CIMDRunConfig struct { // Enabled activates CIMD client lookup when true. Enabled bool `json:"enabled" yaml:"enabled"` @@ -161,8 +157,8 @@ func (c *CIMDRunConfig) Validate() error { if err != nil { return fmt.Errorf("cache_fallback_ttl: %w", err) } - if d < 0 { - return fmt.Errorf("cache_fallback_ttl must be non-negative when CIMD is enabled, got %s", c.CacheFallbackTTL) + if d <= 0 { + return fmt.Errorf("cache_fallback_ttl must be positive when CIMD is enabled, got %s", c.CacheFallbackTTL) } } return nil