feat(oidc): keep /ready healthy under lazy discovery and make backoff configurable

Signed-off-by: Orkhan Huseynli <orkhan.huseyn@outlook.com>
This commit is contained in:
Orkhan Huseynli 2026-08-12 08:46:48 +04:00
parent 857a764cef
commit c57b2c6282
10 changed files with 132 additions and 111 deletions

View File

@ -8,7 +8,7 @@
## Changes since v7.15.3 ## 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 # V7.15.3

View File

@ -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<br/>default set to 'false' | | `insecureSkipIssuerVerification` | _bool_ | InsecureSkipIssuerVerification skips verification of ID token issuers. When false, ID Token Issuers must match the OIDC discovery URL<br/>default set to 'false' |
| `insecureSkipNonce` | _bool_ | InsecureSkipNonce skips verifying the ID Token's nonce claim that must match<br/>the random nonce sent in the initial OAuth flow. Otherwise, the nonce is checked<br/>after the initial OAuth redeem & subsequent token refreshes.<br/>default set to 'true'<br/>Warning: In a future release, this will change to 'false' by default for enhanced security. | | `insecureSkipNonce` | _bool_ | InsecureSkipNonce skips verifying the ID Token's nonce claim that must match<br/>the random nonce sent in the initial OAuth flow. Otherwise, the nonce is checked<br/>after the initial OAuth redeem & subsequent token refreshes.<br/>default set to 'true'<br/>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<br/>default set to 'false' | | `skipDiscovery` | _bool_ | SkipDiscovery allows to skip OIDC discovery and use manually supplied Endpoints<br/>default set to 'false' |
| `lazyDiscovery` | _bool_ | LazyDiscovery allows oauth2-proxy to start even when the OIDC issuer is<br/>unreachable. When enabled, OIDC discovery is performed in the background<br/>(retrying with backoff) instead of blocking startup, so features that do<br/>not depend on the provider - such as Basic Auth via htpasswdFile - remain<br/>available while discovery is pending. The readiness endpoint reports<br/>not-ready until discovery succeeds. Only applies when discovery is enabled<br/>(SkipDiscovery is false). Note that a configuration error (rather than an<br/>unreachable issuer) is retried indefinitely instead of failing startup.<br/>default set to 'false' | | `lazyDiscovery` | _bool_ | LazyDiscovery lets oauth2-proxy start when the OIDC issuer is unreachable,<br/>retrying discovery in the background instead of failing startup. OAuth2<br/>login returns 503 until discovery succeeds; other features (e.g. Basic Auth)<br/>keep working. default set to 'false' |
| `lazyDiscoveryInitialInterval` | _duration_ | LazyDiscoveryInitialInterval is the backoff before the first background<br/>discovery retry, doubling up to LazyDiscoveryMaxInterval. default set to '3s' |
| `lazyDiscoveryMaxInterval` | _duration_ | LazyDiscoveryMaxInterval is the maximum backoff between background<br/>discovery retries. default set to '30s' |
| `jwksURL` | _string_ | JwksURL is the OpenID Connect JWKS URL<br/>eg: https://www.googleapis.com/oauth2/v3/certs | | `jwksURL` | _string_ | JwksURL is the OpenID Connect JWKS URL<br/>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<br/>for verifying JWT tokens | | `publicKeyFiles` | _[]string_ | PublicKeyFiles is a list of paths pointing to public key files in PEM format to use<br/>for verifying JWT tokens |
| `emailClaim` | _string_ | EmailClaim indicates which claim contains the user email,<br/>default set to 'email' | | `emailClaim` | _string_ | EmailClaim indicates which claim contains the user email,<br/>default set to 'email' |

View File

