git.iko.soy/typeable/oauth2-proxy
on master help sign in
Add support for a redis session store
b1bd328Brian Van Klaveren
Gopkg.lock+61
@@ -17,6 +17,25 @@
17 revision = "b26d9c308763d68093482582cea63d69be07a0f0"
17 revision = "b26d9c308763d68093482582cea63d69be07a0f0"
18 version = "v0.3.0"
18 version = "v0.3.0"
19
19
20+[[projects]]
21+ branch = "master"
22+ digest = "1:3cce78d5d0090e3f1162945fba60ba74e72e8422e8e41bb9c701afb67237bb65"
23+ name = "github.com/alicebob/gopher-json"
24+ packages = ["."]
25+ pruneopts = ""
26+ revision = "5a6b3ba71ee69b77cf64febf8b5a7526ca5eaef0"
27+
28+[[projects]]
29+ digest = "1:18a07506ddaa87b1612bfd69eef03f510faf122398df3da774d46dcfe751a060"
30+ name = "github.com/alicebob/miniredis"
31+ packages = [
32+ ".",
33+ "server",
34+ ]
35+ pruneopts = ""
36+ revision = "3d7aa1333af56ab862d446678d93aaa6803e0938"
37+ version = "v2.7.0"
38+
20[[projects]]
39[[projects]]
21 digest = "1:512883404c2a99156e410e9880e3bb35ecccc0c07c1159eb204b5f3ef3c431b3"
40 digest = "1:512883404c2a99156e410e9880e3bb35ecccc0c07c1159eb204b5f3ef3c431b3"
22 name = "github.com/bitly/go-simplejson"
41 name = "github.com/bitly/go-simplejson"
@@ -49,6 +68,22 @@
49 revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e"
68 revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e"
50 version = "v3.2.0"
69 version = "v3.2.0"
51
70
71+[[projects]]
72+ digest = "1:8c7410dae63c74bd92db09bf33af7e0698b635ab6a397fd8e9e10dfcce3138ac"
73+ name = "github.com/go-redis/redis"
74+ packages = [
75+ ".",
76+ "internal",
77+ "internal/consistenthash",
78+ "internal/hashtag",
79+ "internal/pool",
80+ "internal/proto",
81+ "internal/util",
82+ ]
83+ pruneopts = ""
84+ revision = "d22fde8721cc915a55aeb6b00944a76a92bfeb6e"
85+ version = "v6.15.2"
86+
52[[projects]]
87[[projects]]
53 branch = "master"
88 branch = "master"
54 digest = "1:3b760d3b93f994df8eb1d9ebfad17d3e9e37edcb7f7efaa15b427c0d7a64f4e4"
89 digest = "1:3b760d3b93f994df8eb1d9ebfad17d3e9e37edcb7f7efaa15b427c0d7a64f4e4"
@@ -57,6 +92,17 @@
57 pruneopts = ""
92 pruneopts = ""
58 revision = "1e59b77b52bf8e4b449a57e6f79f21226d571845"
93 revision = "1e59b77b52bf8e4b449a57e6f79f21226d571845"
59
94
95+[[projects]]
96+ digest = "1:dcf8316121302735c0ac84e05f4686e3b34e284444435e9a206da48d8be18cb1"
97+ name = "github.com/gomodule/redigo"
98+ packages = [
99+ "internal",
100+ "redis",
101+ ]
102+ pruneopts = ""
103+ revision = "9c11da706d9b7902c6da69c592f75637793fe121"
104+ version = "v2.0.0"
105+
60[[projects]]
106[[projects]]
61 digest = "1:b3c5b95e56c06f5aa72cb2500e6ee5f44fcd122872d4fec2023a488e561218bc"
107 digest = "1:b3c5b95e56c06f5aa72cb2500e6ee5f44fcd122872d4fec2023a488e561218bc"
62 name = "github.com/hpcloud/tail"
108 name = "github.com/hpcloud/tail"
@@ -173,6 +219,19 @@
173 pruneopts = ""
219 pruneopts = ""
174 revision = "1d66fa95c997864ba4d8479f56609620fe542928"
220 revision = "1d66fa95c997864ba4d8479f56609620fe542928"
175
221
222+[[projects]]
223+ branch = "master"
224+ digest = "1:378d29a839ff770e9d9150580b4c01ff0a513a296b0487558a7af7c18adab98e"
225+ name = "github.com/yuin/gopher-lua"
226+ packages = [
227+ ".",
228+ "ast",
229+ "parse",
230+ "pm",
231+ ]
232+ pruneopts = ""
233+ revision = "8bfc7677f583b35a5663a9dd934c08f3b5774bbb"
234+
176[[projects]]
235[[projects]]
177 branch = "master"
236 branch = "master"
178 digest = "1:f6a006d27619a4d93bf9b66fe1999b8c8d1fa62bdc63af14f10fbe6fcaa2aa1a"
237 digest = "1:f6a006d27619a4d93bf9b66fe1999b8c8d1fa62bdc63af14f10fbe6fcaa2aa1a"
@@ -341,9 +400,11 @@
341 analyzer-version = 1
400 analyzer-version = 1
342 input-imports = [
401 input-imports = [
343 "github.com/BurntSushi/toml",
402 "github.com/BurntSushi/toml",
403+ "github.com/alicebob/miniredis",
344 "github.com/bitly/go-simplejson",
404 "github.com/bitly/go-simplejson",
345 "github.com/coreos/go-oidc",
405 "github.com/coreos/go-oidc",
346 "github.com/dgrijalva/jwt-go",
406 "github.com/dgrijalva/jwt-go",
407+ "github.com/go-redis/redis",
347 "github.com/mbland/hmacauth",
408 "github.com/mbland/hmacauth",
348 "github.com/mreiferson/go-options",
409 "github.com/mreiferson/go-options",
349 "github.com/onsi/ginkgo",
410 "github.com/onsi/ginkgo",
Gopkg.toml+8
@@ -46,3 +46,11 @@
46[[constraint]]
46[[constraint]]
47 name = "gopkg.in/natefinch/lumberjack.v2"
47 name = "gopkg.in/natefinch/lumberjack.v2"
48 version = "2.1.0"
48 version = "2.1.0"
49+
50+[[constraint]]
51+ branch = "master"
52+ name = "github.com/go-redis/redis"
53+
54+[[constraint]]
55+ name = "github.com/alicebob/miniredis"
56+ version = "2.7.0"
pkg/apis/options/sessions.go+10
@@ -9,6 +9,7 @@ type SessionOptions struct {
9 Type string `flag:"session-store-type" cfg:"session_store_type" env:"OAUTH2_PROXY_SESSION_STORE_TYPE"`
9 Type string `flag:"session-store-type" cfg:"session_store_type" env:"OAUTH2_PROXY_SESSION_STORE_TYPE"`
10 Cipher *cookie.Cipher
10 Cipher *cookie.Cipher
11 CookieStoreOptions
11 CookieStoreOptions
12+ RedisStoreOptions
12}
13}
13
14
14// CookieSessionStoreType is used to indicate the CookieSessionStore should be
15// CookieSessionStoreType is used to indicate the CookieSessionStore should be
@@ -17,3 +18,12 @@ var CookieSessionStoreType = "cookie"
17
18
18// CookieStoreOptions contains configuration options for the CookieSessionStore.
19// CookieStoreOptions contains configuration options for the CookieSessionStore.
19type CookieStoreOptions struct{}
20type CookieStoreOptions struct{}
21+
22+// RedisSessionStoreType is used to indicate the CookieSessionStore should be
23+// used for storing sessions.
24+var RedisSessionStoreType = "redis"
25+
26+// RedisStoreOptions contains configuration options for the CookieSessionStore.
27+type RedisStoreOptions struct {
28+ RedisConnectionURL string `flag:"redis-connection-url" cfg:"redis_connection_url" env:"OAUTH2_PROXY_REDIS_CONNECTION_URL"`
29+}
pkg/sessions/redis/redis_store.gonew file+271
@@ -0,0 +1,271 @@
1+package redis
2+
3+import (
4+ "crypto/aes"
5+ "crypto/cipher"
6+ "crypto/rand"
7+ "encoding/base64"
8+ "encoding/hex"
9+ "fmt"
10+ "io"
11+ "net/http"
12+ "strings"
13+ "time"
14+
15+ "github.com/go-redis/redis"
16+ "github.com/pusher/oauth2_proxy/cookie"
17+ "github.com/pusher/oauth2_proxy/pkg/apis/options"
18+ "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
19+ "github.com/pusher/oauth2_proxy/pkg/cookies"
20+ "github.com/pusher/oauth2_proxy/pkg/sessions/utils"
21+)
22+
23+// TicketData is a structure representing a used in server session storage
24+type TicketData struct {
25+ TicketID string
26+ Secret []byte
27+}
28+
29+// SessionStore is an implementation of the sessions.SessionStore
30+// interface that stores sessions in redis
31+type SessionStore struct {
32+ CookieCipher *cookie.Cipher
33+ CookieDomain string
34+ CookieExpire time.Duration
35+ CookieHTTPOnly bool
36+ CookieName string
37+ CookiePath string
38+ CookieSecret string
39+ CookieSecure bool
40+ Client *redis.Client
41+}
42+
43+// NewRedisSessionStore initialises a new instance of the SessionStore from
44+// the configuration given
45+func NewRedisSessionStore(opts options.RedisStoreOptions, cookieOpts *options.CookieOptions) (sessions.SessionStore, error) {
46+ opt, err := redis.ParseURL(opts.RedisConnectionURL)
47+ if err != nil {
48+ return nil, fmt.Errorf("unable to parse redis url: %s", err)
49+ }
50+
51+ var cookieCipher *cookie.Cipher
52+ if len(cookieOpts.CookieSecret) > 0 {
53+ var err error
54+ cookieCipher, err = cookie.NewCipher(utils.SecretBytes(cookieOpts.CookieSecret))
55+ if err != nil {
56+ return nil, fmt.Errorf("unable to create cookieCipher: %v", err)
57+ }
58+ }
59+
60+ client := redis.NewClient(opt)
61+
62+ rs := &SessionStore{
63+ Client: client,
64+ CookieCipher: cookieCipher,
65+ CookieDomain: cookieOpts.CookieDomain,
66+ CookieExpire: cookieOpts.CookieExpire,
67+ CookieHTTPOnly: cookieOpts.CookieHTTPOnly,
68+ CookieName: cookieOpts.CookieName,
69+ CookiePath: cookieOpts.CookiePath,
70+ CookieSecret: cookieOpts.CookieSecret,
71+ CookieSecure: cookieOpts.CookieSecure,
72+ }
73+ return rs, nil
74+
75+}
76+
77+// Save takes a sessions.SessionState and stores the information from it
78+// to redies, and adds a new ticket cookie on the HTTP response writer
79+func (store *SessionStore) Save(rw http.ResponseWriter, req *http.Request, s *sessions.SessionState) error {
80+ requestCookie, _ := req.Cookie(store.CookieName)
81+ value, err := s.EncodeSessionState(store.CookieCipher)
82+ if err != nil {
83+ return err
84+ }
85+ ticketString, err := store.storeValue(value, s.ExpiresOn, requestCookie)
86+ if err != nil {
87+ return err
88+ }
89+
90+ ticketCookie := cookies.MakeCookie(
91+ req,
92+ store.CookieName,
93+ ticketString,
94+ store.CookiePath,
95+ store.CookieDomain,
96+ store.CookieHTTPOnly,
97+ store.CookieSecure,
98+ store.CookieExpire,
99+ time.Now(),
100+ )
101+
102+ http.SetCookie(rw, ticketCookie)
103+ return nil
104+}
105+
106+// Load reads sessions.SessionState information from a ticket
107+// cookie within the HTTP request object
108+func (store *SessionStore) Load(req *http.Request) (*sessions.SessionState, error) {
109+ requestCookie, _ := req.Cookie(store.CookieName)
110+ // No cookie validation necessary
111+ session, err := store.LoadSessionFromString(requestCookie.Value)
112+ if err != nil {
113+ return nil, fmt.Errorf("error loading session: %s", err)
114+ }
115+ return session, nil
116+}
117+
118+// LoadSessionFromString loads the session based on the ticket value
119+func (store *SessionStore) LoadSessionFromString(value string) (*sessions.SessionState, error) {
120+ ticket, err := decodeTicket(store.CookieName, value)
121+ if err != nil {
122+ return nil, err
123+ }
124+
125+ result, err := store.Client.Get(ticket.asHandle(store.CookieName)).Result()
126+ if err != nil {
127+ return nil, err
128+ }
129+
130+ resultBytes := []byte(result)
131+ block, err := aes.NewCipher(ticket.Secret)
132+ if err != nil {
133+ return nil, err
134+ }
135+ // Use secret as the IV too, because each entry has it's own key
136+ stream := cipher.NewCFBDecrypter(block, ticket.Secret)
137+ stream.XORKeyStream(resultBytes, resultBytes)
138+
139+ session, err := sessions.DecodeSessionState(string(resultBytes), store.CookieCipher)
140+ if err != nil {
141+ return nil, err
142+ }
143+ return session, nil
144+}
145+
146+// Clear clears any saved session information for a given ticket cookie
147+// from redis, and then clears the session
148+func (store *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) error {
149+ requestCookie, _ := req.Cookie(store.CookieName)
150+
151+ // We go ahead and clear the cookie first, always.
152+ clearCookie := cookies.MakeCookie(
153+ req,
154+ store.CookieName,
155+ "",
156+ store.CookiePath,
157+ store.CookieDomain,
158+ store.CookieHTTPOnly,
159+ store.CookieSecure,
160+ time.Hour*-1,
161+ time.Now(),
162+ )
163+ http.SetCookie(rw, clearCookie)
164+
165+ // We only return an error if we had an issue with redis
166+ // If there's an issue decoding the ticket, ignore it
167+ ticket, _ := decodeTicket(store.CookieName, requestCookie.Value)
168+ if ticket != nil {
169+ deleted, err := store.Client.Del(ticket.asHandle(store.CookieName)).Result()
170+ fmt.Println("delted %n", deleted)
171+ if err != nil {
172+ return fmt.Errorf("error clearing cookie from redis: %s", err)
173+ }
174+ }
175+ return nil
176+}
177+
178+func (store *SessionStore) storeValue(value string, expiresOn time.Time, requestCookie *http.Cookie) (string, error) {
179+ var ticket *TicketData
180+ if requestCookie != nil {
181+ var err error
182+ ticket, err = decodeTicket(store.CookieName, requestCookie.Value)
183+ if err != nil {
184+ return "", err
185+ }
186+ } else {
187+ var err error
188+ ticket, err = newTicket()
189+ if err != nil {
190+ return "", fmt.Errorf("error creating new ticket: %s", err)
191+ }
192+ }
193+
194+ ciphertext := make([]byte, len(value))
195+ block, err := aes.NewCipher(ticket.Secret)
196+ if err != nil {
197+ return "", fmt.Errorf("error initiating cipher block %s", err)
198+ }
199+
200+ // Use secret as the IV too, because each entry has it's own key
201+ stream := cipher.NewCFBEncrypter(block, ticket.Secret)
202+ stream.XORKeyStream(ciphertext, []byte(value))
203+
204+ handle := ticket.asHandle(store.CookieName)
205+ expires := expiresOn.Sub(time.Now())
206+ err = store.Client.Set(handle, ciphertext, expires).Err()
207+ if err != nil {
208+ return "", err
209+ }
210+ return ticket.encodeTicket(store.CookieName), nil
211+}
212+
213+func newTicket() (*TicketData, error) {
214+ rawID := make([]byte, 16)
215+ if _, err := io.ReadFull(rand.Reader, rawID); err != nil {
216+ return nil, fmt.Errorf("failed to create new ticket ID %s", err)
217+ }
218+ // ticketID is hex encoded
219+ ticketID := fmt.Sprintf("%x", rawID)
220+
221+ secret := make([]byte, aes.BlockSize)
222+ if _, err := io.ReadFull(rand.Reader, secret); err != nil {
223+ return nil, fmt.Errorf("failed to create initialization vector %s", err)
224+ }
225+ ticket := &TicketData{
226+ TicketID: ticketID,
227+ Secret: secret,
228+ }
229+ return ticket, nil
230+}
231+
232+func (ticket *TicketData) asHandle(prefix string) string {
233+ return fmt.Sprintf("%s-%s", prefix, ticket.TicketID)
234+}
235+
236+func decodeTicket(cookieName string, ticketString string) (*TicketData, error) {
237+ prefix := cookieName + "-"
238+ if !strings.HasPrefix(ticketString, prefix) {
239+ return nil, fmt.Errorf("failed to decode ticket handle")
240+ }
241+ trimmedTicket := strings.TrimPrefix(ticketString, prefix)
242+
243+ ticketParts := strings.Split(trimmedTicket, ".")
244+ if len(ticketParts) != 2 {
245+ return nil, fmt.Errorf("failed to decode ticket")
246+ }
247+ ticketID, secretBase64 := ticketParts[0], ticketParts[1]
248+
249+ // ticketID must be a hexadecimal string
250+ _, err := hex.DecodeString(ticketID)
251+ if err != nil {
252+ return nil, fmt.Errorf("server ticket failed sanity checks")
253+ // s is not a valid
254+ }
255+
256+ secret, err := base64.RawURLEncoding.DecodeString(secretBase64)
257+ if err != nil {
258+ return nil, fmt.Errorf("failed to decode initialization vector %s", err)
259+ }
260+ ticketData := &TicketData{
261+ TicketID: ticketID,
262+ Secret: secret,
263+ }
264+ return ticketData, nil
265+}
266+
267+func (ticket *TicketData) encodeTicket(prefix string) string {
268+ handle := ticket.asHandle(prefix)
269+ ticketString := handle + "." + base64.RawURLEncoding.EncodeToString(ticket.Secret)
270+ return ticketString
271+}
pkg/sessions/session_store.go+3
@@ -6,6 +6,7 @@ import (
6 "github.com/pusher/oauth2_proxy/pkg/apis/options"
6 "github.com/pusher/oauth2_proxy/pkg/apis/options"
7 "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
7 "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
8 "github.com/pusher/oauth2_proxy/pkg/sessions/cookie"
8 "github.com/pusher/oauth2_proxy/pkg/sessions/cookie"
9+ "github.com/pusher/oauth2_proxy/pkg/sessions/redis"
9)
10)
10
11
11// NewSessionStore creates a SessionStore from the provided configuration
12// NewSessionStore creates a SessionStore from the provided configuration
@@ -13,6 +14,8 @@ func NewSessionStore(opts *options.SessionOptions, cookieOpts *options.CookieOpt
13 switch opts.Type {
14 switch opts.Type {
14 case options.CookieSessionStoreType:
15 case options.CookieSessionStoreType:
15 return cookie.NewCookieSessionStore(opts, cookieOpts)
16 return cookie.NewCookieSessionStore(opts, cookieOpts)
17+ case options.RedisSessionStoreType:
18+ return redis.NewRedisSessionStore(opts.RedisStoreOptions, cookieOpts)
16 default:
19 default:
17 return nil, fmt.Errorf("unknown session store type '%s'", opts.Type)
20 return nil, fmt.Errorf("unknown session store type '%s'", opts.Type)
18 }
21 }
pkg/sessions/session_store_test.go+74−14
@@ -10,14 +10,15 @@ import (
10 "testing"
10 "testing"
11 "time"
11 "time"
12
12
13+ "github.com/alicebob/miniredis"
13 . "github.com/onsi/ginkgo"
14 . "github.com/onsi/ginkgo"
14 . "github.com/onsi/gomega"
15 . "github.com/onsi/gomega"
15 "github.com/pusher/oauth2_proxy/cookie"
16 "github.com/pusher/oauth2_proxy/cookie"
16 "github.com/pusher/oauth2_proxy/pkg/apis/options"
17 "github.com/pusher/oauth2_proxy/pkg/apis/options"
17 sessionsapi "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
18 sessionsapi "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
18− "github.com/pusher/oauth2_proxy/pkg/cookies"
19 "github.com/pusher/oauth2_proxy/pkg/sessions"
19 "github.com/pusher/oauth2_proxy/pkg/sessions"
20 sessionscookie "github.com/pusher/oauth2_proxy/pkg/sessions/cookie"
20 sessionscookie "github.com/pusher/oauth2_proxy/pkg/sessions/cookie"
21+ "github.com/pusher/oauth2_proxy/pkg/sessions/redis"
21 "github.com/pusher/oauth2_proxy/pkg/sessions/utils"
22 "github.com/pusher/oauth2_proxy/pkg/sessions/utils"
22)
23)
23
24
@@ -109,22 +110,18 @@ var _ = Describe("NewSessionStore", func() {
109
110
110 Context("when Clear is called", func() {
111 Context("when Clear is called", func() {
111 BeforeEach(func() {
112 BeforeEach(func() {
112− cookie := cookies.MakeCookie(request,
113+ req := httptest.NewRequest("GET", "http://example.com/", nil)
113− cookieOpts.CookieName,
114+ saveResp := httptest.NewRecorder()
114− "foo",
115+ err := ss.Save(saveResp, req, session)
115− cookieOpts.CookiePath,
116+ Expect(err).ToNot(HaveOccurred())
116− cookieOpts.CookieDomain,
117+
117− cookieOpts.CookieHTTPOnly,
118+ resultCookie := saveResp.Result().Cookies()[0]
118− cookieOpts.CookieSecure,
119− cookieOpts.CookieExpire,
120− time.Now(),
121− )
122− request.AddCookie(cookie)
119+ request.AddCookie(resultCookie)
123− err := ss.Clear(response, request)
120+ err = ss.Clear(response, request)
124 Expect(err).ToNot(HaveOccurred())
121 Expect(err).ToNot(HaveOccurred())
125 })
122 })
126
123
127− It("sets a `set-cookie` header in the response", func() {
124+ It("sets a `set-cookie` header in the response and session to not be loadable after clear", func() {
128 Expect(response.Header().Get("Set-Cookie")).ToNot(BeEmpty())
125 Expect(response.Header().Get("Set-Cookie")).ToNot(BeEmpty())
129 })
126 })
130
127
@@ -171,6 +168,35 @@ var _ = Describe("NewSessionStore", func() {
171 })
168 })
172 }
169 }
173
170
171+ PersistentSessionStoreTests := func() {
172+ Context("when Clear is called the session should not be recoverable", func() {
173+ var loadedAfterClear *sessionsapi.SessionState
174+ BeforeEach(func() {
175+ req := httptest.NewRequest("GET", "http://example.com/", nil)
176+ saveResp := httptest.NewRecorder()
177+ err := ss.Save(saveResp, req, session)
178+ Expect(err).ToNot(HaveOccurred())
179+
180+ resultCookie := saveResp.Result().Cookies()[0]
181+ request.AddCookie(resultCookie)
182+ err = ss.Clear(response, request)
183+ Expect(err).ToNot(HaveOccurred())
184+
185+ // The following should only be for server stores
186+ loadReq := httptest.NewRequest("GET", "http://example.com/", nil)
187+ loadReq.AddCookie(resultCookie)
188+ loadedAfterClear, err = ss.Load(loadReq)
189+ })
190+
191+ It("sets a `set-cookie` header in the response and session to not be loadable after clear", func() {
192+ Expect(response.Header().Get("Set-Cookie")).ToNot(BeEmpty())
193+ Expect(loadedAfterClear).To(BeNil())
194+ })
195+
196+ CheckCookieOptions()
197+ })
198+ }
199+
174 RunSessionTests := func() {
200 RunSessionTests := func() {
175 Context("with default options", func() {
201 Context("with default options", func() {
176 BeforeEach(func() {
202 BeforeEach(func() {
@@ -221,6 +247,18 @@ var _ = Describe("NewSessionStore", func() {
221 })
247 })
222 }
248 }
223
249
250+ RunPersistentSessionStoreTests := func() {
251+ Context("with default options", func() {
252+ BeforeEach(func() {
253+ var err error
254+ ss, err = sessions.NewSessionStore(opts, cookieOpts)
255+ Expect(err).ToNot(HaveOccurred())
256+ })
257+
258+ PersistentSessionStoreTests()
259+ })
260+ }
261+
224 BeforeEach(func() {
262 BeforeEach(func() {
225 ss = nil
263 ss = nil
226 opts = &options.SessionOptions{}
264 opts = &options.SessionOptions{}
@@ -264,6 +302,28 @@ var _ = Describe("NewSessionStore", func() {
264 })
302 })
265 })
303 })
266
304
305+ Context("with type 'redis'", func() {
306+ BeforeEach(func() {
307+ mr, err := miniredis.Run()
308+ if err != nil {
309+ panic(err)
310+ }
311+ opts.Type = options.RedisSessionStoreType
312+ opts.RedisConnectionURL = "redis://" + mr.Addr()
313+ })
314+
315+ It("creates a redis.SessionStore", func() {
316+ ss, err := sessions.NewSessionStore(opts, cookieOpts)
317+ Expect(err).NotTo(HaveOccurred())
318+ Expect(ss).To(BeAssignableToTypeOf(&redis.SessionStore{}))
319+ })
320+
321+ Context("the redis.SessionStore", func() {
322+ RunSessionTests()
323+ RunPersistentSessionStoreTests()
324+ })
325+ })
326+
267 Context("with an invalid type", func() {
327 Context("with an invalid type", func() {
268 BeforeEach(func() {
328 BeforeEach(func() {
269 opts.Type = "invalid-type"
329 opts.Type = "invalid-type"