diff --git a/README.md b/README.md index 7df0164..a04b698 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ manager := sessionup.NewManager(store) Out-of-the-box sessionup's Manager instance comes with recommended [OWASP](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Session_Management_Cheat_Sheet.md#binding-the-session-id-to-other-user-properties) configuration options already set, but if you feel the need to customize the behaviour and the cookie values the Manager -will use, you can painlessly provide your own options: +will use, you can easily provide your own options: ```go manager := sessionup.NewManager(store, sessionup.Secure(false), sessionup.ExpiresIn(time.Hour * 24)) ``` @@ -54,6 +54,18 @@ func login(w http.ResponseWriter, r *http.Request) { } ``` +You can store additional information with your session as well. +```go +func login(w http.ResponseWriter, r *http.Request) { + userID := ... + err := manager.Init(w, r, userID, sessionup.MetaEntry("permission", "write"), sessionup.MetaEntry("age", "111")) + if err != nil { + // handle error + } + // success +} +``` + `Public` / `Auth` middlewares check whether the request has a cookie with a valid session ID and add the session to the request's context. `Public`, contrary to `Auth`, does not call the Manager's rejection function (also customizable), thus allowing the wrapped handler to execute successfully. diff --git a/manager.go b/manager.go index c5b44be..9f4ba9d 100644 --- a/manager.go +++ b/manager.go @@ -218,8 +218,17 @@ func (m *Manager) Clone(opts ...setter) *Manager { // Init creates a fresh session with the provided user key, inserts it in // the store and sets the proper values of the cookie. -func (m *Manager) Init(w http.ResponseWriter, r *http.Request, key string) error { - s := m.newSession(r, key) +func (m *Manager) Init(w http.ResponseWriter, r *http.Request, key string, mm ...Meta) error { + var meta map[string]string + + if len(mm) > 0 { + meta = make(map[string]string) + for _, apply := range mm { + apply(meta) + } + } + + s := m.newSession(r, key, meta) exp := s.ExpiresAt if s.ExpiresAt.IsZero() { s.ExpiresAt = time.Now().Add(time.Hour * 24) // for temporary sessions diff --git a/manager_test.go b/manager_test.go index 9954122..e904eb2 100644 --- a/manager_test.go +++ b/manager_test.go @@ -244,7 +244,7 @@ func TestInit(t *testing.T) { } } - wasCreateCalled := func(count int, key string, t1, t2 time.Time) check { + wasCreateCalled := func(count int, key string, t1, t2 time.Time, m map[string]string) check { return func(t *testing.T, s *StoreMock, _ *httptest.ResponseRecorder, _ error) { ff := s.CreateCalls() if len(ff) != count { @@ -266,6 +266,10 @@ func TestInit(t *testing.T) { if !ff[0].S.ExpiresAt.Before(t2) { t.Errorf("want before %s, got %s", t2.String(), ff[0].S.ExpiresAt.String()) } + + if !reflect.DeepEqual(m, ff[0].S.Meta) { + t.Errorf("want %v, got %v", m, ff[0].S.Meta) + } } } @@ -282,34 +286,64 @@ func TestInit(t *testing.T) { cc := map[string]struct { Store *StoreMock ExpiresIn time.Duration + Meta []Meta Checks []check }{ "Error returned by store.Create": { Store: storeStub(errors.New("error")), ExpiresIn: time.Hour * 24 * 30, + Meta: []Meta{ + MetaEntry("test1", "10"), + MetaEntry("test2", "20"), + }, Checks: checks( hasErr(true), hasCookie(false), wasCreateCalled(1, key, time.Now().Add(time.Hour*24), - time.Now().Add(time.Hour*24*30+time.Second)), + time.Now().Add(time.Hour*24*30+time.Second), + map[string]string{"test1": "10", "test2": "20"}, + ), ), }, "Successful temporary session init": { Store: storeStub(nil), + Meta: []Meta{ + MetaEntry("test1", "10"), + MetaEntry("test2", "20"), + }, Checks: checks( hasErr(false), hasCookie(true), - wasCreateCalled(1, key, time.Time{}, time.Now().Add(time.Hour*24+time.Second)), + wasCreateCalled(1, key, time.Time{}, + time.Now().Add(time.Hour*24+time.Second), + map[string]string{"test1": "10", "test2": "20"}, + ), ), }, "Successful permanent session init": { + Store: storeStub(nil), + ExpiresIn: time.Hour * 24 * 30, + Meta: []Meta{ + MetaEntry("test1", "10"), + MetaEntry("test2", "20"), + }, + Checks: checks( + hasErr(false), + hasCookie(true), + wasCreateCalled(1, key, time.Now().Add(time.Hour*24), + time.Now().Add(time.Hour*24*30+time.Second), + map[string]string{"test1": "10", "test2": "20"}, + ), + ), + }, + "Successful permanent session init with no metadata": { Store: storeStub(nil), ExpiresIn: time.Hour * 24 * 30, Checks: checks( hasErr(false), hasCookie(true), wasCreateCalled(1, key, time.Now().Add(time.Hour*24), - time.Now().Add(time.Hour*24*30+time.Second)), + time.Now().Add(time.Hour*24*30+time.Second), nil), ), }, } @@ -324,7 +358,7 @@ func TestInit(t *testing.T) { rec := httptest.NewRecorder() req := httptest.NewRequest("GET", "http://example.com/", nil) - err := m.Init(rec, req, key) + err := m.Init(rec, req, key, c.Meta...) for _, ch := range c.Checks { ch(t, c.Store, rec, err) } diff --git a/session.go b/session.go index 8312961..43299f5 100644 --- a/session.go +++ b/session.go @@ -46,6 +46,10 @@ type Session struct { OS string `json:"os"` Browser string `json:"browser"` } `json:"agent"` + + // Meta specifies a map of metadata associated with + // the session. + Meta map[string]string `json:"meta,omitempty"` } // IsValid checks whether the incoming request's properties match @@ -73,12 +77,13 @@ func (s Session) IsValid(r *http.Request) bool { // newSession creates a new Session with the data extracted from // the provided request, user key and a freshly generated ID. -func (m *Manager) newSession(r *http.Request, key string) Session { +func (m *Manager) newSession(r *http.Request, key string, meta map[string]string) Session { s := Session{ CreatedAt: time.Now(), ExpiresAt: prepExpiresAt(m.expiresIn), ID: m.genID(), UserKey: key, + Meta: meta, } if m.withIP { @@ -133,3 +138,13 @@ func FromContext(ctx context.Context) (Session, bool) { s, ok := ctx.Value(sessionKey).(Session) return s, ok } + +// Meta is a func that handles session's metadata map. +type Meta func(map[string]string) + +// MetaEntry adds a new entry into the session's metadata map. +func MetaEntry(key, value string) Meta { + return func(m map[string]string) { + m[key] = value + } +} diff --git a/session_test.go b/session_test.go index 0de3c18..f6ae024 100644 --- a/session_test.go +++ b/session_test.go @@ -119,6 +119,9 @@ func TestNewSession(t *testing.T) { key := "key" browser := "Firefox" + meta := map[string]string{ + "test": "test", + } cc := map[string]struct { Manager Manager @@ -168,7 +171,7 @@ func TestNewSession(t *testing.T) { c := c t.Run(cn, func(t *testing.T) { t.Parallel() - s := c.Manager.newSession(c.Req, key) + s := c.Manager.newSession(c.Req, key, meta) if s.CreatedAt.IsZero() { t.Errorf("want %s, got %v", ">0", s.CreatedAt) } @@ -196,6 +199,10 @@ func TestNewSession(t *testing.T) { if !reflect.DeepEqual(c.IP, s.IP) { t.Errorf("want %v, got %v", c.IP, s.IP) } + + if !reflect.DeepEqual(meta, s.Meta) { + t.Errorf("want %v, got %v", meta, s.Meta) + } }) } } @@ -254,3 +261,14 @@ func TestFromContext(t *testing.T) { t.Errorf("want %v, got %v", s, cs) } } + +func TestMetaEntry(t *testing.T) { + m := make(map[string]string) + MetaEntry("test2", "1")(m) + MetaEntry("test1", "2")(m) + + m1 := map[string]string{"test2": "1", "test1": "2"} + if !reflect.DeepEqual(m1, m) { + t.Errorf("want %v, got %v", m1, m) + } +}