Skip to content

Commit a48ad9a

Browse files
amirejazclaude
andcommitted
Wire CIMD config through embedded AS and enable storage decorator
Phase 2 PR 3 — config threading and server wiring. Config chain: RunConfig.CIMD → Config.CIMD* → AuthorizationServerParams → AuthorizationServerConfig → discovery handler. Changes: - config.go: add CIMDRunConfig struct and CIMD* fields to Config; defaults (256 entries, 5 min fallback TTL) applied in applyDefaults(); validation (cacheMaxSize >= 1 when enabled) in Validate() - runner/embeddedauthserver.go: add resolveCIMDConfig helper to unpack nullable *CIMDRunConfig; populate Config.CIMD* from RunConfig.CIMD - server/provider.go: add CIMDEnabled to AuthorizationServerParams and AuthorizationServerConfig; wire through NewAuthorizationServerConfig - server_impl.go: wrap storage with CIMDStorageDecorator when enabled (after legacy migration, before createProvider — decorator must be in place before fosite holds a reference to the storage instance); pass CIMDEnabled to AuthorizationServerParams - server/handlers/discovery.go: set ClientIDMetadataDocumentSupported in buildOAuthMetadata() — both OAuth AS and OIDC discovery endpoints advertise CIMD support when enabled CIMD is opt-in (disabled by default) to avoid introducing outbound HTTPS fetching in existing deployments without explicit operator action. Relates to #4825 Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent 223c488 commit a48ad9a

5 files changed

Lines changed: 83 additions & 4 deletions

File tree

pkg/authserver/config.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ type RunConfig struct {
9090
// Storage configures the storage backend for the auth server.
9191
// If nil, defaults to in-memory storage.
9292
Storage *storage.RunConfig `json:"storage,omitempty" yaml:"storage,omitempty"`
93+
94+
// CIMD controls client_id metadata document support. When enabled, the
95+
// embedded authorization server accepts HTTPS URLs as client_id values
96+
// and resolves them via the CIMD protocol instead of requiring DCR.
97+
CIMD *CIMDRunConfig `json:"cimd,omitempty" yaml:"cimd,omitempty"`
9398
}
9499

95100
// Validate checks that the on-disk RunConfig is internally consistent. Called
@@ -118,6 +123,22 @@ func (c *RunConfig) validateBaselineClientScopes() error {
118123
return registration.ValidateScopeSubset(c.BaselineClientScopes, effective, "baseline_client_scopes")
119124
}
120125

126+
// CIMDRunConfig controls client_id metadata document (CIMD) support.
127+
type CIMDRunConfig struct {
128+
// Enabled activates CIMD client lookup when true.
129+
Enabled bool `json:"enabled" yaml:"enabled"`
130+
131+
// CacheMaxSize is the maximum number of CIMD documents held in the LRU cache.
132+
// Defaults to 256 when Enabled is true and this field is zero.
133+
CacheMaxSize int `json:"cache_max_size,omitempty" yaml:"cache_max_size,omitempty"`
134+
135+
// CacheFallbackTTL is how long a cached CIMD document is considered valid when
136+
// the fetched document carries no Cache-Control header.
137+
// Defaults to 5 minutes when Enabled is true and this field is zero.
138+
//nolint:lll // field tags require full JSON+YAML names
139+
CacheFallbackTTL time.Duration `json:"cache_fallback_ttl,omitempty" yaml:"cache_fallback_ttl,omitempty" swaggertype:"string" example:"5m"`
140+
}
141+
121142
// SigningKeyRunConfig configures where to load signing keys from.
122143
// Keys are loaded from PEM-encoded files on disk (typically mounted from secrets).
123144
type SigningKeyRunConfig struct {
@@ -537,6 +558,20 @@ type Config struct {
537558
// When empty, any request with a "resource" parameter will be rejected with
538559
// "invalid_target". Configure this for proper MCP specification compliance.
539560
AllowedAudiences []string
561+
562+
// CIMDEnabled enables the CIMD storage decorator so the authorization server
563+
// accepts HTTPS URLs as client_id values without prior DCR registration.
564+
CIMDEnabled bool
565+
566+
// CIMDCacheMaxSize is the maximum number of CIMD documents held in the LRU
567+
// cache. Zero is replaced by a default (256) in applyDefaults when CIMDEnabled
568+
// is true.
569+
CIMDCacheMaxSize int
570+
571+
// CIMDCacheFallbackTTL is the TTL applied to cached CIMD documents that carry
572+
// no Cache-Control header. Zero is replaced by a default (5 minutes) in
573+
// applyDefaults when CIMDEnabled is true.
574+
CIMDCacheFallbackTTL time.Duration
540575
}
541576

542577
// Validate checks that the Config is valid.
@@ -589,6 +624,10 @@ func (c *Config) Validate() error {
589624
}
590625
}
591626

