Flush all sessions in logout all (#68)
## Description During logout all sessions, the session should be removed from Redis, otherwise even though the token is expired (cannot write), the user can still read data in the application while the session (15 minutes of refresh token) is still valid for that we are mapping all session keys to a new entry (encrypted) in Redis based on user information and the secret after logout all sessions, we remove the current session from Redis (already existing mechanism), and also read this entry to remove all other active sessions  ## Motivation and Context AB#1625768 ## How Has This Been Tested? Locally with pics ## Checklist: - [ ] My change requires a change to the documentation or CHANGELOG. - [ ] I have updated the documentation/CHANGELOG accordingly. - [x] I have created a feature (non-master) branch for my PR.
This commit is contained in:
commit
b5c83e6649
|
|
@ -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.ClearAllUserSessions(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)
|
||||
|
|
@ -775,6 +779,17 @@ func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request, signOutA
|
|||
return
|
||||
}
|
||||
err = p.ClearSessionCookie(rw, req)
|
||||
if signOutAllSessions {
|
||||
session, errAuthSession := p.getAuthenticatedSession(rw, req)
|
||||
if errAuthSession != nil {
|
||||
logger.Errorf("Error clearing all sessions cookie: %v", errAuthSession)
|
||||
} else {
|
||||
clearAllError := p.ClearAllSessions(req, session)
|
||||
if clearAllError != nil {
|
||||
logger.Errorf("Error clearing session cookie: %v", clearAllError)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
logger.Errorf("Error clearing session cookie: %v", err)
|
||||
p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
ClearAllUserSessions(req *http.Request, session *SessionState) error
|
||||
VerifyConnection(ctx context.Context) error
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -87,6 +87,14 @@ func TestSecretBytesNonBase64(t *testing.T) {
|
|||
assert.Equal(t, 32, len(sb32))
|
||||
}
|
||||
|
||||
func TestEncryptStringWithSecret(t *testing.T) {
|
||||
secret := "my-secret"
|
||||
input := "my-input"
|
||||
|
||||
result := EncryptStringWithSecret(input, secret)
|
||||
assert.Equal(t, result, "7DlH3g1Io9AmyD8tVPEPHdpH9N4jsO07mNCkfNcfW2A")
|
||||
}
|
||||
|
||||
func TestSignAndValidate(t *testing.T) {
|
||||
seed := "0123456789abcdef"
|
||||
key := "cookie-name"
|
||||
|
|
|
|||
|
|
@ -763,9 +763,10 @@ var _ = Describe("Stored Session Suite", func() {
|
|||
})
|
||||
|
||||
type fakeSessionStore struct {
|
||||
SaveFunc func(http.ResponseWriter, *http.Request, *sessionsapi.SessionState) error
|
||||
LoadFunc func(req *http.Request) (*sessionsapi.SessionState, error)
|
||||
ClearFunc func(rw http.ResponseWriter, req *http.Request) error
|
||||
SaveFunc func(http.ResponseWriter, *http.Request, *sessionsapi.SessionState) error
|
||||
LoadFunc func(req *http.Request) (*sessionsapi.SessionState, error)
|
||||
ClearFunc func(rw http.ResponseWriter, req *http.Request) error
|
||||
ClearAllUserSessionsFunc func(req *http.Request, session *sessionsapi.SessionState) error
|
||||
}
|
||||
|
||||
func (f *fakeSessionStore) Save(rw http.ResponseWriter, req *http.Request, s *sessionsapi.SessionState) error {
|
||||
|
|
@ -788,6 +789,13 @@ func (f *fakeSessionStore) Clear(rw http.ResponseWriter, req *http.Request) erro
|
|||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeSessionStore) ClearAllUserSessions(req *http.Request, session *sessionsapi.SessionState) error {
|
||||
if f.ClearAllUserSessionsFunc != nil {
|
||||
return f.ClearAllUserSessionsFunc(req, session)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeSessionStore) VerifyConnection(_ context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,11 @@ type SessionStore struct {
|
|||
Minimal bool
|
||||
}
|
||||
|
||||
// ClearAll implements sessions.SessionStore.
|
||||
func (s *SessionStore) ClearAllUserSessions(_ *http.Request, _ *sessions.SessionState) error {
|
||||
return fmt.Errorf("ClearAllUserSessions is only supported by redis store")
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) 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) ClearAllUserSessions(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 {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ import (
|
|||
// a key string, value []byte & (optional) expiration time.Duration
|
||||
type saveFunc func(string, []byte, time.Duration) 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
|
||||
type loadFunc func(string) ([]byte, error)
|
||||
|
|
@ -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,6 +170,9 @@ 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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -70,15 +70,26 @@ var _ = Describe("Session Ticket Tests", func() {
|
|||
|
||||
ss := &sessions.SessionState{User: "foobar"}
|
||||
store := map[string][]byte{}
|
||||
err = t.saveSession(ss, func(k string, v []byte, e time.Duration) error {
|
||||
store[k] = v
|
||||
return nil
|
||||
})
|
||||
storeUserSessionList := map[string][]string{}
|
||||
storedUserSessionListExpected := map[string][]string{
|
||||
"16-axDAZ63SxeHvCLMjoF5EEX0ipSzNNqxpUITxPxgk": {t.id},
|
||||
}
|
||||
err = t.saveSession(
|
||||
ss,
|
||||
func(k string, v []byte, e time.Duration) error {
|
||||
store[k] = v
|
||||
return nil
|
||||
},
|
||||
func(key string, value string, d time.Duration) error {
|
||||
storeUserSessionList[key] = append(storeUserSessionList[key], value)
|
||||
return nil
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
stored, err := sessions.DecodeSessionState(store[t.id], c, false)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(stored).To(Equal(ss))
|
||||
Expect(storeUserSessionList).To(Equal(storedUserSessionListExpected))
|
||||
})
|
||||
|
||||
It("errors when the saveFunc errors", func() {
|
||||
|
|
@ -89,9 +100,27 @@ var _ = Describe("Session Ticket Tests", func() {
|
|||
&sessions.SessionState{User: "foobar"},
|
||||
func(k string, v []byte, e time.Duration) error {
|
||||
return errors.New("save error")
|
||||
},
|
||||
func(key string, value string, d time.Duration) error {
|
||||
return nil
|
||||
})
|
||||
Expect(err).To(MatchError(errors.New("save error")))
|
||||
})
|
||||
|
||||
It("should not return error when the saverUserMapSession errors", func() {
|
||||
t, err := newTicket(&options.Cookie{Name: "dummy"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
err = t.saveSession(
|
||||
&sessions.SessionState{User: "foobar"},
|
||||
func(k string, v []byte, e time.Duration) error {
|
||||
return nil
|
||||
},
|
||||
func(key string, value string, d time.Duration) error {
|
||||
return errors.New("save user session error")
|
||||
})
|
||||
Expect(err).To(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Context("loadSession", func() {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -14,14 +14,50 @@ type entry struct {
|
|||
expiration time.Duration
|
||||
}
|
||||
|
||||
type entryList struct {
|
||||
data []string
|
||||
expiration time.Duration
|
||||
}
|
||||
|
||||
// MockStore is a generic in-memory implementation of persistence.Store
|
||||
// for mocking in tests
|
||||
type MockStore struct {
|
||||
cache map[string]entry
|
||||
cacheList map[string]entryList
|
||||
lockCache map[string]*MockLock
|
||||
elapsed time.Duration
|
||||
}
|
||||
|
||||
// LoadList implements persistence.Store.
|
||||
func (s *MockStore) LoadList(_ context.Context, key string) ([]string, error) {
|
||||
entry, ok := s.cacheList[key]
|
||||
if !ok || entry.expiration <= s.elapsed {
|
||||
delete(s.cache, key)
|
||||
return nil, fmt.Errorf("key not found: %s", key)
|
||||
}
|
||||
return entry.data, nil
|
||||
}
|
||||
|
||||
// RPush implements persistence.Store.
|
||||
func (s *MockStore) RPush(_ context.Context, key string, value string, time time.Duration) error {
|
||||
entry, ok := s.cacheList[key]
|
||||
if ok {
|
||||
// If the key exists, check if the expiration is still valid
|
||||
entry.data = append(entry.data, value)
|
||||
s.cacheList[key] = entry
|
||||
} else {
|
||||
// If the key does not exist, create a new entryList
|
||||
if s.cacheList == nil {
|
||||
s.cacheList = make(map[string]entryList)
|
||||
}
|
||||
s.cacheList[key] = entryList{
|
||||
data: []string{value},
|
||||
expiration: time,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewMockStore creates a MockStore
|
||||
func NewMockStore() *MockStore {
|
||||
return &MockStore{
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ type testInput struct {
|
|||
request *http.Request
|
||||
response *httptest.ResponseRecorder
|
||||
persistentFastForward PersistentStoreFastForwardFunc
|
||||
opts *options.SessionOptions
|
||||
}
|
||||
|
||||
// sessionStoreFunc is used in testInput to wrap the SessionStore interface.
|
||||
|
|
@ -93,6 +94,7 @@ func RunSessionStoreTests(newSS NewSessionStoreFunc, persistentFastForward Persi
|
|||
request: request,
|
||||
response: response,
|
||||
persistentFastForward: persistentFastForward,
|
||||
opts: opts,
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -451,6 +453,32 @@ func SessionStoreInterfaceTests(in *testInput) {
|
|||
CheckCookieOptions(in)
|
||||
})
|
||||
|
||||
Context("Clear all user sessions", func() {
|
||||
BeforeEach(func() {
|
||||
req := httptest.NewRequest("GET", "http://example.com/", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
err := in.ss().Save(resp, req, in.session)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
resultCookies := resp.Result().Cookies()
|
||||
for _, c := range resultCookies {
|
||||
in.request.AddCookie(c)
|
||||
}
|
||||
})
|
||||
It("should clear all user sessions", func() {
|
||||
if in.opts.Type == options.CookieSessionStoreType {
|
||||
Expect(in.ss().ClearAllUserSessions(in.request, in.session)).To(MatchError("ClearAllUserSessions is only supported by redis store"))
|
||||
return
|
||||
}
|
||||
err := in.ss().ClearAllUserSessions(in.request, in.session)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Verify that the session is cleared
|
||||
loadedSession, loadErr := in.ss().Load(in.request)
|
||||
Expect(loadedSession).To(BeNil())
|
||||
Expect(loadErr).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Context("when Load is called", func() {
|
||||
Context("with a valid session cookie in the request", func() {
|
||||
BeforeEach(func() {
|
||||
|
|
|
|||
Loading…
Reference in New Issue