@@ -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−}