Skip to content

Commit e27ecdf

Browse files
amirejazclaude
andcommitted
Expose CIMD config in MCPExternalAuthConfig CRD
Adds EmbeddedAuthServerCIMDConfig to the CRD so operators can enable CIMD through the normal VirtualMCPServer manifest workflow instead of writing runconfig.json directly. Resolves the TODO(cimd) comment in pkg/authserver/config.go. The new cimd field on EmbeddedAuthServerConfig maps to authserver.CIMDRunConfig in the generated RunConfig. CacheFallbackTTL is stored as a Go duration string in the CRD (e.g. "5m") and parsed to time.Duration by the converter. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent 3fa50fc commit e27ecdf

9 files changed

Lines changed: 407 additions & 4 deletions

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,10 @@ type EmbeddedAuthServerConfig struct {
299299
// +listType=atomic
300300
// +optional
301301
BaselineClientScopes []string `json:"baselineClientScopes,omitempty"`
302+
303+
// CIMD configures Client ID Metadata Document support. When omitted, CIMD is disabled.
304+
// +optional
305+
CIMD *EmbeddedAuthServerCIMDConfig `json:"cimd,omitempty"`
302306
}
303307

304308
// TokenLifespanConfig holds configuration for token lifetimes.
@@ -325,6 +329,31 @@ type TokenLifespanConfig struct {
325329
AuthCodeLifespan string `json:"authCodeLifespan,omitempty"`
326330
}
327331

332+
// EmbeddedAuthServerCIMDConfig configures Client ID Metadata Document (CIMD) support
333+
// on the embedded authorization server. When enabled, the AS accepts HTTPS URLs as
334+
// client_id values and resolves them via the CIMD protocol, allowing clients such as
335+
// VS Code to authenticate without prior Dynamic Client Registration.
336+
type EmbeddedAuthServerCIMDConfig struct {
337+
// Enabled activates CIMD client lookup. When false (the default), the AS only
338+
// accepts client_id values that were registered via DCR.
339+
// +kubebuilder:default=false
340+
Enabled bool `json:"enabled"`
341+
342+
// CacheMaxSize is the maximum number of CIMD documents held in the LRU cache.
343+
// Defaults to 256 when Enabled is true and this field is omitted.
344+
// +kubebuilder:validation:Minimum=1
345+
// +optional
346+
CacheMaxSize int `json:"cacheMaxSize,omitempty"`
347+
348+
// CacheFallbackTTL is the fixed TTL applied to every cached CIMD document.
349+
// Cache-Control header parsing is not yet implemented; all entries use this value.
350+
// Format: Go duration string (e.g. "5m", "10m", "1h").
351+
// Defaults to 5 minutes when Enabled is true and this field is omitted.
352+
// +kubebuilder:validation:Pattern=`^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$`
353+
// +optional
354+
CacheFallbackTTL string `json:"cacheFallbackTtl,omitempty"`
355+
}
356+
328357
// UpstreamProviderType identifies the type of upstream Identity Provider.
329358
type UpstreamProviderType string
330359

cmd/thv-operator/api/v1beta1/zz_generated.deepcopy.go

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"context"
88
"fmt"
99
"strings"
10+
"time"
1011

1112
corev1 "k8s.io/api/core/v1"
1213
apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -582,6 +583,22 @@ func BuildAuthServerRunConfig(
582583
}
583584
config.Storage = storageCfg
584585

586+
// Build CIMD configuration
587+
if authConfig.CIMD != nil && authConfig.CIMD.Enabled {
588+
cimdCfg := &authserver.CIMDRunConfig{
589+
Enabled: authConfig.CIMD.Enabled,
590+
CacheMaxSize: authConfig.CIMD.CacheMaxSize,
591+
}
592+
if authConfig.CIMD.CacheFallbackTTL != "" {
593+
ttl, err := time.ParseDuration(authConfig.CIMD.CacheFallbackTTL)
594+
if err != nil {
595+
return nil, fmt.Errorf("cimd.cacheFallbackTtl: %w", err)
596+
}
597+
cimdCfg.CacheFallbackTTL = ttl
598+
}
599+
config.CIMD = cimdCfg
600+
}
601+
585602
return config, nil
586603
}
587604

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

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"strings"
1010
"testing"
11+
"time"
1112

