1package validation
2
3import (
4	"time"
5
6	"github.com/Bose/minisentinel"
7	"github.com/alicebob/miniredis/v2"
8	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
9	. "github.com/onsi/ginkgo"
10	. "github.com/onsi/ginkgo/extensions/table"
11	. "github.com/onsi/gomega"
12)
13
14var _ = Describe("Sessions", func() {
15	const (
16		idTokenConflictMsg     = "id_token claim for header \"X-ID-Token\" requires oauth tokens in sessions. session_cookie_minimal cannot be set"
17		accessTokenConflictMsg = "access_token claim for header \"X-Access-Token\" requires oauth tokens in sessions. session_cookie_minimal cannot be set"
18		cookieRefreshMsg       = "cookie_refresh > 0 requires oauth tokens in sessions. session_cookie_minimal cannot be set"
19	)
20
21	type cookieMinimalTableInput struct {
22		opts       *options.Options
23		errStrings []string
24	}
25
26	DescribeTable("validateSessionCookieMinimal",
27		func(o *cookieMinimalTableInput) {
28			Expect(validateSessionCookieMinimal(o.opts)).To(ConsistOf(o.errStrings))
29		},
30		Entry("No minimal cookie session", &cookieMinimalTableInput{
31			opts: &options.Options{
32				Session: options.SessionOptions{
33					Cookie: options.CookieStoreOptions{
34						Minimal: false,
35					},
36				},
37			},
38			errStrings: []string{},
39		}),
40		Entry("No minimal cookie session & request header has access_token claim", &cookieMinimalTableInput{
41			opts: &options.Options{
42				Session: options.SessionOptions{
43					Cookie: options.CookieStoreOptions{
44						Minimal: false,
45					},
46				},
47				InjectRequestHeaders: []options.Header{
48					{
49						Name: "X-Access-Token",
50						Values: []options.HeaderValue{
51							{
52								ClaimSource: &options.ClaimSource{
53									Claim: "access_token",
54								},
55							},
56						},
57					},
58				},
59			},
60			errStrings: []string{},
61		}),
62		Entry("Minimal cookie session no conflicts", &cookieMinimalTableInput{
63			opts: &options.Options{
64				Session: options.SessionOptions{
65					Cookie: options.CookieStoreOptions{
66						Minimal: true,
67					},
68				},
69			},
70			errStrings: []string{},
71		}),
72		Entry("Request Header id_token conflict", &cookieMinimalTableInput{
73			opts: &options.Options{
74				Session: options.SessionOptions{
75					Cookie: options.CookieStoreOptions{
76						Minimal: true,
77					},
78				},
79				InjectRequestHeaders: []options.Header{
80					{
81						Name: "X-ID-Token",
82						Values: []options.HeaderValue{
83							{
84								ClaimSource: &options.ClaimSource{
85									Claim: "id_token",
86								},
87							},
88						},
89					},
90				},
91			},
92			errStrings: []string{idTokenConflictMsg},
93		}),
94		Entry("Response Header id_token conflict", &cookieMinimalTableInput{
95			opts: &options.Options{
96				Session: options.SessionOptions{
97					Cookie: options.CookieStoreOptions{
98						Minimal: true,
99					},
100				},
101				InjectResponseHeaders: []options.Header{
102					{
103						Name: "X-ID-Token",
104						Values: []options.HeaderValue{
105							{
106								ClaimSource: &options.ClaimSource{
107									Claim: "id_token",
108								},
109							},
110						},
111					},
112				},
113			},
114			errStrings: []string{idTokenConflictMsg},
115		}),
116		Entry("Request Header access_token conflict", &cookieMinimalTableInput{
117			opts: &options.Options{
118				Session: options.SessionOptions{
119					Cookie: options.CookieStoreOptions{
120						Minimal: true,
121					},
122				},
123				InjectRequestHeaders: []options.Header{
124					{
125						Name: "X-Access-Token",
126						Values: []options.HeaderValue{
127							{
128								ClaimSource: &options.ClaimSource{
129									Claim: "access_token",
130								},
131							},
132						},
133					},
134				},
135			},
136			errStrings: []string{accessTokenConflictMsg},
137		}),
138		Entry("CookieRefresh conflict", &cookieMinimalTableInput{
139			opts: &options.Options{
140				Cookie: options.Cookie{
141					Refresh: time.Hour,
142				},
143				Session: options.SessionOptions{
144					Cookie: options.CookieStoreOptions{
145						Minimal: true,
146					},
147				},
148			},
149			errStrings: []string{cookieRefreshMsg},
150		}),
151		Entry("Multiple conflicts", &cookieMinimalTableInput{
152			opts: &options.Options{
153				Session: options.SessionOptions{
154					Cookie: options.CookieStoreOptions{
155						Minimal: true,
156					},
157				},
158				InjectResponseHeaders: []options.Header{
159					{
160						Name: "X-ID-Token",
161						Values: []options.HeaderValue{
162							{
163								ClaimSource: &options.ClaimSource{
164									Claim: "id_token",
165								},
166							},
167						},
168					},
169				},
170				InjectRequestHeaders: []options.Header{
171					{
172						Name: "X-Access-Token",
173						Values: []options.HeaderValue{
174							{
175								ClaimSource: &options.ClaimSource{
176									Claim: "access_token",
177								},
178							},
179						},
180					},
181				},
182			},
183			errStrings: []string{idTokenConflictMsg, accessTokenConflictMsg},
184		}),
185	)
186
187	const (
188		clusterAndSentinelMsg     = "unable to initialize a redis client: options redis-use-sentinel and redis-use-cluster are mutually exclusive"
189		parseWrongSchemeMsg       = "unable to initialize a redis client: unable to parse redis url: redis: invalid URL scheme: https"
190		parseWrongFormatMsg       = "unable to initialize a redis client: unable to parse redis url: redis: invalid database number: \"wrong\""
191		invalidPasswordSetMsg     = "unable to set a redis initialization key: WRONGPASS invalid username-password pair"
192		invalidPasswordDelMsg     = "unable to delete the redis initialization key: WRONGPASS invalid username-password pair"
193		unreachableRedisSetMsg    = "unable to set a redis initialization key: dial tcp 127.0.0.1:65535: connect: connection refused"
194		unreachableRedisDelMsg    = "unable to delete the redis initialization key: dial tcp 127.0.0.1:65535: connect: connection refused"
195		unreachableSentinelSetMsg = "unable to set a redis initialization key: redis: all sentinels are unreachable"
196		unrechableSentinelDelMsg  = "unable to delete the redis initialization key: redis: all sentinels are unreachable"
197	)
198
199	type redisStoreTableInput struct {
200		// miniredis setup details
201		password        string
202		useSentinel     bool
203		setAddr         bool
204		setSentinelAddr bool
205		setMasterName   bool
206
207		opts       *options.Options
208		errStrings []string
209	}
210
211	DescribeTable("validateRedisSessionStore",
212		func(o *redisStoreTableInput) {
213			mr, err := miniredis.Run()
214			Expect(err).ToNot(HaveOccurred())
215			mr.RequireAuth(o.password)
216			defer mr.Close()
217
218			if o.setAddr && !o.useSentinel {
219				o.opts.Session.Redis.ConnectionURL = "redis://" + mr.Addr()
220			}
221
222			if o.useSentinel {
223				ms := minisentinel.NewSentinel(mr)
224				Expect(ms.Start()).To(Succeed())
225				defer ms.Close()
226
227				if o.setSentinelAddr {
228					o.opts.Session.Redis.SentinelConnectionURLs = []string{"redis://" + ms.Addr()}
229				}
230				if o.setMasterName {
231					o.opts.Session.Redis.SentinelMasterName = ms.MasterInfo().Name
232				}
233			}
234
235			Expect(validateRedisSessionStore(o.opts)).To(ConsistOf(o.errStrings))
236		},
237		Entry("cookie sessions are skipped", &redisStoreTableInput{
238			opts: &options.Options{
239				Session: options.SessionOptions{
240					Type: options.CookieSessionStoreType,
241				},
242			},
243			errStrings: []string{},
244		}),
245		Entry("connect successfully to pure redis", &redisStoreTableInput{
246			setAddr: true,
247
248			opts: &options.Options{
249				Session: options.SessionOptions{
250					Type: options.RedisSessionStoreType,
251				},
252			},
253			errStrings: []string{},
254		}),
255		Entry("failed redis connection with wrong address", &redisStoreTableInput{
256			opts: &options.Options{
257				Session: options.SessionOptions{
258					Type: options.RedisSessionStoreType,
259					Redis: options.RedisStoreOptions{
260						ConnectionURL: "redis://127.0.0.1:65535",
261					},
262				},
263			},
264			errStrings: []string{unreachableRedisSetMsg, unreachableRedisDelMsg},
265		}),
266		Entry("fail to parse an invalid connection URL with wrong scheme", &redisStoreTableInput{
267			opts: &options.Options{
268				Session: options.SessionOptions{
269					Type: options.RedisSessionStoreType,
270					Redis: options.RedisStoreOptions{
271						ConnectionURL: "https://example.com",
272					},
273				},
274			},
275			errStrings: []string{parseWrongSchemeMsg},
276		}),
277		Entry("fail to parse an invalid connection URL with invalid format", &redisStoreTableInput{
278			opts: &options.Options{
279				Session: options.SessionOptions{
280					Type: options.RedisSessionStoreType,
281					Redis: options.RedisStoreOptions{
282						ConnectionURL: "redis://127.0.0.1:6379/wrong",
283					},
284				},
285			},
286			errStrings: []string{parseWrongFormatMsg},
287		}),
288		Entry("connect successfully to pure redis with password", &redisStoreTableInput{
289			password: "abcdef123",
290			setAddr:  true,
291
292			opts: &options.Options{
293				Session: options.SessionOptions{
294					Type: options.RedisSessionStoreType,
295					Redis: options.RedisStoreOptions{
296						Password: "abcdef123",
297					},
298				},
299			},
300			errStrings: []string{},
301		}),
302		Entry("failed connection with wrong password", &redisStoreTableInput{
303			password: "abcdef123",
304			setAddr:  true,
305
306			opts: &options.Options{
307				Session: options.SessionOptions{
308					Type: options.RedisSessionStoreType,
309					Redis: options.RedisStoreOptions{
310						Password: "zyxwtuv987",
311					},
312				},
313			},
314			errStrings: []string{invalidPasswordSetMsg, invalidPasswordDelMsg},
315		}),
316		Entry("connect successfully to sentinel redis", &redisStoreTableInput{
317			useSentinel:     true,
318			setSentinelAddr: true,
319			setMasterName:   true,
320
321			opts: &options.Options{
322				Session: options.SessionOptions{
323					Type: options.RedisSessionStoreType,
324					Redis: options.RedisStoreOptions{
325						UseSentinel: true,
326					},
327				},
328			},
329			errStrings: []string{},
330		}),
331		Entry("connect successfully to sentinel redis with password", &redisStoreTableInput{
332			password:        "abcdef123",
333			useSentinel:     true,
334			setSentinelAddr: true,
335			setMasterName:   true,
336
337			opts: &options.Options{
338				Session: options.SessionOptions{
339					Type: options.RedisSessionStoreType,
340					Redis: options.RedisStoreOptions{
341						Password:    "abcdef123",
342						UseSentinel: true,
343					},
344				},
345			},
346			errStrings: []string{},
347		}),
348		Entry("failed connection to sentinel redis with wrong password", &redisStoreTableInput{
349			password:        "abcdef123",
350			useSentinel:     true,
351			setSentinelAddr: true,
352			setMasterName:   true,
353
354			opts: &options.Options{
355				Session: options.SessionOptions{
356					Type: options.RedisSessionStoreType,
357					Redis: options.RedisStoreOptions{
358						Password:    "zyxwtuv987",
359						UseSentinel: true,
360					},
361				},
362			},
363			errStrings: []string{invalidPasswordSetMsg, invalidPasswordDelMsg},
364		}),
365		Entry("failed connection to sentinel redis with wrong master name", &redisStoreTableInput{
366			useSentinel:     true,
367			setSentinelAddr: true,
368
369			opts: &options.Options{
370				Session: options.SessionOptions{
371					Type: options.RedisSessionStoreType,
372					Redis: options.RedisStoreOptions{
373						UseSentinel:        true,
374						SentinelMasterName: "WRONG",
375					},
376				},
377			},
378			errStrings: []string{unreachableSentinelSetMsg, unrechableSentinelDelMsg},
379		}),
380		Entry("failed connection to sentinel redis with wrong address", &redisStoreTableInput{
381			useSentinel:   true,
382			setMasterName: true,
383
384			opts: &options.Options{
385				Session: options.SessionOptions{
386					Type: options.RedisSessionStoreType,
387					Redis: options.RedisStoreOptions{
388						UseSentinel:            true,
389						SentinelConnectionURLs: []string{"redis://127.0.0.1:65535"},
390					},
391				},
392			},
393			errStrings: []string{unreachableSentinelSetMsg, unrechableSentinelDelMsg},
394		}),
395		Entry("sentinel and cluster both enabled fails", &redisStoreTableInput{
396			opts: &options.Options{
397				Session: options.SessionOptions{
398					Type: options.RedisSessionStoreType,
399					Redis: options.RedisStoreOptions{
400						UseSentinel: true,
401						UseCluster:  true,
402					},
403				},
404			},
405			errStrings: []string{clusterAndSentinelMsg},
406		}),
407	)
408})
409