From 98020cefa8e86f031776dbc52ba697fc7233ce3a Mon Sep 17 00:00:00 2001 From: Caleb Doxsey Date: Tue, 21 May 2019 09:50:44 -0600 Subject: [PATCH 1/2] add new cookieless session store --- cookieless.go | 83 ++++++++++++++++++++++++++++++++++++++++++++++ cookieless_test.go | 40 ++++++++++++++++++++++ go.mod | 1 + redistore.go | 25 ++++++++------ 4 files changed, 138 insertions(+), 11 deletions(-) create mode 100644 cookieless.go create mode 100644 cookieless_test.go diff --git a/cookieless.go b/cookieless.go new file mode 100644 index 0000000..846513b --- /dev/null +++ b/cookieless.go @@ -0,0 +1,83 @@ +package redistore + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "io" + "net/http" + "strings" +) + +// errors +var ( + ErrHeaderNotFound = errors.New("header not found") + ErrInvalidSessionID = errors.New("invalid session id") +) + +// A CookielessSessionIDStore stores session ids in http headers +type CookielessSessionIDStore struct { + name string + password string +} + +// NewCookielessSessionIDStore creates a new CookielessSessionIDStore +func NewCookielessSessionIDStore(name, password string) *CookielessSessionIDStore { + return &CookielessSessionIDStore{ + name: name, + password: password, + } +} + +// Load attempts to load a session id from an http request +func (store CookielessSessionIDStore) Load(r *http.Request) (string, error) { + value := r.Header.Get(store.headerName()) + if value == "" { + return "", ErrHeaderNotFound + } + + idx := strings.IndexByte(value, ':') + if idx < 0 { + return "", ErrInvalidSessionID + } + + mac, sessionID := value[:idx], value[idx+1:] + if !store.verify(mac, sessionID) { + return "", ErrInvalidSessionID + } + + return sessionID, nil +} + +// Save attemps to save a session id to an http response writer +func (store CookielessSessionIDStore) Save(sessionID string, w http.ResponseWriter) { + mac := store.sign(sessionID) + w.Header().Set(store.headerName(), fmt.Sprintf("%s:%s", mac, sessionID)) +} + +func (store CookielessSessionIDStore) sign(message string) (mac string) { + h := hmac.New(sha256.New, []byte(store.password)) + io.WriteString(h, message) + signature := h.Sum(nil) + return hex.EncodeToString(signature) +} + +func (store CookielessSessionIDStore) verify(mac, message string) bool { + mac1, err := hex.DecodeString(mac) + if err != nil { + return false + } + + mac2, err := hex.DecodeString(store.sign(message)) + if err != nil { + return false + } + + return hmac.Equal(mac1, mac2) +} + +func (store CookielessSessionIDStore) headerName() string { + return fmt.Sprintf("X-SESSION-ID-%s", store.name) +} diff --git a/cookieless_test.go b/cookieless_test.go new file mode 100644 index 0000000..ef0c0c8 --- /dev/null +++ b/cookieless_test.go @@ -0,0 +1,40 @@ +package redistore + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCookielessSessionIDStore(t *testing.T) { + store := NewCookielessSessionIDStore("TEST", "ABCD") + + t.Run("load empty", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/", nil) + sessionID, err := store.Load(req) + assert.Empty(t, sessionID) + assert.Equal(t, ErrHeaderNotFound, err) + }) + t.Run("load invalid", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/", nil) + req.Header.Set("X-SESSION-ID-TEST", "XYZ:value") + sessionID, err := store.Load(req) + assert.Empty(t, sessionID) + assert.Equal(t, ErrInvalidSessionID, err) + }) + t.Run("load valid", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/", nil) + req.Header.Set("X-SESSION-ID-TEST", "94d5574a0ef464c629296fc9d263517944b94d1df9f3472fb7fb2d90af42ca36:value") + sessionID, err := store.Load(req) + assert.NotEmpty(t, sessionID) + assert.NoError(t, err) + }) + + t.Run("save", func(t *testing.T) { + rec := httptest.NewRecorder() + store.Save("value", rec) + assert.Equal(t, "94d5574a0ef464c629296fc9d263517944b94d1df9f3472fb7fb2d90af42ca36:value", rec.Header().Get("X-SESSION-ID-TEST")) + }) +} diff --git a/go.mod b/go.mod index e933cf5..d4b8410 100644 --- a/go.mod +++ b/go.mod @@ -10,4 +10,5 @@ require ( github.com/gorilla/securecookie v1.1.1 github.com/gorilla/sessions v1.1.3 github.com/sirupsen/logrus v1.4.1 // indirect + github.com/stretchr/testify v1.2.2 ) diff --git a/redistore.go b/redistore.go index 832ccae..beb358c 100644 --- a/redistore.go +++ b/redistore.go @@ -23,6 +23,7 @@ import ( // Amount of time for cookies/redis keys to expire. var sessionExpire = 86400 * 30 +var sessionPassword = "78998f7e-657e-42d4-973e-87c45d0a9ecb" // SessionSerializer provides an interface hook for alternative serializers type SessionSerializer interface { @@ -244,23 +245,23 @@ func (s *RediStore) New(r *http.Request, name string) (*sessions.Session, error) options := *s.Options session.Options = &options session.IsNew = true - if c, errCookie := r.Cookie(name); errCookie == nil { - err = securecookie.DecodeMulti(name, c.Value, &session.ID, s.Codecs...) - if err == nil { - ok, err := s.load(session) - session.IsNew = !(err == nil && ok) // not new if no error and data available - } - - // Log cases where the session does not exist but the client still has a cookie. - // Most likely this is not malicious. - if session.IsNew && session.ID != "" { + if c, err := r.Cookie(name); err == nil { + err := securecookie.DecodeMulti(name, c.Value, &session.ID, s.Codecs...) + if err != nil { logrus.WithFields(logrus.Fields{ "requestURI": r.RequestURI, "referer": r.Referer(), "clientIP": fmt.Sprintf("%s|%s|%s", r.Header.Get("X-Real-Ip"), r.Header.Get("X-Forwarded-For"), r.RemoteAddr), - }).Warn("Cookie contained SessionID which did not exist.") + }).WithError(err).Warn("invalid encrypted cookie") } + } else if sessionID, err := NewCookielessSessionIDStore(name, sessionPassword).Load(r); err == nil { + session.ID = sessionID + } + + if session.ID != "" { + ok, err := s.load(session) + session.IsNew = !(err == nil && ok) // not new if no error and data available } return session, err @@ -287,6 +288,8 @@ func (s *RediStore) Save(r *http.Request, w http.ResponseWriter, session *sessio return err } http.SetCookie(w, sessions.NewCookie(session.Name(), encoded, session.Options)) + + NewCookielessSessionIDStore(session.Name(), sessionPassword).Save(session.ID, w) } return nil } From a3bbedbbbb4da2b96296ff33196258ad466a9f22 Mon Sep 17 00:00:00 2001 From: Caleb Doxsey Date: Tue, 21 May 2019 10:18:35 -0600 Subject: [PATCH 2/2] use logrus from breadbox --- go.mod | 8 ++++---- go.sum | 9 ++------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index d4b8410..2fbe2fe 100644 --- a/go.mod +++ b/go.mod @@ -2,13 +2,13 @@ module github.com/getbread/redistore go 1.12 -replace github.com/Sirupsen/logrus => github.com/sirupsen/logrus v1.4.2-0.20190403091019-9b3cdde74fbe - require ( - github.com/Sirupsen/logrus v1.4.2-0.20190403091019-9b3cdde74fbe + github.com/Sirupsen/logrus v0.11.5 + github.com/davecgh/go-spew v1.1.1 // indirect github.com/garyburd/redigo v1.6.0 github.com/gorilla/securecookie v1.1.1 github.com/gorilla/sessions v1.1.3 - github.com/sirupsen/logrus v1.4.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/testify v1.2.2 + golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 // indirect ) diff --git a/go.sum b/go.sum index a7629b8..a9076b3 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/Sirupsen/logrus v0.11.5 h1:aIMrrsnipdTlAieMe7FC/iiuJ0+ELiXCT4YiVQiK9j8= +github.com/Sirupsen/logrus v0.11.5/go.mod h1:rmk17hk6i8ZSAJkSDa7nOxamrG+SP4P0mm+DAvExv4U= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/garyburd/redigo v1.6.0 h1:0VruCpn7yAIIu7pWVClQC8wxCJEcG3nyzpMSHKi1PQc= @@ -8,15 +10,8 @@ github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyC github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU= github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= -github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= -github.com/sirupsen/logrus v1.4.2-0.20190403091019-9b3cdde74fbe h1:n9YDAsXFAHGdIJ8Is72ywCNq3089Y22ttyLTJH7K+Ws= -github.com/sirupsen/logrus v1.4.2-0.20190403091019-9b3cdde74fbe/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8=