1213
"github.com/stretchr/testify/assert"
1314
"github.com/stretchr/testify/require"
@@ -2645,3 +2646,127 @@ func TestValidateAndAddAuthServerRefOptions(t *testing.T) {
26452646
})
26462647
}
26472648
}
2649+
2650+
// TestBuildAuthServerRunConfig_CIMD verifies that BuildAuthServerRunConfig
2651+
// correctly converts the CRD EmbeddedAuthServerCIMDConfig into
2652+
// authserver.CIMDRunConfig. The four cases cover the nil path (CIMD off
2653+
// by default), explicit values (fields are mapped and TTL is parsed), zero
2654+
// optional fields (authserver applies its own defaults at startup), and an
2655+
// invalid TTL string (returns a parse error).
2656+
func TestBuildAuthServerRunConfig_CIMD(t *testing.T) {
2657+
t.Parallel()
2658+
2659+
// baseAuthConfig returns a minimal EmbeddedAuthServerConfig that is valid
2660+
// enough for BuildAuthServerRunConfig to proceed past signing-key and
2661+
// upstream validation without requiring real secrets.
2662+
baseAuthConfig := func(cimd *mcpv1beta1.EmbeddedAuthServerCIMDConfig) *mcpv1beta1.EmbeddedAuthServerConfig {
2663+
return &mcpv1beta1.EmbeddedAuthServerConfig{
2664+
Issuer: "https://auth.example.com",
2665+
SigningKeySecretRefs: []mcpv1beta1.SecretKeyRef{
2666+
{Name: "signing-key", Key: "private.pem"},
2667+
},
2668+
HMACSecretRefs: []mcpv1beta1.SecretKeyRef{
2669+
{Name: "hmac-secret", Key: "hmac"},
2670+
},
2671+
CIMD: cimd,
2672+
}
2673+
}
2674+
2675+
defaultAudiences := []string{"https://mcp.example.com"}
2676+
defaultScopes := []string{"openid", "offline_access"}
2677+
2678+
tests := []struct {
2679+
name string
2680+
cimd *mcpv1beta1.EmbeddedAuthServerCIMDConfig
2681+
wantCIMD bool
2682+
wantErr bool
2683+
errContains string
2684+
checkFunc func(t *testing.T, got *authserver.CIMDRunConfig)
2685+
}{
2686+
{
2687+
name: "nil CIMD leaves config.CIMD nil",
2688+
cimd: nil,
2689+
wantCIMD: false,
2690+
},
2691+
{
2692+
name: "CIMD disabled leaves config.CIMD nil",
2693+
cimd: &mcpv1beta1.EmbeddedAuthServerCIMDConfig{
2694+
Enabled: false,
2695+
CacheMaxSize: 100,
2696+
CacheFallbackTTL: "10m",
2697+
},
2698+
wantCIMD: false,
2699+
},
2700+
{
2701+
name: "CIMD enabled with explicit values maps all fields",
2702+
cimd: &mcpv1beta1.EmbeddedAuthServerCIMDConfig{
2703+
Enabled: true,
2704+
CacheMaxSize: 512,
2705+
CacheFallbackTTL: "10m",
2706+
},
2707+
wantCIMD: true,
2708+
checkFunc: func(t *testing.T, got *authserver.CIMDRunConfig) {
2709+
t.Helper()
2710+
assert.True(t, got.Enabled)
2711+
assert.Equal(t, 512, got.CacheMaxSize)
2712+
assert.Equal(t, 10*time.Minute, got.CacheFallbackTTL)
2713+
},
2714+
},
2715+
{
2716+
name: "CIMD enabled with zero optional fields leaves defaults to authserver",
2717+
cimd: &mcpv1beta1.EmbeddedAuthServerCIMDConfig{
2718+
Enabled: true,
2719+
},
2720+
wantCIMD: true,
2721+
checkFunc: func(t *testing.T, got *authserver.CIMDRunConfig) {
2722+
t.Helper()
2723+
assert.True(t, got.Enabled)
2724+
assert.Zero(t, got.CacheMaxSize, "zero means authserver applies its own default at startup")
2725+
assert.Zero(t, got.CacheFallbackTTL, "zero means authserver applies its own default at startup")
2726+
},
2727+
},
2728+
{
2729+
name: "invalid CacheFallbackTTL returns parse error",
2730+
cimd: &mcpv1beta1.EmbeddedAuthServerCIMDConfig{
2731+
Enabled: true,
2732+
CacheFallbackTTL: "not-a-duration",
2733+
},
2734+
wantErr: true,
2735+
errContains: "cimd.cacheFallbackTtl",
2736+
},
2737+
}
2738+
2739+
for _, tt := range tests {
2740+
t.Run(tt.name, func(t *testing.T) {
2741+
t.Parallel()
2742+
2743+
cfg, err := BuildAuthServerRunConfig(
2744+
"default", "test-server",
2745+
baseAuthConfig(tt.cimd),
2746+
defaultAudiences, defaultScopes,
2747+
"https://mcp.example.com",
2748+
)
2749+
2750+
if tt.wantErr {
2751+
require.Error(t, err)
2752+
if tt.errContains != "" {
2753+
assert.Contains(t, err.Error(), tt.errContains)
2754+
}
2755+
return
2756+
}
2757+
2758+
require.NoError(t, err)
2759+
require.NotNil(t, cfg)
2760+
2761+
if !tt.wantCIMD {
2762+
assert.Nil(t, cfg.CIMD, "expected config.CIMD to be nil")
2763+
return
2764+
}
2765+
2766+
require.NotNil(t, cfg.CIMD, "expected config.CIMD to be set")
2767+
if tt.checkFunc != nil {
2768+
tt.checkFunc(t, cfg.CIMD)
2769+
}
2770+
})
2771+
}
2772+
}

deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,33 @@ spec:
237237
maxItems: 10
238238
type: array
239239
x-kubernetes-list-type: atomic
240+
cimd:
241+
description: CIMD configures Client ID Metadata Document support.
242+
When omitted, CIMD is disabled.
243+
properties:
244+
cacheFallbackTtl:
245+
description: |-
246+
CacheFallbackTTL is the fixed TTL applied to every cached CIMD document.
247+
Cache-Control header parsing is not yet implemented; all entries use this value.
248+
Format: Go duration string (e.g. "5m", "10m", "1h").
249+
Defaults to 5 minutes when Enabled is true and this field is omitted.
250+
pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$
251+
type: string
252+
cacheMaxSize:
253+
description: |-
254+
CacheMaxSize is the maximum number of CIMD documents held in the LRU cache.
255+
Defaults to 256 when Enabled is true and this field is omitted.
256+
minimum: 1
257+
type: integer
258+
enabled:
259+
default: false
260+
description: |-
261+
Enabled activates CIMD client lookup. When false (the default), the AS only
262+
accepts client_id values that were registered via DCR.
263+
type: boolean
264+
required:
265+
- enabled
266+
type: object
240267
hmacSecretRefs:
241268
description: |-
242269
HMACSecretRefs references Kubernetes Secrets containing symmetric secrets for signing
@@ -1516,6 +1543,33 @@ spec:
15161543
maxItems: 10
15171544
type: array
15181545
x-kubernetes-list-type: atomic
1546+
cimd:
1547+
description: CIMD configures Client ID Metadata Document support.
1548+
When omitted, CIMD is disabled.
1549+
properties:
1550+
cacheFallbackTtl:
1551+
description: |-
1552+
CacheFallbackTTL is the fixed TTL applied to every cached CIMD document.
1553+
Cache-Control header parsing is not yet implemented; all entries use this value.
1554+
Format: Go duration string (e.g. "5m", "10m", "1h").
1555+
Defaults to 5 minutes when Enabled is true and this field is omitted.
1556+
pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$
1557+
type: string
1558+
cacheMaxSize:
1559+
description: |-
1560+
CacheMaxSize is the maximum number of CIMD documents held in the LRU cache.
1561+
Defaults to 256 when Enabled is true and this field is omitted.
1562+
minimum: 1
1563+
type: integer
1564+
enabled:
1565+
default: false
1566+
description: |-
1567+
Enabled activates CIMD client lookup. When false (the default), the AS only
1568+
accepts client_id values that were registered via DCR.
1569+
type: boolean
1570+
required:
1571+
- enabled
1572+
type: object
15191573
hmacSecretRefs:
15201574
description: |-
15211575
HMACSecretRefs references Kubernetes Secrets containing symmetric secrets for signing

deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_virtualmcpservers.yaml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,33 @@ spec:
110110
maxItems: 10
111111
type: array
112112
x-kubernetes-list-type: atomic
113+
cimd:
114+
description: CIMD configures Client ID Metadata Document support.
115+
When omitted, CIMD is disabled.
116+
properties:
117+
cacheFallbackTtl:
118+
description: |-
119+
CacheFallbackTTL is the fixed TTL applied to every cached CIMD document.
120+
Cache-Control header parsing is not yet implemented; all entries use this value.
121+
Format: Go duration string (e.g. "5m", "10m", "1h").
122+
Defaults to 5 minutes when Enabled is true and this field is omitted.
123+
pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$
124+
type: string
125+
cacheMaxSize:
126+
description: |-
127+
CacheMaxSize is the maximum number of CIMD documents held in the LRU cache.
128+
Defaults to 256 when Enabled is true and this field is omitted.
129+
minimum: 1
130+
type: integer
131+
enabled:
132+
default: false
133+
description: |-
134+
Enabled activates CIMD client lookup. When false (the default), the AS only
135+
accepts client_id values that were registered via DCR.
136+
type: boolean
137+
required:
138+
- enabled
139+
type: object
113140
hmacSecretRefs:
114141
description: |-
115142
HMACSecretRefs references Kubernetes Secrets containing symmetric secrets for signing
@@ -3034,6 +3061,33 @@ spec:
30343061
maxItems: 10
30353062
type: array
30363063
x-kubernetes-list-type: atomic
3064+
cimd:
3065+
description: CIMD configures Client ID Metadata Document support.
3066+
When omitted, CIMD is disabled.
3067+
properties:
3068+
cacheFallbackTtl:
3069+
description: |-
3070+
CacheFallbackTTL is the fixed TTL applied to every cached CIMD document.
3071+
Cache-Control header parsing is not yet implemented; all entries use this value.
3072+
Format: Go duration string (e.g. "5m", "10m", "1h").
3073+
Defaults to 5 minutes when Enabled is true and this field is omitted.
3074+
pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$
3075+
type: string
3076+
cacheMaxSize:
3077+
description: |-
3078+
CacheMaxSize is the maximum number of CIMD documents held in the LRU cache.
3079+
Defaults to 256 when Enabled is true and this field is omitted.
3080+
minimum: 1
3081+
type: integer
3082+
enabled:
3083+
default: false
3084+
description: |-
3085+
Enabled activates CIMD client lookup. When false (the default), the AS only
3086+
accepts client_id values that were registered via DCR.
3087+
type: boolean
3088+
required:
3089+
- enabled
3090+
type: object
30373091
hmacSecretRefs:
30383092
description: |-
30393093
HMACSecretRefs references Kubernetes Secrets containing symmetric secrets for signing

0 commit comments

Comments
 (0)