// 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 } // AtomicSetIfAbsentCache is an optional capability a CacheInterface // implementation can provide: an atomic check-and-set, in place of a // separate Get followed by a Set. CacheInterface itself is not widened to // require it — most of the CacheInterface test doubles across the suite // have no need for it, and every one of them would otherwise need a new // method — so a caller that wants the atomic primitive type-asserts a // CacheInterface value to this interface instead (see logout.go's // checkAndMarkLogoutJTIProcessed, FIX-17). CacheInterfaceWrapper (backed by // UniversalCache) implements it. type AtomicSetIfAbsentCache interface { // SetIfAbsent stores value under key only if key is not already // present, and reports whether this call performed the store. SetIfAbsent(key string, value any, ttl time.Duration) (bool, error) } // 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 perSourceLimiter *perSourceAuthLimiter // optional per-external-source auth throttle (R185), nil when disabled 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 // clientSecretExpiresAt is the RFC 7591 userinfo client_secret_expires_at // (unix seconds) returned at DCR registration, or 0 when the IdP // grants a non-expiring secret. Guarded by metadataMu like the other // DCR-installed credential fields; used by dcrRegistrationNeeded to // re-register a client whose secret has lapsed (R180). clientSecretExpiresAt int64 // dcrClientAssertionBuilder lazily builds a client-assertion signer // from the static config's key material. Used when DCR provisions a // private_key_jwt client but no signer was built at construction // (because the static ClientAuthMethod was not private_key_jwt); see // R162 / updateMetadataEndpoints. dcrClientAssertionBuilder func() (*ClientAssertionSigner, error) registrationURL string // configRevocationURL, configEndSessionURL, and configIntrospectionURL // hold operator-configured endpoint overrides (Config.RevocationURL, // Config.OIDCEndSessionURL, Config.IntrospectionURL). They are set once at // construction and never mutated afterwards, so they are read lock-free, // the same contract as providerURL, which updateMetadataEndpoints already // reads unlocked. A manual override always wins over discovery; an empty // string means use the discovered value. configRevocationURL string configEndSessionURL string configIntrospectionURL string backchannelLogoutPath string frontchannelLogoutPath string scopesSupported []string scopes []string extraAuthParams map[string]string // resource holds the RFC 8707 resource indicator from Config.Resource // (Config.Validate() already guarantees it is either empty or a // non-empty absolute URI). Sent as the `resource` param on the // authorization request and on both the authorization_code and // refresh_token grants. resource string // explicitAudience mirrors Config.Audience verbatim, with no // resource- or clientID-based defaulting applied. audience (below) does // carry that defaulting, for token-validation purposes; explicitAudience // exists so buildAuthURL can decide whether to send the non-standard // `audience` request parameter without that default leaking onto the // wire. explicitAudience string refreshGracePeriod time.Duration maxRefreshTokenAge time.Duration metadataMu sync.RWMutex dcrMu sync.Mutex shutdownOnce sync.Once sessionInvalidationCache CacheInterface refreshResultCache CacheInterface minimalHeaders bool stripAuthCookies bool enableBackchannelLogout bool enableFrontchannelLogout bool requireTokenIntrospection bool allowPrivateIPAddresses bool // allowUnauthenticatedPreflight mirrors Config.AllowUnauthenticatedPreflight // (default false): gates whether shouldBypassAuth's genuine-preflight // check (middleware.go) bypasses OIDC auth at all. allowUnauthenticatedPreflight bool // streamingRefresh mirrors Config.StreamingRefresh (default false). streamingRefresh bool // allowLoopbackHosts permits loopback/localhost hosts in outbound URL // validation (validateHost). Derived at construction time from a loopback // providerURL (local development), never operator-set directly. Mirrors // the allowLoopback derivation in updateMetadataEndpoints. allowLoopbackHosts 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 }