Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/supabase/auth/internal/api/apierrors"
"github.com/supabase/auth/internal/api/apitask"
"github.com/supabase/auth/internal/api/oauthserver"
"github.com/supabase/auth/internal/api/provider"
"github.com/supabase/auth/internal/conf"
"github.com/supabase/auth/internal/hooks/hookshttp"
"github.com/supabase/auth/internal/hooks/hookspgfunc"
Expand Down Expand Up @@ -125,6 +126,11 @@ func NewAPIWithVersion(globalConfig *conf.GlobalConfiguration, db *storage.Conne
api.oauthServer = oauthserver.NewServer(globalConfig, db, api.tokenService)
}

// Configure OIDC provider cache TTL from configuration
if globalConfig.External.OIDCProviderCacheTTL > 0 {
provider.SetDefaultOIDCProviderCacheTTL(globalConfig.External.OIDCProviderCacheTTL)
}

if api.config.Password.HIBP.Enabled {
httpClient := &http.Client{
// all HIBP API requests should finish quickly to avoid
Expand Down
3 changes: 2 additions & 1 deletion internal/api/provider/apple.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ func NewAppleProvider(ctx context.Context, ext conf.OAuthProviderConfiguration)
logrus.Warn("Apple OAuth provider has URL config set which is ignored (check GOTRUE_EXTERNAL_APPLE_URL)")
}

oidcProvider, err := oidc.NewProvider(ctx, DefaultAppleIssuer)
// Use cached OIDC provider to avoid redundant HTTP calls to discovery endpoint
oidcProvider, err := defaultOIDCProviderCache.Get(ctx, DefaultAppleIssuer)
if err != nil {
return nil, err
}
Expand Down
4 changes: 3 additions & 1 deletion internal/api/provider/azure.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,9 @@ func (g azureProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Use
return nil, fmt.Errorf("azure: ID token issuer %q does not match expected issuer %q", issuer, g.ExpectedIssuer)
}

provider, err := oidc.NewProvider(ctx, issuer)
// Use cached OIDC provider to avoid redundant HTTP calls to discovery endpoint
// Azure has multi-tenant support, so each tenant gets its own cache entry
provider, err := defaultOIDCProviderCache.Get(ctx, issuer)
if err != nil {
return nil, err
}
Expand Down
148 changes: 148 additions & 0 deletions internal/api/provider/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package provider

import (
"context"
"sync"
"time"

"github.com/coreos/go-oidc/v3/oidc"
"github.com/sirupsen/logrus"
)

const (
// DefaultOIDCProviderCacheTTL is the default time-to-live for cached OIDC providers
// 1 hour is a reasonable default as OIDC discovery documents rarely change
DefaultOIDCProviderCacheTTL = 1 * time.Hour
)

// OIDCProviderCache provides thread-safe caching of OIDC provider instances
// to avoid redundant HTTP calls to OIDC discovery endpoints (.well-known/openid-configuration)
type OIDCProviderCache struct {
mu sync.RWMutex
entries map[string]*oidcCacheEntry

// Default TTL for cache entries
defaultTTL time.Duration
}

type oidcCacheEntry struct {
provider *oidc.Provider
createdAt time.Time
expiresAt time.Time
}

// Global default OIDC provider cache instance
var defaultOIDCProviderCache = NewOIDCProviderCache(DefaultOIDCProviderCacheTTL)

// NewOIDCProviderCache creates a new OIDC provider cache instance with specified TTL
func NewOIDCProviderCache(ttl time.Duration) *OIDCProviderCache {
if ttl <= 0 {
ttl = DefaultOIDCProviderCacheTTL
}
return &OIDCProviderCache{
entries: make(map[string]*oidcCacheEntry),
defaultTTL: ttl,
}
}

// SetDefaultOIDCProviderCacheTTL updates the default OIDC provider cache TTL
// This should be called during application initialization with the configured value
func SetDefaultOIDCProviderCacheTTL(ttl time.Duration) {
if ttl <= 0 {
ttl = DefaultOIDCProviderCacheTTL
}
defaultOIDCProviderCache.mu.Lock()
defer defaultOIDCProviderCache.mu.Unlock()
defaultOIDCProviderCache.defaultTTL = ttl
}

// Get returns a cached OIDC provider or creates a new one if not cached or expired.
// Uses the cache's default TTL (1 hour).
// This method is thread-safe and supports concurrent access.
//
// Parameters:
// - ctx: Context for the OIDC provider creation (only used if cache miss)
// - issuer: OIDC issuer URL (e.g., "https://accounts.google.com")
//
// Returns the cached or newly created provider instance
func (c *OIDCProviderCache) Get(ctx context.Context, issuer string) (*oidc.Provider, error) {
now := time.Now()

// Fast path: check if cached and not expired (read lock only)
c.mu.RLock()
entry, exists := c.entries[issuer]
c.mu.RUnlock()

if exists && now.Before(entry.expiresAt) {
logrus.WithFields(logrus.Fields{
"issuer": issuer,
"created_at": entry.createdAt,
"expires_at": entry.expiresAt,
}).Debug("OIDC provider cache hit")
return entry.provider, nil
}

// Slow path: need to create new provider (write lock required)
c.mu.Lock()
defer c.mu.Unlock()

// Double-check after acquiring write lock (another goroutine might have created it)
if entry, exists := c.entries[issuer]; exists && now.Before(entry.expiresAt) {
logrus.WithField("issuer", issuer).Debug("OIDC provider cache hit after lock acquisition")
return entry.provider, nil
}

// Create provider with background context to ensure it's not tied to request lifecycle
// Use a background context with a deadline if the original context has one
bgCtx := context.Background()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Severity: MEDIUM

Critical context loss vulnerability: The cache replaces the original context with context.Background(), discarding security-critical context values like InsecureIssuerURLContext used for Apple OIDC. When Apple requests use oidc.InsecureIssuerURLContext(ctx, issuer) (token_oidc.go:139), this context setting is lost during cache miss, creating a provider with different security properties than intended. This could cause Apple authentication to fail or behave unexpectedly.
Helpful? Add 👍 / 👎

💡 Fix Suggestion

Suggestion: Replace context.Background() with context.WithoutCancel(ctx) to preserve context values (like InsecureIssuerURLContext for Apple OIDC) while still detaching from the parent request's cancellation. This maintains the original design intent of decoupling the cached provider from request lifecycle while fixing the security context loss issue.

⚠️ Experimental Feature: This code suggestion is automatically generated. Please review carefully.

Suggested change
bgCtx := context.Background()
bgCtx := context.WithoutCancel(ctx)

if deadline, ok := ctx.Deadline(); ok {
var cancel context.CancelFunc
bgCtx, cancel = context.WithDeadline(bgCtx, deadline)
defer cancel()
}

provider, err := oidc.NewProvider(bgCtx, issuer)
if err != nil {
logrus.WithError(err).WithField("issuer", issuer).Error("Failed to create OIDC provider")
return nil, err
}

expiresAt := now.Add(c.defaultTTL)
c.entries[issuer] = &oidcCacheEntry{
provider: provider,
createdAt: now,
expiresAt: expiresAt,
}

return provider, nil
}

// Invalidate removes a specific OIDC provider from the cache
// Useful for manual cache invalidation or testing
func (c *OIDCProviderCache) Invalidate(issuer string) {
c.mu.Lock()
defer c.mu.Unlock()

delete(c.entries, issuer)
}

// Clear removes all entries from the cache
// Primarily used for testing
func (c *OIDCProviderCache) Clear() {
c.mu.Lock()
defer c.mu.Unlock()

c.entries = make(map[string]*oidcCacheEntry)
}

// Size returns the current number of cached OIDC providers
func (c *OIDCProviderCache) Size() int {
c.mu.RLock()
defer c.mu.RUnlock()
return len(c.entries)
}

// GetDefaultOIDCProviderCache returns the global default OIDC provider cache instance
func GetDefaultOIDCProviderCache() *OIDCProviderCache {
return defaultOIDCProviderCache
}
Loading