mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-13 04:07:37 +00:00
The SSE and WebSocket bypass does not check the token expiry. A session stays usable for streaming until the session cookie expires, also after the IdP disables the user. The bypass now forwards the configured headers (#162), so this gap also gives the proxy secret to such a session. Add the streamingRefresh option. The default is false. When it is true and the access token has expired, the bypass refreshes the session. It uses the ID token expiry if the access token is opaque. If the IdP rejects the refresh with invalid_grant, the request gets 401. If the IdP cannot be reached, the request is forwarded as before. The bypass waits for the full coordinated refresh. A shorter wait can drop a rotated refresh token, and the next refresh then logs the user out. applyBypassUserHeaders now takes the ResponseWriter, so a refresh can write the new session cookie. Add a yaegi check for the invalid_grant path through the real token endpoint and RefreshCoordinator. Claude-Session: https://claude.ai/code/session_01G1d1n1BrHHHw8dBUkbiBPC
176 lines
6.8 KiB
Go
176 lines
6.8 KiB
Go
package traefikoidc
|
|
|
|
import (
|
|
"encoding/json"
|
|
)
|
|
|
|
// REDACTED is the placeholder value for sensitive information
|
|
const REDACTED = "[REDACTED]"
|
|
|
|
// 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
|
|
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
|
|
|
|
// 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["resource"] = c.Resource
|
|
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["allowUnauthenticatedPreflight"] = c.AllowUnauthenticatedPreflight
|
|
result["streamingRefresh"] = c.StreamingRefresh
|
|
result["minimalHeaders"] = c.MinimalHeaders
|
|
result["stripAuthCookies"] = c.StripAuthCookies
|
|
result["cookiePath"] = c.CookiePath
|
|
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["clientAssertionKeyPath"] = c.ClientAssertionKeyPath
|
|
result["perSourceLoginRateLimit"] = c.PerSourceLoginRateLimit
|
|
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 (redacts password)
|
|
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
|
|
}
|
|
|
|
// 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) {
|
|
return c.configToMap(), nil
|
|
}
|
|
|
|
// MarshalJSON for RedisConfig to redact sensitive fields
|
|
// Rewritten without type aliases for yaegi compatibility
|
|
func (r RedisConfig) MarshalJSON() ([]byte, error) {
|
|
result := make(map[string]interface{})
|
|
result["enabled"] = r.Enabled
|
|
result["address"] = r.Address
|
|
result["password"] = REDACTED
|
|
result["db"] = r.DB
|
|
result["poolSize"] = r.PoolSize
|
|
result["cacheMode"] = r.CacheMode
|
|
|
|
return json.Marshal(result)
|
|
}
|
|
|
|
// MarshalYAML for RedisConfig to redact sensitive fields
|
|
// Rewritten without type aliases for yaegi compatibility
|
|
func (r RedisConfig) MarshalYAML() (interface{}, error) {
|
|
result := make(map[string]interface{})
|
|
result["enabled"] = r.Enabled
|
|
result["address"] = r.Address
|
|
result["password"] = REDACTED
|
|
result["db"] = r.DB
|
|
result["poolSize"] = r.PoolSize
|
|
result["cacheMode"] = r.CacheMode
|
|
|
|
return result, nil
|
|
}
|