fix(config,dcr): env fallbacks, DCR self-heal, per-source auth gate

- implement documented OIDC_* env fallbacks; hold metadataMu for clientAssertion write
- re-register client when client_secret expires; require secret for confidential
- add opt-in per-source external auth rate limiting (PerSourceLoginRateLimit)
This commit is contained in:
2026-08-14 16:17:24 +01:00
parent 4d23d618f5
commit 4bd061abee
6 changed files with 610 additions and 60 deletions
+95 -42
View File
@@ -7,10 +7,32 @@ import (
// REDACTED is the placeholder value for sensitive information
const REDACTED = "[REDACTED]"
// MarshalJSON implements custom JSON marshaling to redact sensitive fields
// Rewritten without type aliases for yaegi compatibility
func (c Config) MarshalJSON() ([]byte, error) {
// Build a map manually to avoid type alias issues with yaegi
// marshalDCR builds the JSON/YAML-safe representation of the dynamic
// client registration config, redacting the initial access token.
func marshalDCR(d *DynamicClientRegistrationConfig) map[string]interface{} {
if d == nil {
return nil
}
m := make(map[string]interface{})
m["enabled"] = d.Enabled
m["persistCredentials"] = d.PersistCredentials
m["storageBackend"] = d.StorageBackend
m["initialAccessToken"] = REDACTED
m["registrationEndpoint"] = d.RegistrationEndpoint
m["credentialsFile"] = d.CredentialsFile
m["redisKeyPrefix"] = d.RedisKeyPrefix
return m
}
// configToMap builds the JSON/YAML-safe map representation of Config. Both
// MarshalJSON and MarshalYAML share it so the (hand-maintained for yaegi
// compatibility) field/redaction list cannot drift between the two
// encoders — previously the list silently dropped entire config areas
// (dynamicClientRegistration, securityHeaders, audience, overrideScopes,
// enablePKCE, allowOpaqueTokens, requireTokenIntrospection,
// strictAudienceValidation, extraAuthParams, client assertion settings)
// and never redacted their embedded secrets (R150).
func (c Config) configToMap() map[string]interface{} {
result := make(map[string]interface{})
// Copy public fields
@@ -28,11 +50,60 @@ func (c Config) MarshalJSON() ([]byte, error) {
result["allowedUsers"] = c.AllowedUsers
result["allowedRolesAndGroups"] = c.AllowedRolesAndGroups
// Redact sensitive fields
// Fields that the legacy hand-written list dropped (R150): important
// non-secret config that must survive any JSON/YAML round-trip.
result["audience"] = c.Audience
result["cookiePrefix"] = c.CookiePrefix
result["cookieDomain"] = c.CookieDomain
result["userIdentifierClaim"] = c.UserIdentifierClaim
result["groupClaimName"] = c.GroupClaimName
result["roleClaimName"] = c.RoleClaimName
result["oidcEndSessionURL"] = c.OIDCEndSessionURL
result["revocationURL"] = c.RevocationURL
result["introspectionURL"] = c.IntrospectionURL
result["headers"] = c.Headers
result["allowedClaims"] = c.AllowedClaims
result["extraAuthParams"] = c.ExtraAuthParams
result["refreshGracePeriodSeconds"] = c.RefreshGracePeriodSeconds
result["maxRefreshTokenAgeSeconds"] = c.MaxRefreshTokenAgeSeconds
result["sessionMaxAge"] = c.SessionMaxAge
result["overrideScopes"] = c.OverrideScopes
result["disableReplayDetection"] = c.DisableReplayDetection
result["requireTokenIntrospection"] = c.RequireTokenIntrospection
result["allowOpaqueTokens"] = c.AllowOpaqueTokens
result["strictAudienceValidation"] = c.StrictAudienceValidation
result["enablePKCE"] = c.EnablePKCE
result["allowPrivateIPAddresses"] = c.AllowPrivateIPAddresses
result["minimalHeaders"] = c.MinimalHeaders
result["stripAuthCookies"] = c.StripAuthCookies
result["enableBackchannelLogout"] = c.EnableBackchannelLogout
result["enableFrontchannelLogout"] = c.EnableFrontchannelLogout
result["backchannelLogoutURL"] = c.BackchannelLogoutURL
result["frontchannelLogoutURL"] = c.FrontchannelLogoutURL
result["caCertPath"] = c.CACertPath
result["caCertPEM"] = c.CACertPEM
result["insecureSkipVerify"] = c.InsecureSkipVerify
result["clientAuthMethod"] = c.ClientAuthMethod
result["clientAssertionKeyID"] = c.ClientAssertionKeyID
result["clientAssertionAlg"] = c.ClientAssertionAlg
result["enableBearerAuth"] = c.EnableBearerAuth
result["bearerIdentifierClaim"] = c.BearerIdentifierClaim
result["stripAuthorizationHeader"] = c.StripAuthorizationHeader
result["bearerEmitWWWAuthenticate"] = c.BearerEmitWWWAuthenticate
result["bearerOverridesCookie"] = c.BearerOverridesCookie
result["maxTokenAgeSeconds"] = c.MaxTokenAgeSeconds
result["maxIdentifierLength"] = c.MaxIdentifierLength
result["bearerFailureThreshold"] = c.BearerFailureThreshold
result["bearerFailureWindowSeconds"] = c.BearerFailureWindowSeconds
result["bearerFailurePenaltySeconds"] = c.BearerFailurePenaltySeconds
// Redact sensitive fields (existing secrets plus the previously-leaked
// client assertion private key, R150).
result["clientSecret"] = REDACTED
result["sessionEncryptionKey"] = REDACTED
result["clientAssertionPrivateKey"] = REDACTED
// Handle Redis config
// Handle Redis config (redacts password)
if c.Redis != nil {
redisMap := make(map[string]interface{})
redisMap["enabled"] = c.Redis.Enabled
@@ -44,47 +115,29 @@ func (c Config) MarshalJSON() ([]byte, error) {
result["redis"] = redisMap
}
return json.Marshal(result)
// Handle dynamic client registration (redacts the initial access token)
if c.DynamicClientRegistration != nil {
result["dynamicClientRegistration"] = marshalDCR(c.DynamicClientRegistration)
}
// Handle security headers
if c.SecurityHeaders != nil {
result["securityHeaders"] = c.SecurityHeaders
}
return result
}
// MarshalJSON implements custom JSON marshaling to redact sensitive fields
// Rewritten without type aliases for yaegi compatibility
func (c Config) MarshalJSON() ([]byte, error) {
return json.Marshal(c.configToMap())
}
// MarshalYAML implements custom YAML marshaling to redact sensitive fields
// Rewritten without type aliases for yaegi compatibility
func (c Config) MarshalYAML() (interface{}, error) {
// Build a map manually to avoid type alias issues with yaegi
result := make(map[string]interface{})
// Copy public fields
result["providerURL"] = c.ProviderURL
result["clientID"] = c.ClientID
result["callbackURL"] = c.CallbackURL
result["logoutURL"] = c.LogoutURL
result["postLogoutRedirectURI"] = c.PostLogoutRedirectURI
result["scopes"] = c.Scopes
result["forceHTTPS"] = c.ForceHTTPS
result["logLevel"] = c.LogLevel
result["rateLimit"] = c.RateLimit
result["excludedURLs"] = c.ExcludedURLs
result["allowedUserDomains"] = c.AllowedUserDomains
result["allowedUsers"] = c.AllowedUsers
result["allowedRolesAndGroups"] = c.AllowedRolesAndGroups
// Redact sensitive fields
result["clientSecret"] = REDACTED
result["sessionEncryptionKey"] = REDACTED
// Handle Redis config
if c.Redis != nil {
redisMap := make(map[string]interface{})
redisMap["enabled"] = c.Redis.Enabled
redisMap["address"] = c.Redis.Address
redisMap["password"] = REDACTED
redisMap["db"] = c.Redis.DB
redisMap["poolSize"] = c.Redis.PoolSize
redisMap["cacheMode"] = c.Redis.CacheMode
result["redis"] = redisMap
}
return result, nil
return c.configToMap(), nil
}
// MarshalJSON for RedisConfig to redact sensitive fields