@ -98,7 +98,9 @@ Provider specific options can be found on their respective subpages.
| flag: `--oidc-extra-audience`<br/>toml: `oidc_extra_audiences` | string \| list | additional audiences which are allowed to pass verification | `"[]"` | | flag: `--oidc-extra-audience`<br/>toml: `oidc_extra_audiences` | string \| list | additional audiences which are allowed to pass verification | `"[]"` |
| flag: `--oidc-groups-claim`<br/>toml: `oidc_groups_claim` | string | which OIDC claim contains the user groups | `"groups"` | | flag: `--oidc-groups-claim`<br/>toml: `oidc_groups_claim` | string | which OIDC claim contains the user groups | `"groups"` |
| flag: `--oidc-issuer-url`<br/>toml: `oidc_issuer_url` | string | the OpenID Connect issuer URL, e.g. `"https://accounts.google.com"` | | | flag: `--oidc-issuer-url`<br/>toml: `oidc_issuer_url` | string | the OpenID Connect issuer URL, e.g. `"https://accounts.google.com"` | |
| flag: `--oidc-lazy-discovery`<br/>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`<br/>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`<br/>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`<br/>toml: `oidc_lazy_discovery_max_interval` | duration | maximum backoff between background discovery retries | `30s` |
| flag: `--oidc-jwks-url`<br/>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-jwks-url`<br/>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`<br/>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-public-key-file`<br/>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`<br/>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. | | | flag: `--oidc-enabled-signing-alg`<br/>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. | |

View File

@ -188,6 +188,8 @@ redirect_url="http://localhost:4180/oauth2/callback"
InsecureSkipIssuerVerification: ptr.To(false), InsecureSkipIssuerVerification: ptr.To(false),
SkipDiscovery: ptr.To(false), SkipDiscovery: ptr.To(false),
LazyDiscovery: ptr.To(false), LazyDiscovery: ptr.To(false),
LazyDiscoveryInitialInterval: options.DefaultOIDCLazyDiscoveryInitialInterval,
LazyDiscoveryMaxInterval: options.DefaultOIDCLazyDiscoveryMaxInterval,
EnabledSigningAlgs: []string{}, EnabledSigningAlgs: []string{},
}, },
MicrosoftEntraIDConfig: options.MicrosoftEntraIDOptions{ MicrosoftEntraIDConfig: options.MicrosoftEntraIDOptions{

View File

@ -213,8 +213,7 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
return nil, err return nil, err
} }
readinessVerifiable := readinessVerifiers{sessionStore, providerReadiness{provider: provider}} preAuthChain, err := buildPreAuthChain(opts, sessionStore, trustedProxies)
preAuthChain, err := buildPreAuthChain(opts, readinessVerifiable, trustedProxies)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not build pre-auth chain: %v", err) 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 // buildPreAuthChain constructs a chain that should process every request before
// the OAuth2 Proxy authentication logic kicks in. // the OAuth2 Proxy authentication logic kicks in.
// For example forcing HTTPS or health checks. // 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)) chain := alice.New(middleware.NewScope(opts.ReverseProxy, opts.Logging.RequestIDHeader, trustedProxies))
if opts.ForceHTTPS { if opts.ForceHTTPS {
@ -398,14 +397,14 @@ func buildPreAuthChain(opts *options.Options, readiness middleware.Verifiable, t
if opts.Logging.SilencePing { if opts.Logging.SilencePing {
chain = chain.Append( chain = chain.Append(
middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents), middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents),
middleware.NewReadynessCheck(opts.ReadyPath, readiness), middleware.NewReadynessCheck(opts.ReadyPath, sessionStore),
middleware.NewRequestLogger(), middleware.NewRequestLogger(),
) )
} else { } else {
chain = chain.Append( chain = chain.Append(
middleware.NewRequestLogger(), middleware.NewRequestLogger(),
middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents), 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 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. // providerReady reports whether the identity provider can serve OAuth2 flows.
// A LazyProvider is only ready once background OIDC discovery has completed; all // 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 { func (p *OAuthProxy) providerReady() bool {
if p.lazyProvider != nil { if p.lazyProvider != nil {
return p.lazyProvider.Ready() return p.lazyProvider.Ready()
@ -559,19 +547,6 @@ func (p *OAuthProxy) providerReady() bool {
return true 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 // buildRoutesAllowlist builds an []allowedRoute list from either the legacy
// SkipAuthRegex option (paths only support) or newer SkipAuthRoutes option // SkipAuthRegex option (paths only support) or newer SkipAuthRoutes option
// (method=path support) // (method=path support)

View File

@ -1,7 +1,6 @@
package main package main
import ( import (
"context"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
@ -13,6 +12,27 @@ import (
. "github.com/onsi/gomega" . "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 { func unreachableOIDCProvider(lazy bool) options.Provider {
return options.Provider{ return options.Provider{
ID: "test-provider", ID: "test-provider",
@ -46,18 +66,19 @@ func TestSetupProviderWithoutLazyFailsFast(t *testing.T) {
g.Expect(err).To(HaveOccurred()) 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) g := NewWithT(t)
// A non-lazy provider (google needs no discovery) is always considered ready. proxy := newLazyPendingProxy(g)
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())
// A lazy provider that has not completed discovery is not ready. rw := httptest.NewRecorder()
lazy, err := providers.NewLazyProvider(unreachableOIDCProvider(true)) req, _ := http.NewRequest(http.MethodGet, "/ready", nil)
g.Expect(err).ToNot(HaveOccurred()) proxy.ServeHTTP(rw, req)
g.Expect(providerReadiness{provider: lazy}.VerifyConnection(context.Background())).ToNot(Succeed())
g.Expect(rw.Code).To(Equal(http.StatusOK))
} }
// TestOAuthStartUnavailableWhenProviderNotReady ensures the OAuth2 login flow // TestOAuthStartUnavailableWhenProviderNotReady ensures the OAuth2 login flow
@ -66,22 +87,7 @@ func TestProviderReadinessVerifier(t *testing.T) {
func TestOAuthStartUnavailableWhenProviderNotReady(t *testing.T) { func TestOAuthStartUnavailableWhenProviderNotReady(t *testing.T) {
g := NewWithT(t) g := NewWithT(t)
opts := baseTestOptions() proxy := newLazyPendingProxy(g)
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())
rw := httptest.NewRecorder() rw := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/oauth2/start", nil) req, _ := http.NewRequest(http.MethodGet, "/oauth2/start", nil)

View File

@ -531,37 +531,39 @@ type LegacyProvider struct {
// These options allow for other providers besides Google, with // These options allow for other providers besides Google, with
// potential overrides. // potential overrides.
ProviderType string `flag:"provider" cfg:"provider"` ProviderType string `flag:"provider" cfg:"provider"`
ProviderName string `flag:"provider-display-name" cfg:"provider_display_name"` ProviderName string `flag:"provider-display-name" cfg:"provider_display_name"`
ProviderCAFiles []string `flag:"provider-ca-file" cfg:"provider_ca_files"` ProviderCAFiles []string `flag:"provider-ca-file" cfg:"provider_ca_files"`
UseSystemTrustStore bool `flag:"use-system-trust-store" cfg:"use_system_trust_store"` UseSystemTrustStore bool `flag:"use-system-trust-store" cfg:"use_system_trust_store"`
OIDCIssuerURL string `flag:"oidc-issuer-url" cfg:"oidc_issuer_url"` OIDCIssuerURL string `flag:"oidc-issuer-url" cfg:"oidc_issuer_url"`
InsecureOIDCAllowUnverifiedEmail bool `flag:"insecure-oidc-allow-unverified-email" cfg:"insecure_oidc_allow_unverified_email"` 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"` 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"` InsecureOIDCSkipNonce bool `flag:"insecure-oidc-skip-nonce" cfg:"insecure_oidc_skip_nonce"`
SkipOIDCDiscovery bool `flag:"skip-oidc-discovery" cfg:"skip_oidc_discovery"` SkipOIDCDiscovery bool `flag:"skip-oidc-discovery" cfg:"skip_oidc_discovery"`
OIDCLazyDiscovery bool `flag:"oidc-lazy-discovery" cfg:"oidc_lazy_discovery"` OIDCLazyDiscovery bool `flag:"oidc-lazy-discovery" cfg:"oidc_lazy_discovery"`
OIDCJwksURL string `flag:"oidc-jwks-url" cfg:"oidc_jwks_url"` OIDCLazyDiscoveryInitialInterval time.Duration `flag:"oidc-lazy-discovery-initial-interval" cfg:"oidc_lazy_discovery_initial_interval"`
OIDCEmailClaim string `flag:"oidc-email-claim" cfg:"oidc_email_claim"` OIDCLazyDiscoveryMaxInterval time.Duration `flag:"oidc-lazy-discovery-max-interval" cfg:"oidc_lazy_discovery_max_interval"`
OIDCGroupsClaim string `flag:"oidc-groups-claim" cfg:"oidc_groups_claim"` OIDCJwksURL string `flag:"oidc-jwks-url" cfg:"oidc_jwks_url"`
OIDCAudienceClaims []string `flag:"oidc-audience-claim" cfg:"oidc_audience_claims"` OIDCEmailClaim string `flag:"oidc-email-claim" cfg:"oidc_email_claim"`
OIDCExtraAudiences []string `flag:"oidc-extra-audience" cfg:"oidc_extra_audiences"` OIDCGroupsClaim string `flag:"oidc-groups-claim" cfg:"oidc_groups_claim"`
OIDCPublicKeyFiles []string `flag:"oidc-public-key-file" cfg:"oidc_public_key_files"` OIDCAudienceClaims []string `flag:"oidc-audience-claim" cfg:"oidc_audience_claims"`
OIDCEnabledSigningAlgs []string `flag:"oidc-enabled-signing-alg" cfg:"oidc_enabled_signing_algs"` OIDCExtraAudiences []string `flag:"oidc-extra-audience" cfg:"oidc_extra_audiences"`
LoginURL string `flag:"login-url" cfg:"login_url"` OIDCPublicKeyFiles []string `flag:"oidc-public-key-file" cfg:"oidc_public_key_files"`
AuthRequestResponseMode string `flag:"auth-request-response-mode" cfg:"auth_request_response_mode"` OIDCEnabledSigningAlgs []string `flag:"oidc-enabled-signing-alg" cfg:"oidc_enabled_signing_algs"`
RedeemURL string `flag:"redeem-url" cfg:"redeem_url"` LoginURL string `flag:"login-url" cfg:"login_url"`
ProfileURL string `flag:"profile-url" cfg:"profile_url"` AuthRequestResponseMode string `flag:"auth-request-response-mode" cfg:"auth_request_response_mode"`
SkipClaimsFromProfileURL bool `flag:"skip-claims-from-profile-url" cfg:"skip_claims_from_profile_url"` RedeemURL string `flag:"redeem-url" cfg:"redeem_url"`
ProtectedResource string `flag:"resource" cfg:"resource"` ProfileURL string `flag:"profile-url" cfg:"profile_url"`
ValidateURL string `flag:"validate-url" cfg:"validate_url"` SkipClaimsFromProfileURL bool `flag:"skip-claims-from-profile-url" cfg:"skip_claims_from_profile_url"`
Scope string `flag:"scope" cfg:"scope"` ProtectedResource string `flag:"resource" cfg:"resource"`
Prompt string `flag:"prompt" cfg:"prompt"` ValidateURL string `flag:"validate-url" cfg:"validate_url"`
ApprovalPrompt string `flag:"approval-prompt" cfg:"approval_prompt"` // Deprecated by OIDC 1.0 Scope string `flag:"scope" cfg:"scope"`
UserIDClaim string `flag:"user-id-claim" cfg:"user_id_claim"` Prompt string `flag:"prompt" cfg:"prompt"`
AllowedGroups []string `flag:"allowed-group" cfg:"allowed_groups"` ApprovalPrompt string `flag:"approval-prompt" cfg:"approval_prompt"` // Deprecated by OIDC 1.0
AllowedRoles []string `flag:"allowed-role" cfg:"allowed_roles"` UserIDClaim string `flag:"user-id-claim" cfg:"user_id_claim"`
BackendLogoutURL string `flag:"backend-logout-url" cfg:"backend_logout_url"` 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"` AcrValues string `flag:"acr-values" cfg:"acr_values"`
JWTKey string `flag:"jwt-key" cfg:"jwt_key"` 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("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("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.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-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-groups-claim", OIDCGroupsClaim, "which OIDC claim contains the user groups")
flagSet.String("oidc-email-claim", OIDCEmailClaim, "which OIDC claim contains the user's email") 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, InsecureSkipNonce: &l.InsecureOIDCSkipNonce,
SkipDiscovery: &l.SkipOIDCDiscovery, SkipDiscovery: &l.SkipOIDCDiscovery,
LazyDiscovery: &l.OIDCLazyDiscovery, LazyDiscovery: &l.OIDCLazyDiscovery,
LazyDiscoveryInitialInterval: l.OIDCLazyDiscoveryInitialInterval,
LazyDiscoveryMaxInterval: l.OIDCLazyDiscoveryMaxInterval,
JwksURL: l.OIDCJwksURL, JwksURL: l.OIDCJwksURL,
UserIDClaim: l.UserIDClaim, UserIDClaim: l.UserIDClaim,
EmailClaim: l.OIDCEmailClaim, EmailClaim: l.OIDCEmailClaim,

View File

@ -44,6 +44,9 @@ var _ = Describe("Load", func() {
OIDCGroupsClaim: "groups", OIDCGroupsClaim: "groups",
OIDCAudienceClaims: []string{"aud"}, OIDCAudienceClaims: []string{"aud"},
InsecureOIDCSkipNonce: true, InsecureOIDCSkipNonce: true,
OIDCLazyDiscoveryInitialInterval: DefaultOIDCLazyDiscoveryInitialInterval,
OIDCLazyDiscoveryMaxInterval: DefaultOIDCLazyDiscoveryMaxInterval,
}, },
Options: Options{ Options: Options{

View File

@ -1,6 +1,10 @@
package options 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 ( const (
// OIDCEmailClaim is the generic email claim used by the OIDC provider. // 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). // When false, a failed OIDC discovery aborts startup (historical behaviour).
DefaultOIDCLazyDiscovery bool = false 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 // DefaultInsecureSkipNonce is the default value
// for OIDCOptions.InsecureSkipNonce // for OIDCOptions.InsecureSkipNonce
DefaultInsecureSkipNonce bool = true DefaultInsecureSkipNonce bool = true
@ -304,16 +316,17 @@ type OIDCOptions struct {
// SkipDiscovery allows to skip OIDC discovery and use manually supplied Endpoints // SkipDiscovery allows to skip OIDC discovery and use manually supplied Endpoints
// default set to 'false' // default set to 'false'
SkipDiscovery *bool `yaml:"skipDiscovery,omitempty"` SkipDiscovery *bool `yaml:"skipDiscovery,omitempty"`
// LazyDiscovery allows oauth2-proxy to start even when the OIDC issuer is // LazyDiscovery lets oauth2-proxy start when the OIDC issuer is unreachable,
// unreachable. When enabled, OIDC discovery is performed in the background // retrying discovery in the background instead of failing startup. OAuth2
// (retrying with backoff) instead of blocking startup, so features that do // login returns 503 until discovery succeeds; other features (e.g. Basic Auth)
// not depend on the provider - such as Basic Auth via htpasswdFile - remain // keep working. default set to 'false'
// 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 `yaml:"lazyDiscovery,omitempty"` 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 // JwksURL is the OpenID Connect JWKS URL
// eg: https://www.googleapis.com/oauth2/v3/certs // eg: https://www.googleapis.com/oauth2/v3/certs
JwksURL string `yaml:"jwksURL,omitempty"` JwksURL string `yaml:"jwksURL,omitempty"`
@ -412,6 +425,15 @@ func (o *OIDCOptions) EnsureDefaults() {
if o.LazyDiscovery == nil { if o.LazyDiscovery == nil {
o.LazyDiscovery = ptr.To(DefaultOIDCLazyDiscovery) 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 == "" { if o.UserIDClaim == "" {
o.UserIDClaim = OIDCEmailClaim o.UserIDClaim = OIDCEmailClaim
} }

View File

@ -18,12 +18,6 @@ import (
// keep working while discovery is pending. // keep working while discovery is pending.
var ErrProviderNotReady = errors.New("provider not ready: OIDC discovery has not completed yet") 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. // LazyProvider wraps a Provider whose construction depends on OIDC discovery.
// It starts out not-ready, delegating only Data() to a discovery-independent // It starts out not-ready, delegating only Data() to a discovery-independent
// placeholder, and returns ErrProviderNotReady from OAuth flow methods. Once // 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. // succeeds the real provider is swapped in and the LazyProvider becomes ready.
// It is intended to be run in a goroutine. // It is intended to be run in a goroutine.
func (l *LazyProvider) InitWithRetry(ctx context.Context) { 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 attempt := 0
for { for {
attempt++ attempt++
@ -100,8 +103,8 @@ func (l *LazyProvider) InitWithRetry(ctx context.Context) {
} }
interval *= 2 interval *= 2
if interval > lazyDiscoveryMaxInterval { if interval > maxInterval {
interval = lazyDiscoveryMaxInterval interval = maxInterval
} }
} }
} }