Merge c57b2c6282 into 1f049e5ebd
This commit is contained in:
commit
246e7d637e
|
|
@ -8,6 +8,8 @@
|
||||||
|
|
||||||
## 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 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
|
||||||
|
|
||||||
## Release Highlights
|
## Release Highlights
|
||||||
|
|
|
||||||
|
|
@ -544,6 +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 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' |
|
||||||
|
|
|
||||||
|
|
@ -98,6 +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 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. | |
|
||||||
|
|
|
||||||
|
|
@ -187,6 +187,9 @@ redirect_url="http://localhost:4180/oauth2/callback"
|
||||||
InsecureAllowUnverifiedEmail: ptr.To(false),
|
InsecureAllowUnverifiedEmail: ptr.To(false),
|
||||||
InsecureSkipIssuerVerification: ptr.To(false),
|
InsecureSkipIssuerVerification: ptr.To(false),
|
||||||
SkipDiscovery: ptr.To(false),
|
SkipDiscovery: ptr.To(false),
|
||||||
|
LazyDiscovery: ptr.To(false),
|
||||||
|
LazyDiscoveryInitialInterval: options.DefaultOIDCLazyDiscoveryInitialInterval,
|
||||||
|
LazyDiscoveryMaxInterval: options.DefaultOIDCLazyDiscoveryMaxInterval,
|
||||||
EnabledSigningAlgs: []string{},
|
EnabledSigningAlgs: []string{},
|
||||||
},
|
},
|
||||||
MicrosoftEntraIDConfig: options.MicrosoftEntraIDOptions{
|
MicrosoftEntraIDConfig: options.MicrosoftEntraIDOptions{
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import (
|
||||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/encryption"
|
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/encryption"
|
||||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/proxyhttp"
|
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/proxyhttp"
|
||||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util"
|
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util"
|
||||||
|
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util/ptr"
|
||||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/version"
|
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/version"
|
||||||
|
|
||||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
|
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
|
||||||
|
|
@ -95,6 +96,10 @@ type OAuthProxy struct {
|
||||||
relativeRedirectURL bool
|
relativeRedirectURL bool
|
||||||
whitelistDomains []string
|
whitelistDomains []string
|
||||||
provider providers.Provider
|
provider providers.Provider
|
||||||
|
// lazyProvider is non-nil only when the provider is being initialised in the
|
||||||
|
// background (see --oidc-lazy-discovery). Its background discovery loop is
|
||||||
|
// started in Start so it can be tied to the proxy's shutdown context.
|
||||||
|
lazyProvider *providers.LazyProvider
|
||||||
sessionStore sessionsapi.SessionStore
|
sessionStore sessionsapi.SessionStore
|
||||||
ProxyPrefix string
|
ProxyPrefix string
|
||||||
basicAuthValidator basic.Validator
|
basicAuthValidator basic.Validator
|
||||||
|
|
@ -137,10 +142,13 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
provider, err := providers.NewProvider(opts.Providers[0])
|
provider, err := setupProvider(opts.Providers[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error initialising provider: %v", err)
|
return nil, fmt.Errorf("error initialising provider: %v", err)
|
||||||
}
|
}
|
||||||
|
// A LazyProvider needs its background discovery loop started; it is launched
|
||||||
|
// in Start so it can be cancelled on shutdown (see p.lazyProvider usage).
|
||||||
|
lazyProvider, _ := provider.(*providers.LazyProvider)
|
||||||
|
|
||||||
pageWriter, err := pagewriter.NewWriter(pagewriter.Opts{
|
pageWriter, err := pagewriter.NewWriter(pagewriter.Opts{
|
||||||
TemplatesPath: opts.Templates.Path,
|
TemplatesPath: opts.Templates.Path,
|
||||||
|
|
@ -229,6 +237,7 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
|
||||||
|
|
||||||
ProxyPrefix: opts.ProxyPrefix,
|
ProxyPrefix: opts.ProxyPrefix,
|
||||||
provider: provider,
|
provider: provider,
|
||||||
|
lazyProvider: lazyProvider,
|
||||||
sessionStore: sessionStore,
|
sessionStore: sessionStore,
|
||||||
redirectURL: redirectURL,
|
redirectURL: redirectURL,
|
||||||
relativeRedirectURL: opts.RelativeRedirectURL,
|
relativeRedirectURL: opts.RelativeRedirectURL,
|
||||||
|
|
@ -280,6 +289,12 @@ func (p *OAuthProxy) Start() error {
|
||||||
cancel() // cancel the context
|
cancel() // cancel the context
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// When lazy OIDC discovery is enabled, perform discovery in the background
|
||||||
|
// and tie its lifetime to the proxy's shutdown context so it stops cleanly.
|
||||||
|
if p.lazyProvider != nil {
|
||||||
|
go p.lazyProvider.InitWithRetry(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
return p.server.Start(ctx)
|
return p.server.Start(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -478,6 +493,60 @@ func buildProviderName(p providers.Provider, override string) string {
|
||||||
return p.Data().ProviderName
|
return p.Data().ProviderName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setupProvider constructs the identity provider. When OIDC discovery fails and
|
||||||
|
// lazy discovery is enabled for an OIDC-based provider, it returns a
|
||||||
|
// providers.LazyProvider that serves OIDC flows as "not ready" while background
|
||||||
|
// discovery is performed (started by Start) - so Basic Auth and other
|
||||||
|
// provider-independent features keep working. Otherwise a discovery failure is
|
||||||
|
// returned to the caller and aborts startup, preserving the historical
|
||||||
|
// behaviour.
|
||||||
|
func setupProvider(providerConfig options.Provider) (providers.Provider, error) {
|
||||||
|
provider, err := providers.NewProvider(providerConfig)
|
||||||
|
if err == nil {
|
||||||
|
return provider, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lazyEnabled := ptr.Deref(providerConfig.OIDCConfig.LazyDiscovery, options.DefaultOIDCLazyDiscovery)
|
||||||
|
skipDiscovery := ptr.Deref(providerConfig.OIDCConfig.SkipDiscovery, options.DefaultSkipDiscovery)
|
||||||
|
needsVerifier, verifierErr := providers.ProviderRequiresOIDCProviderVerifier(providerConfig.Type)
|
||||||
|
if verifierErr != nil {
|
||||||
|
return nil, verifierErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only defer to lazy initialisation when OIDC discovery is the failing step:
|
||||||
|
// the user must have opted in, discovery must be enabled, and the provider
|
||||||
|
// must actually use OIDC discovery.
|
||||||
|
if !lazyEnabled || skipDiscovery || !needsVerifier {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy, lazyErr := providers.NewLazyProvider(providerConfig)
|
||||||
|
if lazyErr != nil {
|
||||||
|
// If even the discovery-independent placeholder cannot be built, the
|
||||||
|
// failure is a genuine configuration error rather than an unreachable
|
||||||
|
// issuer, so surface the original error and abort startup.
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Errorf("OIDC discovery failed at startup; continuing with lazy discovery: %v", err)
|
||||||
|
logger.Printf("WARNING: with --oidc-lazy-discovery, discovery is retried indefinitely in the background. " +
|
||||||
|
"If the failure above is a configuration error (e.g. issuer mismatch or no common signing algorithms) " +
|
||||||
|
"rather than an unreachable issuer, the provider will never become ready - watch the readiness endpoint and logs.")
|
||||||
|
return lazy, 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. 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()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// 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)
|
||||||
|
|
@ -823,6 +892,15 @@ func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *OAuthProxy) doOAuthStart(rw http.ResponseWriter, req *http.Request, overrides url.Values) {
|
func (p *OAuthProxy) doOAuthStart(rw http.ResponseWriter, req *http.Request, overrides url.Values) {
|
||||||
|
// With lazy OIDC discovery the provider may not be ready yet. Returning an
|
||||||
|
// error here avoids a silent self-redirect loop (GetLoginURL would be empty)
|
||||||
|
// and keeps provider-independent auth (e.g. Basic Auth) usable meanwhile.
|
||||||
|
if !p.providerReady() {
|
||||||
|
logger.Errorf("cannot start OAuth2 login flow: identity provider is not ready (OIDC discovery is still pending)")
|
||||||
|
p.ErrorPage(rw, req, http.StatusServiceUnavailable, "The identity provider is not ready yet. Please try again shortly.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
extraParams := p.provider.Data().LoginURLParams(overrides)
|
extraParams := p.provider.Data().LoginURLParams(overrides)
|
||||||
prepareNoCache(rw)
|
prepareNoCache(rw)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
|
||||||
|
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util/ptr"
|
||||||
|
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/validation"
|
||||||
|
"github.com/oauth2-proxy/oauth2-proxy/v7/providers"
|
||||||
|
. "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",
|
||||||
|
Type: "oidc",
|
||||||
|
ClientID: "client-id",
|
||||||
|
OIDCConfig: options.OIDCOptions{
|
||||||
|
IssuerURL: "http://127.0.0.1:1/realms/test",
|
||||||
|
SkipDiscovery: ptr.To(false),
|
||||||
|
LazyDiscovery: ptr.To(lazy),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetupProviderLazyFallback(t *testing.T) {
|
||||||
|
g := NewWithT(t)
|
||||||
|
|
||||||
|
// setupProvider does not start the background loop (Start does), so there is
|
||||||
|
// no goroutine to cancel here.
|
||||||
|
provider, err := setupProvider(unreachableOIDCProvider(true))
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
lazy, ok := provider.(*providers.LazyProvider)
|
||||||
|
g.Expect(ok).To(BeTrue(), "expected a LazyProvider when discovery fails and lazy discovery is enabled")
|
||||||
|
g.Expect(lazy.Ready()).To(BeFalse())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetupProviderWithoutLazyFailsFast(t *testing.T) {
|
||||||
|
g := NewWithT(t)
|
||||||
|
|
||||||
|
_, err := setupProvider(unreachableOIDCProvider(false))
|
||||||
|
g.Expect(err).To(HaveOccurred())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
proxy := newLazyPendingProxy(g)
|
||||||
|
|
||||||
|
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
|
||||||
|
// returns 503 (rather than a silent self-redirect loop) while lazy discovery is
|
||||||
|
// still pending.
|
||||||
|
func TestOAuthStartUnavailableWhenProviderNotReady(t *testing.T) {
|
||||||
|
g := NewWithT(t)
|
||||||
|
|
||||||
|
proxy := newLazyPendingProxy(g)
|
||||||
|
|
||||||
|
rw := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest(http.MethodGet, "/oauth2/start", nil)
|
||||||
|
proxy.ServeHTTP(rw, req)
|
||||||
|
|
||||||
|
g.Expect(rw.Code).To(Equal(http.StatusServiceUnavailable))
|
||||||
|
}
|
||||||
|
|
@ -540,6 +540,9 @@ type LegacyProvider struct {
|
||||||
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"`
|
||||||
|
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"`
|
OIDCJwksURL string `flag:"oidc-jwks-url" cfg:"oidc_jwks_url"`
|
||||||
OIDCEmailClaim string `flag:"oidc-email-claim" cfg:"oidc_email_claim"`
|
OIDCEmailClaim string `flag:"oidc-email-claim" cfg:"oidc_email_claim"`
|
||||||
OIDCGroupsClaim string `flag:"oidc-groups-claim" cfg:"oidc_groups_claim"`
|
OIDCGroupsClaim string `flag:"oidc-groups-claim" cfg:"oidc_groups_claim"`
|
||||||
|
|
@ -602,6 +605,9 @@ func legacyProviderFlagSet() *pflag.FlagSet {
|
||||||
flagSet.Bool("insecure-oidc-skip-issuer-verification", false, "Do not verify if issuer matches OIDC discovery URL")
|
flagSet.Bool("insecure-oidc-skip-issuer-verification", false, "Do not verify if issuer matches OIDC discovery URL")
|
||||||
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.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")
|
||||||
|
|
@ -723,6 +729,9 @@ func (l *LegacyProvider) convert() (Providers, error) {
|
||||||
InsecureSkipIssuerVerification: &l.InsecureOIDCSkipIssuerVerification,
|
InsecureSkipIssuerVerification: &l.InsecureOIDCSkipIssuerVerification,
|
||||||
InsecureSkipNonce: &l.InsecureOIDCSkipNonce,
|
InsecureSkipNonce: &l.InsecureOIDCSkipNonce,
|
||||||
SkipDiscovery: &l.SkipOIDCDiscovery,
|
SkipDiscovery: &l.SkipOIDCDiscovery,
|
||||||
|
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,
|
||||||
|
|
|
||||||
|
|
@ -951,6 +951,7 @@ var _ = Describe("Legacy Options", func() {
|
||||||
|
|
||||||
defaultOIDCOptions := OIDCOptions{
|
defaultOIDCOptions := OIDCOptions{
|
||||||
SkipDiscovery: ptr.To(false),
|
SkipDiscovery: ptr.To(false),
|
||||||
|
LazyDiscovery: ptr.To(false),
|
||||||
InsecureSkipNonce: ptr.To(false),
|
InsecureSkipNonce: ptr.To(false),
|
||||||
InsecureAllowUnverifiedEmail: ptr.To(false),
|
InsecureAllowUnverifiedEmail: ptr.To(false),
|
||||||
InsecureSkipIssuerVerification: ptr.To(false),
|
InsecureSkipIssuerVerification: ptr.To(false),
|
||||||
|
|
|
||||||
|
|
@ -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{
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -13,6 +17,18 @@ const (
|
||||||
// for OIDCOptions.SkipDiscovery
|
// for OIDCOptions.SkipDiscovery
|
||||||
DefaultSkipDiscovery bool = false
|
DefaultSkipDiscovery bool = false
|
||||||
|
|
||||||
|
// DefaultOIDCLazyDiscovery is the default value for OIDCOptions.LazyDiscovery.
|
||||||
|
// 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
|
// DefaultInsecureSkipNonce is the default value
|
||||||
// for OIDCOptions.InsecureSkipNonce
|
// for OIDCOptions.InsecureSkipNonce
|
||||||
DefaultInsecureSkipNonce bool = true
|
DefaultInsecureSkipNonce bool = true
|
||||||
|
|
@ -300,6 +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 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
|
// 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"`
|
||||||
|
|
@ -349,6 +376,7 @@ func providerDefaults() Providers {
|
||||||
InsecureAllowUnverifiedEmail: ptr.To(DefaultInsecureAllowUnverifiedEmail),
|
InsecureAllowUnverifiedEmail: ptr.To(DefaultInsecureAllowUnverifiedEmail),
|
||||||
InsecureSkipNonce: ptr.To(DefaultInsecureSkipNonce),
|
InsecureSkipNonce: ptr.To(DefaultInsecureSkipNonce),
|
||||||
SkipDiscovery: ptr.To(DefaultSkipDiscovery),
|
SkipDiscovery: ptr.To(DefaultSkipDiscovery),
|
||||||
|
LazyDiscovery: ptr.To(DefaultOIDCLazyDiscovery),
|
||||||
UserIDClaim: OIDCEmailClaim, // Deprecated: Use OIDCEmailClaim
|
UserIDClaim: OIDCEmailClaim, // Deprecated: Use OIDCEmailClaim
|
||||||
EmailClaim: OIDCEmailClaim,
|
EmailClaim: OIDCEmailClaim,
|
||||||
GroupsClaim: OIDCGroupsClaim,
|
GroupsClaim: OIDCGroupsClaim,
|
||||||
|
|
@ -394,6 +422,18 @@ func (o *OIDCOptions) EnsureDefaults() {
|
||||||
if o.SkipDiscovery == nil {
|
if o.SkipDiscovery == nil {
|
||||||
o.SkipDiscovery = ptr.To(DefaultSkipDiscovery)
|
o.SkipDiscovery = ptr.To(DefaultSkipDiscovery)
|
||||||
}
|
}
|
||||||
|
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 == "" {
|
if o.UserIDClaim == "" {
|
||||||
o.UserIDClaim = OIDCEmailClaim
|
o.UserIDClaim = OIDCEmailClaim
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package options
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("OIDCOptions EnsureDefaults", func() {
|
||||||
|
It("defaults LazyDiscovery to false when unset", func() {
|
||||||
|
o := &OIDCOptions{}
|
||||||
|
o.EnsureDefaults()
|
||||||
|
|
||||||
|
Expect(o.LazyDiscovery).ToNot(BeNil())
|
||||||
|
Expect(*o.LazyDiscovery).To(Equal(DefaultOIDCLazyDiscovery))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,176 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/oauth2-proxy/mockoidc"
|
||||||
|
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
|
||||||
|
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util/ptr"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
)
|
||||||
|
|
||||||
|
// unreachableLazyConfig builds an OIDC provider config pointing at an
|
||||||
|
// unreachable issuer, with lazy discovery enabled.
|
||||||
|
func unreachableLazyConfig() options.Provider {
|
||||||
|
return options.Provider{
|
||||||
|
ID: providerID,
|
||||||
|
Type: "oidc",
|
||||||
|
ClientID: clientID,
|
||||||
|
OIDCConfig: options.OIDCOptions{
|
||||||
|
// Port 1 is not listenable, so discovery fails fast.
|
||||||
|
IssuerURL: "http://127.0.0.1:1/realms/test",
|
||||||
|
SkipDiscovery: ptr.To(false),
|
||||||
|
LazyDiscovery: ptr.To(true),
|
||||||
|
AudienceClaims: []string{"aud"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLazyProviderNotReadyGating(t *testing.T) {
|
||||||
|
g := NewWithT(t)
|
||||||
|
|
||||||
|
lazy, err := NewLazyProvider(unreachableLazyConfig())
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
// Not ready until background discovery completes.
|
||||||
|
g.Expect(lazy.Ready()).To(BeFalse())
|
||||||
|
|
||||||
|
// Data() serves the discovery-independent placeholder.
|
||||||
|
g.Expect(lazy.Data()).ToNot(BeNil())
|
||||||
|
g.Expect(lazy.Data().ProviderName).To(Equal("OpenID Connect"))
|
||||||
|
|
||||||
|
// OAuth-flow methods report not-ready rather than panicking.
|
||||||
|
g.Expect(lazy.GetLoginURL("https://rd", "", "", url.Values{})).To(BeEmpty())
|
||||||
|
g.Expect(lazy.ValidateSession(context.Background(), nil)).To(BeFalse())
|
||||||
|
|
||||||
|
_, err = lazy.Redeem(context.Background(), "https://rd", "code", "")
|
||||||
|
g.Expect(err).To(MatchError(ErrProviderNotReady))
|
||||||
|
|
||||||
|
_, err = lazy.GetEmailAddress(context.Background(), nil)
|
||||||
|
g.Expect(err).To(MatchError(ErrProviderNotReady))
|
||||||
|
|
||||||
|
err = lazy.EnrichSession(context.Background(), nil)
|
||||||
|
g.Expect(err).To(MatchError(ErrProviderNotReady))
|
||||||
|
|
||||||
|
_, err = lazy.Authorize(context.Background(), nil)
|
||||||
|
g.Expect(err).To(MatchError(ErrProviderNotReady))
|
||||||
|
|
||||||
|
_, err = lazy.RefreshSession(context.Background(), nil)
|
||||||
|
g.Expect(err).To(MatchError(ErrProviderNotReady))
|
||||||
|
|
||||||
|
_, err = lazy.CreateSessionFromToken(context.Background(), "token")
|
||||||
|
g.Expect(err).To(MatchError(ErrProviderNotReady))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLazyProviderBecomesReady(t *testing.T) {
|
||||||
|
g := NewWithT(t)
|
||||||
|
|
||||||
|
m, err := mockoidc.Run()
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
defer func() {
|
||||||
|
g.Expect(m.Shutdown()).To(Succeed())
|
||||||
|
}()
|
||||||
|
|
||||||
|
providerConfig := options.Provider{
|
||||||
|
ID: providerID,
|
||||||
|
Type: "oidc",
|
||||||
|
ClientID: m.Config().ClientID,
|
||||||
|
OIDCConfig: options.OIDCOptions{
|
||||||
|
IssuerURL: m.Issuer(),
|
||||||
|
SkipDiscovery: ptr.To(false),
|
||||||
|
LazyDiscovery: ptr.To(true),
|
||||||
|
AudienceClaims: []string{"aud"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy, err := NewLazyProvider(providerConfig)
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
g.Expect(lazy.Ready()).To(BeFalse())
|
||||||
|
|
||||||
|
// The issuer is reachable, so discovery succeeds on the first attempt and
|
||||||
|
// InitWithRetry returns promptly.
|
||||||
|
lazy.InitWithRetry(context.Background())
|
||||||
|
|
||||||
|
g.Expect(lazy.Ready()).To(BeTrue())
|
||||||
|
// Once ready, delegation to the real provider works.
|
||||||
|
g.Expect(lazy.GetLoginURL("https://rd", "", "nonce", url.Values{})).ToNot(BeEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLazyProviderInitWithRetryStopsOnContextCancel(t *testing.T) {
|
||||||
|
g := NewWithT(t)
|
||||||
|
|
||||||
|
lazy, err := NewLazyProvider(unreachableLazyConfig())
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
lazy.InitWithRetry(ctx)
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
g.Eventually(done, "2s").Should(BeClosed())
|
||||||
|
g.Expect(lazy.Ready()).To(BeFalse())
|
||||||
|
}
|
||||||
|
|
@ -37,6 +37,25 @@ func NewProvider(providerConfig options.Provider) (Provider, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not create provider data: %v", err)
|
return nil, fmt.Errorf("could not create provider data: %v", err)
|
||||||
}
|
}
|
||||||
|
return providerFromData(providerConfig, providerData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// newPlaceholderProvider constructs a Provider without performing OIDC
|
||||||
|
// discovery. The resulting provider has the correct display name and defaults
|
||||||
|
// but no verifier or discovered endpoints, so it must not be used to serve OIDC
|
||||||
|
// flows. It is used as the initial value of a LazyProvider until background
|
||||||
|
// discovery completes.
|
||||||
|
func newPlaceholderProvider(providerConfig options.Provider) (Provider, error) {
|
||||||
|
providerData, err := buildProviderData(providerConfig, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not create provider data: %v", err)
|
||||||
|
}
|
||||||
|
return providerFromData(providerConfig, providerData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// providerFromData constructs the concrete Provider implementation for the
|
||||||
|
// configured provider type from an already-built ProviderData.
|
||||||
|
func providerFromData(providerConfig options.Provider, providerData *ProviderData) (Provider, error) {
|
||||||
switch providerConfig.Type {
|
switch providerConfig.Type {
|
||||||
case options.ADFSProvider:
|
case options.ADFSProvider:
|
||||||
return NewADFSProvider(providerData, providerConfig), nil
|
return NewADFSProvider(providerData, providerConfig), nil
|
||||||
|
|
@ -78,6 +97,15 @@ func NewProvider(providerConfig options.Provider) (Provider, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func newProviderDataFromConfig(providerConfig options.Provider) (*ProviderData, error) {
|
func newProviderDataFromConfig(providerConfig options.Provider) (*ProviderData, error) {
|
||||||
|
return buildProviderData(providerConfig, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildProviderData builds the ProviderData for the given configuration. When
|
||||||
|
// skipDiscovery is true, the OIDC discovery step (which reaches out to the
|
||||||
|
// issuer over the network) is skipped, leaving the Verifier and discovered
|
||||||
|
// endpoints unset. This is used to build a placeholder provider for lazy
|
||||||
|
// initialisation, so oauth2-proxy can start before the issuer is reachable.
|
||||||
|
func buildProviderData(providerConfig options.Provider, skipDiscovery bool) (*ProviderData, error) {
|
||||||
p := &ProviderData{
|
p := &ProviderData{
|
||||||
Scope: providerConfig.Scope,
|
Scope: providerConfig.Scope,
|
||||||
ClientID: providerConfig.ClientID,
|
ClientID: providerConfig.ClientID,
|
||||||
|
|
@ -87,12 +115,12 @@ func newProviderDataFromConfig(providerConfig options.Provider) (*ProviderData,
|
||||||
AdditionalClaims: providerConfig.AdditionalClaims,
|
AdditionalClaims: providerConfig.AdditionalClaims,
|
||||||
}
|
}
|
||||||
|
|
||||||
needsVerifier, err := providerRequiresOIDCProviderVerifier(providerConfig.Type)
|
needsVerifier, err := ProviderRequiresOIDCProviderVerifier(providerConfig.Type)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if needsVerifier {
|
if needsVerifier && !skipDiscovery {
|
||||||
pv, err := internaloidc.NewProviderVerifier(context.TODO(), internaloidc.ProviderVerifierOptions{
|
pv, err := internaloidc.NewProviderVerifier(context.TODO(), internaloidc.ProviderVerifierOptions{
|
||||||
AudienceClaims: providerConfig.OIDCConfig.AudienceClaims,
|
AudienceClaims: providerConfig.OIDCConfig.AudienceClaims,
|
||||||
ClientID: providerConfig.ClientID,
|
ClientID: providerConfig.ClientID,
|
||||||
|
|
@ -187,7 +215,10 @@ func parseCodeChallengeMethod(providerConfig options.Provider) string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func providerRequiresOIDCProviderVerifier(providerType options.ProviderType) (bool, error) {
|
// ProviderRequiresOIDCProviderVerifier reports whether the given provider type
|
||||||
|
// relies on the OIDC ProviderVerifier (and therefore on OIDC discovery when it
|
||||||
|
// is enabled).
|
||||||
|
func ProviderRequiresOIDCProviderVerifier(providerType options.ProviderType) (bool, error) {
|
||||||
switch providerType {
|
switch providerType {
|
||||||
case options.BitbucketProvider, options.DigitalOceanProvider, options.FacebookProvider, options.GitHubProvider,
|
case options.BitbucketProvider, options.DigitalOceanProvider, options.FacebookProvider, options.GitHubProvider,
|
||||||
options.GoogleProvider, options.KeycloakProvider, options.LinkedInProvider, options.LoginGovProvider,
|
options.GoogleProvider, options.KeycloakProvider, options.LinkedInProvider, options.LoginGovProvider,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue