1
0
mirror of https://github.com/volatiletech/authboss.git synced 2024-11-28 08:58:38 +02:00

WIP fixing expiry

This commit is contained in:
Kris Runzer 2015-03-02 08:04:31 -08:00
parent 21c35ac1d5
commit 8901ad4ed7
7 changed files with 141 additions and 177 deletions

View File

@ -9,6 +9,9 @@ const (
// the remember module. This serves as a way to force full authentication
// by denying half-authed users acccess to sensitive areas.
SessionHalfAuthKey = "halfauth"
// SessionLastAction is the session key to retrieve the last action of a user.
SessionLastAction = "last_action"
// FlashSuccessKey is used for storing sucess flash messages on the session
FlashSuccessKey = "flash_success"
// FlashErrorKey is used for storing sucess flash messages on the session

View File

@ -109,7 +109,7 @@ func NewConfig() *Config {
StorePassword, ConfirmPrefix + StorePassword,
},
ExpireAfter: time.Duration(60) * time.Minute,
ExpireAfter: 60 * time.Minute,
RecoverOKPath: "/",
RecoverTokenDuration: time.Duration(24) * time.Hour,

67
expire.go Normal file
View File

@ -0,0 +1,67 @@
package authboss
import (
"net/http"
"time"
)
var nowTime = time.Now
// TimeToExpiry returns zero if the user session is expired else the time until expiry.
func TimeToExpiry(w http.ResponseWriter, r *http.Request) time.Duration {
return timeToExpiry(Cfg.SessionStoreMaker(w, r))
}
func timeToExpiry(session ClientStorer) time.Duration {
dateStr, ok := session.Get(SessionLastAction)
if !ok {
return Cfg.ExpireAfter
}
date, err := time.Parse(time.RFC3339, dateStr)
if err != nil {
panic("last_action is not a valid RFC3339 date")
}
remaining := date.Add(Cfg.ExpireAfter).Sub(nowTime().UTC())
if remaining > 0 {
return remaining
}
return 0
}
// RefreshExpiry updates the last action for the user, so he doesn't become expired.
func RefreshExpiry(w http.ResponseWriter, r *http.Request) {
session := Cfg.SessionStoreMaker(w, r)
refreshExpiry(session)
}
func refreshExpiry(session ClientStorer) {
session.Put(SessionLastAction, nowTime().UTC().Format(time.RFC3339))
}
type expireMiddleware struct {
next http.Handler
}
// ExpireMiddleware ensures that the user's expiry information is kept up-to-date
// on each request. Deletes the SessionKey from the session if the user is
// expired (Cfg.ExpireAfter duration since SessionLastAction).
func ExpireMiddleware(next http.Handler) http.Handler {
return expireMiddleware{next}
}
func (m expireMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
session := Cfg.SessionStoreMaker(w, r)
if _, ok := session.Get(SessionKey); ok {
ttl := timeToExpiry(session)
if ttl != 0 {
refreshExpiry(session)
} else {
session.Del(SessionKey)
}
}
m.next.ServeHTTP(w, r)
}

View File

@ -1,4 +0,0 @@
Expire
=========
Expires user's active sessions if they have not taken action for a configurable amount of time.

View File

@ -1,76 +0,0 @@
// Package expire implements user session timeouts.
// To take advantage of this the expire.Middleware must be installed
// into your http stack.
package expire
import (
"net/http"
"time"
"gopkg.in/authboss.v0"
)
const (
// SessionLastAction is the session key to retrieve the last action of a user.
SessionLastAction = "last_action"
)
func init() {
authboss.RegisterModule("expire", &Expire{})
}
type Expire struct{}
func (e *Expire) Initialize() error {
authboss.Cfg.Callbacks.Before(authboss.EventGet, e.BeforeGet)
return nil
}
func (_ *Expire) Routes() authboss.RouteTable { return nil }
func (_ *Expire) Storage() authboss.StorageOptions { return nil }
// BeforeGet ensures the account is not expired.
func (e *Expire) BeforeGet(ctx *authboss.Context) (authboss.Interrupt, error) {
if _, ok := ctx.SessionStorer.Get(authboss.SessionKey); !ok {
return authboss.InterruptNone, nil
}
dateStr, ok := ctx.SessionStorer.Get(SessionLastAction)
if ok {
if date, err := time.Parse(time.RFC3339, dateStr); err != nil {
Touch(ctx.SessionStorer)
} else if time.Now().UTC().After(date.Add(authboss.Cfg.ExpireAfter)) {
ctx.SessionStorer.Del(authboss.SessionKey)
return authboss.InterruptSessionExpired, nil
}
}
return authboss.InterruptNone, nil
}
// Touch updates the last action for the user, so he doesn't become expired.
func Touch(session authboss.ClientStorer) {
session.Put(SessionLastAction, time.Now().UTC().Format(time.RFC3339))
}
type middleware struct {
sessionMaker authboss.SessionStoreMaker
next http.Handler
}
// Middleware ensures that the user's expiry information is kept up-to-date
// on each request.
func Middleware(sessionMaker authboss.SessionStoreMaker, next http.Handler) http.Handler {
return middleware{sessionMaker, next}
}
func (m middleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
session := m.sessionMaker(w, r)
if _, ok := session.Get(authboss.SessionKey); ok {
Touch(session)
}
m.next.ServeHTTP(w, r)
}

View File

@ -1,96 +0,0 @@
package expire
import (
"net/http"
"testing"
"time"
"gopkg.in/authboss.v0"
"gopkg.in/authboss.v0/internal/mocks"
)
func TestExpire_Touch(t *testing.T) {
authboss.NewConfig()
session := mocks.NewMockClientStorer()
if _, ok := session.Get(SessionLastAction); ok {
t.Error("It should not have been set")
}
Touch(session)
if dateStr, ok := session.Get(SessionLastAction); !ok || len(dateStr) == 0 {
t.Error("It should have been set")
} else if date, err := time.Parse(time.RFC3339, dateStr); err != nil {
t.Error("Date is malformed:", dateStr)
} else if date.After(time.Now().UTC()) {
t.Error("The time is set in the future.")
}
}
func TestExpire_BeforeGet(t *testing.T) {
authboss.NewConfig()
authboss.Cfg.ExpireAfter = time.Hour
expire := &Expire{}
session := mocks.NewMockClientStorer()
ctx := mocks.MockRequestContext()
ctx.SessionStorer = session
if interrupted, err := expire.BeforeGet(ctx); err != nil || interrupted != authboss.InterruptNone {
t.Error("There's no user in session, should be no-op.")
}
session.Values[authboss.SessionKey] = "moo"
session.Values[SessionLastAction] = "cow"
if interrupted, err := expire.BeforeGet(ctx); err != nil || interrupted != authboss.InterruptNone {
t.Error("There's a malformed date, this should not error, just fix it:", err, interrupted)
}
if dateStr, ok := session.Get(SessionLastAction); !ok || len(dateStr) == 0 {
t.Error("It should have been set")
} else if date, err := time.Parse(time.RFC3339, dateStr); err != nil {
t.Error("Date is malformed:", dateStr)
} else if date.After(time.Now().UTC()) {
t.Error("The time is set in the future.")
}
session.Values[SessionLastAction] = time.Now().UTC().Add(-2 * time.Hour).Format(time.RFC3339)
if interrupted, err := expire.BeforeGet(ctx); err != nil {
t.Error(err)
} else if interrupted != authboss.InterruptSessionExpired {
t.Error("Expected a session expired interrupt:", interrupted)
}
if _, ok := session.Values[authboss.SessionKey]; ok {
t.Error("The user session should have been expired.")
}
}
type testHandler bool
func (t *testHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
*t = true
}
func TestExpire_Middleware(t *testing.T) {
authboss.NewConfig()
session := mocks.NewMockClientStorer()
session.Values = map[string]string{
authboss.SessionKey: "email@email.com",
}
maker := func(w http.ResponseWriter, r *http.Request) authboss.ClientStorer { return session }
handler := new(testHandler)
touch := Middleware(maker, handler)
touch.ServeHTTP(nil, nil)
if !*handler {
t.Error("Expected middleware's chain to be called.")
}
if dateStr, ok := session.Get(SessionLastAction); !ok || len(dateStr) == 0 {
t.Error("It should have been set")
} else if date, err := time.Parse(time.RFC3339, dateStr); err != nil {
t.Error("Date is malformed:", dateStr)
} else if date.After(time.Now().UTC()) {
t.Error("The time is set in the future.")
}
}

70
expire_test.go Normal file
View File

@ -0,0 +1,70 @@
package authboss
import (
"net/http"
"net/http/httptest"
"testing"
"time"
)
func TestDudeIsExpired(t *testing.T) {
Cfg = NewConfig()
session := mockClientStore{SessionKey: "username"}
refreshExpiry(session)
nowTime = func() time.Time {
return time.Now().UTC().Add(Cfg.ExpireAfter * 2)
}
Cfg.SessionStoreMaker = func(_ http.ResponseWriter, _ *http.Request) ClientStorer {
return session
}
r, _ := http.NewRequest("GET", "tra/la/la", nil)
w := httptest.NewRecorder()
called := false
m := ExpireMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
called = true
}))
m.ServeHTTP(w, r)
if !called {
t.Error("Expected middleware to call handler")
}
if key, ok := session.Get(SessionKey); ok {
t.Error("Unexpcted session key:", key)
}
}
func TestDudeIsNotExpired(t *testing.T) {
Cfg = NewConfig()
session := mockClientStore{SessionKey: "username"}
refreshExpiry(session)
nowTime = func() time.Time {
return time.Now().UTC().Add(Cfg.ExpireAfter / 2)
}
Cfg.SessionStoreMaker = func(_ http.ResponseWriter, _ *http.Request) ClientStorer {
return session
}
r, _ := http.NewRequest("GET", "tra/la/la", nil)
w := httptest.NewRecorder()
called := false
m := ExpireMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
called = true
}))
m.ServeHTTP(w, r)
if !called {
t.Error("Expected middleware to call handler")
}
if key, ok := session.Get(SessionKey); !ok {
t.Error("Expected session key:", key)
}
}