627+
if c.CIMDEnabled && c.CIMDCacheMaxSize < 1 {
628+
return fmt.Errorf("cimd.cache_max_size must be >= 1 when CIMD is enabled")
629+
}
630+
592631
slog.Debug("authserver config validation passed",
593632
"issuer", c.Issuer,
594633
"upstream_count", len(c.Upstreams),
@@ -819,6 +858,12 @@ func (c *Config) applyDefaults() error {
819858
c.ScopesSupported = registration.DefaultScopes
820859
slog.Debug("applied default scopes_supported", "scopes", c.ScopesSupported)
821860
}
861+
if c.CIMDEnabled && c.CIMDCacheMaxSize == 0 {
862+
c.CIMDCacheMaxSize = 256
863+
}
864+
if c.CIMDEnabled && c.CIMDCacheFallbackTTL == 0 {
865+
c.CIMDCacheFallbackTTL = 5 * time.Minute
866+
}
822867
return nil
823868
}
824869

pkg/authserver/runner/embeddedauthserver.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,8 @@ func newEmbeddedAuthServerWithStorage(
176176
// here once at the boundary lets all downstream stages share by reference
177177
// safely. Cost is negligible — each slice is bounded by validation (≤10
178178
// for BaselineClientScopes, low cardinality in practice for the others).
179+
cimdEnabled, cimdCacheMaxSize, cimdCacheFallbackTTL := resolveCIMDConfig(cfg.CIMD)
180+
179181
resolvedCfg := authserver.Config{
180182
Issuer: cfg.Issuer,
181183
AuthorizationEndpointBaseURL: cfg.AuthorizationEndpointBaseURL,
@@ -188,6 +190,9 @@ func newEmbeddedAuthServerWithStorage(
188190
ScopesSupported: slices.Clone(cfg.ScopesSupported),
189191
BaselineClientScopes: slices.Clone(cfg.BaselineClientScopes),
190192
AllowedAudiences: slices.Clone(cfg.AllowedAudiences),
193+
CIMDEnabled: cimdEnabled,
194+
CIMDCacheMaxSize: cimdCacheMaxSize,
195+
CIMDCacheFallbackTTL: cimdCacheFallbackTTL,
191196
}
192197

193198
// 7. Create the auth server. authserver.New also asserts the DCR
@@ -782,6 +787,15 @@ func convertRedisTLSRunConfig(rc *storage.RedisTLSRunConfig) (*tcredis.TLSConfig
782787
return cfg, nil
783788
}
784789

790+
// resolveCIMDConfig extracts CIMD settings from a CIMDRunConfig.
791+
// Returns zero values when cfg is nil (CIMD disabled).
792+
func resolveCIMDConfig(cfg *authserver.CIMDRunConfig) (enabled bool, cacheMaxSize int, cacheFallbackTTL time.Duration) {
793+
if cfg == nil {
794+
return false, 0, 0
795+
}
796+
return cfg.Enabled, cfg.CacheMaxSize, cfg.CacheFallbackTTL
797+
}
798+
785799
// resolveEnvVar reads a value from the named environment variable.
786800
func resolveEnvVar(envVar string) (string, error) {
787801
if envVar == "" {

pkg/authserver/server/handlers/discovery.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ func (h *Handler) buildOAuthMetadata() sharedobauth.AuthorizationServerMetadata
114114
},
115115
CodeChallengeMethodsSupported: []string{crypto.PKCEChallengeMethodS256},
116116
TokenEndpointAuthMethodsSupported: []string{sharedobauth.TokenEndpointAuthMethodNone},
117+
118+
ClientIDMetadataDocumentSupported: h.config.CIMDEnabled,
117119
}
118120
}
119121

pkg/authserver/server/provider.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ type AuthorizationServerConfig struct {
6767
// AuthorizationEndpointBaseURL overrides the base URL for the authorization_endpoint
6868
// in the discovery document. When empty, defaults to the issuer (AccessTokenIssuer).
6969
AuthorizationEndpointBaseURL string
70+
// CIMDEnabled indicates that the CIMD storage decorator is active. When true,
71+
// the discovery document advertises client_id_metadata_document_supported.
72+
CIMDEnabled bool
7073
}
7174

7275
// Factory is a constructor which is used to create an OAuth2 endpoint handler.
@@ -102,6 +105,9 @@ type AuthorizationServerParams struct {
102105
// AuthorizationEndpointBaseURL overrides the base URL for the authorization_endpoint
103106
// in the discovery document. When empty, defaults to Issuer.
104107
AuthorizationEndpointBaseURL string
108+
// CIMDEnabled indicates that the CIMD storage decorator is active. When true,
109+
// the discovery document advertises client_id_metadata_document_supported.
110+
CIMDEnabled bool
105111
}
106112

107113
// validateIssuerURL validates that the issuer is a valid URL with http or https scheme
@@ -256,6 +262,7 @@ func NewAuthorizationServerConfig(cfg *AuthorizationServerParams) (*Authorizatio
256262
ScopesSupported: cfg.ScopesSupported,
257263
BaselineClientScopes: cfg.BaselineClientScopes,
258264
AuthorizationEndpointBaseURL: cfg.AuthorizationEndpointBaseURL,
265+
CIMDEnabled: cfg.CIMDEnabled,
259266
}, nil
260267
}
261268

