@@ -6,6 +6,7 @@ import (
8 "net/http/httptest"
8 "net/http/httptest"
11 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
12 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
@@ -17,9 +18,104 @@ import (
17 . "github.com/onsi/gomega"
18 . "github.com/onsi/gomega"
21+type TestLock struct {
27+ LockedOnPeekCount int
34+func (l *TestLock) Obtain(_ context.Context, _ time.Duration) error {
35+ if l.ObtainError != nil {
43+func (l *TestLock) Peek(_ context.Context) (bool, error) {
44+ if l.PeekError != nil {
45+ return false, l.PeekError
50+ // mainly used to test case when peek initially returns false,
51+ // but when trying to obtain lock, it returns true.
52+ if l.LockedOnPeekCount == l.PeekedCount {
58+func (l *TestLock) Refresh(_ context.Context, _ time.Duration) error {
59+ if l.RefreshError != nil {
60+ return l.ReleaseError
62+ l.WasRefreshed = true
66+func (l *TestLock) Release(_ context.Context) error {
67+ if l.ReleaseError != nil {
68+ return l.ReleaseError
75+type LockConc struct {
81+func (l *LockConc) Obtain(_ context.Context, _ time.Duration) error {
85+ return sessionsapi.ErrLockNotObtained
92+func (l *LockConc) Peek(_ context.Context) (bool, error) {
101+ return response, nil
104+func (l *LockConc) Refresh(_ context.Context, _ time.Duration) error {
108+func (l *LockConc) Release(_ context.Context) error {
20var _ = Describe("Stored Session Suite", func() {
115var _ = Describe("Stored Session Suite", func() {
22 refresh = "Refresh"
117 refresh = "Refresh"
118+ refreshed = "Refreshed"
23 noRefresh = "NoRefresh"
119 noRefresh = "NoRefresh"
24 notImplemented = "NotImplemented"
120 notImplemented = "NotImplemented"
@@ -34,7 +130,7 @@ var _ = Describe("Stored Session Suite", func() {
34 var defaultRefreshFunc = func(_ context.Context, ss *sessionsapi.SessionState) (bool, error) {
130 var defaultRefreshFunc = func(_ context.Context, ss *sessionsapi.SessionState) (bool, error) {
35 switch ss.RefreshToken {
131 switch ss.RefreshToken {
36 case refresh:
132 case refresh:
37− ss.RefreshToken = "Refreshed"
133+ ss.RefreshToken = refreshed
38 return true, nil
134 return true, nil
39 case noRefresh:
135 case noRefresh:
40 return false, nil
136 return false, nil
@@ -181,6 +277,7 @@ var _ = Describe("Stored Session Suite", func() {
181 RefreshToken: noRefresh,
277 RefreshToken: noRefresh,
182 CreatedAt: &createdPast,
278 CreatedAt: &createdPast,
183 ExpiresOn: &createdFuture,
279 ExpiresOn: &createdFuture,
280+ Lock: &sessionsapi.NoOpLock{},
185 store: defaultSessionStore,
282 store: defaultSessionStore,
186 refreshPeriod: 1 * time.Minute,
283 refreshPeriod: 1 * time.Minute,
@@ -222,6 +319,7 @@ var _ = Describe("Stored Session Suite", func() {
222 RefreshToken: "Refreshed",
319 RefreshToken: "Refreshed",
223 CreatedAt: &now,
320 CreatedAt: &now,
224 ExpiresOn: &createdFuture,
321 ExpiresOn: &createdFuture,
322+ Lock: &sessionsapi.NoOpLock{},
226 store: defaultSessionStore,
324 store: defaultSessionStore,
227 refreshPeriod: 1 * time.Minute,
325 refreshPeriod: 1 * time.Minute,
@@ -237,6 +335,7 @@ var _ = Describe("Stored Session Suite", func() {
237 RefreshToken: "RefreshError",
335 RefreshToken: "RefreshError",
238 CreatedAt: &createdPast,
336 CreatedAt: &createdPast,
239 ExpiresOn: &createdFuture,
337 ExpiresOn: &createdFuture,
338+ Lock: &sessionsapi.NoOpLock{},
241 store: defaultSessionStore,
340 store: defaultSessionStore,
242 refreshPeriod: 1 * time.Minute,
341 refreshPeriod: 1 * time.Minute,
@@ -266,15 +365,109 @@ var _ = Describe("Stored Session Suite", func() {
266 validateSession: defaultValidateFunc,
365 validateSession: defaultValidateFunc,
369+ type storedSessionLoaderConcurrentTableInput struct {
370+ existingSession *sessionsapi.SessionState
371+ refreshPeriod time.Duration
375+ DescribeTable("when serving concurrent requests",
376+ func(in storedSessionLoaderConcurrentTableInput) {
377+ lockConc := &LockConc{}
379+ refreshedChan := make(chan bool, in.numConcReqs)
380+ for i := 0; i < in.numConcReqs; i++ {
381+ go func(refreshedChan chan bool, lockConc sessionsapi.Lock) {
382+ existingSession := *in.existingSession // deep copy existingSession state
383+ existingSession.Lock = lockConc
384+ store := &fakeSessionStore{
385+ LoadFunc: func(req *http.Request) (*sessionsapi.SessionState, error) {
386+ return &existingSession, nil
388+ SaveFunc: func(http.ResponseWriter, *http.Request, *sessionsapi.SessionState) error {
393+ scope := &middlewareapi.RequestScope{
397+ // Set up the request with the request header and a request scope
398+ req := httptest.NewRequest("", "/", nil)
399+ req = middlewareapi.AddRequestScope(req, scope)
401+ rw := httptest.NewRecorder()
403+ sessionRefreshed := false
404+ opts := &StoredSessionLoaderOptions{
405+ SessionStore: store,
406+ RefreshPeriod: in.refreshPeriod,
407+ RefreshSession: func(ctx context.Context, s *sessionsapi.SessionState) (bool, error) {
408+ time.Sleep(10 * time.Millisecond)
409+ sessionRefreshed = true
412+ ValidateSession: func(context.Context, *sessionsapi.SessionState) bool {
417+ handler := NewStoredSessionLoader(opts)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
418+ handler.ServeHTTP(rw, req)
420+ refreshedChan <- sessionRefreshed
421+ }(refreshedChan, lockConc)
423+ var refreshedSlice []bool
424+ for i := 0; i < in.numConcReqs; i++ {
425+ refreshedSlice = append(refreshedSlice, <-refreshedChan)
427+ sessionRefreshedCount := 0
428+ for _, sessionRefreshed := range refreshedSlice {
429+ if sessionRefreshed {
430+ sessionRefreshedCount++
433+ Expect(sessionRefreshedCount).To(Equal(1))
435+ Entry("with two concurrent requests", storedSessionLoaderConcurrentTableInput{
436+ existingSession: &sessionsapi.SessionState{
437+ RefreshToken: refresh,
438+ CreatedAt: &createdPast,
441+ refreshPeriod: 1 * time.Minute,
443+ Entry("with 5 concurrent requests", storedSessionLoaderConcurrentTableInput{
444+ existingSession: &sessionsapi.SessionState{
445+ RefreshToken: refresh,
446+ CreatedAt: &createdPast,
449+ refreshPeriod: 1 * time.Minute,
451+ Entry("with one request", storedSessionLoaderConcurrentTableInput{
452+ existingSession: &sessionsapi.SessionState{
453+ RefreshToken: refresh,
454+ CreatedAt: &createdPast,
457+ refreshPeriod: 1 * time.Minute,
271 Context("refreshSessionIfNeeded", func() {
462 Context("refreshSessionIfNeeded", func() {
272 type refreshSessionIfNeededTableInput struct {
463 type refreshSessionIfNeededTableInput struct {
273− refreshPeriod time.Duration
464+ refreshPeriod time.Duration
274− session *sessionsapi.SessionState
466+ session *sessionsapi.SessionState
275− expectedErr error
467+ expectedErr error
276− expectRefreshed bool
468+ expectRefreshed bool
277− expectValidated bool
469+ expectValidated bool
470+ expectedLockState TestLock
280 createdPast := time.Now().Add(-5 * time.Minute)
473 createdPast := time.Now().Add(-5 * time.Minute)
@@ -285,9 +478,18 @@ var _ = Describe("Stored Session Suite", func() {
285 refreshed := false
478 refreshed := false
286 validated := false
479 validated := false
481+ store := &fakeSessionStore{}
482+ if in.sessionStored {
483+ store = &fakeSessionStore{
484+ LoadFunc: func(req *http.Request) (*sessionsapi.SessionState, error) {
485+ return in.session, nil
288 s := &storedSessionLoader{
490 s := &storedSessionLoader{
289 refreshPeriod: in.refreshPeriod,
491 refreshPeriod: in.refreshPeriod,
290− store: &fakeSessionStore{},
492+ store: store,
291 sessionRefresher: func(_ context.Context, ss *sessionsapi.SessionState) (bool, error) {
493 sessionRefresher: func(_ context.Context, ss *sessionsapi.SessionState) (bool, error) {
292 refreshed = true
494 refreshed = true
293 switch ss.RefreshToken {
495 switch ss.RefreshToken {
@@ -316,89 +518,171 @@ var _ = Describe("Stored Session Suite", func() {
317 Expect(refreshed).To(Equal(in.expectRefreshed))
519 Expect(refreshed).To(Equal(in.expectRefreshed))
318 Expect(validated).To(Equal(in.expectValidated))
520 Expect(validated).To(Equal(in.expectValidated))
521+ testLock, ok := in.session.Lock.(*TestLock)
522+ Expect(ok).To(Equal(true))
524+ Expect(testLock).To(Equal(&in.expectedLockState))
320 Entry("when the refresh period is 0, and the session does not need refreshing", refreshSessionIfNeededTableInput{
526 Entry("when the refresh period is 0, and the session does not need refreshing", refreshSessionIfNeededTableInput{
321 refreshPeriod: time.Duration(0),
527 refreshPeriod: time.Duration(0),
322 session: &sessionsapi.SessionState{
528 session: &sessionsapi.SessionState{
323 RefreshToken: refresh,
529 RefreshToken: refresh,
324 CreatedAt: &createdFuture,
530 CreatedAt: &createdFuture,
326− expectedErr: nil,
533+ expectedErr: nil,
327− expectRefreshed: false,
534+ expectRefreshed: false,
328− expectValidated: false,
535+ expectValidated: false,
536+ expectedLockState: TestLock{},
330 Entry("when the refresh period is 0, and the session needs refreshing", refreshSessionIfNeededTableInput{
538 Entry("when the refresh period is 0, and the session needs refreshing", refreshSessionIfNeededTableInput{
331 refreshPeriod: time.Duration(0),
539 refreshPeriod: time.Duration(0),
332 session: &sessionsapi.SessionState{
540 session: &sessionsapi.SessionState{
333 RefreshToken: refresh,
541 RefreshToken: refresh,
334 CreatedAt: &createdPast,
542 CreatedAt: &createdPast,
336− expectedErr: nil,
545+ expectedErr: nil,
337− expectRefreshed: false,
546+ expectRefreshed: false,
338− expectValidated: false,
547+ expectValidated: false,
548+ expectedLockState: TestLock{},
340 Entry("when the session does not need refreshing", refreshSessionIfNeededTableInput{
550 Entry("when the session does not need refreshing", refreshSessionIfNeededTableInput{
341 refreshPeriod: 1 * time.Minute,
551 refreshPeriod: 1 * time.Minute,
342 session: &sessionsapi.SessionState{
552 session: &sessionsapi.SessionState{
343 RefreshToken: refresh,
553 RefreshToken: refresh,
344 CreatedAt: &createdFuture,
554 CreatedAt: &createdFuture,
346− expectedErr: nil,
557+ expectedErr: nil,
347− expectRefreshed: false,
558+ expectRefreshed: false,
348− expectValidated: false,
559+ expectValidated: false,
560+ expectedLockState: TestLock{},
350 Entry("when the session is refreshed by the provider", refreshSessionIfNeededTableInput{
562 Entry("when the session is refreshed by the provider", refreshSessionIfNeededTableInput{
351 refreshPeriod: 1 * time.Minute,
563 refreshPeriod: 1 * time.Minute,
352 session: &sessionsapi.SessionState{
564 session: &sessionsapi.SessionState{
353 RefreshToken: refresh,
565 RefreshToken: refresh,
354 CreatedAt: &createdPast,
566 CreatedAt: &createdPast,
356 expectedErr: nil,
569 expectedErr: nil,
357 expectRefreshed: true,
570 expectRefreshed: true,
358 expectValidated: true,
571 expectValidated: true,
572+ expectedLockState: TestLock{
360− Entry("when the session is not refreshed by the provider", refreshSessionIfNeededTableInput{
579+ Entry("when the session is locked and instead loaded from storage", refreshSessionIfNeededTableInput{
361 refreshPeriod: 1 * time.Minute,
580 refreshPeriod: 1 * time.Minute,
362 session: &sessionsapi.SessionState{
581 session: &sessionsapi.SessionState{
363 RefreshToken: noRefresh,
582 RefreshToken: noRefresh,
364 CreatedAt: &createdPast,
583 CreatedAt: &createdPast,
365− ExpiresOn: &createdFuture,
584+ Lock: &TestLock{
588+ sessionStored: true,
367 expectedErr: nil,
589 expectedErr: nil,
368− expectRefreshed: true,
590+ expectRefreshed: false,
369 expectValidated: true,
591 expectValidated: true,
592+ expectedLockState: TestLock{
371− Entry("when the provider doesn't implement refresh but validation succeeds", refreshSessionIfNeededTableInput{
597+ Entry("when obtaining lock failed, but concurrent request refreshed", refreshSessionIfNeededTableInput{
372 refreshPeriod: 1 * time.Minute,
598 refreshPeriod: 1 * time.Minute,
373 session: &sessionsapi.SessionState{
599 session: &sessionsapi.SessionState{
374− RefreshToken: notImplemented,
600+ RefreshToken: noRefresh,
375 CreatedAt: &createdPast,
601 CreatedAt: &createdPast,
603+ ObtainError: errors.New("not able to obtain lock"),
604+ LockedOnPeekCount: 2,
608+ expectRefreshed: false,
609+ expectValidated: true,
610+ expectedLockState: TestLock{
612+ LockedOnPeekCount: 2,
613+ ObtainError: errors.New("not able to obtain lock"),
616+ Entry("when obtaining lock failed", refreshSessionIfNeededTableInput{
617+ refreshPeriod: 1 * time.Minute,
618+ session: &sessionsapi.SessionState{
619+ RefreshToken: noRefresh,
620+ CreatedAt: &createdPast,
622+ ObtainError: errors.New("not able to obtain lock"),
626+ expectRefreshed: false,
627+ expectValidated: true,
628+ expectedLockState: TestLock{
630+ ObtainError: errors.New("not able to obtain lock"),
633+ Entry("when the session is not refreshed by the provider", refreshSessionIfNeededTableInput{
634+ refreshPeriod: 1 * time.Minute,
635+ session: &sessionsapi.SessionState{
636+ RefreshToken: noRefresh,
637+ CreatedAt: &createdPast,
638+ ExpiresOn: &createdFuture,
377 expectedErr: nil,
641 expectedErr: nil,
378 expectRefreshed: true,
642 expectRefreshed: true,
379 expectValidated: true,
643 expectValidated: true,
644+ expectedLockState: TestLock{
381− Entry("when the provider refresh fails but validation succeeds", refreshSessionIfNeededTableInput{
651+ Entry("when the provider doesn't implement refresh", refreshSessionIfNeededTableInput{
382 refreshPeriod: 1 * time.Minute,
652 refreshPeriod: 1 * time.Minute,
383 session: &sessionsapi.SessionState{
653 session: &sessionsapi.SessionState{
384− RefreshToken: "RefreshError",
654+ RefreshToken: notImplemented,
385 CreatedAt: &createdPast,
655 CreatedAt: &createdPast,
387 expectedErr: nil,
658 expectedErr: nil,
388 expectRefreshed: true,
659 expectRefreshed: true,
389 expectValidated: true,
660 expectValidated: true,
661+ expectedLockState: TestLock{
391− Entry("when the session is not refreshed by the provider and validation fails", refreshSessionIfNeededTableInput{
668+ Entry("when the session is not refreshed by the provider", refreshSessionIfNeededTableInput{
392 refreshPeriod: 1 * time.Minute,
669 refreshPeriod: 1 * time.Minute,
393 session: &sessionsapi.SessionState{
670 session: &sessionsapi.SessionState{
394 AccessToken: "Invalid",
671 AccessToken: "Invalid",
395 RefreshToken: noRefresh,
672 RefreshToken: noRefresh,
396 CreatedAt: &createdPast,
673 CreatedAt: &createdPast,
397 ExpiresOn: &createdFuture,
674 ExpiresOn: &createdFuture,
399 expectedErr: errors.New("session is invalid"),
677 expectedErr: errors.New("session is invalid"),
400 expectRefreshed: true,
678 expectRefreshed: true,
401 expectValidated: true,
679 expectValidated: true,
680+ expectedLockState: TestLock{