177 lines
5.4 KiB
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
|
|
}
|