Skip to content
This repository has been archived by the owner on Dec 14, 2023. It is now read-only.

Commit

Permalink
Merge pull request #8 from getbread/cdoxsey/grow20
Browse files Browse the repository at this point in the history
add new cookieless session store
  • Loading branch information
Caleb Doxsey authored Jul 3, 2019
2 parents 001af52 + ddfe78f commit f994f39
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 17 deletions.
83 changes: 83 additions & 0 deletions cookieless.go
Original file line number Diff line number Diff line change
@@ -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)
}
40 changes: 40 additions & 0 deletions cookieless_test.go
Original file line number Diff line number Diff line change
@@ -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"))
})
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ require (
github.com/gorilla/securecookie v1.1.1
github.com/gorilla/sessions v1.1.3
github.com/sirupsen/logrus v1.4.2
github.com/stretchr/testify v1.2.2
)
6 changes: 0 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,10 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGi
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/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
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=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
25 changes: 14 additions & 11 deletions redistore.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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
}
Expand Down

0 comments on commit f994f39

Please sign in to comment.