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:
parent
21c35ac1d5
commit
8901ad4ed7
@ -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
|
||||
|
@ -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
67
expire.go
Normal 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)
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
Expire
|
||||
=========
|
||||
|
||||
Expires user's active sessions if they have not taken action for a configurable amount of time.
|
@ -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)
|
||||
}
|
@ -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
70
expire_test.go
Normal 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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user