oauth2-proxy/providers/lazy_provider.go

177 lines
5.4 KiB
Go

package providers
import (
"context"
"errors"
"net/url"
"sync"
"time"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
)
// ErrProviderNotReady is returned by a LazyProvider's methods while background
// OIDC discovery has not yet completed. Provider-independent features (such as
// Basic Auth via htpasswd-file) do not go through these methods and therefore
// keep working while discovery is pending.
var ErrProviderNotReady = errors.New("provider not ready: OIDC discovery has not completed yet")
// LazyProvider wraps a Provider whose construction depends on OIDC discovery.
// It starts out not-ready, delegating only Data() to a discovery-independent
// placeholder, and returns ErrProviderNotReady from OAuth flow methods. Once
// background discovery succeeds, the real provider is swapped in atomically and
// all methods delegate to it.
type LazyProvider struct {
providerConfig options.Provider
placeholder Provider
mu sync.RWMutex
inner Provider
}
var _ Provider = (*LazyProvider)(nil)
// NewLazyProvider builds a LazyProvider for the given configuration. The
// placeholder provider is constructed without performing OIDC discovery so it
// cannot fail on an unreachable issuer. Call InitWithRetry (typically in a
// goroutine) to perform discovery in the background.
func NewLazyProvider(providerConfig options.Provider) (*LazyProvider, error) {
placeholder, err := newPlaceholderProvider(providerConfig)
if err != nil {
return nil, err
}
return &LazyProvider{
providerConfig: providerConfig,
placeholder: placeholder,
}, nil
}
// current returns the real provider if discovery has completed, otherwise nil.
func (l *LazyProvider) current() Provider {
l.mu.RLock()
defer l.mu.RUnlock()
return l.inner
}
// Ready reports whether background discovery has completed and the real
// provider is available.
func (l *LazyProvider) Ready() bool {
return l.current() != nil
}
func (l *LazyProvider) setInner(p Provider) {
l.mu.Lock()
l.inner = p
l.mu.Unlock()
}
// InitWithRetry repeatedly attempts to construct the real provider (performing
// OIDC discovery) until it succeeds or ctx is cancelled. Once construction
// succeeds the real provider is swapped in and the LazyProvider becomes ready.
// It is intended to be run in a goroutine.
func (l *LazyProvider) InitWithRetry(ctx context.Context) {
initialInterval := l.providerConfig.OIDCConfig.LazyDiscoveryInitialInterval
if initialInterval <= 0 {
initialInterval = options.DefaultOIDCLazyDiscoveryInitialInterval
}
maxInterval := l.providerConfig.OIDCConfig.LazyDiscoveryMaxInterval
if maxInterval < initialInterval {
maxInterval = initialInterval
}
interval := initialInterval
attempt := 0
for {
attempt++
provider, err := NewProvider(l.providerConfig)
if err == nil {
l.setInner(provider)
logger.Printf("OIDC discovery succeeded after %d attempt(s); provider is now ready", attempt)
return
}
logger.Errorf("lazy OIDC discovery attempt %d failed, will retry in %s: %v", attempt, interval, err)
timer := time.NewTimer(interval)
select {
case <-ctx.Done():
timer.Stop()
logger.Errorf("stopping lazy OIDC discovery: %v", ctx.Err())
return
case <-timer.C:
}
interval *= 2
if interval > maxInterval {
interval = maxInterval
}
}
}
// Data returns the real provider's data once ready, otherwise the placeholder's.
func (l *LazyProvider) Data() *ProviderData {
if p := l.current(); p != nil {
return p.Data()
}
return l.placeholder.Data()
}
// GetLoginURL returns an empty string until the provider is ready.
func (l *LazyProvider) GetLoginURL(redirectURI, finalRedirect, nonce string, extraParams url.Values) string {
if p := l.current(); p != nil {
return p.GetLoginURL(redirectURI, finalRedirect, nonce, extraParams)
}
return ""
}
func (l *LazyProvider) Redeem(ctx context.Context, redirectURI, code, codeVerifier string) (*sessions.SessionState, error) {
if p := l.current(); p != nil {
return p.Redeem(ctx, redirectURI, code, codeVerifier)
}
return nil, ErrProviderNotReady
}
func (l *LazyProvider) GetEmailAddress(ctx context.Context, s *sessions.SessionState) (string, error) {
if p := l.current(); p != nil {
return p.GetEmailAddress(ctx, s)
}
return "", ErrProviderNotReady
}
func (l *LazyProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
if p := l.current(); p != nil {
return p.EnrichSession(ctx, s)
}
return ErrProviderNotReady
}
func (l *LazyProvider) Authorize(ctx context.Context, s *sessions.SessionState) (bool, error) {
if p := l.current(); p != nil {
return p.Authorize(ctx, s)
}
return false, ErrProviderNotReady
}
// ValidateSession returns false until the provider is ready.
func (l *LazyProvider) ValidateSession(ctx context.Context, s *sessions.SessionState) bool {
if p := l.current(); p != nil {
return p.ValidateSession(ctx, s)
}
return false
}
func (l *LazyProvider) RefreshSession(ctx context.Context, s *sessions.SessionState) (bool, error) {
if p := l.current(); p != nil {
return p.RefreshSession(ctx, s)
}
return false, ErrProviderNotReady
}
func (l *LazyProvider) CreateSessionFromToken(ctx context.Context, token string) (*sessions.SessionState, error) {
if p := l.current(); p != nil {
return p.CreateSessionFromToken(ctx, token)
}
return nil, ErrProviderNotReady
}