pkg/authserver/server_impl.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ func newServer(ctx context.Context, cfg Config, stor storage.Storage, opts ...se
135135
BaselineClientScopes: cfg.BaselineClientScopes,
136136
AllowedAudiences: cfg.AllowedAudiences,
137137
AuthorizationEndpointBaseURL: cfg.AuthorizationEndpointBaseURL,
138+
CIMDEnabled: cfg.CIMDEnabled,
138139
}
139140
authServerConfig, err := oauthserver.NewAuthorizationServerConfig(oauthParams)
140141
if err != nil {
@@ -147,10 +148,6 @@ func newServer(ctx context.Context, cfg Config, stor storage.Storage, opts ...se
147148
"auth_code_lifespan", cfg.AuthCodeLifespan,
148149
)
149150

150-
// Create fosite provider
151-
slog.Debug("creating fosite OAuth2 provider")
152-
fositeProvider := createProvider(authServerConfig, stor)
153-
154151
// Build ordered upstream provider list from all configured upstreams.
155152
upstreams := make([]handlers.NamedUpstream, 0, len(cfg.Upstreams))
156153
for i := range cfg.Upstreams {
@@ -173,6 +170,20 @@ func newServer(ctx context.Context, cfg Config, stor storage.Storage, opts ...se
173170
return nil, err
174171
}
175172

173+
// Wrap storage with the CIMD decorator before constructing the fosite provider
174+
// so that GetClient calls for HTTPS client_id values are intercepted at the
175+
// fosite level (not just the handler level).
176+
if cfg.CIMDEnabled {
177+
stor, err = storage.NewCIMDStorageDecorator(stor, true, cfg.CIMDCacheMaxSize, cfg.CIMDCacheFallbackTTL)
178+
if err != nil {
179+
return nil, fmt.Errorf("failed to initialize CIMD storage decorator: %w", err)
180+
}
181+
}
182+
183+
// Create fosite provider with the (possibly decorated) storage.
184+
slog.Debug("creating fosite OAuth2 provider")
185+
fositeProvider := createProvider(authServerConfig, stor)
186+
176187
handlerInstance, err := handlers.NewHandler(fositeProvider, authServerConfig, stor, upstreams)
177188
if err != nil {
178189
return nil, fmt.Errorf("failed to create handler: %w", err)

0 commit comments

Comments
 (0)