git.iko.soy/typeable/oauth2-proxy
on master help sign in
feat: readiness check (#1839)

* feat: readiness check * fix: no need for query param * docs: add a note * chore: move the readyness check to its own endpoint * docs(cr): add godoc Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk>

commitf753ec1ca5c42cd4c4e400afa1f76817638f3010 authorKobi Meirson <kobi@meirson.com> dateFri 23 Dec 2022, 11:08 (4 years ago) parent8b77c97
23 files changed+382−35
CHANGELOG.md+2 contrib/local-environment/kubernetes/oauth2-proxy-example-full.yaml+2−2 contrib/oauth2-proxy_autocomplete.sh+1−1 docs/docs/configuration/overview.md+3−2 docs/docs/features/endpoints.md+1 oauthproxy.go+4−2 pkg/apis/options/load_test.go+1 pkg/apis/options/logging.go+1−1 pkg/apis/options/options.go+3 pkg/apis/sessions/interfaces.go+1 pkg/middleware/readynesscheck.go+40 pkg/middleware/readynesscheck_test.go+84 pkg/middleware/stored_session_test.go+4 pkg/sessions/cookie/session_store.go+7 pkg/sessions/persistence/interfaces.go+1 pkg/sessions/persistence/manager.go+6 pkg/sessions/redis/client.go+9 pkg/sessions/redis/client_test.go+159 pkg/sessions/redis/redis_store.go+8 pkg/sessions/redis/redis_store_test.go−27 pkg/sessions/redis/redis_test.go+35 pkg/sessions/tests/mock_store.go+4 pkg/sessions/tests/session_store_tests.go+6
CHANGELOG.md+2
@@ -12,8 +12,10 @@
1212- [#1882](https://github.com/oauth2-proxy/oauth2-proxy/pull/1882) Make `htpasswd.GetUsers` racecondition safe
1313- [#1883](https://github.com/oauth2-proxy/oauth2-proxy/pull/1883) Ensure v8 manifest variant is set on docker images
1414- [#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)
1516- [#1927](https://github.com/oauth2-proxy/oauth2-proxy/pull/1927) Fix default scope settings for none oidc providers
1617
18+
1719# V7.4.0
1820
1921## Release Highlights
contrib/local-environment/kubernetes/oauth2-proxy-example-full.yaml+2−2
@@ -449,11 +449,11 @@ spec:
449449 timeoutSeconds: 1
450450 readinessProbe:
451451 httpGet:
452− path: /ping
452+ path: /ready
453453 port: http
454454 scheme: HTTP
455455 initialDelaySeconds: 0
456− timeoutSeconds: 1
456+ timeoutSeconds: 5
457457 successThreshold: 1
458458 periodSeconds: 10
459459 resources:
contrib/oauth2-proxy_autocomplete.sh+1−1
@@ -24,7 +24,7 @@ _oauth2_proxy() {
2424 COMPREPLY=( $(compgen -W 'X-Real-IP X-Forwarded-For X-ProxyUser-IP' -- ${cur}) )
2525 return 0
2626 ;;
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))
2828 return 0
2929 ;;
3030 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/
155155| `--provider-display-name` | string | Override the provider's name with the given string; used for the sign-in page | (depends on provider) |
156156| `--ping-path` | string | the ping endpoint that can be used for basic health checks | `"/ping"` |
157157| `--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"` |
158159| `--metrics-address` | string | the address prometheus metrics will be scraped from | `""` |
159160| `--proxy-prefix` | string | the url root path that this proxy should be nested under (e.g. /`<oauth2>/sign_in`) | `"/oauth2"` |
160161| `--proxy-websockets` | bool | enables WebSocket proxying | true |
@@ -184,7 +185,7 @@ An example [oauth2-proxy.cfg](https://github.com/oauth2-proxy/oauth2-proxy/blob/
184185| `--set-basic-auth` | bool | set HTTP Basic Auth information in response (useful in Nginx auth_request mode) | false |
185186| `--show-debug-on-error` | bool | show detailed error information on error pages (WARNING: this may contain sensitive information - do not use in production) | false |
186187| `--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 |
188189| `--skip-auth-preflight` | bool | will skip authentication for OPTIONS requests | false |
189190| `--skip-auth-regex` | string \| list | (DEPRECATED for `--skip-auth-route`) bypass authentication for requests paths that match (may be given multiple times) | |
190191| `--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
246247
247248Each type of logging has its own configurable format and variables. By default these formats are similar to the Apache Combined Log.
248249
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.
250251
251252### Auth Log Format
252253Authentication 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
77
88- /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
99- /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
1011- /metrics - Metrics endpoint for Prometheus to scrape, serve on the address specified by `--metrics-address`, disabled by default
1112- /oauth2/sign_in - the login page, which also doubles as a sign out page (it clears cookies)
1213- /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
185185 return nil, err
186186 }
187187
188− preAuthChain, err := buildPreAuthChain(opts)
188+ preAuthChain, err := buildPreAuthChain(opts, sessionStore)
189189 if err != nil {
190190 return nil, fmt.Errorf("could not build pre-auth chain: %v", err)
191191 }
@@ -327,7 +327,7 @@ func (p *OAuthProxy) buildProxySubrouter(s *mux.Router) {
327327// buildPreAuthChain constructs a chain that should process every request before
328328// the OAuth2 Proxy authentication logic kicks in.
329329// 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) {
331331 chain := alice.New(middleware.NewScope(opts.ReverseProxy, opts.Logging.RequestIDHeader))
332332
333333 if opts.ForceHTTPS {
@@ -351,12 +351,14 @@ func buildPreAuthChain(opts *options.Options) (alice.Chain, error) {
351351 if opts.Logging.SilencePing {
352352 chain = chain.Append(
353353 middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents),
354+ middleware.NewReadynessCheck(opts.ReadyPath, sessionStore),
354355 middleware.NewRequestLogger(),
355356 )
356357 } else {
357358 chain = chain.Append(
358359 middleware.NewRequestLogger(),
359360 middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents),
361+ middleware.NewReadynessCheck(opts.ReadyPath, sessionStore),
360362 )
361363 }
362364
pkg/apis/options/load_test.go+1
@@ -49,6 +49,7 @@ var _ = Describe("Load", func() {
4949 Options: Options{
5050 ProxyPrefix: "/oauth2",
5151 PingPath: "/ping",
52+ ReadyPath: "/ready",
5253 RealClientIPHeader: "X-Real-IP",
5354 ForceHTTPS: false,
5455 Cookie: cookieDefaults(),
pkg/apis/options/logging.go+1−1
@@ -43,7 +43,7 @@ func loggingFlagSet() *pflag.FlagSet {
4343
4444 flagSet.StringSlice("exclude-logging-path", []string{}, "Exclude logging requests to paths (eg: '/path1,/path2,/path3')")
4545 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")
4747 flagSet.String("request-id-header", "X-Request-Id", "Request header to use as the request ID")
4848
4949 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 {
2121 ProxyPrefix string `flag:"proxy-prefix" cfg:"proxy_prefix"`
2222 PingPath string `flag:"ping-path" cfg:"ping_path"`
2323 PingUserAgent string `flag:"ping-user-agent" cfg:"ping_user_agent"`
24+ ReadyPath string `flag:"ready-path" cfg:"ready_path"`
2425 ReverseProxy bool `flag:"reverse-proxy" cfg:"reverse_proxy"`
2526 RealClientIPHeader string `flag:"real-client-ip-header" cfg:"real_client_ip_header"`
2627 TrustedIPs []string `flag:"trusted-ip" cfg:"trusted_ips"`
@@ -96,6 +97,7 @@ func NewOptions() *Options {
9697 ProxyPrefix: "/oauth2",
9798 Providers: providerDefaults(),
9899 PingPath: "/ping",
100+ ReadyPath: "/ready",
99101 RealClientIPHeader: "X-Real-IP",
100102 ForceHTTPS: false,
101103 Cookie: cookieDefaults(),
@@ -133,6 +135,7 @@ func NewFlagSet() *pflag.FlagSet {
133135 flagSet.String("proxy-prefix", "/oauth2", "the url root path that this proxy should be nested under (e.g. /<oauth2>/sign_in)")
134136 flagSet.String("ping-path", "/ping", "the ping endpoint that can be used for basic health checks")
135137 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")
136139 flagSet.String("session-store-type", "cookie", "the session storage provider to use")
137140 flagSet.Bool("session-cookie-minimal", false, "strip OAuth tokens from cookie session stores if they aren't needed (cookie session store only)")
138141 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 {
1212 Save(rw http.ResponseWriter, req *http.Request, s *SessionState) error
1313 Load(req *http.Request) (*SessionState, error)
1414 Clear(rw http.ResponseWriter, req *http.Request) error
15+ VerifyConnection(ctx context.Context) error
1516}
1617
1718var 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
793793 }
794794 return nil
795795}
796+
797+func (f *fakeSessionStore) VerifyConnection(_ context.Context) error {
798+ return nil
799+}
pkg/sessions/cookie/session_store.go+7
@@ -1,6 +1,7 @@
11package cookie
22
33import (
4+ "context"
45 "errors"
56 "fmt"
67 "net/http"
@@ -82,6 +83,12 @@ func (s *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) error {
8283 return nil
8384}
8485
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+
8592// cookieForSession serializes a session state for storage in a cookie
8693func (s *SessionStore) cookieForSession(ss *sessions.SessionState) ([]byte, error) {
8794 if s.Minimal && (ss.AccessToken != "" || ss.IDToken != "" || ss.RefreshToken != "") {
pkg/sessions/persistence/interfaces.go+1
@@ -15,4 +15,5 @@ type Store interface {
1515 Load(context.Context, string) ([]byte, error)
1616 Clear(context.Context, string) error
1717 Lock(key string) sessions.Lock
18+ VerifyConnection(context.Context) error
1819}
pkg/sessions/persistence/manager.go+6
@@ -1,6 +1,7 @@
11package persistence
22
33import (
4+ "context"
45 "fmt"
56 "net/http"
67 "time"
@@ -90,3 +91,8 @@ func (m *Manager) Clear(rw http.ResponseWriter, req *http.Request) error {
9091 return m.Store.Clear(req.Context(), key)
9192 })
9293}
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 {
1414 Lock(key string) sessions.Lock
1515 Set(ctx context.Context, key string, value []byte, expiration time.Duration) error
1616 Del(ctx context.Context, key string) error
17+ Ping(ctx context.Context) error
1718}
1819
1920var _ Client = (*client)(nil)
@@ -44,6 +45,10 @@ func (c *client) Lock(key string) sessions.Lock {
4445 return NewLock(c.Client, key)
4546}
4647
48+func (c *client) Ping(ctx context.Context) error {
49+ return c.Client.Ping(ctx).Err()
50+}
51+
4752var _ Client = (*clusterClient)(nil)
4853
4954type clusterClient struct {
@@ -71,3 +76,7 @@ func (c *clusterClient) Del(ctx context.Context, key string) error {
7176func (c *clusterClient) Lock(key string) sessions.Lock {
7277 return NewLock(c.ClusterClient, key)
7378}
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 {
7070 return store.Client.Lock(key)
7171}
7272
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+
7379// NewRedisClient makes a redis.Client (either standalone, sentinel aware, or
7480// redis cluster)
7581func NewRedisClient(opts options.RedisStoreOptions) (Client, error) {
@@ -205,3 +211,5 @@ func parseRedisURLs(urls []string) ([]string, *redis.Options, error) {
205211 }
206212 return addrs, redisOptions, nil
207213}
214+
215+var _ persistence.Store = (*SessionStore)(nil)
pkg/sessions/redis/redis_store_test.go−27
@@ -2,20 +2,15 @@ package redis
22
33import (
44 "bytes"
5− "context"
65 "crypto/tls"
76 "encoding/pem"
8− "log"
97 "os"
10− "testing"
118 "time"
129
1310 "github.com/Bose/minisentinel"
1411 "github.com/alicebob/miniredis/v2"
15− "github.com/go-redis/redis/v9"
1612 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
1713 sessionsapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
18− "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
1914 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/sessions/persistence"
2015 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/sessions/tests"
2116 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util"
@@ -25,28 +20,6 @@ import (
2520
2621const redisPassword = "0123456789abcdefghijklmnopqrstuv"
2722
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−
5023var (
5124 cert tls.Certificate
5225 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 {
6565 return lock
6666}
6767
68+func (s *MockStore) VerifyConnection(_ context.Context) error {
69+ return nil
70+}
71+
6872// FastForward simulates the flow of time to test expirations
6973func (s *MockStore) FastForward(duration time.Duration) {
7074 for _, mockLock := range s.lockCache {
pkg/sessions/tests/session_store_tests.go+6
@@ -485,6 +485,12 @@ func SessionStoreInterfaceTests(in *testInput) {
485485 })
486486 })
487487 })
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+ })
488494}
489495
490496func LoadSessionTests(in *testInput) {