git.iko.soy/typeable/oauth2-proxy
on master help sign in
Centralize Ticket management of persistent stores (#682)

* Centralize Ticket management of persistent stores persistence package with Manager & Ticket will handle all the details about keys, secrets, ticket into cookies, etc. Persistent stores just need to pass Save, Load & Clear function handles to the persistent manager now. * Shift to persistence.Manager wrapping a persistence.Store * Break up the Redis client builder logic * Move error messages to Store from Manager * Convert ticket to private for Manager use only * Add persistence Manager & ticket tests * Make a custom MockStore that handles time FastForwards

commit9643a0b10c9bac2899a241d94e66ac2a64cf6d57 authorNick Meves <nick.meves@greenhouse.io> dateSun 19 Jul 2020, 13:25 (6 years ago) parentf141f7c
12 files changed+727−393
CHANGELOG.md+1 pkg/apis/sessions/legacy_v5_tester.go+3−3 pkg/sessions/persistence/interfaces.go+15 pkg/sessions/persistence/manager.go+91 pkg/sessions/persistence/manager_test.go+34 pkg/sessions/persistence/ticket.go+221 pkg/sessions/persistence/ticket_test.go+223 pkg/sessions/redis/redis_store.go+73−309 pkg/sessions/redis/redis_store_test.go+4−67 pkg/sessions/session_store_test.go+4−2 pkg/sessions/tests/mock_store.go+58 pkg/sessions/tests/session_store_tests.go−12
CHANGELOG.md+1
@@ -11,6 +11,7 @@
1111
1212## Changes since v6.0.0
1313
14+- [#682](https://github.com/oauth2-proxy/oauth2-proxy/pull/682) Refactor persistent session store session ticket management (@NickMeves)
1415- [#688](https://github.com/oauth2-proxy/oauth2-proxy/pull/688) Refactor session loading to make use of middleware pattern (@JoelSpeed)
1516- [#593](https://github.com/oauth2-proxy/oauth2-proxy/pull/593) Integrate upstream package with OAuth2 Proxy (@JoelSpeed)
1617- [#687](https://github.com/oauth2-proxy/oauth2-proxy/pull/687) Refactor HTPasswd Validator (@JoelSpeed)
pkg/apis/sessions/legacy_v5_tester.go+3−3
@@ -9,6 +9,8 @@ import (
99 "github.com/stretchr/testify/assert"
1010)
1111
12+const LegacyV5TestSecret = "0123456789abcdefghijklmnopqrstuv"
13+
1214// LegacyV5TestCase provides V5 JSON based test cases for legacy fallback code
1315type LegacyV5TestCase struct {
1416 Input string
@@ -22,8 +24,6 @@ type LegacyV5TestCase struct {
2224//
2325// TODO: Remove when this is deprecated (likely V7)
2426func CreateLegacyV5TestCases(t *testing.T) (map[string]LegacyV5TestCase, encryption.Cipher, encryption.Cipher) {
25− const secret = "0123456789abcdefghijklmnopqrstuv"
26−
2727 created := time.Now()
2828 createdJSON, err := created.MarshalJSON()
2929 assert.NoError(t, err)
@@ -33,7 +33,7 @@ func CreateLegacyV5TestCases(t *testing.T) (map[string]LegacyV5TestCase, encrypt
3333 assert.NoError(t, err)
3434 eString := string(eJSON)
3535
36− cfbCipher, err := encryption.NewCFBCipher([]byte(secret))
36+ cfbCipher, err := encryption.NewCFBCipher([]byte(LegacyV5TestSecret))
3737 assert.NoError(t, err)
3838 legacyCipher := encryption.NewBase64Cipher(cfbCipher)
3939
pkg/sessions/persistence/interfaces.gonew file+15
@@ -0,0 +1,15 @@
1+package persistence
2+
3+import (
4+ "context"
5+ "time"
6+)
7+
8+// Store is used for persistent session stores (IE not Cookie)
9+// Implementing this interface allows it to easily use the persistence.Manager
10+// for session ticket + encryption details.
11+type Store interface {
12+ Save(context.Context, string, []byte, time.Duration) error
13+ Load(context.Context, string) ([]byte, error)
14+ Clear(context.Context, string) error
15+}
pkg/sessions/persistence/manager.gonew file+91
@@ -0,0 +1,91 @@
1+package persistence
2+
3+import (
4+ "fmt"
5+ "net/http"
6+ "time"
7+
8+ "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
9+ "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
10+)
11+
12+// Manager wraps a Store and handles the implementation details of the
13+// sessions.SessionStore with its use of session tickets
14+type Manager struct {
15+ Store Store
16+ Options *options.Cookie
17+}
18+
19+// NewManager creates a Manager that can wrap a Store and manage the
20+// sessions.SessionStore implementation details
21+func NewManager(store Store, cookieOpts *options.Cookie) *Manager {
22+ return &Manager{
23+ Store: store,
24+ Options: cookieOpts,
25+ }
26+}
27+
28+// Save saves a session in a persistent Store. Save will generate (or reuse an
29+// existing) ticket which manages unique per session encryption & retrieval
30+// from the persistent data store.
31+func (m *Manager) Save(rw http.ResponseWriter, req *http.Request, s *sessions.SessionState) error {
32+ if s.CreatedAt == nil || s.CreatedAt.IsZero() {
33+ now := time.Now()
34+ s.CreatedAt = &now
35+ }
36+
37+ tckt, err := decodeTicketFromRequest(req, m.Options)
38+ if err != nil {
39+ tckt, err = newTicket(m.Options)
40+ if err != nil {
41+ return fmt.Errorf("error creating a session ticket: %v", err)
42+ }
43+ }
44+
45+ err = tckt.saveSession(s, func(key string, val []byte, exp time.Duration) error {
46+ return m.Store.Save(req.Context(), key, val, exp)
47+ })
48+ if err != nil {
49+ return err
50+ }
51+ tckt.setCookie(rw, req, s)
52+
53+ return nil
54+}
55+
56+// Load reads sessions.SessionState information from a session store. It will
57+// use the session ticket from the http.Request's cookie.
58+func (m *Manager) Load(req *http.Request) (*sessions.SessionState, error) {
59+ tckt, err := decodeTicketFromRequest(req, m.Options)
60+ if err != nil {
61+ return nil, err
62+ }
63+
64+ return tckt.loadSession(func(key string) ([]byte, error) {
65+ return m.Store.Load(req.Context(), key)
66+ })
67+}
68+
69+// Clear clears any saved session information for a given ticket cookie.
70+// Then it clears all session data for that ticket in the Store.
71+func (m *Manager) Clear(rw http.ResponseWriter, req *http.Request) error {
72+ tckt, err := decodeTicketFromRequest(req, m.Options)
73+ if err != nil {
74+ // Always clear the cookie, even when we can't load a cookie from
75+ // the request
76+ tckt = &ticket{
77+ options: m.Options,
78+ }
79+ tckt.clearCookie(rw, req)
80+ // Don't raise an error if we didn't have a Cookie
81+ if err == http.ErrNoCookie {
82+ return nil
83+ }
84+ return fmt.Errorf("error decoding ticket to clear session: %v", err)
85+ }
86+
87+ tckt.clearCookie(rw, req)
88+ return tckt.clearSession(func(key string) error {
89+ return m.Store.Clear(req.Context(), key)
90+ })
91+}
pkg/sessions/persistence/manager_test.gonew file+34
@@ -0,0 +1,34 @@
1+package persistence
2+
3+import (
4+ "testing"
5+ "time"
6+
7+ "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
8+ sessionsapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
9+ "github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
10+ "github.com/oauth2-proxy/oauth2-proxy/pkg/sessions/tests"
11+ . "github.com/onsi/ginkgo"
12+ . "github.com/onsi/gomega"
13+)
14+
15+func TestManager(t *testing.T) {
16+ logger.SetOutput(GinkgoWriter)
17+ RegisterFailHandler(Fail)
18+ RunSpecs(t, "Persistence Manager SessionStore")
19+}
20+
21+var _ = Describe("Persistence Manager SessionStore Tests", func() {
22+ var ms *tests.MockStore
23+ BeforeEach(func() {
24+ ms = tests.NewMockStore()
25+ })
26+ tests.RunSessionStoreTests(
27+ func(_ *options.SessionOptions, cookieOpts *options.Cookie) (sessionsapi.SessionStore, error) {
28+ return NewManager(ms, cookieOpts), nil
29+ },
30+ func(d time.Duration) error {
31+ ms.FastForward(d)
32+ return nil
33+ })
34+})
pkg/sessions/persistence/ticket.gonew file+221
@@ -0,0 +1,221 @@
1+package persistence
2+
3+import (
4+ "crypto/aes"
5+ "crypto/cipher"
6+ "crypto/rand"
7+ "encoding/base64"
8+ "encoding/hex"
9+ "errors"
10+ "fmt"
11+ "io"
12+ "net/http"
13+ "strings"
14+ "time"
15+
16+ "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
17+ "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
18+ "github.com/oauth2-proxy/oauth2-proxy/pkg/cookies"
19+ "github.com/oauth2-proxy/oauth2-proxy/pkg/encryption"
20+)
21+
22+// saveFunc performs a persistent store's save functionality using
23+// a key string, value []byte & (optional) expiration time.Duration
24+type saveFunc func(string, []byte, time.Duration) error
25+
26+// loadFunc performs a load from a persistent store using a
27+// string key and returning the stored value as []byte
28+type loadFunc func(string) ([]byte, error)
29+
30+// clearFunc performs a persistent store's clear functionality using
31+// a string key for the target of the deletion.
32+type clearFunc func(string) error
33+
34+// ticket is a structure representing the ticket used in server based
35+// session storage. It provides a unique per session decryption secret giving
36+// more security than the shared CookieSecret.
37+type ticket struct {
38+ id string
39+ secret []byte
40+ options *options.Cookie
41+}
42+
43+// newTicket creates a new ticket. The ID & secret will be randomly created
44+// with 16 byte sizes. The ID will be prefixed & hex encoded.
45+func newTicket(cookieOpts *options.Cookie) (*ticket, error) {
46+ rawID := make([]byte, 16)
47+ if _, err := io.ReadFull(rand.Reader, rawID); err != nil {
48+ return nil, fmt.Errorf("failed to create new ticket ID: %v", err)
49+ }
50+ // ticketID is hex encoded
51+ ticketID := fmt.Sprintf("%s-%s", cookieOpts.Name, hex.EncodeToString(rawID))
52+
53+ secret := make([]byte, aes.BlockSize)
54+ if _, err := io.ReadFull(rand.Reader, secret); err != nil {
55+ return nil, fmt.Errorf("failed to create encryption secret: %v", err)
56+ }
57+
58+ return &ticket{
59+ id: ticketID,
60+ secret: secret,
61+ options: cookieOpts,
62+ }, nil
63+}
64+
65+// encodeTicket encodes the Ticket to a string for usage in cookies
66+func (t *ticket) encodeTicket() string {
67+ return fmt.Sprintf("%s.%s", t.id, base64.RawURLEncoding.EncodeToString(t.secret))
68+}
69+
70+// decodeTicket decodes an encoded ticket string
71+func decodeTicket(encTicket string, cookieOpts *options.Cookie) (*ticket, error) {
72+ ticketParts := strings.Split(encTicket, ".")
73+ if len(ticketParts) != 2 {
74+ return nil, errors.New("failed to decode ticket")
75+ }
76+ ticketID, secretBase64 := ticketParts[0], ticketParts[1]
77+
78+ secret, err := base64.RawURLEncoding.DecodeString(secretBase64)
79+ if err != nil {
80+ return nil, fmt.Errorf("failed to decode encryption secret: %v", err)
81+ }
82+
83+ return &ticket{
84+ id: ticketID,
85+ secret: secret,
86+ options: cookieOpts,
87+ }, nil
88+}
89+
90+// decodeTicketFromRequest retrieves a potential ticket cookie from a request
91+// and decodes it to a ticket.
92+func decodeTicketFromRequest(req *http.Request, cookieOpts *options.Cookie) (*ticket, error) {
93+ requestCookie, err := req.Cookie(cookieOpts.Name)
94+ if err != nil {
95+ // Don't wrap this error to allow `err == http.ErrNoCookie` checks
96+ return nil, err
97+ }
98+
99+ // An existing cookie exists, try to retrieve the ticket
100+ val, _, ok := encryption.Validate(requestCookie, cookieOpts.Secret, cookieOpts.Expire)
101+ if !ok {
102+ return nil, fmt.Errorf("session ticket cookie failed validation: %v", err)
103+ }
104+
105+ // Valid cookie, decode the ticket
106+ return decodeTicket(string(val), cookieOpts)
107+}
108+
109+// saveSession encodes the SessionState with the ticket's secret and persists
110+// it to disk via the passed saveFunc.
111+func (t *ticket) saveSession(s *sessions.SessionState, saver saveFunc) error {
112+ c, err := t.makeCipher()
113+ if err != nil {
114+ return err
115+ }
116+ ciphertext, err := s.EncodeSessionState(c, false)
117+ if err != nil {
118+ return fmt.Errorf("failed to encode the session state with the ticket: %v", err)
119+ }
120+ return saver(t.id, ciphertext, t.options.Expire)
121+}
122+
123+// loadSession loads a session from the disk store via the passed loadFunc
124+// using the ticket.id as the key. It then decodes the SessionState using
125+// ticket.secret to make the AES-GCM cipher.
126+//
127+// TODO (@NickMeves): Remove legacyV5LoadSession support in V7
128+func (t *ticket) loadSession(loader loadFunc) (*sessions.SessionState, error) {
129+ ciphertext, err := loader(t.id)
130+ if err != nil {
131+ return nil, fmt.Errorf("failed to load the session state with the ticket: %v", err)
132+ }
133+ c, err := t.makeCipher()
134+ if err != nil {
135+ return nil, err
136+ }
137+ ss, err := sessions.DecodeSessionState(ciphertext, c, false)
138+ if err != nil {
139+ return t.legacyV5LoadSession(ciphertext)
140+ }
141+ return ss, nil
142+}
143+
144+// clearSession uses the passed clearFunc to delete a session stored with a
145+// key of ticket.id
146+func (t *ticket) clearSession(clearer clearFunc) error {
147+ return clearer(t.id)
148+}
149+
150+// setCookie sets the encoded ticket as a cookie
151+func (t *ticket) setCookie(rw http.ResponseWriter, req *http.Request, s *sessions.SessionState) {
152+ ticketCookie := t.makeCookie(
153+ req,
154+ t.encodeTicket(),
155+ t.options.Expire,
156+ *s.CreatedAt,
157+ )
158+
159+ http.SetCookie(rw, ticketCookie)
160+}
161+
162+// clearCookie removes any cookies that would be where this ticket
163+// would set them
164+func (t *ticket) clearCookie(rw http.ResponseWriter, req *http.Request) {
165+ clearCookie := t.makeCookie(
166+ req,
167+ "",
168+ time.Hour*-1,
169+ time.Now(),
170+ )
171+ http.SetCookie(rw, clearCookie)
172+}
173+
174+// makeCookie makes a cookie, signing the value if present
175+func (t *ticket) makeCookie(req *http.Request, value string, expires time.Duration, now time.Time) *http.Cookie {
176+ if value != "" {
177+ value = encryption.SignedValue(t.options.Secret, t.options.Name, []byte(value), now)
178+ }
179+ return cookies.MakeCookieFromOptions(
180+ req,
181+ t.options.Name,
182+ value,
183+ t.options,
184+ expires,
185+ now,
186+ )
187+}
188+
189+// makeCipher makes a AES-GCM cipher out of the ticket's secret
190+func (t *ticket) makeCipher() (encryption.Cipher, error) {
191+ c, err := encryption.NewGCMCipher(t.secret)
192+ if err != nil {
193+ return nil, fmt.Errorf("failed to make an AES-GCM cipher from the ticket secret: %v", err)
194+ }
195+ return c, nil
196+}
197+
198+// legacyV5LoadSession loads a Redis session created in V5 with historical logic
199+//
200+// TODO (@NickMeves): Remove in V7
201+func (t *ticket) legacyV5LoadSession(resultBytes []byte) (*sessions.SessionState, error) {
202+ block, err := aes.NewCipher(t.secret)
203+ if err != nil {
204+ return nil, fmt.Errorf("failed to create a legacy AES-CFB cipher from the ticket secret: %v", err)
205+ }
206+
207+ stream := cipher.NewCFBDecrypter(block, t.secret)
208+ stream.XORKeyStream(resultBytes, resultBytes)
209+
210+ cfbCipher, err := encryption.NewCFBCipher(encryption.SecretBytes(t.options.Secret))
211+ if err != nil {
212+ return nil, err
213+ }
214+ legacyCipher := encryption.NewBase64Cipher(cfbCipher)
215+
216+ session, err := sessions.LegacyV5DecodeSessionState(string(resultBytes), legacyCipher)
217+ if err != nil {
218+ return nil, err
219+ }
220+ return session, nil
221+}
pkg/sessions/persistence/ticket_test.gonew file+223
@@ -0,0 +1,223 @@
1+package persistence
2+
3+import (
4+ "crypto/aes"
5+ "crypto/cipher"
6+ "crypto/rand"
7+ "encoding/base64"
8+ "errors"
9+ "fmt"
10+ "io"
11+ "testing"
12+ "time"
13+
14+ "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
15+ "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
16+ "github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
17+ . "github.com/onsi/ginkgo"
18+ . "github.com/onsi/ginkgo/extensions/table"
19+ . "github.com/onsi/gomega"
20+)
21+
22+func Test_ticket(t *testing.T) {
23+ logger.SetOutput(GinkgoWriter)
24+ RegisterFailHandler(Fail)
25+ RunSpecs(t, "Session Ticket")
26+}
27+
28+var _ = Describe("Session Ticket Tests", func() {
29+ Context("encodeTicket & decodeTicket", func() {
30+ type ticketTableInput struct {
31+ ticket *ticket
32+ encodedTicket string
33+ expectedError error
34+ }
35+
36+ DescribeTable("encodeTicket should decodeTicket back when valid",
37+ func(in ticketTableInput) {
38+ if in.ticket != nil {
39+ enc := in.ticket.encodeTicket()
40+ Expect(enc).To(Equal(in.encodedTicket))
41+
42+ dec, err := decodeTicket(enc, in.ticket.options)
43+ Expect(err).ToNot(HaveOccurred())
44+ Expect(dec).To(Equal(in.ticket))
45+ } else {
46+ _, err := decodeTicket(in.encodedTicket, nil)
47+ Expect(err).To(MatchError(in.expectedError))
48+ }
49+ },
50+ Entry("with a valid ticket", ticketTableInput{
51+ ticket: &ticket{
52+ id: "dummy-0123456789abcdef",
53+ secret: []byte("0123456789abcdef"),
54+ options: &options.Cookie{
55+ Name: "dummy",
56+ },
57+ },
58+ encodedTicket: fmt.Sprintf("%s.%s",
59+ "dummy-0123456789abcdef",
60+ base64.RawURLEncoding.EncodeToString([]byte("0123456789abcdef"))),
61+ expectedError: nil,
62+ }),
63+ Entry("with an invalid encoded ticket with 1 part", ticketTableInput{
64+ ticket: nil,
65+ encodedTicket: "dummy-0123456789abcdef",
66+ expectedError: errors.New("failed to decode ticket"),
67+ }),
68+ Entry("with an invalid base64 encoded secret", ticketTableInput{
69+ ticket: nil,
70+ encodedTicket: "dummy-0123456789abcdef.@)#($*@)#(*$@)#(*$",
71+ expectedError: fmt.Errorf("failed to decode encryption secret: illegal base64 data at input byte 0"),
72+ }),
73+ )
74+ })
75+
76+ Context("saveSession", func() {
77+ It("uses the passed save function", func() {
78+ t, err := newTicket(&options.Cookie{Name: "dummy"})
79+ Expect(err).ToNot(HaveOccurred())
80+
81+ c, err := t.makeCipher()
82+ Expect(err).ToNot(HaveOccurred())
83+
84+ ss := &sessions.SessionState{User: "foobar"}
85+ store := map[string][]byte{}
86+ err = t.saveSession(ss, func(k string, v []byte, e time.Duration) error {
87+ store[k] = v
88+ return nil
89+ })
90+ Expect(err).ToNot(HaveOccurred())
91+
92+ stored, err := sessions.DecodeSessionState(store[t.id], c, false)
93+ Expect(err).ToNot(HaveOccurred())
94+ Expect(stored).To(Equal(ss))
95+ })
96+
97+ It("errors when the saveFunc errors", func() {
98+ t, err := newTicket(&options.Cookie{Name: "dummy"})
99+ Expect(err).ToNot(HaveOccurred())
100+
101+ err = t.saveSession(
102+ &sessions.SessionState{User: "foobar"},
103+ func(k string, v []byte, e time.Duration) error {
104+ return errors.New("save error")
105+ })
106+ Expect(err).To(MatchError(errors.New("save error")))
107+ })
108+ })
109+
110+ Context("loadSession", func() {
111+ It("uses the passed load function", func() {
112+ t, err := newTicket(&options.Cookie{Name: "dummy"})
113+ Expect(err).ToNot(HaveOccurred())
114+
115+ c, err := t.makeCipher()
116+ Expect(err).ToNot(HaveOccurred())
117+
118+ ss := &sessions.SessionState{User: "foobar"}
119+ loadedSession, err := t.loadSession(func(k string) ([]byte, error) {
120+ return ss.EncodeSessionState(c, false)
121+ })
122+ Expect(err).ToNot(HaveOccurred())
123+ Expect(loadedSession).To(Equal(ss))
124+ })
125+
126+ It("errors when the loadFunc errors", func() {
127+ t, err := newTicket(&options.Cookie{Name: "dummy"})
128+ Expect(err).ToNot(HaveOccurred())
129+
130+ data, err := t.loadSession(func(k string) ([]byte, error) {
131+ return nil, errors.New("load error")
132+ })
133+ Expect(data).To(BeNil())
134+ Expect(err).To(MatchError(errors.New("failed to load the session state with the ticket: load error")))
135+ })
136+ })
137+
138+ Context("clearSession", func() {
139+ It("uses the passed clear function", func() {
140+ t, err := newTicket(&options.Cookie{Name: "dummy"})
141+ Expect(err).ToNot(HaveOccurred())
142+
143+ var tracker string
144+ err = t.clearSession(func(k string) error {
145+ tracker = k
146+ return nil
147+ })
148+ Expect(err).ToNot(HaveOccurred())
149+ Expect(tracker).To(Equal(t.id))
150+ })
151+
152+ It("errors when the clearFunc errors", func() {
153+ t, err := newTicket(&options.Cookie{Name: "dummy"})
154+ Expect(err).ToNot(HaveOccurred())
155+
156+ err = t.clearSession(func(k string) error {
157+ return errors.New("clear error")
158+ })
159+ Expect(err).To(MatchError(errors.New("clear error")))
160+ })
161+ })
162+})
163+
164+// TestLegacyV5DecodeSession tests the fallback to LegacyV5DecodeSession
165+// when a V5 encoded session is in Redis
166+//
167+// TODO (@NickMeves): Remove when this is deprecated (likely V7)
168+func Test_legacyV5LoadSession(t *testing.T) {
169+ testCases, _, _ := sessions.CreateLegacyV5TestCases(t)
170+
171+ for testName, tc := range testCases {
172+ t.Run(testName, func(t *testing.T) {
173+ g := NewWithT(t)
174+
175+ secret := make([]byte, aes.BlockSize)
176+ _, err := io.ReadFull(rand.Reader, secret)
177+ g.Expect(err).ToNot(HaveOccurred())
178+ tckt := &ticket{
179+ secret: secret,
180+ options: &options.Cookie{
181+ Secret: base64.RawURLEncoding.EncodeToString([]byte(sessions.LegacyV5TestSecret)),
182+ },
183+ }
184+
185+ encrypted, err := legacyStoreValue(tc.Input, tckt.secret)
186+ g.Expect(err).ToNot(HaveOccurred())
187+
188+ ss, err := tckt.legacyV5LoadSession(encrypted)
189+ if tc.Error {
190+ g.Expect(err).To(HaveOccurred())
191+ g.Expect(ss).To(BeNil())
192+ return
193+ }
194+ g.Expect(err).ToNot(HaveOccurred())
195+
196+ // Compare sessions without *time.Time fields
197+ exp := *tc.Output
198+ exp.CreatedAt = nil
199+ exp.ExpiresOn = nil
200+ act := *ss
201+ act.CreatedAt = nil
202+ act.ExpiresOn = nil
203+ g.Expect(exp).To(Equal(act))
204+ })
205+ }
206+}
207+
208+// legacyStoreValue implements the legacy V5 Redis persistence AES-CFB value encryption
209+//
210+// TODO (@NickMeves): Remove when this is deprecated (likely V7)
211+func legacyStoreValue(value string, ticketSecret []byte) ([]byte, error) {
212+ ciphertext := make([]byte, len(value))
213+ block, err := aes.NewCipher(ticketSecret)
214+ if err != nil {
215+ return nil, fmt.Errorf("error initiating cipher block: %v", err)
216+ }
217+
218+ // Use secret as the Initialization Vector too, because each entry has it's own key
219+ stream := cipher.NewCFBEncrypter(block, ticketSecret)
220+ stream.XORKeyStream(ciphertext, []byte(value))
221+
222+ return ciphertext, nil
223+}
pkg/sessions/redis/redis_store.go+73−309
@@ -2,91 +2,113 @@ package redis
22
33import (
44 "context"
5− "crypto/aes"
6− "crypto/cipher"
7− "crypto/rand"
85 "crypto/x509"
9− "encoding/base64"
10− "encoding/hex"
116 "fmt"
12− "io"
137 "io/ioutil"
14− "net/http"
15− "strings"
168 "time"
179
1810 "github.com/go-redis/redis/v7"
1911 "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
2012 "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
21− "github.com/oauth2-proxy/oauth2-proxy/pkg/cookies"
22− "github.com/oauth2-proxy/oauth2-proxy/pkg/encryption"
2313 "github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
14+ "github.com/oauth2-proxy/oauth2-proxy/pkg/sessions/persistence"
2415)
2516
26−// TicketData is a structure representing the ticket used in server session storage
27−type TicketData struct {
28− TicketID string
29− Secret []byte
30−}
31−
32−// SessionStore is an implementation of the sessions.SessionStore
17+// SessionStore is an implementation of the persistence.Store
3318// interface that stores sessions in redis
3419type SessionStore struct {
35− CookieCipher encryption.Cipher
36− Cookie *options.Cookie
37− Client Client
20+ Client Client
3821}
3922
40−// NewRedisSessionStore initialises a new instance of the SessionStore from
41−// the configuration given
23+// NewRedisSessionStore initialises a new instance of the SessionStore and wraps
24+// it in a persistence.Manager
4225func NewRedisSessionStore(opts *options.SessionOptions, cookieOpts *options.Cookie) (sessions.SessionStore, error) {
43− cfbCipher, err := encryption.NewCFBCipher(encryption.SecretBytes(cookieOpts.Secret))
26+ client, err := newRedisClient(opts.Redis)
4427 if err != nil {
45− return nil, fmt.Errorf("error initialising cipher: %v", err)
28+ return nil, fmt.Errorf("error constructing redis client: %v", err)
4629 }
4730
48− client, err := newRedisCmdable(opts.Redis)
31+ rs := &SessionStore{
32+ Client: client,
33+ }
34+ return persistence.NewManager(rs, cookieOpts), nil
35+}
36+
37+// Save takes a sessions.SessionState and stores the information from it
38+// to redies, and adds a new persistence cookie on the HTTP response writer
39+func (store *SessionStore) Save(ctx context.Context, key string, value []byte, exp time.Duration) error {
40+ err := store.Client.Set(ctx, key, value, exp)
4941 if err != nil {
50− return nil, fmt.Errorf("error constructing redis client: %v", err)
42+ return fmt.Errorf("error saving redis session: %v", err)
5143 }
44+ return nil
45+}
5246
53− rs := &SessionStore{
54− Client: client,
55− CookieCipher: cfbCipher,
56− Cookie: cookieOpts,
47+// Load reads sessions.SessionState information from a persistence
48+// cookie within the HTTP request object
49+func (store *SessionStore) Load(ctx context.Context, key string) ([]byte, error) {
50+ value, err := store.Client.Get(ctx, key)
51+ if err != nil {
52+ return nil, fmt.Errorf("error loading redis session: %v", err)
5753 }
58− return rs, nil
54+ return value, nil
55+}
5956
57+// Clear clears any saved session information for a given persistence cookie
58+// from redis, and then clears the session
59+func (store *SessionStore) Clear(ctx context.Context, key string) error {
60+ err := store.Client.Del(ctx, key)
61+ if err != nil {
62+ return fmt.Errorf("error clearing the session from redis: %v", err)
63+ }
64+ return nil
6065}
6166
62−func newRedisCmdable(opts options.RedisStoreOptions) (Client, error) {
67+// newRedisClient makes a redis.Client (either standalone, sentinel aware, or
68+// redis cluster)
69+func newRedisClient(opts options.RedisStoreOptions) (Client, error) {
6370 if opts.UseSentinel && opts.UseCluster {
6471 return nil, fmt.Errorf("options redis-use-sentinel and redis-use-cluster are mutually exclusive")
6572 }
66−
6773 if opts.UseSentinel {
68− addrs, err := parseRedisURLs(opts.SentinelConnectionURLs)
69− if err != nil {
70− return nil, fmt.Errorf("could not parse redis urls: %v", err)
71− }
72− client := redis.NewFailoverClient(&redis.FailoverOptions{
73− MasterName: opts.SentinelMasterName,
74− SentinelAddrs: addrs,
75− })
76− return newClient(client), nil
74+ return buildSentinelClient(opts)
7775 }
78−
7976 if opts.UseCluster {
80− addrs, err := parseRedisURLs(opts.ClusterConnectionURLs)
81− if err != nil {
82− return nil, fmt.Errorf("could not parse redis urls: %v", err)
83− }
84− client := redis.NewClusterClient(&redis.ClusterOptions{
85− Addrs: addrs,
86− })
87− return newClusterClient(client), nil
77+ return buildClusterClient(opts)
8878 }
8979
80+ return buildStandaloneClient(opts)
81+}
82+
83+// buildSentinelClient makes a redis.Client that connects to Redis Sentinel
84+// for Primary/Replica Redis node coordination
85+func buildSentinelClient(opts options.RedisStoreOptions) (Client, error) {
86+ addrs, err := parseRedisURLs(opts.SentinelConnectionURLs)
87+ if err != nil {
88+ return nil, fmt.Errorf("could not parse redis urls: %v", err)
89+ }
90+ client := redis.NewFailoverClient(&redis.FailoverOptions{
91+ MasterName: opts.SentinelMasterName,
92+ SentinelAddrs: addrs,
93+ })
94+ return newClient(client), nil
95+}
96+
97+// buildClusterClient makes a redis.Client that is Redis Cluster aware
98+func buildClusterClient(opts options.RedisStoreOptions) (Client, error) {
99+ addrs, err := parseRedisURLs(opts.ClusterConnectionURLs)
100+ if err != nil {
101+ return nil, fmt.Errorf("could not parse redis urls: %v", err)
102+ }
103+ client := redis.NewClusterClient(&redis.ClusterOptions{
104+ Addrs: addrs,
105+ })
106+ return newClusterClient(client), nil
107+}
108+
109+// buildStandaloneClient makes a redis.Client that connects to a simple
110+// Redis node
111+func buildStandaloneClient(opts options.RedisStoreOptions) (Client, error) {
90112 opt, err := redis.ParseURL(opts.ConnectionURL)
91113 if err != nil {
92114 return nil, fmt.Errorf("unable to parse redis url: %s", err)
@@ -134,261 +156,3 @@ func parseRedisURLs(urls []string) ([]string, error) {
134156 }
135157 return addrs, nil
136158}
137−
138−// Save takes a sessions.SessionState and stores the information from it
139−// to redies, and adds a new ticket cookie on the HTTP response writer
140−func (store *SessionStore) Save(rw http.ResponseWriter, req *http.Request, s *sessions.SessionState) error {
141− if s.CreatedAt == nil || s.CreatedAt.IsZero() {
142− now := time.Now()
143− s.CreatedAt = &now
144− }
145−
146− // Old sessions that we are refreshing would have a request cookie
147− // New sessions don't, so we ignore the error. storeValue will check requestCookie
148− requestCookie, _ := req.Cookie(store.Cookie.Name)
149− ctx := req.Context()
150− ticketString, err := store.saveSession(ctx, s, store.Cookie.Expire, requestCookie)
151− if err != nil {
152− return err
153− }
154−
155− ticketCookie := store.makeCookie(
156− req,
157− ticketString,
158− store.Cookie.Expire,
159− *s.CreatedAt,
160− )
161−
162− http.SetCookie(rw, ticketCookie)
163− return nil
164−}
165−
166−// Load reads sessions.SessionState information from a ticket
167−// cookie within the HTTP request object
168−func (store *SessionStore) Load(req *http.Request) (*sessions.SessionState, error) {
169− requestCookie, err := req.Cookie(store.Cookie.Name)
170− if err != nil {
171− return nil, fmt.Errorf("error loading session: %s", err)
172− }
173−
174− val, _, ok := encryption.Validate(requestCookie, store.Cookie.Secret, store.Cookie.Expire)
175− if !ok {
176− return nil, fmt.Errorf("cookie signature not valid")
177− }
178− ctx := req.Context()
179− session, err := store.loadSessionFromTicket(ctx, string(val))
180− if err != nil {
181− return nil, fmt.Errorf("error loading session: %s", err)
182− }
183− return session, nil
184−}
185−
186−// Clear clears any saved session information for a given ticket cookie
187−// from redis, and then clears the session
188−func (store *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) error {
189− // We go ahead and clear the cookie first, always.
190− clearCookie := store.makeCookie(
191− req,
192− "",
193− time.Hour*-1,
194− time.Now(),
195− )
196− http.SetCookie(rw, clearCookie)
197−
198− // If there was an existing cookie we should clear the session in redis
199− requestCookie, err := req.Cookie(store.Cookie.Name)
200− if err != nil && err == http.ErrNoCookie {
201− // No existing cookie so can't clear redis
202− return nil
203− } else if err != nil {
204− return fmt.Errorf("error retrieving cookie: %v", err)
205− }
206−
207− val, _, ok := encryption.Validate(requestCookie, store.Cookie.Secret, store.Cookie.Expire)
208− if !ok {
209− return fmt.Errorf("cookie signature not valid")
210− }
211−
212− // We only return an error if we had an issue with redis
213− // If there's an issue decoding the ticket, ignore it
214− ticket, _ := decodeTicket(store.Cookie.Name, string(val))
215− if ticket != nil {
216− ctx := req.Context()
217− err := store.Client.Del(ctx, ticket.asHandle(store.Cookie.Name))
218− if err != nil {
219− return fmt.Errorf("error clearing cookie from redis: %s", err)
220− }
221− }
222− return nil
223−}
224−
225−// saveSession encodes a session with a GCM cipher & saves the data into Redis
226−func (store *SessionStore) saveSession(ctx context.Context, s *sessions.SessionState, expiration time.Duration, requestCookie *http.Cookie) (string, error) {
227− ticket, err := store.getTicket(requestCookie)
228− if err != nil {
229− return "", fmt.Errorf("error getting ticket: %v", err)
230− }
231−
232− c, err := encryption.NewGCMCipher(ticket.Secret)
233− if err != nil {
234− return "", fmt.Errorf("error initiating cipher block %s", err)
235− }
236−
237− // Use AES-GCM since it provides authenticated encryption
238− // AES-CFB used in cookies has the cookie signing SHA to get around the lack of
239− // authentication in AES-CFB
240− ciphertext, err := s.EncodeSessionState(c, false)
241− if err != nil {
242− return "", err
243− }
244−
245− handle := ticket.asHandle(store.Cookie.Name)
246− err = store.Client.Set(ctx, handle, ciphertext, expiration)
247− if err != nil {
248− return "", err
249− }
250− return ticket.encodeTicket(store.Cookie.Name), nil
251−}
252−
253−// loadSessionFromTicket loads the session based on the ticket value
254−func (store *SessionStore) loadSessionFromTicket(ctx context.Context, value string) (*sessions.SessionState, error) {
255− ticket, err := decodeTicket(store.Cookie.Name, value)
256− if err != nil {
257− return nil, err
258− }
259−
260− resultBytes, err := store.Client.Get(ctx, ticket.asHandle(store.Cookie.Name))
261− if err != nil {
262− return nil, err
263− }
264−
265− c, err := encryption.NewGCMCipher(ticket.Secret)
266− if err != nil {
267− return nil, err
268− }
269−
270− session, err := sessions.DecodeSessionState(resultBytes, c, false)
271− if err != nil {
272− // The GCM cipher will error due to a legacy JSON payload not passing
273− // the authentication check part of AES GCM encryption.
274− // In that case, we can attempt to fallback to try a legacy load
275− legacyCipher := encryption.NewBase64Cipher(store.CookieCipher)
276− return legacyV5DecodeSession(resultBytes, ticket, legacyCipher)
277− }
278− return session, nil
279−}
280−
281−// legacyV5DecodeSession loads the session based on the ticket value
282−// This fallback uses V5 style encryption of Base64 + AES CFB
283−func legacyV5DecodeSession(resultBytes []byte, ticket *TicketData, c encryption.Cipher) (*sessions.SessionState, error) {
284− block, err := aes.NewCipher(ticket.Secret)
285− if err != nil {
286− return nil, err
287− }
288− // Use secret as the IV too, because each entry has it's own key
289− stream := cipher.NewCFBDecrypter(block, ticket.Secret)
290− stream.XORKeyStream(resultBytes, resultBytes)
291−
292− session, err := sessions.LegacyV5DecodeSessionState(string(resultBytes), c)
293− if err != nil {
294− return nil, err
295− }
296− return session, nil
297−}
298−
299−// makeCookie makes a cookie, signing the value if present
300−func (store *SessionStore) makeCookie(req *http.Request, value string, expires time.Duration, now time.Time) *http.Cookie {
301− if value != "" {
302− value = encryption.SignedValue(store.Cookie.Secret, store.Cookie.Name, []byte(value), now)
303− }
304− return cookies.MakeCookieFromOptions(
305− req,
306− store.Cookie.Name,
307− value,
308− store.Cookie,
309− expires,
310− now,
311− )
312−}
313−
314−// getTicket retrieves an existing ticket from the cookie if present,
315−// or creates a new ticket
316−func (store *SessionStore) getTicket(requestCookie *http.Cookie) (*TicketData, error) {
317− if requestCookie == nil {
318− return newTicket()
319− }
320−
321− // An existing cookie exists, try to retrieve the ticket
322− val, _, ok := encryption.Validate(requestCookie, store.Cookie.Secret, store.Cookie.Expire)
323− if !ok {
324− // Cookie is invalid, create a new ticket
325− return newTicket()
326− }
327−
328− // Valid cookie, decode the ticket
329− ticket, err := decodeTicket(store.Cookie.Name, string(val))
330− if err != nil {
331− // If we can't decode the ticket we have to create a new one
332− return newTicket()
333− }
334− return ticket, nil
335−}
336−
337−func newTicket() (*TicketData, error) {
338− rawID := make([]byte, 16)
339− if _, err := io.ReadFull(rand.Reader, rawID); err != nil {
340− return nil, fmt.Errorf("failed to create new ticket ID %s", err)
341− }
342− // ticketID is hex encoded
343− ticketID := hex.EncodeToString(rawID)
344−
345− secret := make([]byte, aes.BlockSize)
346− if _, err := io.ReadFull(rand.Reader, secret); err != nil {
347− return nil, fmt.Errorf("failed to create initialization vector %s", err)
348− }
349− ticket := &TicketData{
350− TicketID: ticketID,
351− Secret: secret,
352− }
353− return ticket, nil
354−}
355−
356−func (ticket *TicketData) asHandle(prefix string) string {
357− return fmt.Sprintf("%s-%s", prefix, ticket.TicketID)
358−}
359−
360−func decodeTicket(cookieName string, ticketString string) (*TicketData, error) {
361− prefix := cookieName + "-"
362− if !strings.HasPrefix(ticketString, prefix) {
363− return nil, fmt.Errorf("failed to decode ticket handle")
364− }
365− trimmedTicket := strings.TrimPrefix(ticketString, prefix)
366−
367− ticketParts := strings.Split(trimmedTicket, ".")
368− if len(ticketParts) != 2 {
369− return nil, fmt.Errorf("failed to decode ticket")
370− }
371− ticketID, secretBase64 := ticketParts[0], ticketParts[1]
372−
373− // ticketID must be a hexadecimal string
374− _, err := hex.DecodeString(ticketID)
375− if err != nil {
376− return nil, fmt.Errorf("server ticket failed sanity checks")
377− }
378−
379− secret, err := base64.RawURLEncoding.DecodeString(secretBase64)
380− if err != nil {
381− return nil, fmt.Errorf("failed to decode initialization vector %s", err)
382− }
383− ticketData := &TicketData{
384− TicketID: ticketID,
385− Secret: secret,
386− }
387− return ticketData, nil
388−}
389−
390−func (ticket *TicketData) encodeTicket(prefix string) string {
391− handle := ticket.asHandle(prefix)
392− ticketString := handle + "." + base64.RawURLEncoding.EncodeToString(ticket.Secret)
393− return ticketString
394−}
pkg/sessions/redis/redis_store_test.go+4−67
@@ -1,11 +1,6 @@
11package redis
22
33import (
4− "crypto/aes"
5− "crypto/cipher"
6− "crypto/rand"
7− "fmt"
8− "io"
94 "log"
105 "os"
116 "testing"
@@ -17,70 +12,12 @@ import (
1712 "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
1813 sessionsapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
1914 "github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
15+ "github.com/oauth2-proxy/oauth2-proxy/pkg/sessions/persistence"
2016 "github.com/oauth2-proxy/oauth2-proxy/pkg/sessions/tests"
2117 . "github.com/onsi/ginkgo"
2218 . "github.com/onsi/gomega"
2319)
2420
25−// TestLegacyV5DecodeSession tests the fallback to LegacyV5DecodeSession
26−// when a V5 encoded session is in Redis
27−//
28−// TODO: Remove when this is deprecated (likely V7)
29−func Test_legacyV5DecodeSession(t *testing.T) {
30− testCases, _, legacyCipher := sessionsapi.CreateLegacyV5TestCases(t)
31−
32− for testName, tc := range testCases {
33− t.Run(testName, func(t *testing.T) {
34− g := NewWithT(t)
35−
36− secret := make([]byte, aes.BlockSize)
37− _, err := io.ReadFull(rand.Reader, secret)
38− g.Expect(err).ToNot(HaveOccurred())
39− ticket := &TicketData{
40− TicketID: "",
41− Secret: secret,
42− }
43−
44− encrypted, err := legacyStoreValue(tc.Input, ticket.Secret)
45− g.Expect(err).ToNot(HaveOccurred())
46−
47− ss, err := legacyV5DecodeSession(encrypted, ticket, legacyCipher)
48− if tc.Error {
49− g.Expect(err).To(HaveOccurred())
50− g.Expect(ss).To(BeNil())
51− return
52− }
53− g.Expect(err).ToNot(HaveOccurred())
54−
55− // Compare sessions without *time.Time fields
56− exp := *tc.Output
57− exp.CreatedAt = nil
58− exp.ExpiresOn = nil
59− act := *ss
60− act.CreatedAt = nil
61− act.ExpiresOn = nil
62− g.Expect(exp).To(Equal(act))
63− })
64− }
65−}
66−
67−// legacyStoreValue implements the legacy V5 Redis store AES-CFB value encryption
68−//
69−// TODO: Remove when this is deprecated (likely V7)
70−func legacyStoreValue(value string, ticketSecret []byte) ([]byte, error) {
71− ciphertext := make([]byte, len(value))
72− block, err := aes.NewCipher(ticketSecret)
73− if err != nil {
74− return nil, fmt.Errorf("error initiating cipher block: %v", err)
75− }
76−
77− // Use secret as the Initialization Vector too, because each entry has it's own key
78− stream := cipher.NewCFBEncrypter(block, ticketSecret)
79− stream.XORKeyStream(ciphertext, []byte(value))
80−
81− return ciphertext, nil
82−}
83−
8421func TestSessionStore(t *testing.T) {
8522 logger.SetOutput(GinkgoWriter)
8623
@@ -114,9 +51,9 @@ var _ = Describe("Redis SessionStore Tests", func() {
11451
11552 JustAfterEach(func() {
11653 // Release any connections immediately after the test ends
117− if redisStore, ok := ss.(*SessionStore); ok {
118− if redisStore.Client != nil {
119− Expect(redisStore.Client.(closer).Close()).To(Succeed())
54+ if redisManager, ok := ss.(*persistence.Manager); ok {
55+ if redisManager.Store.(*SessionStore).Client != nil {
56+ Expect(redisManager.Store.(*SessionStore).Client.(closer).Close()).To(Succeed())
12057 }
12158 }
12259 })
pkg/sessions/session_store_test.go+4−2
@@ -10,6 +10,7 @@ import (
1010 "github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
1111 "github.com/oauth2-proxy/oauth2-proxy/pkg/sessions"
1212 sessionscookie "github.com/oauth2-proxy/oauth2-proxy/pkg/sessions/cookie"
13+ "github.com/oauth2-proxy/oauth2-proxy/pkg/sessions/persistence"
1314 "github.com/oauth2-proxy/oauth2-proxy/pkg/sessions/redis"
1415 . "github.com/onsi/ginkgo"
1516 . "github.com/onsi/gomega"
@@ -66,10 +67,11 @@ var _ = Describe("NewSessionStore", func() {
6667 opts.Redis.ConnectionURL = "redis://"
6768 })
6869
69− It("creates a redis.SessionStore", func() {
70+ It("creates a persistence.Manager that wraps a redis.SessionStore", func() {
7071 ss, err := sessions.NewSessionStore(opts, cookieOpts)
7172 Expect(err).NotTo(HaveOccurred())
72− Expect(ss).To(BeAssignableToTypeOf(&redis.SessionStore{}))
73+ Expect(ss).To(BeAssignableToTypeOf(&persistence.Manager{}))
74+ Expect(ss.(*persistence.Manager).Store).To(BeAssignableToTypeOf(&redis.SessionStore{}))
7375 })
7476 })
7577
pkg/sessions/tests/mock_store.gonew file+58
@@ -0,0 +1,58 @@
1+package tests
2+
3+import (
4+ "context"
5+ "fmt"
6+ "time"
7+)
8+
9+// entry is a MockStore cache entry with an expiration
10+type entry struct {
11+ data []byte
12+ expiration time.Duration
13+}
14+
15+// MockStore is a generic in-memory implementation of persistence.Store
16+// for mocking in tests
17+type MockStore struct {
18+ cache map[string]entry
19+ elapsed time.Duration
20+}
21+
22+// NewMockStore creates a MockStore
23+func NewMockStore() *MockStore {
24+ return &MockStore{
25+ cache: map[string]entry{},
26+ elapsed: 0 * time.Second,
27+ }
28+}
29+
30+// Save sets a key to the data to the memory cache
31+func (s *MockStore) Save(_ context.Context, key string, value []byte, exp time.Duration) error {
32+ s.cache[key] = entry{
33+ data: value,
34+ expiration: exp,
35+ }
36+ return nil
37+}
38+
39+// Load gets data from the memory cache via a key
40+func (s *MockStore) Load(_ context.Context, key string) ([]byte, error) {
41+ entry, ok := s.cache[key]
42+ if !ok || entry.expiration <= s.elapsed {
43+ delete(s.cache, key)
44+ return nil, fmt.Errorf("key not found: %s", key)
45+ }
46+ return entry.data, nil
47+}
48+
49+// Clear deletes an entry from the memory cache
50+func (s *MockStore) Clear(_ context.Context, key string) error {
51+ delete(s.cache, key)
52+ return nil
53+}
54+
55+// FastForward simulates the flow of time to test expirations
56+func (s *MockStore) FastForward(duration time.Duration) {
57+ s.elapsed += duration
58+}
pkg/sessions/tests/session_store_tests.go−12
@@ -133,18 +133,6 @@ func RunSessionStoreTests(newSS NewSessionStoreFunc, persistentFastForward Persi
133133 PersistentSessionStoreInterfaceTests(&input)
134134 }
135135 })
136−
137− Context("with an invalid cookie secret", func() {
138− BeforeEach(func() {
139− input.cookieOpts.Secret = "invalid"
140− })
141−
142− It("returns an error when initialising the session store", func() {
143− ss, err := newSS(opts, input.cookieOpts)
144− Expect(err).To(MatchError("error initialising cipher: crypto/aes: invalid key size 7"))
145− Expect(ss).To(BeNil())
146− })
147− })
148136 })
149137}
150138