diff --git a/oauthproxy.go b/oauthproxy.go index 00ac1d49..888e0918 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -564,6 +564,10 @@ func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Reques return p.sessionStore.Clear(rw, req) } +func (p *OAuthProxy) ClearAllSessions(req *http.Request, session *sessionsapi.SessionState) error { + return p.sessionStore.ClearAll(req, session) +} + // LoadCookiedSession reads the user's authentication details from the request func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*sessionsapi.SessionState, error) { return p.sessionStore.Load(req) @@ -774,7 +778,14 @@ func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request, signOutA p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error()) return } + session, err := p.getAuthenticatedSession(rw, req) + if err != nil { + logger.Errorf("Error clearing all sessions cookie: %v", err) + } err = p.ClearSessionCookie(rw, req) + if signOutAllSessions { + err = p.ClearAllSessions(req, session) + } if err != nil { logger.Errorf("Error clearing session cookie: %v", err) p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error()) diff --git a/pkg/apis/sessions/interfaces.go b/pkg/apis/sessions/interfaces.go index 97c364cf..83a3f309 100644 --- a/pkg/apis/sessions/interfaces.go +++ b/pkg/apis/sessions/interfaces.go @@ -12,6 +12,7 @@ type SessionStore interface { Save(rw http.ResponseWriter, req *http.Request, s *SessionState) error Load(req *http.Request) (*SessionState, error) Clear(rw http.ResponseWriter, req *http.Request) error + ClearAll(req *http.Request, session *SessionState) error VerifyConnection(ctx context.Context) error } diff --git a/pkg/encryption/utils.go b/pkg/encryption/utils.go index 426a3131..39eb2ddf 100644 --- a/pkg/encryption/utils.go +++ b/pkg/encryption/utils.go @@ -96,6 +96,14 @@ func GenerateRandomASCIIString(length int) (string, error) { return string(b), nil } +// Encrypts a string with a secret using HMAC-SHA256 and returns a base64-encoded string. +func EncryptStringWithSecret(input, secret string) string { + mac := hmac.New(sha256.New, []byte(secret)) + mac.Write([]byte(input)) + sum := mac.Sum(nil) + return base64.RawURLEncoding.EncodeToString(sum) +} + func GenerateCodeChallenge(method, codeVerifier string) (string, error) { switch method { case CodeChallengeMethodPlain: diff --git a/pkg/sessions/cookie/session_store.go b/pkg/sessions/cookie/session_store.go index f2f4045f..415d4971 100644 --- a/pkg/sessions/cookie/session_store.go +++ b/pkg/sessions/cookie/session_store.go @@ -33,6 +33,11 @@ type SessionStore struct { Minimal bool } +// ClearAll implements sessions.SessionStore. +func (s *SessionStore) ClearAll(req *http.Request, session *sessions.SessionState) error { + panic("unimplemented") +} + // Save takes a sessions.SessionState and stores the information from it // within Cookies set on the HTTP response writer func (s *SessionStore) Save(rw http.ResponseWriter, req *http.Request, ss *sessions.SessionState) error { diff --git a/pkg/sessions/persistence/interfaces.go b/pkg/sessions/persistence/interfaces.go index 5bab9912..04fe3d64 100644 --- a/pkg/sessions/persistence/interfaces.go +++ b/pkg/sessions/persistence/interfaces.go @@ -13,7 +13,9 @@ import ( type Store interface { Save(context.Context, string, []byte, time.Duration) error Load(context.Context, string) ([]byte, error) + LoadList(ctx context.Context, key string) ([]string, error) Clear(context.Context, string) error Lock(key string) sessions.Lock + RPush(context.Context, string, string, time.Duration) error VerifyConnection(context.Context) error } diff --git a/pkg/sessions/persistence/manager.go b/pkg/sessions/persistence/manager.go index 9652f015..56ac56b1 100644 --- a/pkg/sessions/persistence/manager.go +++ b/pkg/sessions/persistence/manager.go @@ -8,6 +8,7 @@ import ( "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/encryption" ) // Manager wraps a Store and handles the implementation details of the @@ -42,9 +43,14 @@ func (m *Manager) Save(rw http.ResponseWriter, req *http.Request, s *sessions.Se } } - err = tckt.saveSession(s, func(key string, val []byte, exp time.Duration) error { - return m.Store.Save(req.Context(), key, val, exp) - }) + err = tckt.saveSession( + s, + func(key string, val []byte, exp time.Duration, s *sessions.SessionState) error { + return m.Store.Save(req.Context(), key, val, exp) + }, + func(key string, val string, exp time.Duration) error { + return m.Store.RPush(req.Context(), key, val, exp) + }) if err != nil { return err } @@ -68,6 +74,30 @@ func (m *Manager) Load(req *http.Request) (*sessions.SessionState, error) { ) } +// ClearAll implements sessions.SessionStore. +func (m *Manager) ClearAll(req *http.Request, session *sessions.SessionState) error { + ticket, _ := decodeTicketFromRequest(req, m.Options) + sessionKey := encryption.EncryptStringWithSecret(session.User+session.Email, ticket.options.Secret) + keys, err := m.Store.LoadList(req.Context(), sessionKey) + if err != nil { + return fmt.Errorf("error decoding ticket to clear session: %v", err) + } + + for _, key := range keys { + err = m.Store.Clear(req.Context(), key) + if err != nil { + return fmt.Errorf("error clearing session for key: %v", err) + } + } + + err = m.Store.Clear(req.Context(), sessionKey) + if err != nil { + return fmt.Errorf("error clearing sessions keys: %v", err) + } + + return err +} + // Clear clears any saved session information for a given ticket cookie. // Then it clears all session data for that ticket in the Store. func (m *Manager) Clear(rw http.ResponseWriter, req *http.Request) error { diff --git a/pkg/sessions/persistence/ticket.go b/pkg/sessions/persistence/ticket.go index 5020ada9..e4c8f8e5 100644 --- a/pkg/sessions/persistence/ticket.go +++ b/pkg/sessions/persistence/ticket.go @@ -20,7 +20,11 @@ import ( // saveFunc performs a persistent store's save functionality using // a key string, value []byte & (optional) expiration time.Duration -type saveFunc func(string, []byte, time.Duration) error +type saveFunc func(string, []byte, time.Duration, *sessions.SessionState) error + +// saveUserStateFunc performs a persistent store's save functionality using +// a key string, value []byte & (optional) expiration time.Duration +type saverUserMapSessionFunc func(string, string, time.Duration) error // loadFunc performs a load from a persistent store using a // string key and returning the stored value as []byte @@ -157,7 +161,7 @@ func decodeTicketFromRequest(req *http.Request, cookieOpts *options.Cookie) (*ti // saveSession encodes the SessionState with the ticket's secret and persists // it to disk via the passed saveFunc. -func (t *ticket) saveSession(s *sessions.SessionState, saver saveFunc) error { +func (t *ticket) saveSession(s *sessions.SessionState, saver saveFunc, saverUserMapSession saverUserMapSessionFunc) error { c, err := t.makeCipher() if err != nil { return err @@ -166,7 +170,10 @@ func (t *ticket) saveSession(s *sessions.SessionState, saver saveFunc) error { if err != nil { return fmt.Errorf("failed to encode the session state with the ticket: %v", err) } - return saver(t.id, ciphertext, t.options.Expire) + + encodedUserState := encryption.EncryptStringWithSecret(s.User+s.Email, t.options.Secret) + saverUserMapSession(encodedUserState, t.id, 2*time.Hour) + return saver(t.id, ciphertext, t.options.Expire, s) } // loadSession loads a session from the disk store via the passed loadFunc diff --git a/pkg/sessions/redis/client.go b/pkg/sessions/redis/client.go index 00cff17c..1376d5f6 100644 --- a/pkg/sessions/redis/client.go +++ b/pkg/sessions/redis/client.go @@ -12,6 +12,9 @@ import ( type Client interface { Get(ctx context.Context, key string) ([]byte, error) Lock(key string) sessions.Lock + Expire(ctx context.Context, key string, expiration time.Duration) error + RPush(ctx context.Context, key string, value string) error + LRange(ctx context.Context, key string) ([]string, error) Set(ctx context.Context, key string, value []byte, expiration time.Duration) error Del(ctx context.Context, key string) error Ping(ctx context.Context) error @@ -29,6 +32,18 @@ func newClient(c *redis.Client) Client { } } +func (c *client) Expire(ctx context.Context, key string, expiration time.Duration) error { + return c.Client.Expire(ctx, key, expiration).Err() +} + +func (c *client) LRange(ctx context.Context, key string) ([]string, error) { + return c.Client.LRange(ctx, key, 0, -1).Result() +} + +func (c *client) RPush(ctx context.Context, key string, value string) error { + return c.Client.RPush(ctx, key, value).Err() +} + func (c *client) Get(ctx context.Context, key string) ([]byte, error) { return c.Client.Get(ctx, key).Bytes() } @@ -61,6 +76,12 @@ func newClusterClient(c *redis.ClusterClient) Client { } } +// Expire implements Client. +// Subtle: this method shadows the method (*ClusterClient).Expire of clusterClient.ClusterClient. +func (c *clusterClient) Expire(ctx context.Context, key string, expiration time.Duration) error { + return c.ClusterClient.Expire(ctx, key, expiration).Err() +} + func (c *clusterClient) Get(ctx context.Context, key string) ([]byte, error) { return c.ClusterClient.Get(ctx, key).Bytes() } @@ -69,6 +90,14 @@ func (c *clusterClient) Set(ctx context.Context, key string, value []byte, expir return c.ClusterClient.Set(ctx, key, value, expiration).Err() } +func (c *clusterClient) RPush(ctx context.Context, key string, value string) error { + return c.ClusterClient.RPush(ctx, key, value).Err() +} + +func (c *clusterClient) LRange(ctx context.Context, key string) ([]string, error) { + return c.ClusterClient.LRange(ctx, key, 0, -1).Result() +} + func (c *clusterClient) Del(ctx context.Context, key string) error { return c.ClusterClient.Del(ctx, key).Err() } diff --git a/pkg/sessions/redis/redis_store.go b/pkg/sessions/redis/redis_store.go index e41a1e1f..e6ec4c3e 100644 --- a/pkg/sessions/redis/redis_store.go +++ b/pkg/sessions/redis/redis_store.go @@ -45,6 +45,31 @@ func (store *SessionStore) Save(ctx context.Context, key string, value []byte, e return nil } +// Save takes a sessions.SessionState and stores the information from it +// to redis, and adds a new persistence cookie on the HTTP response writer +func (store *SessionStore) RPush(ctx context.Context, key string, value string, exp time.Duration) error { + err := store.Client.RPush(ctx, key, value) + if err != nil { + return fmt.Errorf("error appending redis session: %v", err) + } + + if exp > 0 { + if err := store.Client.Expire(ctx, key, exp); err != nil { + return fmt.Errorf("error settings expiration time on appending redis session: %v", err) + } + } + return nil +} + +// LoadList reads a list of strings from Redis at the given key and returns. +func (store *SessionStore) LoadList(ctx context.Context, key string) ([]string, error) { + values, err := store.Client.LRange(ctx, key) + if err != nil { + return nil, fmt.Errorf("error loading redis list: %v", err) + } + return values, nil +} + // Load reads sessions.SessionState information from a persistence // cookie within the HTTP request object func (store *SessionStore) Load(ctx context.Context, key string) ([]byte, error) {