mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-05 22:44:17 +00:00
17e3f8ef62
Two related lock-free snapshot refactors addressing the remaining
post-v1.0.16 code-review findings.
1. refreshAttemptTracker: per-field atomic.Load/Store -> atomic.Value
snapshot of *attemptState (refresh_coordinator.go).
Previously each tracker held five independently-atomic fields. The
cooldown-exit reset wrote cooldownEndNano = 0 first, then separately
stored attempts = 1 and windowStartNano = now. A concurrent
isInCooldown call could observe cooldownEndNano = 0 (reset just
completed) with attempts still at MaxRefreshAttempts, immediately
triggering a fresh cooldown — a benign double-trigger race that
nonetheless meant the state machine had observable intermediate
states.
New design: state is a *attemptState (immutable) published via
atomic.Value. All transitions (record/success/failure/window-reset/
cooldown-enter/cooldown-exit) go through mutateState, which runs a
CAS loop: load current snapshot -> construct fresh snapshot ->
CompareAndSwap. Either the entire new state publishes or none of
it does — no intermediate visibility, no cross-field race.
Under Yaegi this collapses 3-5 per-field atomic dispatches into one
atomic.Value.Load on the read path. Write paths pay an extra
allocation for the new snapshot but avoid the cross-field hazard.
2. MetadataSnapshot: hot-path readers use atomic.Value instead of
metadataMu.RLock (middleware.go, types.go, main.go, utilities.go).
middleware.ServeHTTP previously took metadataMu.RLock on every
non-bypass request to read the single field issuerURL. Under Yaegi
each RLock acquisition costs 1-5ms of interpreter dispatch.
updateMetadataEndpoints now also publishes an immutable
*MetadataSnapshot via atomic.Value; the hot-path reader loads it
in one op via t.metadataSnap(). Falls back to the legacy
metadataMu.RLock pattern when the snapshot is unpublished (some
test setups initialize the struct fields directly without going
through updateMetadataEndpoints).
Less-frequent callers (helpers, logout, token_introspection) still
take metadataMu.RLock and are unchanged. The snapshot strictly
subsets the metadataMu-protected fields, so those readers see
identical data.
Note on atomic.Pointer[T]: this would have been the cleaner type but
yaegi v0.16.1's stdlib (used by traefik:v3.7.1) exposes only the
legacy unsafe.Pointer-based atomic primitives — no generic Pointer[T].
atomic.Value provides the same semantics via interface{} + type assert.
All tests pass with -race; golangci-lint clean.
201 lines
8.2 KiB
Go
201 lines
8.2 KiB
Go
// Package traefikoidc provides OIDC authentication middleware for Traefik.
|
|
package traefikoidc
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"sync"
|
|
"sync/atomic"
|
|
"text/template"
|
|
"time"
|
|
|
|
"golang.org/x/time/rate"
|
|
)
|
|
|
|
// CacheInterface defines the common cache operations
|
|
type CacheInterface interface {
|
|
Set(key string, value any, ttl time.Duration)
|
|
Get(key string) (any, bool)
|
|
Delete(key string)
|
|
SetMaxSize(size int)
|
|
Size() int
|
|
Clear()
|
|
Cleanup()
|
|
Close()
|
|
GetStats() map[string]any // For testing and monitoring
|
|
}
|
|
|
|
// TokenVerifier interface defines token verification capabilities.
|
|
// Implementations should validate token format, signature, and claims.
|
|
type TokenVerifier interface {
|
|
VerifyToken(token string) error
|
|
}
|
|
|
|
// JWTVerifier interface defines JWT-specific verification capabilities.
|
|
// Implementations should validate JWT structure, signature using JWKs, and standard claims.
|
|
type JWTVerifier interface {
|
|
VerifyJWTSignatureAndClaims(jwt *JWT, token string) error
|
|
}
|
|
|
|
// TokenExchanger interface defines OAuth 2.0 and OpenID Connect token exchange capabilities.
|
|
// Implementations should handle authorization code exchange, refresh tokens, and revocation
|
|
// according to the OAuth 2.0 and OpenID Connect specifications.
|
|
type TokenExchanger interface {
|
|
ExchangeCodeForToken(ctx context.Context, grantType string, codeOrToken string, redirectURL string, codeVerifier string) (*TokenResponse, error)
|
|
GetNewTokenWithRefreshToken(refreshToken string) (*TokenResponse, error)
|
|
RevokeTokenWithProvider(token, tokenType string) error
|
|
}
|
|
|
|
// ProviderMetadata represents OIDC provider configuration data.
|
|
// This data is typically retrieved from the provider's .well-known/openid-configuration endpoint
|
|
// and contains essential URLs for authentication, token exchange, and key retrieval.
|
|
type ProviderMetadata struct {
|
|
Issuer string `json:"issuer"`
|
|
AuthURL string `json:"authorization_endpoint"`
|
|
TokenURL string `json:"token_endpoint"`
|
|
JWKSURL string `json:"jwks_uri"`
|
|
RevokeURL string `json:"revocation_endpoint"`
|
|
EndSessionURL string `json:"end_session_endpoint"`
|
|
IntrospectionURL string `json:"introspection_endpoint,omitempty"`
|
|
RegistrationURL string `json:"registration_endpoint,omitempty"`
|
|
ScopesSupported []string `json:"scopes_supported,omitempty"`
|
|
}
|
|
|
|
// TraefikOidc is the main middleware struct that implements OIDC authentication for Traefik.
|
|
// It integrates with various OIDC providers, manages sessions, caches tokens, and handles
|
|
// the complete authentication flow. It's designed to work seamlessly with Traefik's
|
|
// plugin system and provides flexible configuration options.
|
|
// MetadataSnapshot is an immutable bundle of provider-metadata URLs that the
|
|
// plugin needs on the hot request path. Published atomically via
|
|
// TraefikOidc.metadataSnapshot; readers do exactly one atomic.Value.Load to
|
|
// access all fields. Replaces 3 per-request metadataMu.RLock acquisitions
|
|
// in middleware.ServeHTTP + token_manager paths, each of which paid
|
|
// 1-5ms of Yaegi-dispatch overhead.
|
|
//
|
|
// The fields are a strict subset of the metadataMu-guarded TraefikOidc
|
|
// fields; the legacy fields are still written under metadataMu for
|
|
// less-frequent code paths that have not been migrated.
|
|
type MetadataSnapshot struct {
|
|
IssuerURL string
|
|
JWKSURL string
|
|
TokenURL string
|
|
AuthURL string
|
|
RevocationURL string
|
|
EndSessionURL string
|
|
IntrospectionURL string
|
|
RegistrationURL string
|
|
}
|
|
|
|
type TraefikOidc struct {
|
|
// metadataSnapshot atomically publishes the read-mostly URL bundle.
|
|
// Hot-path readers (middleware.ServeHTTP, token verification) load it
|
|
// directly; less-frequent paths still acquire metadataMu.RLock and
|
|
// read the individual fields below.
|
|
metadataSnapshot atomic.Value
|
|
// lastMetadataRetryNano is the UnixNano timestamp of the last metadata
|
|
// recovery attempt. Stored atomically so the hot ServeHTTP path can
|
|
// throttle retries without acquiring metadataRetryMutex on every request.
|
|
lastMetadataRetryNano int64
|
|
// firstRequestStarted is 0 until the very first non-health request fires
|
|
// the background-task bootstrap; then it flips to 1 via CAS. Replaces the
|
|
// firstRequestMutex + firstRequestReceived combo which previously took
|
|
// a write lock on every non-health request forever.
|
|
firstRequestStarted int32
|
|
// metadataRefreshStartedAtomic is the CAS-only variant of the old
|
|
// metadataRefreshStarted bool. Both flags live under the same atomic so
|
|
// concurrent first-request goroutines race exactly once.
|
|
metadataRefreshStartedAtomic int32
|
|
jwkCache JWKCacheInterface
|
|
jwtVerifier JWTVerifier
|
|
ctx context.Context
|
|
tokenVerifier TokenVerifier
|
|
next http.Handler
|
|
tokenExchanger TokenExchanger
|
|
tokenBlacklist CacheInterface
|
|
tokenTypeCache CacheInterface
|
|
introspectionCache CacheInterface
|
|
initComplete chan struct{}
|
|
limiter *rate.Limiter
|
|
headerTemplates map[string]*template.Template
|
|
sessionManager *SessionManager
|
|
tokenCleanupStopChan chan struct{}
|
|
excludedURLs map[string]struct{}
|
|
extractClaimsFunc func(tokenString string) (map[string]any, error)
|
|
initiateAuthenticationFunc func(rw http.ResponseWriter, req *http.Request, session *SessionData, redirectURL string)
|
|
metadataCache *MetadataCache
|
|
allowedRolesAndGroups map[string]struct{}
|
|
allowedUsers map[string]struct{}
|
|
allowedUserDomains map[string]struct{}
|
|
tokenCache *TokenCache
|
|
httpClient *http.Client
|
|
tokenHTTPClient *http.Client
|
|
logger *Logger
|
|
metadataRefreshStopChan chan struct{}
|
|
cancelFunc context.CancelFunc
|
|
errorRecoveryManager *ErrorRecoveryManager
|
|
tokenResilienceManager *TokenResilienceManager
|
|
refreshCoordinator *RefreshCoordinator
|
|
goroutineWG *sync.WaitGroup
|
|
dcrConfig *DynamicClientRegistrationConfig
|
|
dynamicClientRegistrar *DynamicClientRegistrar
|
|
scopeFilter *ScopeFilter
|
|
securityHeadersApplier func(http.ResponseWriter, *http.Request)
|
|
userIdentifierClaim string
|
|
revocationURL string
|
|
name string
|
|
redirURLPath string
|
|
logoutURLPath string
|
|
tokenURL string
|
|
authURL string
|
|
endSessionURL string
|
|
postLogoutRedirectURI string
|
|
jwksURL string
|
|
issuerURL string
|
|
groupClaimName string
|
|
introspectionURL string
|
|
providerURL string
|
|
roleClaimName string
|
|
audience string
|
|
clientID string
|
|
clientSecret string
|
|
clientAuthMethod string
|
|
clientAssertion *ClientAssertionSigner
|
|
registrationURL string
|
|
backchannelLogoutPath string
|
|
frontchannelLogoutPath string
|
|
scopesSupported []string
|
|
scopes []string
|
|
refreshGracePeriod time.Duration
|
|
maxRefreshTokenAge time.Duration
|
|
metadataMu sync.RWMutex
|
|
shutdownOnce sync.Once
|
|
sessionInvalidationCache CacheInterface
|
|
refreshResultCache CacheInterface
|
|
minimalHeaders bool
|
|
stripAuthCookies bool
|
|
enableBackchannelLogout bool
|
|
enableFrontchannelLogout bool
|
|
requireTokenIntrospection bool
|
|
allowPrivateIPAddresses bool
|
|
disableReplayDetection bool
|
|
allowOpaqueTokens bool
|
|
strictAudienceValidation bool
|
|
overrideScopes bool
|
|
enablePKCE bool
|
|
forceHTTPS bool
|
|
suppressDiagnosticLogs bool
|
|
|
|
// Bearer-auth runtime state (populated only when EnableBearerAuth=true).
|
|
bearerIdentifierClaim string
|
|
bearerFailureTracker *bearerFailureTracker
|
|
maxTokenAge time.Duration
|
|
maxIdentifierLength int
|
|
bearerFailureThreshold int
|
|
bearerFailureWindow time.Duration
|
|
bearerFailurePenalty time.Duration
|
|
enableBearerAuth bool
|
|
stripAuthorizationHeader bool
|
|
bearerEmitWWWAuthenticate bool
|
|
bearerOverridesCookie bool
|
|
}
|