git.iko.soy/typeable/oauth2-proxy
on master help sign in
Add session loader from session storage
commit034f057b6032731f701a724e1037e83b699d7296 authorJoel Speed <joel.speed@hotmail.co.uk> dateFri 17 Jul 2020, 11:47 (6 years ago) parent7d6f2a3
2 files changed+689
pkg/middleware/stored_session.go+165 pkg/middleware/stored_session_test.go+524
pkg/middleware/stored_session.gonew file+165
@@ -0,0 +1,165 @@
1+package middleware
2+
3+import (
4+ "context"
5+ "errors"
6+ "fmt"
7+ "net/http"
8+ "time"
9+
10+ "github.com/justinas/alice"
11+ sessionsapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
12+ "github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
13+)
14+
15+// StoredSessionLoaderOptions cotnains all of the requirements to construct
16+// a stored session loader.
17+// All options must be provided.
18+type StoredSessionLoaderOptions struct {
19+ // Session storage basckend
20+ SessionStore sessionsapi.SessionStore
21+
22+ // How often should sessions be refreshed
23+ RefreshPeriod time.Duration
24+
25+ // Provider based sesssion refreshing
26+ RefreshSessionIfNeeded func(context.Context, *sessionsapi.SessionState) (bool, error)
27+
28+ // Provider based session validation.
29+ // If the sesssion is older than `RefreshPeriod` but the provider doesn't
30+ // refresh it, we must re-validate using this validation.
31+ ValidateSessionState func(context.Context, *sessionsapi.SessionState) bool
32+}
33+
34+// NewStoredSessionLoader creates a new storedSessionLoader which loads
35+// sessions from the session store.
36+// If no session is found, the request will be passed to the nex handler.
37+// If a session was loader by a previous handler, it will not be replaced.
38+func NewStoredSessionLoader(opts *StoredSessionLoaderOptions) alice.Constructor {
39+ ss := &storedSessionLoader{
40+ store: opts.SessionStore,
41+ refreshPeriod: opts.RefreshPeriod,
42+ refreshSessionWithProviderIfNeeded: opts.RefreshSessionIfNeeded,
43+ validateSessionState: opts.ValidateSessionState,
44+ }
45+ return ss.loadSession
46+}
47+
48+// storedSessionLoader is responsible for loading sessions from cookie
49+// identified sessions in the session store.
50+type storedSessionLoader struct {
51+ store sessionsapi.SessionStore
52+ refreshPeriod time.Duration
53+ refreshSessionWithProviderIfNeeded func(context.Context, *sessionsapi.SessionState) (bool, error)
54+ validateSessionState func(context.Context, *sessionsapi.SessionState) bool
55+}
56+
57+// loadSession attempts to load a session as identified by the request cookies.
58+// If no session is found, the request will be passed to the nex handler.
59+// If a session was loader by a previous handler, it will not be replaced.
60+func (s *storedSessionLoader) loadSession(next http.Handler) http.Handler {
61+ return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
62+ scope := GetRequestScope(req)
63+ // If scope is nil, this will panic.
64+ // A scope should always be injected before this handler is called.
65+ if scope.Session != nil {
66+ // The session was already loaded, pass to the next handler
67+ next.ServeHTTP(rw, req)
68+ return
69+ }
70+
71+ session, err := s.getValidatedSession(rw, req)
72+ if err != nil {
73+ // In the case when there was an error loading the session,
74+ // we should clear the session
75+ logger.Printf("Error loading cookied session: %v, removing session", err)
76+ s.store.Clear(rw, req)
77+ }
78+
79+ // Add the session to the scope if it was found
80+ scope.Session = session
81+ next.ServeHTTP(rw, req)
82+ })
83+}
84+
85+// getValidatedSession is responsible for loading a session and making sure
86+// that is is valid.
87+func (s *storedSessionLoader) getValidatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
88+ session, err := s.store.Load(req)
89+ if err != nil {
90+ return nil, err
91+ }
92+ if session == nil {
93+ // No session was found in the storage, nothing more to do
94+ return nil, nil
95+ }
96+
97+ err = s.refreshSessionIfNeeded(rw, req, session)
98+ if err != nil {
99+ return nil, fmt.Errorf("error refreshing access token for session (%s): %v", session, err)
100+ }
101+
102+ return session, nil
103+}
104+
105+// refreshSessionIfNeeded will attempt to refresh a session if the session
106+// is older than the refresh period.
107+// It is assumed that if the provider refreshes the session, the session is now
108+// valid.
109+// If the session requires refreshing but the provider does not refresh it,
110+// we must validate the session to ensure that the returned session is still
111+// valid.
112+func (s *storedSessionLoader) refreshSessionIfNeeded(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) error {
113+ if s.refreshPeriod <= time.Duration(0) || session.Age() < s.refreshPeriod {
114+ // Refresh is disabled or the session is not old enough, do nothing
115+ return nil
116+ }
117+
118+ logger.Printf("Refreshing %s old session cookie for %s (refresh after %s)", session.Age(), session, s.refreshPeriod)
119+ refreshed, err := s.refreshSessionWithProvider(rw, req, session)
120+ if err != nil {
121+ return err
122+ }
123+
124+ if !refreshed {
125+ // Session wasn't refreshed, so make sure it's still valid
126+ return s.validateSession(req.Context(), session)
127+ }
128+ return nil
129+}
130+
131+// refreshSessionWithProvider attempts to refresh the sessinon with the provider
132+// and will save the session if it was updated.
133+func (s *storedSessionLoader) refreshSessionWithProvider(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) (bool, error) {
134+ refreshed, err := s.refreshSessionWithProviderIfNeeded(req.Context(), session)
135+ if err != nil {
136+ return false, fmt.Errorf("error refreshing access token: %v", err)
137+ }
138+
139+ if !refreshed {
140+ return false, nil
141+ }
142+
143+ // Because the session was refreshed, make sure to save it
144+ err = s.store.Save(rw, req, session)
145+ if err != nil {
146+ logger.PrintAuthf(session.Email, req, logger.AuthError, "error saving session: %v", err)
147+ return false, fmt.Errorf("error saving session: %v", err)
148+ }
149+ return true, nil
150+}
151+
152+// validateSession checks whether the session has expired and performs
153+// provider validation on the session.
154+// An error implies the session is not longer valid.
155+func (s *storedSessionLoader) validateSession(ctx context.Context, session *sessionsapi.SessionState) error {
156+ if session.IsExpired() {
157+ return errors.New("session is expired")
158+ }
159+
160+ if !s.validateSessionState(ctx, session) {
161+ return errors.New("session is invalid")
162+ }
163+
164+ return nil
165+}
pkg/middleware/stored_session_test.gonew file+524
@@ -0,0 +1,524 @@
1+package middleware
2+
3+import (
4+ "context"
5+ "errors"
6+ "fmt"
7+ "net/http"
8+ "net/http/httptest"
9+ "time"
10+
11+ middlewareapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/middleware"
12+ sessionsapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
13+ . "github.com/onsi/ginkgo"
14+ . "github.com/onsi/ginkgo/extensions/table"
15+ . "github.com/onsi/gomega"
16+)
17+
18+var _ = Describe("Stored Session Suite", func() {
19+ const (
20+ refresh = "Refresh"
21+ noRefresh = "NoRefresh"
22+ )
23+
24+ var ctx = context.Background()
25+
26+ Context("StoredSessionLoader", func() {
27+ createdPast := time.Now().Add(-5 * time.Minute)
28+ createdFuture := time.Now().Add(5 * time.Minute)
29+
30+ var defaultRefreshFunc = func(_ context.Context, ss *sessionsapi.SessionState) (bool, error) {
31+ switch ss.RefreshToken {
32+ case refresh:
33+ ss.RefreshToken = "Refreshed"
34+ return true, nil
35+ case noRefresh:
36+ return false, nil
37+ default:
38+ return false, errors.New("error refreshing session")
39+ }
40+ }
41+
42+ var defaultValidateFunc = func(_ context.Context, ss *sessionsapi.SessionState) bool {
43+ return ss.AccessToken != "Invalid"
44+ }
45+
46+ var defaultSessionStore = &fakeSessionStore{
47+ LoadFunc: func(req *http.Request) (*sessionsapi.SessionState, error) {
48+ switch req.Header.Get("Cookie") {
49+ case "_oauth2_proxy=NoRefreshSession":
50+ return &sessionsapi.SessionState{
51+ RefreshToken: noRefresh,
52+ CreatedAt: &createdPast,
53+ ExpiresOn: &createdFuture,
54+ }, nil
55+ case "_oauth2_proxy=InvalidNoRefreshSession":
56+ return &sessionsapi.SessionState{
57+ AccessToken: "Invalid",
58+ RefreshToken: noRefresh,
59+ CreatedAt: &createdPast,
60+ ExpiresOn: &createdFuture,
61+ }, nil
62+ case "_oauth2_proxy=ExpiredNoRefreshSession":
63+ return &sessionsapi.SessionState{
64+ RefreshToken: noRefresh,
65+ CreatedAt: &createdPast,
66+ ExpiresOn: &createdPast,
67+ }, nil
68+ case "_oauth2_proxy=RefreshSession":
69+ return &sessionsapi.SessionState{
70+ RefreshToken: refresh,
71+ CreatedAt: &createdPast,
72+ ExpiresOn: &createdFuture,
73+ }, nil
74+ case "_oauth2_proxy=RefreshError":
75+ return &sessionsapi.SessionState{
76+ RefreshToken: "RefreshError",
77+ CreatedAt: &createdPast,
78+ ExpiresOn: &createdFuture,
79+ }, nil
80+ case "_oauth2_proxy=NonExistent":
81+ return nil, fmt.Errorf("invalid cookie")
82+ default:
83+ return nil, nil
84+ }
85+ },
86+ }
87+
88+ type storedSessionLoaderTableInput struct {
89+ requestHeaders http.Header
90+ existingSession *sessionsapi.SessionState
91+ expectedSession *sessionsapi.SessionState
92+ store sessionsapi.SessionStore
93+ refreshPeriod time.Duration
94+ refreshSession func(context.Context, *sessionsapi.SessionState) (bool, error)
95+ validateSession func(context.Context, *sessionsapi.SessionState) bool
96+ }
97+
98+ DescribeTable("when serving a request",
99+ func(in storedSessionLoaderTableInput) {
100+ scope := &middlewareapi.RequestScope{
101+ Session: in.existingSession,
102+ }
103+
104+ // Set up the request with the request headesr and a request scope
105+ req := httptest.NewRequest("", "/", nil)
106+ req.Header = in.requestHeaders
107+ contextWithScope := context.WithValue(req.Context(), requestScopeKey, scope)
108+ req = req.WithContext(contextWithScope)
109+
110+ rw := httptest.NewRecorder()
111+
112+ opts := &StoredSessionLoaderOptions{
113+ SessionStore: in.store,
114+ RefreshPeriod: in.refreshPeriod,
115+ RefreshSessionIfNeeded: in.refreshSession,
116+ ValidateSessionState: in.validateSession,
117+ }
118+
119+ // Create the handler with a next handler that will capture the session
120+ // from the scope
121+ var gotSession *sessionsapi.SessionState
122+ handler := NewStoredSessionLoader(opts)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
123+ gotSession = r.Context().Value(requestScopeKey).(*middlewareapi.RequestScope).Session
124+ }))
125+ handler.ServeHTTP(rw, req)
126+
127+ Expect(gotSession).To(Equal(in.expectedSession))
128+ },
129+ Entry("with no cookie", storedSessionLoaderTableInput{
130+ requestHeaders: http.Header{},
131+ existingSession: nil,
132+ expectedSession: nil,
133+ store: defaultSessionStore,
134+ refreshPeriod: 1 * time.Minute,
135+ refreshSession: defaultRefreshFunc,
136+ validateSession: defaultValidateFunc,
137+ }),
138+ Entry("with an invalid cookie", storedSessionLoaderTableInput{
139+ requestHeaders: http.Header{
140+ "Cookie": []string{"_oauth2_proxy=NonExistent"},
141+ },
142+ existingSession: nil,
143+ expectedSession: nil,
144+ store: defaultSessionStore,
145+ refreshPeriod: 1 * time.Minute,
146+ refreshSession: defaultRefreshFunc,
147+ validateSession: defaultValidateFunc,
148+ }),
149+ Entry("with an existing session", storedSessionLoaderTableInput{
150+ requestHeaders: http.Header{
151+ "Cookie": []string{"_oauth2_proxy=RefreshSession"},
152+ },
153+ existingSession: &sessionsapi.SessionState{
154+ RefreshToken: "Existing",
155+ },
156+ expectedSession: &sessionsapi.SessionState{
157+ RefreshToken: "Existing",
158+ },
159+ store: defaultSessionStore,
160+ refreshPeriod: 1 * time.Minute,
161+ refreshSession: defaultRefreshFunc,
162+ validateSession: defaultValidateFunc,
163+ }),
164+ Entry("with a session that has not expired", storedSessionLoaderTableInput{
165+ requestHeaders: http.Header{
166+ "Cookie": []string{"_oauth2_proxy=NoRefreshSession"},
167+ },
168+ existingSession: nil,
169+ expectedSession: &sessionsapi.SessionState{
170+ RefreshToken: noRefresh,
171+ CreatedAt: &createdPast,
172+ ExpiresOn: &createdFuture,
173+ },
174+ store: defaultSessionStore,
175+ refreshPeriod: 1 * time.Minute,
176+ refreshSession: defaultRefreshFunc,
177+ validateSession: defaultValidateFunc,
178+ }),
179+ Entry("with a session that cannot refresh and has expired", storedSessionLoaderTableInput{
180+ requestHeaders: http.Header{
181+ "Cookie": []string{"_oauth2_proxy=ExpiredNoRefreshSession"},
182+ },
183+ existingSession: nil,
184+ expectedSession: nil,
185+ store: defaultSessionStore,
186+ refreshPeriod: 1 * time.Minute,
187+ refreshSession: defaultRefreshFunc,
188+ validateSession: defaultValidateFunc,
189+ }),
190+ Entry("with a session that can refresh, but is younger than refresh period", storedSessionLoaderTableInput{
191+ requestHeaders: http.Header{
192+ "Cookie": []string{"_oauth2_proxy=RefreshSession"},
193+ },
194+ existingSession: nil,
195+ expectedSession: &sessionsapi.SessionState{
196+ RefreshToken: refresh,
197+ CreatedAt: &createdPast,
198+ ExpiresOn: &createdFuture,
199+ },
200+ store: defaultSessionStore,
201+ refreshPeriod: 10 * time.Minute,
202+ refreshSession: defaultRefreshFunc,
203+ validateSession: defaultValidateFunc,
204+ }),
205+ Entry("with a session that can refresh and is older than the refresh period", storedSessionLoaderTableInput{
206+ requestHeaders: http.Header{
207+ "Cookie": []string{"_oauth2_proxy=RefreshSession"},
208+ },
209+ existingSession: nil,
210+ expectedSession: &sessionsapi.SessionState{
211+ RefreshToken: "Refreshed",
212+ CreatedAt: &createdPast,
213+ ExpiresOn: &createdFuture,
214+ },
215+ store: defaultSessionStore,
216+ refreshPeriod: 1 * time.Minute,
217+ refreshSession: defaultRefreshFunc,
218+ validateSession: defaultValidateFunc,
219+ }),
220+ Entry("when the provider refresh fails", storedSessionLoaderTableInput{
221+ requestHeaders: http.Header{
222+ "Cookie": []string{"_oauth2_proxy=RefreshError"},
223+ },
224+ existingSession: nil,
225+ expectedSession: nil,
226+ store: defaultSessionStore,
227+ refreshPeriod: 1 * time.Minute,
228+ refreshSession: defaultRefreshFunc,
229+ validateSession: defaultValidateFunc,
230+ }),
231+ Entry("when the session is not refreshed and is no longer valid", storedSessionLoaderTableInput{
232+ requestHeaders: http.Header{
233+ "Cookie": []string{"_oauth2_proxy=InvalidNoRefreshSession"},
234+ },
235+ existingSession: nil,
236+ expectedSession: nil,
237+ store: defaultSessionStore,
238+ refreshPeriod: 1 * time.Minute,
239+ refreshSession: defaultRefreshFunc,
240+ validateSession: defaultValidateFunc,
241+ }),
242+ )
243+ })
244+
245+ Context("refreshSessionIfNeeded", func() {
246+ type refreshSessionIfNeededTableInput struct {
247+ refreshPeriod time.Duration
248+ session *sessionsapi.SessionState
249+ expectedErr error
250+ expectRefreshed bool
251+ expectValidated bool
252+ }
253+
254+ createdPast := time.Now().Add(-5 * time.Minute)
255+ createdFuture := time.Now().Add(5 * time.Minute)
256+
257+ DescribeTable("with a session",
258+ func(in refreshSessionIfNeededTableInput) {
259+ refreshed := false
260+ validated := false
261+
262+ s := &storedSessionLoader{
263+ refreshPeriod: in.refreshPeriod,
264+ store: &fakeSessionStore{},
265+ refreshSessionWithProviderIfNeeded: func(_ context.Context, ss *sessionsapi.SessionState) (bool, error) {
266+ refreshed = true
267+ switch ss.RefreshToken {
268+ case refresh:
269+ return true, nil
270+ case noRefresh:
271+ return false, nil
272+ default:
273+ return false, errors.New("error refreshing session")
274+ }
275+ },
276+ validateSessionState: func(_ context.Context, ss *sessionsapi.SessionState) bool {
277+ validated = true
278+ return ss.AccessToken != "Invalid"
279+ },
280+ }
281+
282+ req := httptest.NewRequest("", "/", nil)
283+ err := s.refreshSessionIfNeeded(nil, req, in.session)
284+ if in.expectedErr != nil {
285+ Expect(err).To(MatchError(in.expectedErr))
286+ } else {
287+ Expect(err).ToNot(HaveOccurred())
288+ }
289+ Expect(refreshed).To(Equal(in.expectRefreshed))
290+ Expect(validated).To(Equal(in.expectValidated))
291+ },
292+ Entry("when the refresh period is 0, and the session does not need refreshing", refreshSessionIfNeededTableInput{
293+ refreshPeriod: time.Duration(0),
294+ session: &sessionsapi.SessionState{
295+ RefreshToken: refresh,
296+ CreatedAt: &createdFuture,
297+ },
298+ expectedErr: nil,
299+ expectRefreshed: false,
300+ expectValidated: false,
301+ }),
302+ Entry("when the refresh period is 0, and the session needs refreshing", refreshSessionIfNeededTableInput{
303+ refreshPeriod: time.Duration(0),
304+ session: &sessionsapi.SessionState{
305+ RefreshToken: refresh,
306+ CreatedAt: &createdPast,
307+ },
308+ expectedErr: nil,
309+ expectRefreshed: false,
310+ expectValidated: false,
311+ }),
312+ Entry("when the session does not need refreshing", refreshSessionIfNeededTableInput{
313+ refreshPeriod: 1 * time.Minute,
314+ session: &sessionsapi.SessionState{
315+ RefreshToken: refresh,
316+ CreatedAt: &createdFuture,
317+ },
318+ expectedErr: nil,
319+ expectRefreshed: false,
320+ expectValidated: false,
321+ }),
322+ Entry("when the session is refreshed by the provider", refreshSessionIfNeededTableInput{
323+ refreshPeriod: 1 * time.Minute,
324+ session: &sessionsapi.SessionState{
325+ RefreshToken: refresh,
326+ CreatedAt: &createdPast,
327+ },
328+ expectedErr: nil,
329+ expectRefreshed: true,
330+ expectValidated: false,
331+ }),
332+ Entry("when the session is not refreshed by the provider", refreshSessionIfNeededTableInput{
333+ refreshPeriod: 1 * time.Minute,
334+ session: &sessionsapi.SessionState{
335+ RefreshToken: noRefresh,
336+ CreatedAt: &createdPast,
337+ ExpiresOn: &createdFuture,
338+ },
339+ expectedErr: nil,
340+ expectRefreshed: true,
341+ expectValidated: true,
342+ }),
343+ Entry("when the provider refresh fails", refreshSessionIfNeededTableInput{
344+ refreshPeriod: 1 * time.Minute,
345+ session: &sessionsapi.SessionState{
346+ RefreshToken: "RefreshError",
347+ CreatedAt: &createdPast,
348+ },
349+ expectedErr: errors.New("error refreshing access token: error refreshing session"),
350+ expectRefreshed: true,
351+ expectValidated: false,
352+ }),
353+ Entry("when the session is not refreshed by the provider and validation fails", refreshSessionIfNeededTableInput{
354+ refreshPeriod: 1 * time.Minute,
355+ session: &sessionsapi.SessionState{
356+ AccessToken: "Invalid",
357+ RefreshToken: noRefresh,
358+ CreatedAt: &createdPast,
359+ ExpiresOn: &createdFuture,
360+ },
361+ expectedErr: errors.New("session is invalid"),
362+ expectRefreshed: true,
363+ expectValidated: true,
364+ }),
365+ )
366+ })
367+
368+ Context("refreshSessionWithProvider", func() {
369+ type refreshSessionWithProviderTableInput struct {
370+ session *sessionsapi.SessionState
371+ expectedErr error
372+ expectRefreshed bool
373+ expectSaved bool
374+ }
375+
376+ now := time.Now()
377+
378+ DescribeTable("when refreshing with the provider",
379+ func(in refreshSessionWithProviderTableInput) {
380+ saved := false
381+
382+ s := &storedSessionLoader{
383+ store: &fakeSessionStore{
384+ SaveFunc: func(_ http.ResponseWriter, _ *http.Request, ss *sessionsapi.SessionState) error {
385+ saved = true
386+ if ss.AccessToken == "NoSave" {
387+ return errors.New("unable to save session")
388+ }
389+ return nil
390+ },
391+ },
392+ refreshSessionWithProviderIfNeeded: func(_ context.Context, ss *sessionsapi.SessionState) (bool, error) {
393+ switch ss.RefreshToken {
394+ case refresh:
395+ return true, nil
396+ case noRefresh:
397+ return false, nil
398+ default:
399+ return false, errors.New("error refreshing session")
400+ }
401+ },
402+ }
403+
404+ req := httptest.NewRequest("", "/", nil)
405+ refreshed, err := s.refreshSessionWithProvider(nil, req, in.session)
406+ if in.expectedErr != nil {
407+ Expect(err).To(MatchError(in.expectedErr))
408+ } else {
409+ Expect(err).ToNot(HaveOccurred())
410+ }
411+ Expect(refreshed).To(Equal(in.expectRefreshed))
412+ Expect(saved).To(Equal(in.expectSaved))
413+ },
414+ Entry("when the provider does not refresh the session", refreshSessionWithProviderTableInput{
415+ session: &sessionsapi.SessionState{
416+ RefreshToken: noRefresh,
417+ },
418+ expectedErr: nil,
419+ expectRefreshed: false,
420+ expectSaved: false,
421+ }),
422+ Entry("when the provider refreshes the session", refreshSessionWithProviderTableInput{
423+ session: &sessionsapi.SessionState{
424+ RefreshToken: refresh,
425+ },
426+ expectedErr: nil,
427+ expectRefreshed: true,
428+ expectSaved: true,
429+ }),
430+ Entry("when the provider returns an error", refreshSessionWithProviderTableInput{
431+ session: &sessionsapi.SessionState{
432+ RefreshToken: "RefreshError",
433+ CreatedAt: &now,
434+ ExpiresOn: &now,
435+ },
436+ expectedErr: errors.New("error refreshing access token: error refreshing session"),
437+ expectRefreshed: false,
438+ expectSaved: false,
439+ }),
440+ Entry("when the saving the session returns an error", refreshSessionWithProviderTableInput{
441+ session: &sessionsapi.SessionState{
442+ RefreshToken: refresh,
443+ AccessToken: "NoSave",
444+ },
445+ expectedErr: errors.New("error saving session: unable to save session"),
446+ expectRefreshed: false,
447+ expectSaved: true,
448+ }),
449+ )
450+ })
451+
452+ Context("validateSession", func() {
453+ var s *storedSessionLoader
454+
455+ BeforeEach(func() {
456+ s = &storedSessionLoader{
457+ validateSessionState: func(_ context.Context, ss *sessionsapi.SessionState) bool {
458+ return ss.AccessToken == "Valid"
459+ },
460+ }
461+ })
462+
463+ Context("with a valid session", func() {
464+ It("does not return an error", func() {
465+ expires := time.Now().Add(1 * time.Minute)
466+ session := &sessionsapi.SessionState{
467+ AccessToken: "Valid",
468+ ExpiresOn: &expires,
469+ }
470+ Expect(s.validateSession(ctx, session)).To(Succeed())
471+ })
472+ })
473+
474+ Context("with an expired session", func() {
475+ It("returns an error", func() {
476+ created := time.Now().Add(-5 * time.Minute)
477+ expires := time.Now().Add(-1 * time.Minute)
478+ session := &sessionsapi.SessionState{
479+ AccessToken: "Valid",
480+ CreatedAt: &created,
481+ ExpiresOn: &expires,
482+ }
483+ Expect(s.validateSession(ctx, session)).To(MatchError("session is expired"))
484+ })
485+ })
486+
487+ Context("with an invalid session", func() {
488+ It("returns an error", func() {
489+ expires := time.Now().Add(1 * time.Minute)
490+ session := &sessionsapi.SessionState{
491+ AccessToken: "Invalid",
492+ ExpiresOn: &expires,
493+ }
494+ Expect(s.validateSession(ctx, session)).To(MatchError("session is invalid"))
495+ })
496+ })
497+ })
498+})
499+
500+type fakeSessionStore struct {
501+ SaveFunc func(http.ResponseWriter, *http.Request, *sessionsapi.SessionState) error
502+ LoadFunc func(req *http.Request) (*sessionsapi.SessionState, error)
503+ ClearFunc func(rw http.ResponseWriter, req *http.Request) error
504+}
505+
506+func (f *fakeSessionStore) Save(rw http.ResponseWriter, req *http.Request, s *sessionsapi.SessionState) error {
507+ if f.SaveFunc != nil {
508+ return f.SaveFunc(rw, req, s)
509+ }
510+ return nil
511+}
512+func (f *fakeSessionStore) Load(req *http.Request) (*sessionsapi.SessionState, error) {
513+ if f.LoadFunc != nil {
514+ return f.LoadFunc(req)
515+ }
516+ return nil, nil
517+}
518+
519+func (f *fakeSessionStore) Clear(rw http.ResponseWriter, req *http.Request) error {
520+ if f.ClearFunc != nil {
521+ return f.ClearFunc(rw, req)
522+ }
523+ return nil
524+}