Skip to content

Commit

Permalink
Merge pull request #14 from swithek/session-metadata
Browse files Browse the repository at this point in the history
add metadata field to session type
  • Loading branch information
swithek authored Nov 14, 2020
2 parents 23fb9a8 + 03c7902 commit 3fc4d37
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 10 deletions.
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
```
Expand All @@ -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.
Expand Down
13 changes: 11 additions & 2 deletions manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 39 additions & 5 deletions manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}
}
}

Expand All @@ -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),
),
},
}
Expand All @@ -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)
}
Expand Down
17 changes: 16 additions & 1 deletion session.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
}
20 changes: 19 additions & 1 deletion session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
})
}
}
Expand Down Expand Up @@ -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)
}
}

0 comments on commit 3fc4d37

Please sign in to comment.