From c57b2c62824c6a42f02b233439a6ddb386cc9b57 Mon Sep 17 00:00:00 2001 From: Orkhan Huseynli Date: Wed, 12 Aug 2026 08:46:48 +0400 Subject: [PATCH] feat(oidc): keep /ready healthy under lazy discovery and make backoff configurable Signed-off-by: Orkhan Huseynli --- CHANGELOG.md | 2 +- docs/docs/configuration/alpha_config.md | 4 +- docs/docs/configuration/overview.md | 4 +- main_test.go | 2 + oauthproxy.go | 39 +++----------- oauthproxy_lazy_test.go | 58 +++++++++++---------- pkg/apis/options/legacy_options.go | 68 ++++++++++++++----------- pkg/apis/options/load_test.go | 3 ++ pkg/apis/options/providers.go | 42 +++++++++++---- providers/lazy_provider.go | 21 ++++---- 10 files changed, 132 insertions(+), 111 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c40e0a90..724eb653 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ ## Changes since v7.15.3 -- [#3487](https://github.com/oauth2-proxy/oauth2-proxy/issues/3487) Add opt-in `--oidc-lazy-discovery` (default off): when the OIDC issuer is unreachable at startup, oauth2-proxy starts anyway and performs OIDC discovery in the background (retrying with backoff), so Basic Auth via `--htpasswd-file` and other provider-independent features keep working while discovery is pending. The readiness endpoint reports not-ready and OAuth2 login returns 503 until discovery succeeds. +- [#3487](https://github.com/oauth2-proxy/oauth2-proxy/issues/3487) Add opt-in `--oidc-lazy-discovery` (default off): when the OIDC issuer is unreachable at startup, oauth2-proxy starts anyway and performs OIDC discovery in the background (retrying with backoff), so Basic Auth via `--htpasswd-file` and other provider-independent features keep working while discovery is pending. The readiness endpoint stays healthy (keeping the pod in load-balancer rotation) while OAuth2 login returns 503 until discovery succeeds. The background backoff is configurable via `--oidc-lazy-discovery-initial-interval` (default `3s`) and `--oidc-lazy-discovery-max-interval` (default `30s`). # V7.15.3 diff --git a/docs/docs/configuration/alpha_config.md b/docs/docs/configuration/alpha_config.md index bdc3928d..4160f26a 100644 --- a/docs/docs/configuration/alpha_config.md +++ b/docs/docs/configuration/alpha_config.md @@ -544,7 +544,9 @@ character. | `insecureSkipIssuerVerification` | _bool_ | InsecureSkipIssuerVerification skips verification of ID token issuers. When false, ID Token Issuers must match the OIDC discovery URL
default set to 'false' | | `insecureSkipNonce` | _bool_ | InsecureSkipNonce skips verifying the ID Token's nonce claim that must match
the random nonce sent in the initial OAuth flow. Otherwise, the nonce is checked
after the initial OAuth redeem & subsequent token refreshes.
default set to 'true'
Warning: In a future release, this will change to 'false' by default for enhanced security. | | `skipDiscovery` | _bool_ | SkipDiscovery allows to skip OIDC discovery and use manually supplied Endpoints
default set to 'false' | -| `lazyDiscovery` | _bool_ | LazyDiscovery allows oauth2-proxy to start even when the OIDC issuer is
unreachable. When enabled, OIDC discovery is performed in the background
(retrying with backoff) instead of blocking startup, so features that do
not depend on the provider - such as Basic Auth via htpasswdFile - remain
available while discovery is pending. The readiness endpoint reports
not-ready until discovery succeeds. Only applies when discovery is enabled
(SkipDiscovery is false). Note that a configuration error (rather than an
unreachable issuer) is retried indefinitely instead of failing startup.
default set to 'false' | +| `lazyDiscovery` | _bool_ | LazyDiscovery lets oauth2-proxy start when the OIDC issuer is unreachable,
retrying discovery in the background instead of failing startup. OAuth2
login returns 503 until discovery succeeds; other features (e.g. Basic Auth)
keep working. default set to 'false' | +| `lazyDiscoveryInitialInterval` | _duration_ | LazyDiscoveryInitialInterval is the backoff before the first background
discovery retry, doubling up to LazyDiscoveryMaxInterval. default set to '3s' | +| `lazyDiscoveryMaxInterval` | _duration_ | LazyDiscoveryMaxInterval is the maximum backoff between background
discovery retries. default set to '30s' | | `jwksURL` | _string_ | JwksURL is the OpenID Connect JWKS URL
eg: https://www.googleapis.com/oauth2/v3/certs | | `publicKeyFiles` | _[]string_ | PublicKeyFiles is a list of paths pointing to public key files in PEM format to use
for verifying JWT tokens | | `emailClaim` | _string_ | EmailClaim indicates which claim contains the user email,
default set to 'email' | diff --git a/docs/docs/configuration/overview.md b/docs/docs/configuration/overview.md index d5ef7dc0..a6252f13 100644 --- a/docs/docs/configuration/overview.md +++ b/docs/docs/configuration/overview.md @@ -98,7 +98,9 @@ Provider specific options can be found on their respective subpages. | flag: `--oidc-extra-audience`
toml: `oidc_extra_audiences` | string \| list | additional audiences which are allowed to pass verification | `"[]"` | | flag: `--oidc-groups-claim`
toml: `oidc_groups_claim` | string | which OIDC claim contains the user groups | `"groups"` | | flag: `--oidc-issuer-url`
toml: `oidc_issuer_url` | string | the OpenID Connect issuer URL, e.g. `"https://accounts.google.com"` | | -| flag: `--oidc-lazy-discovery`
toml: `oidc_lazy_discovery` | bool | start oauth2-proxy even if the OIDC issuer is unreachable and perform discovery in the background (retrying with backoff) instead of failing startup. Provider-independent features such as Basic Auth via `--htpasswd-file` keep working while discovery is pending; the readiness endpoint reports not-ready and OAuth2 login returns `503` until it succeeds. Only applies when OIDC discovery is enabled. **Note:** a configuration error (e.g. issuer mismatch) is retried indefinitely instead of failing startup, so the provider never becomes ready - watch the readiness endpoint and logs | false | +| flag: `--oidc-lazy-discovery`
toml: `oidc_lazy_discovery` | bool | start oauth2-proxy even when the OIDC issuer is unreachable, retrying discovery in the background instead of failing startup. OAuth2 login returns `503` until discovery succeeds; other features (e.g. Basic Auth) keep working | false | +| flag: `--oidc-lazy-discovery-initial-interval`
toml: `oidc_lazy_discovery_initial_interval` | duration | backoff before the first background discovery retry, doubling up to `--oidc-lazy-discovery-max-interval` | `3s` | +| flag: `--oidc-lazy-discovery-max-interval`
toml: `oidc_lazy_discovery_max_interval` | duration | maximum backoff between background discovery retries | `30s` | | flag: `--oidc-jwks-url`
toml: `oidc_jwks_url` | string | OIDC JWKS URI for token verification; required if OIDC discovery is disabled and public key files are not provided | | | flag: `--oidc-public-key-file`
toml: `oidc_public_key_files` | string | Path to public key file in PEM format to use for verifying JWT tokens (may be given multiple times). Required if OIDC discovery is disabled na JWKS URL isn't provided | | | flag: `--oidc-enabled-signing-alg`
toml: `oidc_enabled_signing_algs` | string \| list | List of allowed JWT signing algorithms. When oidc discovery is enabled, the effective set is the intersection between this list and the provider's discovered supported algorithms. | | diff --git a/main_test.go b/main_test.go index 4edb16fa..8218437f 100644 --- a/main_test.go +++ b/main_test.go @@ -188,6 +188,8 @@ redirect_url="http://localhost:4180/oauth2/callback" InsecureSkipIssuerVerification: ptr.To(false), SkipDiscovery: ptr.To(false), LazyDiscovery: ptr.To(false), + LazyDiscoveryInitialInterval: options.DefaultOIDCLazyDiscoveryInitialInterval, + LazyDiscoveryMaxInterval: options.DefaultOIDCLazyDiscoveryMaxInterval, EnabledSigningAlgs: []string{}, }, MicrosoftEntraIDConfig: options.MicrosoftEntraIDOptions{ diff --git a/oauthproxy.go b/oauthproxy.go index eea1b63c..8558e2ba 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -213,8 +213,7 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr return nil, err } - readinessVerifiable := readinessVerifiers{sessionStore, providerReadiness{provider: provider}} - preAuthChain, err := buildPreAuthChain(opts, readinessVerifiable, trustedProxies) + preAuthChain, err := buildPreAuthChain(opts, sessionStore, trustedProxies) if err != nil { return nil, fmt.Errorf("could not build pre-auth chain: %v", err) } @@ -374,7 +373,7 @@ func (p *OAuthProxy) buildProxySubrouter(s *mux.Router) { // buildPreAuthChain constructs a chain that should process every request before // the OAuth2 Proxy authentication logic kicks in. // For example forcing HTTPS or health checks. -func buildPreAuthChain(opts *options.Options, readiness middleware.Verifiable, trustedProxies *ip.NetSet) (alice.Chain, error) { +func buildPreAuthChain(opts *options.Options, sessionStore sessionsapi.SessionStore, trustedProxies *ip.NetSet) (alice.Chain, error) { chain := alice.New(middleware.NewScope(opts.ReverseProxy, opts.Logging.RequestIDHeader, trustedProxies)) if opts.ForceHTTPS { @@ -398,14 +397,14 @@ func buildPreAuthChain(opts *options.Options, readiness middleware.Verifiable, t if opts.Logging.SilencePing { chain = chain.Append( middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents), - middleware.NewReadynessCheck(opts.ReadyPath, readiness), + middleware.NewReadynessCheck(opts.ReadyPath, sessionStore), middleware.NewRequestLogger(), ) } else { chain = chain.Append( middleware.NewRequestLogger(), middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents), - middleware.NewReadynessCheck(opts.ReadyPath, readiness), + middleware.NewReadynessCheck(opts.ReadyPath, sessionStore), ) } @@ -536,22 +535,11 @@ func setupProvider(providerConfig options.Provider) (providers.Provider, error) return lazy, nil } -// providerReadiness reports the readiness of a LazyProvider for the /ready deep -// health check. Non-lazy providers are always considered ready. -type providerReadiness struct { - provider providers.Provider -} - -func (p providerReadiness) VerifyConnection(_ context.Context) error { - if lazy, ok := p.provider.(*providers.LazyProvider); ok && !lazy.Ready() { - return errors.New("provider not ready: OIDC discovery is still pending") - } - return nil -} - // providerReady reports whether the identity provider can serve OAuth2 flows. // A LazyProvider is only ready once background OIDC discovery has completed; all -// other providers are always ready. +// other providers are always ready. Note this does not gate the /ready endpoint +// (which stays healthy under lazy discovery so the pod remains in load-balancer +// rotation); it only gates the OAuth2 login flow. func (p *OAuthProxy) providerReady() bool { if p.lazyProvider != nil { return p.lazyProvider.Ready() @@ -559,19 +547,6 @@ func (p *OAuthProxy) providerReady() bool { return true } -// readinessVerifiers combines multiple Verifiable checks into one; VerifyConnection -// fails if any of them fail. -type readinessVerifiers []middleware.Verifiable - -func (rs readinessVerifiers) VerifyConnection(ctx context.Context) error { - for _, r := range rs { - if err := r.VerifyConnection(ctx); err != nil { - return err - } - } - return nil -} - // buildRoutesAllowlist builds an []allowedRoute list from either the legacy // SkipAuthRegex option (paths only support) or newer SkipAuthRoutes option // (method=path support) diff --git a/oauthproxy_lazy_test.go b/oauthproxy_lazy_test.go index a393d2e6..c83cb9ac 100644 --- a/oauthproxy_lazy_test.go +++ b/oauthproxy_lazy_test.go @@ -1,7 +1,6 @@ package main import ( - "context" "net/http" "net/http/httptest" "testing" @@ -13,6 +12,27 @@ import ( . "github.com/onsi/gomega" ) +// newLazyPendingProxy builds a proxy whose OIDC issuer is unreachable with lazy +// discovery enabled. Start is not called, so background discovery never runs and +// the provider stays not-ready for the duration of the test. +func newLazyPendingProxy(g *WithT) *OAuthProxy { + opts := baseTestOptions() + opts.Providers[0].Type = "oidc" + opts.Providers[0].OIDCConfig = options.OIDCOptions{ + IssuerURL: "http://127.0.0.1:1/realms/test", + SkipDiscovery: ptr.To(false), + LazyDiscovery: ptr.To(true), + EmailClaim: options.OIDCEmailClaim, + AudienceClaims: []string{"aud"}, + } + g.Expect(validation.Validate(opts)).To(Succeed()) + + proxy, err := NewOAuthProxy(opts, func(string) bool { return true }) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(proxy.providerReady()).To(BeFalse()) + return proxy +} + func unreachableOIDCProvider(lazy bool) options.Provider { return options.Provider{ ID: "test-provider", @@ -46,18 +66,19 @@ func TestSetupProviderWithoutLazyFailsFast(t *testing.T) { g.Expect(err).To(HaveOccurred()) } -func TestProviderReadinessVerifier(t *testing.T) { +// TestReadyEndpointOKWhenProviderNotReady ensures /ready stays healthy under +// lazy discovery so the pod remains in load-balancer rotation while discovery +// is still pending. +func TestReadyEndpointOKWhenProviderNotReady(t *testing.T) { g := NewWithT(t) - // A non-lazy provider (google needs no discovery) is always considered ready. - nonLazy, err := providers.NewProvider(options.Provider{ID: "g", Type: "google", ClientID: "client-id"}) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(providerReadiness{provider: nonLazy}.VerifyConnection(context.Background())).To(Succeed()) + proxy := newLazyPendingProxy(g) - // A lazy provider that has not completed discovery is not ready. - lazy, err := providers.NewLazyProvider(unreachableOIDCProvider(true)) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(providerReadiness{provider: lazy}.VerifyConnection(context.Background())).ToNot(Succeed()) + rw := httptest.NewRecorder() + req, _ := http.NewRequest(http.MethodGet, "/ready", nil) + proxy.ServeHTTP(rw, req) + + g.Expect(rw.Code).To(Equal(http.StatusOK)) } // TestOAuthStartUnavailableWhenProviderNotReady ensures the OAuth2 login flow @@ -66,22 +87,7 @@ func TestProviderReadinessVerifier(t *testing.T) { func TestOAuthStartUnavailableWhenProviderNotReady(t *testing.T) { g := NewWithT(t) - opts := baseTestOptions() - opts.Providers[0].Type = "oidc" - opts.Providers[0].OIDCConfig = options.OIDCOptions{ - IssuerURL: "http://127.0.0.1:1/realms/test", - SkipDiscovery: ptr.To(false), - LazyDiscovery: ptr.To(true), - EmailClaim: options.OIDCEmailClaim, - AudienceClaims: []string{"aud"}, - } - g.Expect(validation.Validate(opts)).To(Succeed()) - - // Not calling Start, so background discovery never runs and the provider - // stays not-ready for the duration of the test. - proxy, err := NewOAuthProxy(opts, func(string) bool { return true }) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(proxy.providerReady()).To(BeFalse()) + proxy := newLazyPendingProxy(g) rw := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/oauth2/start", nil) diff --git a/pkg/apis/options/legacy_options.go b/pkg/apis/options/legacy_options.go index 3e900df5..452a3084 100644 --- a/pkg/apis/options/legacy_options.go +++ b/pkg/apis/options/legacy_options.go @@ -531,37 +531,39 @@ type LegacyProvider struct { // These options allow for other providers besides Google, with // potential overrides. - ProviderType string `flag:"provider" cfg:"provider"` - ProviderName string `flag:"provider-display-name" cfg:"provider_display_name"` - ProviderCAFiles []string `flag:"provider-ca-file" cfg:"provider_ca_files"` - UseSystemTrustStore bool `flag:"use-system-trust-store" cfg:"use_system_trust_store"` - OIDCIssuerURL string `flag:"oidc-issuer-url" cfg:"oidc_issuer_url"` - InsecureOIDCAllowUnverifiedEmail bool `flag:"insecure-oidc-allow-unverified-email" cfg:"insecure_oidc_allow_unverified_email"` - InsecureOIDCSkipIssuerVerification bool `flag:"insecure-oidc-skip-issuer-verification" cfg:"insecure_oidc_skip_issuer_verification"` - InsecureOIDCSkipNonce bool `flag:"insecure-oidc-skip-nonce" cfg:"insecure_oidc_skip_nonce"` - SkipOIDCDiscovery bool `flag:"skip-oidc-discovery" cfg:"skip_oidc_discovery"` - OIDCLazyDiscovery bool `flag:"oidc-lazy-discovery" cfg:"oidc_lazy_discovery"` - OIDCJwksURL string `flag:"oidc-jwks-url" cfg:"oidc_jwks_url"` - OIDCEmailClaim string `flag:"oidc-email-claim" cfg:"oidc_email_claim"` - OIDCGroupsClaim string `flag:"oidc-groups-claim" cfg:"oidc_groups_claim"` - OIDCAudienceClaims []string `flag:"oidc-audience-claim" cfg:"oidc_audience_claims"` - OIDCExtraAudiences []string `flag:"oidc-extra-audience" cfg:"oidc_extra_audiences"` - OIDCPublicKeyFiles []string `flag:"oidc-public-key-file" cfg:"oidc_public_key_files"` - OIDCEnabledSigningAlgs []string `flag:"oidc-enabled-signing-alg" cfg:"oidc_enabled_signing_algs"` - LoginURL string `flag:"login-url" cfg:"login_url"` - AuthRequestResponseMode string `flag:"auth-request-response-mode" cfg:"auth_request_response_mode"` - RedeemURL string `flag:"redeem-url" cfg:"redeem_url"` - ProfileURL string `flag:"profile-url" cfg:"profile_url"` - SkipClaimsFromProfileURL bool `flag:"skip-claims-from-profile-url" cfg:"skip_claims_from_profile_url"` - ProtectedResource string `flag:"resource" cfg:"resource"` - ValidateURL string `flag:"validate-url" cfg:"validate_url"` - Scope string `flag:"scope" cfg:"scope"` - Prompt string `flag:"prompt" cfg:"prompt"` - ApprovalPrompt string `flag:"approval-prompt" cfg:"approval_prompt"` // Deprecated by OIDC 1.0 - UserIDClaim string `flag:"user-id-claim" cfg:"user_id_claim"` - AllowedGroups []string `flag:"allowed-group" cfg:"allowed_groups"` - AllowedRoles []string `flag:"allowed-role" cfg:"allowed_roles"` - BackendLogoutURL string `flag:"backend-logout-url" cfg:"backend_logout_url"` + ProviderType string `flag:"provider" cfg:"provider"` + ProviderName string `flag:"provider-display-name" cfg:"provider_display_name"` + ProviderCAFiles []string `flag:"provider-ca-file" cfg:"provider_ca_files"` + UseSystemTrustStore bool `flag:"use-system-trust-store" cfg:"use_system_trust_store"` + OIDCIssuerURL string `flag:"oidc-issuer-url" cfg:"oidc_issuer_url"` + InsecureOIDCAllowUnverifiedEmail bool `flag:"insecure-oidc-allow-unverified-email" cfg:"insecure_oidc_allow_unverified_email"` + InsecureOIDCSkipIssuerVerification bool `flag:"insecure-oidc-skip-issuer-verification" cfg:"insecure_oidc_skip_issuer_verification"` + InsecureOIDCSkipNonce bool `flag:"insecure-oidc-skip-nonce" cfg:"insecure_oidc_skip_nonce"` + SkipOIDCDiscovery bool `flag:"skip-oidc-discovery" cfg:"skip_oidc_discovery"` + OIDCLazyDiscovery bool `flag:"oidc-lazy-discovery" cfg:"oidc_lazy_discovery"` + OIDCLazyDiscoveryInitialInterval time.Duration `flag:"oidc-lazy-discovery-initial-interval" cfg:"oidc_lazy_discovery_initial_interval"` + OIDCLazyDiscoveryMaxInterval time.Duration `flag:"oidc-lazy-discovery-max-interval" cfg:"oidc_lazy_discovery_max_interval"` + OIDCJwksURL string `flag:"oidc-jwks-url" cfg:"oidc_jwks_url"` + OIDCEmailClaim string `flag:"oidc-email-claim" cfg:"oidc_email_claim"` + OIDCGroupsClaim string `flag:"oidc-groups-claim" cfg:"oidc_groups_claim"` + OIDCAudienceClaims []string `flag:"oidc-audience-claim" cfg:"oidc_audience_claims"` + OIDCExtraAudiences []string `flag:"oidc-extra-audience" cfg:"oidc_extra_audiences"` + OIDCPublicKeyFiles []string `flag:"oidc-public-key-file" cfg:"oidc_public_key_files"` + OIDCEnabledSigningAlgs []string `flag:"oidc-enabled-signing-alg" cfg:"oidc_enabled_signing_algs"` + LoginURL string `flag:"login-url" cfg:"login_url"` + AuthRequestResponseMode string `flag:"auth-request-response-mode" cfg:"auth_request_response_mode"` + RedeemURL string `flag:"redeem-url" cfg:"redeem_url"` + ProfileURL string `flag:"profile-url" cfg:"profile_url"` + SkipClaimsFromProfileURL bool `flag:"skip-claims-from-profile-url" cfg:"skip_claims_from_profile_url"` + ProtectedResource string `flag:"resource" cfg:"resource"` + ValidateURL string `flag:"validate-url" cfg:"validate_url"` + Scope string `flag:"scope" cfg:"scope"` + Prompt string `flag:"prompt" cfg:"prompt"` + ApprovalPrompt string `flag:"approval-prompt" cfg:"approval_prompt"` // Deprecated by OIDC 1.0 + UserIDClaim string `flag:"user-id-claim" cfg:"user_id_claim"` + AllowedGroups []string `flag:"allowed-group" cfg:"allowed_groups"` + AllowedRoles []string `flag:"allowed-role" cfg:"allowed_roles"` + BackendLogoutURL string `flag:"backend-logout-url" cfg:"backend_logout_url"` AcrValues string `flag:"acr-values" cfg:"acr_values"` JWTKey string `flag:"jwt-key" cfg:"jwt_key"` @@ -604,6 +606,8 @@ func legacyProviderFlagSet() *pflag.FlagSet { flagSet.Bool("insecure-oidc-skip-nonce", true, "skip verifying the OIDC ID Token's nonce claim") flagSet.Bool("skip-oidc-discovery", false, "Skip OIDC discovery and use manually supplied Endpoints") flagSet.Bool("oidc-lazy-discovery", false, "Start oauth2-proxy even if the OIDC issuer is unreachable and perform discovery in the background, retrying with backoff. Features that do not depend on the provider (e.g. Basic Auth via htpasswd-file) remain available while discovery is pending") + flagSet.Duration("oidc-lazy-discovery-initial-interval", DefaultOIDCLazyDiscoveryInitialInterval, "Initial backoff before the first background OIDC discovery retry when oidc-lazy-discovery is enabled; doubles after each attempt up to oidc-lazy-discovery-max-interval") + flagSet.Duration("oidc-lazy-discovery-max-interval", DefaultOIDCLazyDiscoveryMaxInterval, "Maximum backoff between background OIDC discovery retries when oidc-lazy-discovery is enabled") flagSet.String("oidc-jwks-url", "", "OpenID Connect JWKS URL (ie: https://www.googleapis.com/oauth2/v3/certs)") flagSet.String("oidc-groups-claim", OIDCGroupsClaim, "which OIDC claim contains the user groups") flagSet.String("oidc-email-claim", OIDCEmailClaim, "which OIDC claim contains the user's email") @@ -726,6 +730,8 @@ func (l *LegacyProvider) convert() (Providers, error) { InsecureSkipNonce: &l.InsecureOIDCSkipNonce, SkipDiscovery: &l.SkipOIDCDiscovery, LazyDiscovery: &l.OIDCLazyDiscovery, + LazyDiscoveryInitialInterval: l.OIDCLazyDiscoveryInitialInterval, + LazyDiscoveryMaxInterval: l.OIDCLazyDiscoveryMaxInterval, JwksURL: l.OIDCJwksURL, UserIDClaim: l.UserIDClaim, EmailClaim: l.OIDCEmailClaim, diff --git a/pkg/apis/options/load_test.go b/pkg/apis/options/load_test.go index 40f9a725..4d581e2b 100644 --- a/pkg/apis/options/load_test.go +++ b/pkg/apis/options/load_test.go @@ -44,6 +44,9 @@ var _ = Describe("Load", func() { OIDCGroupsClaim: "groups", OIDCAudienceClaims: []string{"aud"}, InsecureOIDCSkipNonce: true, + + OIDCLazyDiscoveryInitialInterval: DefaultOIDCLazyDiscoveryInitialInterval, + OIDCLazyDiscoveryMaxInterval: DefaultOIDCLazyDiscoveryMaxInterval, }, Options: Options{ diff --git a/pkg/apis/options/providers.go b/pkg/apis/options/providers.go index ab2cb76f..d0ab0685 100644 --- a/pkg/apis/options/providers.go +++ b/pkg/apis/options/providers.go @@ -1,6 +1,10 @@ package options -import "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util/ptr" +import ( + "time" + + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util/ptr" +) const ( // OIDCEmailClaim is the generic email claim used by the OIDC provider. @@ -17,6 +21,14 @@ const ( // When false, a failed OIDC discovery aborts startup (historical behaviour). DefaultOIDCLazyDiscovery bool = false + // DefaultOIDCLazyDiscoveryInitialInterval is the default value for + // OIDCOptions.LazyDiscoveryInitialInterval. + DefaultOIDCLazyDiscoveryInitialInterval time.Duration = 3 * time.Second + + // DefaultOIDCLazyDiscoveryMaxInterval is the default value for + // OIDCOptions.LazyDiscoveryMaxInterval. + DefaultOIDCLazyDiscoveryMaxInterval time.Duration = 30 * time.Second + // DefaultInsecureSkipNonce is the default value // for OIDCOptions.InsecureSkipNonce DefaultInsecureSkipNonce bool = true @@ -304,16 +316,17 @@ type OIDCOptions struct { // SkipDiscovery allows to skip OIDC discovery and use manually supplied Endpoints // default set to 'false' SkipDiscovery *bool `yaml:"skipDiscovery,omitempty"` - // LazyDiscovery allows oauth2-proxy to start even when the OIDC issuer is - // unreachable. When enabled, OIDC discovery is performed in the background - // (retrying with backoff) instead of blocking startup, so features that do - // not depend on the provider - such as Basic Auth via htpasswdFile - remain - // available while discovery is pending. The readiness endpoint reports - // not-ready until discovery succeeds. Only applies when discovery is enabled - // (SkipDiscovery is false). Note that a configuration error (rather than an - // unreachable issuer) is retried indefinitely instead of failing startup. - // default set to 'false' + // LazyDiscovery lets oauth2-proxy start when the OIDC issuer is unreachable, + // retrying discovery in the background instead of failing startup. OAuth2 + // login returns 503 until discovery succeeds; other features (e.g. Basic Auth) + // keep working. default set to 'false' LazyDiscovery *bool `yaml:"lazyDiscovery,omitempty"` + // LazyDiscoveryInitialInterval is the backoff before the first background + // discovery retry, doubling up to LazyDiscoveryMaxInterval. default set to '3s' + LazyDiscoveryInitialInterval time.Duration `yaml:"lazyDiscoveryInitialInterval,omitempty"` + // LazyDiscoveryMaxInterval is the maximum backoff between background + // discovery retries. default set to '30s' + LazyDiscoveryMaxInterval time.Duration `yaml:"lazyDiscoveryMaxInterval,omitempty"` // JwksURL is the OpenID Connect JWKS URL // eg: https://www.googleapis.com/oauth2/v3/certs JwksURL string `yaml:"jwksURL,omitempty"` @@ -412,6 +425,15 @@ func (o *OIDCOptions) EnsureDefaults() { if o.LazyDiscovery == nil { o.LazyDiscovery = ptr.To(DefaultOIDCLazyDiscovery) } + if o.LazyDiscoveryInitialInterval <= 0 { + o.LazyDiscoveryInitialInterval = DefaultOIDCLazyDiscoveryInitialInterval + } + if o.LazyDiscoveryMaxInterval <= 0 { + o.LazyDiscoveryMaxInterval = DefaultOIDCLazyDiscoveryMaxInterval + } + if o.LazyDiscoveryMaxInterval < o.LazyDiscoveryInitialInterval { + o.LazyDiscoveryMaxInterval = o.LazyDiscoveryInitialInterval + } if o.UserIDClaim == "" { o.UserIDClaim = OIDCEmailClaim } diff --git a/providers/lazy_provider.go b/providers/lazy_provider.go index 3a8c69be..4d20d92a 100644 --- a/providers/lazy_provider.go +++ b/providers/lazy_provider.go @@ -18,12 +18,6 @@ import ( // keep working while discovery is pending. var ErrProviderNotReady = errors.New("provider not ready: OIDC discovery has not completed yet") -// Background discovery backoff bounds. -const ( - lazyDiscoveryInitialInterval = 3 * time.Second - lazyDiscoveryMaxInterval = 30 * time.Second -) - // 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 @@ -78,7 +72,16 @@ func (l *LazyProvider) setInner(p Provider) { // 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) { - interval := lazyDiscoveryInitialInterval + 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++ @@ -100,8 +103,8 @@ func (l *LazyProvider) InitWithRetry(ctx context.Context) { } interval *= 2 - if interval > lazyDiscoveryMaxInterval { - interval = lazyDiscoveryMaxInterval + if interval > maxInterval { + interval = maxInterval } } }