mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-12 04:04:26 +00:00
* Multiple fixes - refresh coordinator dedup + memory pressure wire - middleware sse consolidation + timer leak + claim cache - universal cache sync backfill + isDebug gate - lazy background task race - memory monitor stw cached + refresh() api * fix(auth): suppress OIDC redirects on non-navigation requests - [x] Add isNonNavigationRequest using Sec-Fetch-Mode and Accept headers - [x] Add comprehensive TestIsNonNavigationRequest - [x] Update ServeHTTP to 401 non-navigation and AJAX requests Fixes #129 * feat(config): add custom CA and insecure skip verify for OIDC TLS - [x] Add CACertPath, CACertPEM, InsecureSkipVerify to Config - [x] Implement loadCACertPool for CA bundle loading - [x] Update HTTPClientConfig with RootCAs and InsecureSkipVerify - [x] Apply CA pool and skip verify to pooled HTTP clients - [x] Enhance configKey to distinguish TLS configs - [x] Add comprehensive ca_cert_test.go Fixes #125 * feat(oidc): add custom CA certificate support for private OIDC providers - [x] Add caCertPath, caCertPEM, insecureSkipVerify config options - [x] Update traefik.yml with new OIDC client config fields - [x] Add configuration schema descriptions for new options - [x] Update README table and add Custom CA Certificates section * Fix the documentation. * test(redis): add oversized argument rejection test - [x] Add TestRedisConn_RejectOversizedArgumentBytes - [x] Import strings package * Dependencies cleanup
175 lines
4.9 KiB
Go
175 lines
4.9 KiB
Go
package providers
|
|
|
|
import (
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// ProviderRegistry manages a collection of OIDC provider implementations.
|
|
// It provides thread-safe access to provider instances and caches detection results.
|
|
type ProviderRegistry struct {
|
|
cache map[string]OIDCProvider
|
|
typeMap map[ProviderType]OIDCProvider
|
|
providers []OIDCProvider
|
|
mu sync.RWMutex
|
|
// Bounded cache configuration to prevent memory leaks
|
|
maxCacheSize int
|
|
cacheCount int
|
|
}
|
|
|
|
// NewProviderRegistry creates and initializes a new ProviderRegistry.
|
|
func NewProviderRegistry() *ProviderRegistry {
|
|
return &ProviderRegistry{
|
|
providers: make([]OIDCProvider, 0),
|
|
cache: make(map[string]OIDCProvider),
|
|
typeMap: make(map[ProviderType]OIDCProvider),
|
|
maxCacheSize: 1000, // Prevent unbounded cache growth
|
|
cacheCount: 0,
|
|
}
|
|
}
|
|
|
|
// RegisterProvider adds a new provider to the registry.
|
|
// It maintains both a list of providers and a type-to-provider mapping for efficient lookups.
|
|
func (r *ProviderRegistry) RegisterProvider(provider OIDCProvider) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.providers = append(r.providers, provider)
|
|
r.typeMap[provider.GetType()] = provider
|
|
}
|
|
|
|
// GetProviderByType retrieves a provider instance by its type.
|
|
// Returns nil if the provider type is not registered.
|
|
func (r *ProviderRegistry) GetProviderByType(providerType ProviderType) OIDCProvider {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
return r.typeMap[providerType]
|
|
}
|
|
|
|
// GetRegisteredProviders returns a slice of all registered provider types.
|
|
func (r *ProviderRegistry) GetRegisteredProviders() []ProviderType {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
|
|
types := make([]ProviderType, 0, len(r.typeMap))
|
|
for providerType := range r.typeMap {
|
|
types = append(types, providerType)
|
|
}
|
|
return types
|
|
}
|
|
|
|
// ClearCache removes all cached provider detection results.
|
|
// This can be useful for testing or when provider configuration changes.
|
|
func (r *ProviderRegistry) ClearCache() {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.cache = make(map[string]OIDCProvider)
|
|
r.cacheCount = 0
|
|
}
|
|
|
|
// evictOldestCacheEntry removes the first cache entry when cache is full
|
|
// This is a simple eviction strategy - in production, LRU might be preferred
|
|
func (r *ProviderRegistry) evictOldestCacheEntry() {
|
|
// Simple eviction: remove first entry found
|
|
for key := range r.cache {
|
|
delete(r.cache, key)
|
|
r.cacheCount--
|
|
break
|
|
}
|
|
}
|
|
|
|
// DetectProvider identifies the appropriate OIDC provider for an issuer URL.
|
|
// Uses double-checked locking pattern to avoid race conditions while caching results.
|
|
func (r *ProviderRegistry) DetectProvider(issuerURL string) OIDCProvider {
|
|
r.mu.RLock()
|
|
if provider, found := r.cache[issuerURL]; found {
|
|
r.mu.RUnlock()
|
|
return provider
|
|
}
|
|
r.mu.RUnlock()
|
|
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
if provider, found := r.cache[issuerURL]; found {
|
|
return provider
|
|
}
|
|
|
|
detectedProvider := r.detectProviderUnsafe(issuerURL)
|
|
|
|
// Check if cache is full and evict if necessary
|
|
if r.cacheCount >= r.maxCacheSize {
|
|
r.evictOldestCacheEntry()
|
|
}
|
|
|
|
r.cache[issuerURL] = detectedProvider
|
|
r.cacheCount++
|
|
|
|
return detectedProvider
|
|
}
|
|
|
|
// detectProviderUnsafe performs the actual provider detection logic.
|
|
// This method assumes the caller holds the appropriate lock and should not be called directly.
|
|
func (r *ProviderRegistry) detectProviderUnsafe(issuerURL string) OIDCProvider {
|
|
normalizedURL, err := url.Parse(issuerURL)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
// Check if the URL has a valid scheme and host
|
|
if normalizedURL.Scheme == "" || normalizedURL.Host == "" {
|
|
return nil
|
|
}
|
|
|
|
// Convert host to lowercase for case-insensitive matching
|
|
host := strings.ToLower(normalizedURL.Host)
|
|
|
|
for _, p := range r.providers {
|
|
switch p.GetType() {
|
|
case ProviderTypeGoogle:
|
|
if strings.Contains(host, "accounts.google.com") {
|
|
return p
|
|
}
|
|
case ProviderTypeAzure:
|
|
if strings.Contains(host, "login.microsoftonline.com") || strings.Contains(host, "sts.windows.net") {
|
|
return p
|
|
}
|
|
case ProviderTypeGitHub:
|
|
if strings.Contains(host, "github.com") {
|
|
return p
|
|
}
|
|
case ProviderTypeAuth0:
|
|
if strings.Contains(host, ".auth0.com") {
|
|
return p
|
|
}
|
|
case ProviderTypeOkta:
|
|
if strings.Contains(host, ".okta.com") || strings.Contains(host, ".oktapreview.com") || strings.Contains(host, ".okta-emea.com") {
|
|
return p
|
|
}
|
|
case ProviderTypeKeycloak:
|
|
// Match both Keycloak <17 (`/auth/realms/`) and 17+ (`/realms/`).
|
|
if strings.Contains(host, "keycloak") || strings.Contains(normalizedURL.Path, "/realms/") {
|
|
return p
|
|
}
|
|
case ProviderTypeAWSCognito:
|
|
if strings.Contains(host, "cognito-idp") && strings.Contains(host, ".amazonaws.com") {
|
|
return p
|
|
}
|
|
case ProviderTypeGitLab:
|
|
// Match gitlab.com, self-hosted (gitlab.*), and instances with gitlab in subdomain
|
|
if strings.Contains(host, "gitlab.com") ||
|
|
strings.Contains(host, "gitlab") {
|
|
return p
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, p := range r.providers {
|
|
if p.GetType() == ProviderTypeGeneric {
|
|
return p
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|