git.iko.soy/typeable/oauth2-proxy
on master help sign in
feat: readiness check (#1839)
f753ec1Kobi Meirson
CHANGELOG.md+2
@@ -12,8 +12,10 @@
12- [#1882](https://github.com/oauth2-proxy/oauth2-proxy/pull/1882) Make `htpasswd.GetUsers` racecondition safe
12- [#1882](https://github.com/oauth2-proxy/oauth2-proxy/pull/1882) Make `htpasswd.GetUsers` racecondition safe
13- [#1883](https://github.com/oauth2-proxy/oauth2-proxy/pull/1883) Ensure v8 manifest variant is set on docker images
13- [#1883](https://github.com/oauth2-proxy/oauth2-proxy/pull/1883) Ensure v8 manifest variant is set on docker images
14- [#1906](https://github.com/oauth2-proxy/oauth2-proxy/pull/1906) Fix PKCE code verifier generation to never use UTF-8 characters
14- [#1906](https://github.com/oauth2-proxy/oauth2-proxy/pull/1906) Fix PKCE code verifier generation to never use UTF-8 characters
15+- [#1839](https://github.com/oauth2-proxy/oauth2-proxy/pull/1839) Add readiness checks for deeper health checks (@kobim)
15- [#1927](https://github.com/oauth2-proxy/oauth2-proxy/pull/1927) Fix default scope settings for none oidc providers
16- [#1927](https://github.com/oauth2-proxy/oauth2-proxy/pull/1927) Fix default scope settings for none oidc providers
16
17
18+
17# V7.4.0
19# V7.4.0
18
20
19## Release Highlights
21## Release Highlights
contrib/local-environment/kubernetes/oauth2-proxy-example-full.yaml+2−2
@@ -449,11 +449,11 @@ spec:
449 timeoutSeconds: 1
449 timeoutSeconds: 1
450 readinessProbe:
450 readinessProbe:
451 httpGet:
451 httpGet:
452− path: /ping
452+ path: /ready
453 port: http
453 port: http
454 scheme: HTTP
454 scheme: HTTP
455 initialDelaySeconds: 0
455 initialDelaySeconds: 0
456− timeoutSeconds: 1
456+ timeoutSeconds: 5
457 successThreshold: 1
457 successThreshold: 1
458 periodSeconds: 10
458 periodSeconds: 10
459 resources:
459 resources:
contrib/oauth2-proxy_autocomplete.sh+1−1
@@ -24,7 +24,7 @@ _oauth2_proxy() {
24 COMPREPLY=( $(compgen -W 'X-Real-IP X-Forwarded-For X-ProxyUser-IP' -- ${cur}) )
24 COMPREPLY=( $(compgen -W 'X-Real-IP X-Forwarded-For X-ProxyUser-IP' -- ${cur}) )
25 return 0
25 return 0
26 ;;
26 ;;
27− --@(http-address|https-address|redirect-url|upstream|basic-auth-password|skip-auth-regex|flush-interval|extra-jwt-issuers|email-domain|whitelist-domain|trusted-ip|keycloak-group|azure-tenant|bitbucket-team|bitbucket-repository|github-org|github-team|github-repo|github-token|gitlab-group|github-user|google-group|google-admin-email|google-service-account-json|client-id|client_secret|banner|footer|proxy-prefix|ping-path|cookie-name|cookie-secret|cookie-domain|cookie-path|cookie-expire|cookie-refresh|cookie-samesite|redist-sentinel-master-name|redist-sentinel-connection-urls|redist-cluster-connection-urls|logging-max-size|logging-max-age|logging-max-backups|standard-logging-format|request-logging-format|exclude-logging-paths|auth-logging-format|oidc-issuer-url|oidc-jwks-url|login-url|redeem-url|profile-url|resource|validate-url|scope|approval-prompt|signature-key|acr-values|jwt-key|pubjwk-url|force-json-errors))
27+ --@(http-address|https-address|redirect-url|upstream|basic-auth-password|skip-auth-regex|flush-interval|extra-jwt-issuers|email-domain|whitelist-domain|trusted-ip|keycloak-group|azure-tenant|bitbucket-team|bitbucket-repository|github-org|github-team|github-repo|github-token|gitlab-group|github-user|google-group|google-admin-email|google-service-account-json|client-id|client_secret|banner|footer|proxy-prefix|ping-path|ready-path|cookie-name|cookie-secret|cookie-domain|cookie-path|cookie-expire|cookie-refresh|cookie-samesite|redist-sentinel-master-name|redist-sentinel-connection-urls|redist-cluster-connection-urls|logging-max-size|logging-max-age|logging-max-backups|standard-logging-format|request-logging-format|exclude-logging-paths|auth-logging-format|oidc-issuer-url|oidc-jwks-url|login-url|redeem-url|profile-url|resource|validate-url|scope|approval-prompt|signature-key|acr-values|jwt-key|pubjwk-url|force-json-errors))
28 return 0
28 return 0
29 ;;
29 ;;
30 esac
30 esac
docs/docs/configuration/overview.md+3−2
@@ -155,6 +155,7 @@ An example [oauth2-proxy.cfg](https://github.com/oauth2-proxy/oauth2-proxy/blob/
155| `--provider-display-name` | string | Override the provider's name with the given string; used for the sign-in page | (depends on provider) |
155| `--provider-display-name` | string | Override the provider's name with the given string; used for the sign-in page | (depends on provider) |
156| `--ping-path` | string | the ping endpoint that can be used for basic health checks | `"/ping"` |
156| `--ping-path` | string | the ping endpoint that can be used for basic health checks | `"/ping"` |
157| `--ping-user-agent` | string | a User-Agent that can be used for basic health checks | `""` (don't check user agent) |
157| `--ping-user-agent` | string | a User-Agent that can be used for basic health checks | `""` (don't check user agent) |
158+| `--ready-path` | string | the ready endpoint that can be used for deep health checks | `"/ready"` |
158| `--metrics-address` | string | the address prometheus metrics will be scraped from | `""` |
159| `--metrics-address` | string | the address prometheus metrics will be scraped from | `""` |
159| `--proxy-prefix` | string | the url root path that this proxy should be nested under (e.g. /`<oauth2>/sign_in`) | `"/oauth2"` |
160| `--proxy-prefix` | string | the url root path that this proxy should be nested under (e.g. /`<oauth2>/sign_in`) | `"/oauth2"` |
160| `--proxy-websockets` | bool | enables WebSocket proxying | true |
161| `--proxy-websockets` | bool | enables WebSocket proxying | true |
@@ -184,7 +185,7 @@ An example [oauth2-proxy.cfg](https://github.com/oauth2-proxy/oauth2-proxy/blob/
184| `--set-basic-auth` | bool | set HTTP Basic Auth information in response (useful in Nginx auth_request mode) | false |
185| `--set-basic-auth` | bool | set HTTP Basic Auth information in response (useful in Nginx auth_request mode) | false |
185| `--show-debug-on-error` | bool | show detailed error information on error pages (WARNING: this may contain sensitive information - do not use in production) | false |
186| `--show-debug-on-error` | bool | show detailed error information on error pages (WARNING: this may contain sensitive information - do not use in production) | false |
186| `--signature-key` | string | GAP-Signature request signature key (algorithm:secretkey) | |
187| `--signature-key` | string | GAP-Signature request signature key (algorithm:secretkey) | |
187−| `--silence-ping-logging` | bool | disable logging of requests to ping endpoint | false |
188+| `--silence-ping-logging` | bool | disable logging of requests to ping & ready endpoints | false |
188| `--skip-auth-preflight` | bool | will skip authentication for OPTIONS requests | false |
189| `--skip-auth-preflight` | bool | will skip authentication for OPTIONS requests | false |
189| `--skip-auth-regex` | string \| list | (DEPRECATED for `--skip-auth-route`) bypass authentication for requests paths that match (may be given multiple times) | |
190| `--skip-auth-regex` | string \| list | (DEPRECATED for `--skip-auth-route`) bypass authentication for requests paths that match (may be given multiple times) | |
190| `--skip-auth-route` | string \| list | bypass authentication for requests that match the method & path. Format: method=path_regex OR method!=path_regex. For all methods: path_regex OR !=path_regex | |
191| `--skip-auth-route` | string \| list | bypass authentication for requests that match the method & path. Format: method=path_regex OR method!=path_regex. For all methods: path_regex OR !=path_regex | |
@@ -246,7 +247,7 @@ There are three different types of logging: standard, authentication, and HTTP r
246
247
247Each type of logging has its own configurable format and variables. By default these formats are similar to the Apache Combined Log.
248Each type of logging has its own configurable format and variables. By default these formats are similar to the Apache Combined Log.
248
249
249−Logging of requests to the `/ping` endpoint (or using `--ping-user-agent`) can be disabled with `--silence-ping-logging` reducing log volume. This flag appends the `--ping-path` to `--exclude-logging-paths`.
250+Logging of requests to the `/ping` endpoint (or using `--ping-user-agent`) and the `/ready` endpoint can be disabled with `--silence-ping-logging` reducing log volume.
250
251
251### Auth Log Format
252### Auth Log Format
252Authentication logs are logs which are guaranteed to contain a username or email address of a user attempting to authenticate. These logs are output by default in the below format:
253Authentication logs are logs which are guaranteed to contain a username or email address of a user attempting to authenticate. These logs are output by default in the below format:
docs/docs/features/endpoints.md+1
@@ -7,6 +7,7 @@ OAuth2 Proxy responds directly to the following endpoints. All other endpoints w
7
7
8- /robots.txt - returns a 200 OK response that disallows all User-agents from all paths; see [robotstxt.org](http://www.robotstxt.org/) for more info
8- /robots.txt - returns a 200 OK response that disallows all User-agents from all paths; see [robotstxt.org](http://www.robotstxt.org/) for more info
9- /ping - returns a 200 OK response, which is intended for use with health checks
9- /ping - returns a 200 OK response, which is intended for use with health checks
10+- /ready - returns a 200 OK response if all the underlying connections (e.g., Redis store) are connected
10- /metrics - Metrics endpoint for Prometheus to scrape, serve on the address specified by `--metrics-address`, disabled by default
11- /metrics - Metrics endpoint for Prometheus to scrape, serve on the address specified by `--metrics-address`, disabled by default
11- /oauth2/sign_in - the login page, which also doubles as a sign out page (it clears cookies)
12- /oauth2/sign_in - the login page, which also doubles as a sign out page (it clears cookies)
12- /oauth2/sign_out - this URL is used to clear the session cookie
13- /oauth2/sign_out - this URL is used to clear the session cookie
oauthproxy.go+4−2
@@ -185,7 +185,7 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
185 return nil, err
185 return nil, err
186 }
186 }
187
187
188− preAuthChain, err := buildPreAuthChain(opts)
188+ preAuthChain, err := buildPreAuthChain(opts, sessionStore)
189 if err != nil {
189 if err != nil {
190 return nil, fmt.Errorf("could not build pre-auth chain: %v", err)
190 return nil, fmt.Errorf("could not build pre-auth chain: %v", err)
191 }
191 }
@@ -327,7 +327,7 @@ func (p *OAuthProxy) buildProxySubrouter(s *mux.Router) {
327// buildPreAuthChain constructs a chain that should process every request before
327// buildPreAuthChain constructs a chain that should process every request before
328// the OAuth2 Proxy authentication logic kicks in.
328// the OAuth2 Proxy authentication logic kicks in.
329// For example forcing HTTPS or health checks.
329// For example forcing HTTPS or health checks.
330−func buildPreAuthChain(opts *options.Options) (alice.Chain, error) {
330+func buildPreAuthChain(opts *options.Options, sessionStore sessionsapi.SessionStore) (alice.Chain, error) {
331 chain := alice.New(middleware.NewScope(opts.ReverseProxy, opts.Logging.RequestIDHeader))
331 chain := alice.New(middleware.NewScope(opts.ReverseProxy, opts.Logging.RequestIDHeader))
332
332
333 if opts.ForceHTTPS {
333 if opts.ForceHTTPS {
@@ -351,12 +351,14 @@ func buildPreAuthChain(opts *options.Options) (alice.Chain, error) {
351 if opts.Logging.SilencePing {
351 if opts.Logging.SilencePing {
352 chain = chain.Append(
352 chain = chain.Append(
353 middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents),
353 middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents),
354+ middleware.NewReadynessCheck(opts.ReadyPath, sessionStore),
354 middleware.NewRequestLogger(),
355 middleware.NewRequestLogger(),
355 )
356 )
356 } else {
357 } else {
357 chain = chain.Append(
358 chain = chain.Append(
358 middleware.NewRequestLogger(),
359 middleware.NewRequestLogger(),
359 middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents),
360 middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents),
361+ middleware.NewReadynessCheck(opts.ReadyPath, sessionStore),
360 )
362 )
361 }
363 }
362
364
pkg/apis/options/load_test.go+1
@@ -49,6 +49,7 @@ var _ = Describe("Load", func() {
49 Options: Options{
49 Options: Options{
50 ProxyPrefix: "/oauth2",
50 ProxyPrefix: "/oauth2",
51 PingPath: "/ping",
51 PingPath: "/ping",
52+ ReadyPath: "/ready",
52 RealClientIPHeader: "X-Real-IP",
53 RealClientIPHeader: "X-Real-IP",
53 ForceHTTPS: false,
54 ForceHTTPS: false,
54 Cookie: cookieDefaults(),
55 Cookie: cookieDefaults(),
pkg/apis/options/logging.go+1−1
@@ -43,7 +43,7 @@ func loggingFlagSet() *pflag.FlagSet {
43
43
44 flagSet.StringSlice("exclude-logging-path", []string{}, "Exclude logging requests to paths (eg: '/path1,/path2,/path3')")
44 flagSet.StringSlice("exclude-logging-path", []string{}, "Exclude logging requests to paths (eg: '/path1,/path2,/path3')")
45 flagSet.Bool("logging-local-time", true, "If the time in log files and backup filenames are local or UTC time")
45 flagSet.Bool("logging-local-time", true, "If the time in log files and backup filenames are local or UTC time")
46− flagSet.Bool("silence-ping-logging", false, "Disable logging of requests to ping endpoint")
46+ flagSet.Bool("silence-ping-logging", false, "Disable logging of requests to ping & ready endpoints")
47 flagSet.String("request-id-header", "X-Request-Id", "Request header to use as the request ID")
47 flagSet.String("request-id-header", "X-Request-Id", "Request header to use as the request ID")
48
48
49 flagSet.String("logging-filename", "", "File to log requests to, empty for stdout")
49 flagSet.String("logging-filename", "", "File to log requests to, empty for stdout")
pkg/apis/options/options.go+3
@@ -21,6 +21,7 @@ type Options struct {
21 ProxyPrefix string `flag:"proxy-prefix" cfg:"proxy_prefix"`
21 ProxyPrefix string `flag:"proxy-prefix" cfg:"proxy_prefix"`
22 PingPath string `flag:"ping-path" cfg:"ping_path"`
22 PingPath string `flag:"ping-path" cfg:"ping_path"`
23 PingUserAgent string `flag:"ping-user-agent" cfg:"ping_user_agent"`
23 PingUserAgent string `flag:"ping-user-agent" cfg:"ping_user_agent"`
24+ ReadyPath string `flag:"ready-path" cfg:"ready_path"`
24 ReverseProxy bool `flag:"reverse-proxy" cfg:"reverse_proxy"`
25 ReverseProxy bool `flag:"reverse-proxy" cfg:"reverse_proxy"`
25 RealClientIPHeader string `flag:"real-client-ip-header" cfg:"real_client_ip_header"`
26 RealClientIPHeader string `flag:"real-client-ip-header" cfg:"real_client_ip_header"`
26 TrustedIPs []string `flag:"trusted-ip" cfg:"trusted_ips"`
27 TrustedIPs []string `flag:"trusted-ip" cfg:"trusted_ips"`
@@ -96,6 +97,7 @@ func NewOptions() *Options {
96 ProxyPrefix: "/oauth2",
97 ProxyPrefix: "/oauth2",
97 Providers: providerDefaults(),
98 Providers: providerDefaults(),
98 PingPath: "/ping",
99 PingPath: "/ping",
100+ ReadyPath: "/ready",
99 RealClientIPHeader: "X-Real-IP",
101 RealClientIPHeader: "X-Real-IP",
100 ForceHTTPS: false,
102 ForceHTTPS: false,
101 Cookie: cookieDefaults(),
103 Cookie: cookieDefaults(),
@@ -133,6 +135,7 @@ func NewFlagSet() *pflag.FlagSet {
133 flagSet.String("proxy-prefix", "/oauth2", "the url root path that this proxy should be nested under (e.g. /<oauth2>/sign_in)")
135 flagSet.String("proxy-prefix", "/oauth2", "the url root path that this proxy should be nested under (e.g. /<oauth2>/sign_in)")
134 flagSet.String("ping-path", "/ping", "the ping endpoint that can be used for basic health checks")
136 flagSet.String("ping-path", "/ping", "the ping endpoint that can be used for basic health checks")
135 flagSet.String("ping-user-agent", "", "special User-Agent that will be used for basic health checks")
137 flagSet.String("ping-user-agent", "", "special User-Agent that will be used for basic health checks")
138+ flagSet.String("ready-path", "/ready", "the ready endpoint that can be used for deep health checks")
136 flagSet.String("session-store-type", "cookie", "the session storage provider to use")
139 flagSet.String("session-store-type", "cookie", "the session storage provider to use")
137 flagSet.Bool("session-cookie-minimal", false, "strip OAuth tokens from cookie session stores if they aren't needed (cookie session store only)")
140 flagSet.Bool("session-cookie-minimal", false, "strip OAuth tokens from cookie session stores if they aren't needed (cookie session store only)")
138 flagSet.String("redis-connection-url", "", "URL of redis server for redis session storage (eg: redis://HOST[:PORT])")
141 flagSet.String("redis-connection-url", "", "URL of redis server for redis session storage (eg: redis://HOST[:PORT])")
pkg/apis/sessions/interfaces.go+1
@@ -12,6 +12,7 @@ type SessionStore interface {
12 Save(rw http.ResponseWriter, req *http.Request, s *SessionState) error
12 Save(rw http.ResponseWriter, req *http.Request, s *SessionState) error
13 Load(req *http.Request) (*SessionState, error)
13 Load(req *http.Request) (*SessionState, error)
14 Clear(rw http.ResponseWriter, req *http.Request) error
14 Clear(rw http.ResponseWriter, req *http.Request) error
15+ VerifyConnection(ctx context.Context) error
15}
16}
16
17
17var ErrLockNotObtained = errors.New("lock: not obtained")
18var ErrLockNotObtained = errors.New("lock: not obtained")
pkg/middleware/readynesscheck.gonew file+40
@@ -0,0 +1,40 @@
1+package middleware
2+
3+import (
4+ "context"
5+ "fmt"
6+ "net/http"
7+
8+ "github.com/justinas/alice"
9+)
10+
11+// Verifiable an interface for an object that has a connection to external
12+// data source and exports a function to validate that connection
13+type Verifiable interface {
14+ VerifyConnection(context.Context) error
15+}
16+
17+// NewReadynessCheck returns a middleware that performs deep health checks
18+// (verifies the connection to any underlying store) on a specific `path`
19+func NewReadynessCheck(path string, verifiable Verifiable) alice.Constructor {
20+ return func(next http.Handler) http.Handler {
21+ return readynessCheck(path, verifiable, next)
22+ }
23+}
24+
25+func readynessCheck(path string, verifiable Verifiable, next http.Handler) http.Handler {
26+ return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
27+ if path != "" && req.URL.EscapedPath() == path {
28+ if err := verifiable.VerifyConnection(req.Context()); err != nil {
29+ rw.WriteHeader(http.StatusInternalServerError)
30+ fmt.Fprintf(rw, "error: %v", err)
31+ return
32+ }
33+ rw.WriteHeader(http.StatusOK)
34+ fmt.Fprintf(rw, "OK")
35+ return
36+ }
37+
38+ next.ServeHTTP(rw, req)
39+ })
40+}
pkg/middleware/readynesscheck_test.gonew file+84
@@ -0,0 +1,84 @@
1+package middleware
2+
3+import (
4+ "context"
5+ "errors"
6+ "net/http"
7+ "net/http/httptest"
8+
9+ . "github.com/onsi/ginkgo"
10+ . "github.com/onsi/ginkgo/extensions/table"
11+ . "github.com/onsi/gomega"
12+)
13+
14+var _ = Describe("ReadynessCheck suite", func() {
15+ type requestTableInput struct {
16+ readyPath string
17+ healthVerifiable Verifiable
18+ requestString string
19+ expectedStatus int
20+ expectedBody string
21+ }
22+
23+ DescribeTable("when serving a request",
24+ func(in *requestTableInput) {
25+ req := httptest.NewRequest("", in.requestString, nil)
26+
27+ rw := httptest.NewRecorder()
28+
29+ handler := NewReadynessCheck(in.readyPath, in.healthVerifiable)(http.NotFoundHandler())
30+ handler.ServeHTTP(rw, req)
31+
32+ Expect(rw.Code).To(Equal(in.expectedStatus))
33+ Expect(rw.Body.String()).To(Equal(in.expectedBody))
34+ },
35+ Entry("when requesting the readyness check path", &requestTableInput{
36+ readyPath: "/ready",
37+ healthVerifiable: &fakeVerifiable{nil},
38+ requestString: "http://example.com/ready",
39+ expectedStatus: 200,
40+ expectedBody: "OK",
41+ }),
42+ Entry("when requesting a different path", &requestTableInput{
43+ readyPath: "/ready",
44+ healthVerifiable: &fakeVerifiable{nil},
45+ requestString: "http://example.com/different",
46+ expectedStatus: 404,
47+ expectedBody: "404 page not found\n",
48+ }),
49+ Entry("when a blank string is configured as a readyness check path and the request has no specific path", &requestTableInput{
50+ readyPath: "",
51+ healthVerifiable: &fakeVerifiable{nil},
52+ requestString: "http://example.com",
53+ expectedStatus: 404,
54+ expectedBody: "404 page not found\n",
55+ }),
56+ Entry("with full health check and without an underlying error", &requestTableInput{
57+ readyPath: "/ready",
58+ healthVerifiable: &fakeVerifiable{nil},
59+ requestString: "http://example.com/ready",
60+ expectedStatus: 200,
61+ expectedBody: "OK",
62+ }),
63+ Entry("with full health check and with an underlying error", &requestTableInput{
64+ readyPath: "/ready",
65+ healthVerifiable: &fakeVerifiable{func(ctx context.Context) error { return errors.New("failed to check") }},
66+ requestString: "http://example.com/ready",
67+ expectedStatus: 500,
68+ expectedBody: "error: failed to check",
69+ }),
70+ )
71+})
72+
73+type fakeVerifiable struct {
74+ mock func(context.Context) error
75+}
76+
77+func (v *fakeVerifiable) VerifyConnection(ctx context.Context) error {
78+ if v.mock != nil {
79+ return v.mock(ctx)
80+ }
81+ return nil
82+}
83+
84+var _ Verifiable = (*fakeVerifiable)(nil)
pkg/middleware/stored_session_test.go+4
@@ -793,3 +793,7 @@ func (f *fakeSessionStore) Clear(rw http.ResponseWriter, req *http.Request) erro
793 }
793 }
794 return nil
794 return nil
795}
795}
796+
797+func (f *fakeSessionStore) VerifyConnection(_ context.Context) error {
798+ return nil
799+}
pkg/sessions/cookie/session_store.go+7
@@ -1,6 +1,7 @@
1package cookie
1package cookie
2
2
3import (
3import (
4+ "context"
4 "errors"
5 "errors"
5 "fmt"
6 "fmt"
6 "net/http"
7 "net/http"
@@ -82,6 +83,12 @@ func (s *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) error {
82 return nil
83 return nil
83}
84}
84
85
86+// VerifyConnection always return no-error, as there's no connection
87+// in this store
88+func (s *SessionStore) VerifyConnection(_ context.Context) error {
89+ return nil
90+}
91+
85// cookieForSession serializes a session state for storage in a cookie
92// cookieForSession serializes a session state for storage in a cookie
86func (s *SessionStore) cookieForSession(ss *sessions.SessionState) ([]byte, error) {
93func (s *SessionStore) cookieForSession(ss *sessions.SessionState) ([]byte, error) {
87 if s.Minimal && (ss.AccessToken != "" || ss.IDToken != "" || ss.RefreshToken != "") {
94 if s.Minimal && (ss.AccessToken != "" || ss.IDToken != "" || ss.RefreshToken != "") {
pkg/sessions/persistence/interfaces.go+1
@@ -15,4 +15,5 @@ type Store interface {
15 Load(context.Context, string) ([]byte, error)
15 Load(context.Context, string) ([]byte, error)
16 Clear(context.Context, string) error
16 Clear(context.Context, string) error
17 Lock(key string) sessions.Lock
17 Lock(key string) sessions.Lock
18+ VerifyConnection(context.Context) error
18}
19}
pkg/sessions/persistence/manager.go+6
@@ -1,6 +1,7 @@
1package persistence
1package persistence
2
2
3import (
3import (
4+ "context"
4 "fmt"
5 "fmt"
5 "net/http"
6 "net/http"
6 "time"
7 "time"
@@ -90,3 +91,8 @@ func (m *Manager) Clear(rw http.ResponseWriter, req *http.Request) error {
90 return m.Store.Clear(req.Context(), key)
91 return m.Store.Clear(req.Context(), key)
91 })
92 })
92}
93}
94+
95+// VerifyConnection validates the underlying store is ready and connected
96+func (m *Manager) VerifyConnection(ctx context.Context) error {
97+ return m.Store.VerifyConnection(ctx)
98+}
pkg/sessions/redis/client.go+9
@@ -14,6 +14,7 @@ type Client interface {
14 Lock(key string) sessions.Lock
14 Lock(key string) sessions.Lock
15 Set(ctx context.Context, key string, value []byte, expiration time.Duration) error
15 Set(ctx context.Context, key string, value []byte, expiration time.Duration) error
16 Del(ctx context.Context, key string) error
16 Del(ctx context.Context, key string) error
17+ Ping(ctx context.Context) error
17}
18}
18
19
19var _ Client = (*client)(nil)
20var _ Client = (*client)(nil)
@@ -44,6 +45,10 @@ func (c *client) Lock(key string) sessions.Lock {
44 return NewLock(c.Client, key)
45 return NewLock(c.Client, key)
45}
46}
46
47
48+func (c *client) Ping(ctx context.Context) error {
49+ return c.Client.Ping(ctx).Err()
50+}
51+
47var _ Client = (*clusterClient)(nil)
52var _ Client = (*clusterClient)(nil)
48
53
49type clusterClient struct {
54type clusterClient struct {
@@ -71,3 +76,7 @@ func (c *clusterClient) Del(ctx context.Context, key string) error {
71func (c *clusterClient) Lock(key string) sessions.Lock {
76func (c *clusterClient) Lock(key string) sessions.Lock {
72 return NewLock(c.ClusterClient, key)
77 return NewLock(c.ClusterClient, key)
73}
78}
79+
80+func (c *clusterClient) Ping(ctx context.Context) error {
81+ return c.ClusterClient.Ping(ctx).Err()
82+}
pkg/sessions/redis/client_test.gonew file+159
@@ -0,0 +1,159 @@
1+package redis_test
2+
3+import (
4+ "context"
5+ "encoding/base64"
6+ "time"
7+
8+ "github.com/alicebob/miniredis/v2"
9+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
10+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/encryption"
11+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/sessions/redis"
12+ . "github.com/onsi/ginkgo"
13+ . "github.com/onsi/gomega"
14+)
15+
16+var _ = Describe("Redis Client Tests", func() {
17+ Context("with basic client", func() {
18+ RunClientTests(func(mr *miniredis.Miniredis) options.RedisStoreOptions {
19+ return options.RedisStoreOptions{
20+ ConnectionURL: "redis://" + mr.Addr(),
21+ }
22+ })
23+ })
24+
25+ Context("with cluster client", func() {
26+ RunClientTests(func(mr *miniredis.Miniredis) options.RedisStoreOptions {
27+ return options.RedisStoreOptions{
28+ ClusterConnectionURLs: []string{"redis://" + mr.Addr()},
29+ UseCluster: true,
30+ }
31+ })
32+ })
33+})
34+
35+type getOptsFunc func(mr *miniredis.Miniredis) options.RedisStoreOptions
36+
37+func RunClientTests(getOptsFunc getOptsFunc) {
38+ var mr *miniredis.Miniredis
39+ var client redis.Client
40+ var err error
41+ var key string
42+ var ctx context.Context
43+
44+ BeforeEach(func() {
45+ mr, err = miniredis.Run()
46+ Expect(err).ToNot(HaveOccurred())
47+
48+ client, err = redis.NewRedisClient(getOptsFunc(mr))
49+ Expect(err).ToNot(HaveOccurred())
50+
51+ nonce, err := encryption.Nonce(32)
52+ Expect(err).ToNot(HaveOccurred())
53+ key = base64.RawURLEncoding.EncodeToString(nonce)
54+
55+ ctx = context.Background()
56+ })
57+
58+ AfterEach(func() {
59+ if mr != nil {
60+ mr.Close()
61+ mr = nil
62+ }
63+ })
64+
65+ Context("when Get is called", func() {
66+ expectedValue := []byte("value")
67+
68+ BeforeEach(func() {
69+ client.Set(context.Background(), key, expectedValue, time.Duration(1*time.Minute))
70+ })
71+
72+ It("returns the saved value", func() {
73+ value, err := client.Get(ctx, key)
74+ Expect(err).ToNot(HaveOccurred())
75+ Expect(value).To(Equal(value))
76+ })
77+
78+ It("does not return expired values", func() {
79+ mr.FastForward(5 * time.Minute)
80+
81+ _, err = client.Get(ctx, key)
82+ Expect(err).To(HaveOccurred())
83+ })
84+
85+ It("returns an error if value does not exist", func() {
86+ _, err = client.Get(ctx, "does-not-exists")
87+ Expect(err).To(HaveOccurred())
88+ })
89+ })
90+
91+ Context("using Lock", func() {
92+ It("maintains the lock", func() {
93+ lock := client.Lock(key)
94+
95+ err = lock.Obtain(ctx, 1*time.Minute)
96+ Expect(err).ToNot(HaveOccurred())
97+
98+ isLocked, err := lock.Peek(ctx)
99+ Expect(err).ToNot(HaveOccurred())
100+ Expect(isLocked).To(BeTrue())
101+
102+ err = lock.Release(ctx)
103+ Expect(err).ToNot(HaveOccurred())
104+ })
105+
106+ It("reflects non-locked instance", func() {
107+ lock := client.Lock(key)
108+
109+ isLocked, err := lock.Peek(ctx)
110+ Expect(err).ToNot(HaveOccurred())
111+ Expect(isLocked).To(BeFalse())
112+ })
113+ })
114+
115+ Context("when Set is called", func() {
116+ expectedValue := []byte("value")
117+
118+ It("sets the expected value", func() {
119+ err = client.Set(ctx, key, expectedValue, 1*time.Minute)
120+ Expect(err).ToNot(HaveOccurred())
121+
122+ value, err := client.Get(ctx, key)
123+ Expect(value).To(Equal(expectedValue))
124+ Expect(err).ToNot(HaveOccurred())
125+ })
126+ })
127+
128+ Context("when Del is called", func() {
129+ It("does not return an error when key exists", func() {
130+ err = client.Set(ctx, key, []byte("dummy"), 1*time.Minute)
131+ Expect(err).ToNot(HaveOccurred())
132+
133+ err = client.Del(ctx, key)
134+ Expect(err).ToNot(HaveOccurred())
135+
136+ _, err = client.Get(ctx, key)
137+ Expect(err).To(HaveOccurred())
138+ })
139+ })
140+
141+ Context("when Ping is called", func() {
142+ Context("when redis is up", func() {
143+ It("does not return an error", func() {
144+ err = client.Ping(ctx)
145+ Expect(err).ToNot(HaveOccurred())
146+ })
147+ })
148+
149+ Context("when redis is down", func() {
150+ It("returns an error", func() {
151+ mr.Close()
152+ mr = nil
153+
154+ err = client.Ping(ctx)
155+ Expect(err).To(HaveOccurred())
156+ })
157+ })
158+ })
159+}
pkg/sessions/redis/redis_store.go+8
@@ -70,6 +70,12 @@ func (store *SessionStore) Lock(key string) sessions.Lock {
70 return store.Client.Lock(key)
70 return store.Client.Lock(key)
71}
71}
72
72
73+// VerifyConnection verifies the redis connection is valid and the
74+// server is responsive
75+func (store *SessionStore) VerifyConnection(ctx context.Context) error {
76+ return store.Client.Ping(ctx)
77+}
78+
73// NewRedisClient makes a redis.Client (either standalone, sentinel aware, or
79// NewRedisClient makes a redis.Client (either standalone, sentinel aware, or
74// redis cluster)
80// redis cluster)
75func NewRedisClient(opts options.RedisStoreOptions) (Client, error) {
81func NewRedisClient(opts options.RedisStoreOptions) (Client, error) {
@@ -205,3 +211,5 @@ func parseRedisURLs(urls []string) ([]string, *redis.Options, error) {
205 }
211 }
206 return addrs, redisOptions, nil
212 return addrs, redisOptions, nil
207}
213}
214+
215+var _ persistence.Store = (*SessionStore)(nil)
pkg/sessions/redis/redis_store_test.go−27
@@ -2,20 +2,15 @@ package redis
2
2
3import (
3import (
4 "bytes"
4 "bytes"
5− "context"
6 "crypto/tls"
5 "crypto/tls"
7 "encoding/pem"
6 "encoding/pem"
8− "log"
9 "os"
7 "os"
10− "testing"
11 "time"
8 "time"
12
9
13 "github.com/Bose/minisentinel"
10 "github.com/Bose/minisentinel"
14 "github.com/alicebob/miniredis/v2"
11 "github.com/alicebob/miniredis/v2"
15− "github.com/go-redis/redis/v9"
16 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
12 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
17 sessionsapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
13 sessionsapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
18− "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
19 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/sessions/persistence"
14 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/sessions/persistence"
20 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/sessions/tests"
15 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/sessions/tests"
21 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util"
16 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util"
@@ -25,28 +20,6 @@ import (
25
20
26const redisPassword = "0123456789abcdefghijklmnopqrstuv"
21const redisPassword = "0123456789abcdefghijklmnopqrstuv"
27
22
28−// wrappedRedisLogger wraps a logger so that we can coerce the logger to
29−// fit the expected signature for go-redis logging
30−type wrappedRedisLogger struct {
31− *log.Logger
32−}
33−
34−func (l *wrappedRedisLogger) Printf(_ context.Context, format string, v ...interface{}) {
35− l.Logger.Printf(format, v...)
36−}
37−
38−func TestSessionStore(t *testing.T) {
39− logger.SetOutput(GinkgoWriter)
40− logger.SetErrOutput(GinkgoWriter)
41−
42− redisLogger := &wrappedRedisLogger{Logger: log.New(os.Stderr, "redis: ", log.LstdFlags|log.Lshortfile)}
43− redisLogger.SetOutput(GinkgoWriter)
44− redis.SetLogger(redisLogger)
45−
46− RegisterFailHandler(Fail)
47− RunSpecs(t, "Redis SessionStore")
48−}
49−
50var (
23var (
51 cert tls.Certificate
24 cert tls.Certificate
52 caPath string
25 caPath string
pkg/sessions/redis/redis_test.gonew file+35
@@ -0,0 +1,35 @@
1+package redis_test
2+
3+import (
4+ "context"
5+ "log"
6+ "os"
7+ "testing"
8+
9+ "github.com/go-redis/redis/v9"
10+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
11+ . "github.com/onsi/ginkgo"
12+ . "github.com/onsi/gomega"
13+)
14+
15+// wrappedRedisLogger wraps a logger so that we can coerce the logger to
16+// fit the expected signature for go-redis logging
17+type wrappedRedisLogger struct {
18+ *log.Logger
19+}
20+
21+func (l *wrappedRedisLogger) Printf(_ context.Context, format string, v ...interface{}) {
22+ l.Logger.Printf(format, v...)
23+}
24+
25+func TestRedis(t *testing.T) {
26+ logger.SetOutput(GinkgoWriter)
27+ logger.SetErrOutput(GinkgoWriter)
28+
29+ redisLogger := &wrappedRedisLogger{Logger: log.New(os.Stderr, "redis: ", log.LstdFlags|log.Lshortfile)}
30+ redisLogger.SetOutput(GinkgoWriter)
31+ redis.SetLogger(redisLogger)
32+
33+ RegisterFailHandler(Fail)
34+ RunSpecs(t, "Redis")
35+}
pkg/sessions/tests/mock_store.go+4
@@ -65,6 +65,10 @@ func (s *MockStore) Lock(key string) sessions.Lock {
65 return lock
65 return lock
66}
66}
67
67
68+func (s *MockStore) VerifyConnection(_ context.Context) error {
69+ return nil
70+}
71+
68// FastForward simulates the flow of time to test expirations
72// FastForward simulates the flow of time to test expirations
69func (s *MockStore) FastForward(duration time.Duration) {
73func (s *MockStore) FastForward(duration time.Duration) {
70 for _, mockLock := range s.lockCache {
74 for _, mockLock := range s.lockCache {
pkg/sessions/tests/session_store_tests.go+6
@@ -485,6 +485,12 @@ func SessionStoreInterfaceTests(in *testInput) {
485 })
485 })
486 })
486 })
487 })
487 })
488+
489+ Context("when VerifyConnection is called", func() {
490+ It("should return without an error", func() {
491+ Expect(in.ss().VerifyConnection(in.request.Context())).ToNot(HaveOccurred())
492+ })
493+ })
488}
494}
489
495
490func LoadSessionTests(in *testInput) {
496func LoadSessionTests(in *testInput) {