mirror of
https://github.com/volatiletech/authboss.git
synced 2025-01-10 04:17:59 +02:00
Rewrite the lock module
- Add lock module pieces to those that needed it (mocks/user)
This commit is contained in:
parent
62dd36b71e
commit
9ef2a06dcb
@ -23,6 +23,9 @@ type Config struct {
|
||||
// confirm their account, this is where they should be redirected to.
|
||||
ConfirmNotOK string
|
||||
|
||||
// LockNotOK is a path to go to when the user fails
|
||||
LockNotOK string
|
||||
|
||||
// RecoverOK is the redirect path after a successful recovery of a password.
|
||||
RecoverOK string
|
||||
|
||||
|
@ -22,9 +22,9 @@ type User struct {
|
||||
RecoverTokenExpiry time.Time
|
||||
ConfirmToken string
|
||||
Confirmed bool
|
||||
Locked bool
|
||||
AttemptNumber int
|
||||
AttemptTime time.Time
|
||||
AttemptCount int
|
||||
LastAttempt time.Time
|
||||
Locked time.Time
|
||||
OAuthToken string
|
||||
OAuthRefresh string
|
||||
OAuthExpiry time.Time
|
||||
@ -40,9 +40,9 @@ func (m User) GetRecoverToken() string { return m.RecoverToken }
|
||||
func (m User) GetRecoverTokenExpiry() time.Time { return m.RecoverTokenExpiry }
|
||||
func (m User) GetConfirmToken() string { return m.ConfirmToken }
|
||||
func (m User) GetConfirmed() bool { return m.Confirmed }
|
||||
func (m User) GetLocked() bool { return m.Locked }
|
||||
func (m User) GetAttemptNumber() int { return m.AttemptNumber }
|
||||
func (m User) GetAttemptTime() time.Time { return m.AttemptTime }
|
||||
func (m User) GetAttemptCount() int { return m.AttemptCount }
|
||||
func (m User) GetLastAttempt() time.Time { return m.LastAttempt }
|
||||
func (m User) GetLocked() time.Time { return m.Locked }
|
||||
func (m User) GetOAuthToken() string { return m.OAuthToken }
|
||||
func (m User) GetOAuthRefresh() string { return m.OAuthRefresh }
|
||||
func (m User) GetOAuthExpiry() time.Time { return m.OAuthExpiry }
|
||||
@ -58,9 +58,9 @@ func (m *User) PutRecoverTokenExpiry(recoverTokenExpiry time.Time) {
|
||||
}
|
||||
func (m *User) PutConfirmToken(confirmToken string) { m.ConfirmToken = confirmToken }
|
||||
func (m *User) PutConfirmed(confirmed bool) { m.Confirmed = confirmed }
|
||||
func (m *User) PutLocked(locked bool) { m.Locked = locked }
|
||||
func (m *User) PutAttemptNumber(attemptNumber int) { m.AttemptNumber = attemptNumber }
|
||||
func (m *User) PutAttemptTime(attemptTime time.Time) { m.AttemptTime = attemptTime }
|
||||
func (m *User) PutAttemptCount(attemptCount int) { m.AttemptCount = attemptCount }
|
||||
func (m *User) PutLastAttempt(attemptTime time.Time) { m.LastAttempt = attemptTime }
|
||||
func (m *User) PutLocked(locked time.Time) { m.Locked = locked }
|
||||
func (m *User) PutOAuthToken(oAuthToken string) { m.OAuthToken = oAuthToken }
|
||||
func (m *User) PutOAuthRefresh(oAuthRefresh string) { m.OAuthRefresh = oAuthRefresh }
|
||||
func (m *User) PutOAuthExpiry(oAuthExpiry time.Time) { m.OAuthExpiry = oAuthExpiry }
|
||||
|
209
lock/lock.go
209
lock/lock.go
@ -2,6 +2,8 @@
|
||||
package lock
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
@ -29,139 +31,158 @@ type Lock struct {
|
||||
*authboss.Authboss
|
||||
}
|
||||
|
||||
// Initialize the module
|
||||
func (l *Lock) Initialize(ab *authboss.Authboss) error {
|
||||
// Init the module
|
||||
func (l *Lock) Init(ab *authboss.Authboss) error {
|
||||
l.Authboss = ab
|
||||
if l.Storer == nil && l.StoreMaker == nil {
|
||||
return errors.New("need a storer")
|
||||
}
|
||||
|
||||
// Events
|
||||
l.Events.After(authboss.EventGetUser, func(ctx *authboss.Context) error {
|
||||
_, err := l.beforeAuth(ctx)
|
||||
return err
|
||||
})
|
||||
l.Events.Before(authboss.EventAuth, l.beforeAuth)
|
||||
l.Events.After(authboss.EventAuth, l.afterAuth)
|
||||
l.Events.After(authboss.EventAuthFail, l.afterAuthFail)
|
||||
l.Events.Before(authboss.EventAuth, l.BeforeAuth)
|
||||
l.Events.After(authboss.EventAuth, l.AfterAuthSuccess)
|
||||
l.Events.After(authboss.EventAuthFail, l.AfterAuthFail)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Routes for the module
|
||||
func (l *Lock) Routes() authboss.RouteTable {
|
||||
return nil
|
||||
// BeforeAuth ensures the account is not locked.
|
||||
func (l *Lock) BeforeAuth(w http.ResponseWriter, r *http.Request, handled bool) (bool, error) {
|
||||
user, err := l.Authboss.CurrentUser(w, r)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
lu := authboss.MustBeLockable(user)
|
||||
if !IsLocked(lu) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
ro := authboss.RedirectOptions{
|
||||
Code: http.StatusTemporaryRedirect,
|
||||
Failure: "Your account is locked. Please contact the administrator.",
|
||||
RedirectPath: l.Authboss.Config.Paths.LockNotOK,
|
||||
}
|
||||
return true, l.Authboss.Config.Core.Redirector.Redirect(w, r, ro)
|
||||
}
|
||||
|
||||
// Storage requirements
|
||||
func (l *Lock) Storage() authboss.StorageOptions {
|
||||
return authboss.StorageOptions{
|
||||
l.PrimaryID: authboss.String,
|
||||
StoreAttemptNumber: authboss.Integer,
|
||||
StoreAttemptTime: authboss.DateTime,
|
||||
StoreLocked: authboss.DateTime,
|
||||
// AfterAuthSuccess resets the attempt number field.
|
||||
func (l *Lock) AfterAuthSuccess(w http.ResponseWriter, r *http.Request, handled bool) (bool, error) {
|
||||
user, err := l.Authboss.CurrentUser(w, r)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
lu := authboss.MustBeLockable(user)
|
||||
lu.PutAttemptCount(0)
|
||||
lu.PutLastAttempt(time.Now().UTC())
|
||||
|
||||
return false, l.Authboss.Config.Storage.Server.Save(r.Context(), lu)
|
||||
}
|
||||
|
||||
// beforeAuth ensures the account is not locked.
|
||||
func (l *Lock) beforeAuth(ctx *authboss.Context) (authboss.Interrupt, error) {
|
||||
if ctx.User == nil {
|
||||
return authboss.InterruptNone, errUserMissing
|
||||
// AfterAuthFail adjusts the attempt number and time negatively
|
||||
// and locks the user if they're beyond limits.
|
||||
func (l *Lock) AfterAuthFail(w http.ResponseWriter, r *http.Request, handled bool) (bool, error) {
|
||||
user, err := l.Authboss.CurrentUser(w, r)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if locked, ok := ctx.User.DateTime(StoreLocked); ok && locked.After(time.Now().UTC()) {
|
||||
return authboss.InterruptAccountLocked, nil
|
||||
}
|
||||
lu := authboss.MustBeLockable(user)
|
||||
last := lu.GetLastAttempt()
|
||||
attempts := lu.GetAttemptCount()
|
||||
attempts++
|
||||
|
||||
return authboss.InterruptNone, nil
|
||||
}
|
||||
nowLocked := false
|
||||
|
||||
// afterAuth resets the attempt number field.
|
||||
func (l *Lock) afterAuth(ctx *authboss.Context) error {
|
||||
if ctx.User == nil {
|
||||
return errUserMissing
|
||||
}
|
||||
|
||||
ctx.User[StoreAttemptNumber] = int64(0)
|
||||
ctx.User[StoreAttemptTime] = time.Now().UTC()
|
||||
|
||||
if err := ctx.SaveUser(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// afterAuthFail adjusts the attempt number and time.
|
||||
func (l *Lock) afterAuthFail(ctx *authboss.Context) error {
|
||||
if ctx.User == nil {
|
||||
return errUserMissing
|
||||
}
|
||||
|
||||
lastAttempt := time.Now().UTC()
|
||||
if attemptTime, ok := ctx.User.DateTime(StoreAttemptTime); ok {
|
||||
lastAttempt = attemptTime
|
||||
}
|
||||
|
||||
var nAttempts int64
|
||||
if attempts, ok := ctx.User.Int64(StoreAttemptNumber); ok {
|
||||
nAttempts = attempts
|
||||
}
|
||||
|
||||
nAttempts++
|
||||
|
||||
if time.Now().UTC().Sub(lastAttempt) <= l.LockWindow {
|
||||
if nAttempts >= int64(l.LockAfter) {
|
||||
ctx.User[StoreLocked] = time.Now().UTC().Add(l.LockDuration)
|
||||
if time.Now().UTC().Sub(last) <= l.Modules.LockWindow {
|
||||
if attempts >= l.Modules.LockAfter {
|
||||
lu.PutLocked(time.Now().UTC().Add(l.Modules.LockDuration))
|
||||
nowLocked = true
|
||||
}
|
||||
|
||||
ctx.User[StoreAttemptNumber] = nAttempts
|
||||
lu.PutAttemptCount(attempts)
|
||||
} else {
|
||||
ctx.User[StoreAttemptNumber] = int64(1)
|
||||
lu.PutAttemptCount(1)
|
||||
}
|
||||
ctx.User[StoreAttemptTime] = time.Now().UTC()
|
||||
lu.PutLastAttempt(time.Now().UTC())
|
||||
|
||||
if err := ctx.SaveUser(); err != nil {
|
||||
return err
|
||||
if err := l.Authboss.Config.Storage.Server.Save(r.Context(), lu); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return nil
|
||||
if !nowLocked {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
ro := authboss.RedirectOptions{
|
||||
Code: http.StatusTemporaryRedirect,
|
||||
Failure: "Your account has been locked, please contact the administrator.",
|
||||
RedirectPath: l.Authboss.Config.Paths.LockNotOK,
|
||||
}
|
||||
return true, l.Authboss.Config.Core.Redirector.Redirect(w, r, ro)
|
||||
}
|
||||
|
||||
// Lock a user manually.
|
||||
func (l *Lock) Lock(key string) error {
|
||||
user, err := l.Storer.Get(key)
|
||||
func (l *Lock) Lock(ctx context.Context, key string) error {
|
||||
user, err := l.Authboss.Config.Storage.Server.Load(ctx, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attr := authboss.Unbind(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lu := authboss.MustBeLockable(user)
|
||||
lu.PutLocked(time.Now().UTC().Add(l.Authboss.Config.Modules.LockDuration))
|
||||
|
||||
attr[StoreLocked] = time.Now().UTC().Add(l.LockDuration)
|
||||
|
||||
return l.Storer.Put(key, attr)
|
||||
return l.Authboss.Config.Storage.Server.Save(ctx, lu)
|
||||
}
|
||||
|
||||
// Unlock a user that was locked by this module.
|
||||
func (l *Lock) Unlock(key string) error {
|
||||
user, err := l.Storer.Get(key)
|
||||
func (l *Lock) Unlock(ctx context.Context, key string) error {
|
||||
user, err := l.Authboss.Config.Storage.Server.Load(ctx, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attr := authboss.Unbind(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lu := authboss.MustBeLockable(user)
|
||||
|
||||
// Set the last attempt to be -window*2 to avoid immediately
|
||||
// giving another login failure.
|
||||
attr[StoreAttemptTime] = time.Now().UTC().Add(-l.LockWindow * 2)
|
||||
attr[StoreAttemptNumber] = int64(0)
|
||||
attr[StoreLocked] = time.Now().UTC().Add(-l.LockDuration)
|
||||
// giving another login failure. Don't reset Locked to Zero time
|
||||
// because some databases may have trouble storing values before
|
||||
// unix_time(0): Jan 1st, 1970
|
||||
now := time.Now().UTC()
|
||||
lu.PutAttemptCount(0)
|
||||
lu.PutLastAttempt(now.Add(-l.Authboss.Config.Modules.LockWindow * 2))
|
||||
lu.PutLocked(now.Add(-l.Authboss.Config.Modules.LockDuration))
|
||||
|
||||
return l.Storer.Put(key, attr)
|
||||
return l.Authboss.Config.Storage.Server.Save(ctx, lu)
|
||||
}
|
||||
|
||||
// Middleware ensures that a user is confirmed, or else it will intercept the request
|
||||
// and send them to the confirm page, this will load the user if he's not been loaded
|
||||
// yet from the session.
|
||||
//
|
||||
// Panics if the user was not able to be loaded in order to allow a panic handler to show
|
||||
// a nice error page, also panics if it failed to redirect for whatever reason.
|
||||
// TODO(aarondl): Document this middleware better
|
||||
func Middleware(ab *authboss.Authboss, next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
user := ab.LoadCurrentUserP(w, &r)
|
||||
|
||||
lu := authboss.MustBeLockable(user)
|
||||
if IsLocked(lu) {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
logger := ab.RequestLogger(r)
|
||||
logger.Infof("user %s prevented from accessing %s: locked", user.GetPID(), r.URL.Path)
|
||||
ro := authboss.RedirectOptions{
|
||||
Code: http.StatusTemporaryRedirect,
|
||||
Failure: "Your account has been locked, please contact the administrator.",
|
||||
RedirectPath: ab.Config.Paths.LockNotOK,
|
||||
}
|
||||
ab.Config.Core.Redirector.Redirect(w, r, ro)
|
||||
})
|
||||
}
|
||||
|
||||
// IsLocked checks if a user is locked
|
||||
func IsLocked(lu authboss.LockableUser) bool {
|
||||
return lu.GetLocked().After(time.Now().UTC())
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
package lock
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -8,221 +11,323 @@ import (
|
||||
"github.com/volatiletech/authboss/internal/mocks"
|
||||
)
|
||||
|
||||
func TestStorage(t *testing.T) {
|
||||
func TestInit(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
l := &Lock{authboss.New()}
|
||||
storage := l.Storage()
|
||||
if _, ok := storage[StoreAttemptNumber]; !ok {
|
||||
t.Error("Expected attempt number storage option.")
|
||||
}
|
||||
if _, ok := storage[StoreAttemptTime]; !ok {
|
||||
t.Error("Expected attempt number time option.")
|
||||
}
|
||||
if _, ok := storage[StoreLocked]; !ok {
|
||||
t.Error("Expected locked storage option.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBeforeAuth(t *testing.T) {
|
||||
t.Parallel()
|
||||
ab := authboss.New()
|
||||
|
||||
l := &Lock{}
|
||||
ab := authboss.New()
|
||||
ctx := ab.NewContext()
|
||||
|
||||
if interrupt, err := l.beforeAuth(ctx); err != errUserMissing {
|
||||
t.Error("Expected an error because of missing user:", err)
|
||||
} else if interrupt != authboss.InterruptNone {
|
||||
t.Error("Interrupt should not be set:", interrupt)
|
||||
}
|
||||
|
||||
ctx.User = authboss.Attributes{"locked": time.Now().Add(1 * time.Hour)}
|
||||
|
||||
if interrupt, err := l.beforeAuth(ctx); err != nil {
|
||||
t.Error(err)
|
||||
} else if interrupt != authboss.InterruptAccountLocked {
|
||||
t.Error("Expected a locked interrupt:", interrupt)
|
||||
if err := l.Init(ab); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAfterAuth(t *testing.T) {
|
||||
type testHarness struct {
|
||||
lock *Lock
|
||||
ab *authboss.Authboss
|
||||
|
||||
bodyReader *mocks.BodyReader
|
||||
mailer *mocks.Emailer
|
||||
redirector *mocks.Redirector
|
||||
renderer *mocks.Renderer
|
||||
responder *mocks.Responder
|
||||
session *mocks.ClientStateRW
|
||||
storer *mocks.ServerStorer
|
||||
}
|
||||
|
||||
func testSetup() *testHarness {
|
||||
harness := &testHarness{}
|
||||
|
||||
harness.ab = authboss.New()
|
||||
harness.bodyReader = &mocks.BodyReader{}
|
||||
harness.mailer = &mocks.Emailer{}
|
||||
harness.redirector = &mocks.Redirector{}
|
||||
harness.renderer = &mocks.Renderer{}
|
||||
harness.responder = &mocks.Responder{}
|
||||
harness.session = mocks.NewClientRW()
|
||||
harness.storer = mocks.NewServerStorer()
|
||||
|
||||
harness.ab.Paths.LockNotOK = "/lock/not/ok"
|
||||
harness.ab.Modules.LockAfter = 3
|
||||
harness.ab.Modules.LockDuration = time.Hour
|
||||
harness.ab.Modules.LockWindow = time.Minute
|
||||
|
||||
harness.ab.Config.Core.BodyReader = harness.bodyReader
|
||||
harness.ab.Config.Core.Logger = mocks.Logger{}
|
||||
harness.ab.Config.Core.Mailer = harness.mailer
|
||||
harness.ab.Config.Core.Redirector = harness.redirector
|
||||
harness.ab.Config.Core.MailRenderer = harness.renderer
|
||||
harness.ab.Config.Core.Responder = harness.responder
|
||||
harness.ab.Config.Storage.SessionState = harness.session
|
||||
harness.ab.Config.Storage.Server = harness.storer
|
||||
|
||||
harness.lock = &Lock{harness.ab}
|
||||
|
||||
return harness
|
||||
}
|
||||
|
||||
func TestBeforeAuthAllow(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ab := authboss.New()
|
||||
lock := Lock{}
|
||||
ctx := ab.NewContext()
|
||||
harness := testSetup()
|
||||
|
||||
if err := lock.afterAuth(ctx); err != errUserMissing {
|
||||
t.Error("Expected an error because of missing user:", err)
|
||||
user := &mocks.User{
|
||||
Locked: time.Time{},
|
||||
}
|
||||
|
||||
storer := mocks.NewMockStorer()
|
||||
ab.Storage.Server = storer
|
||||
ctx.User = authboss.Attributes{ab.PrimaryID: "john@john.com"}
|
||||
r := mocks.Request("GET")
|
||||
r = r.WithContext(context.WithValue(r.Context(), authboss.CTXKeyUser, user))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
if err := lock.afterAuth(ctx); err != nil {
|
||||
handled, err := harness.lock.BeforeAuth(w, r, false)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if storer.Users["john@john.com"][StoreAttemptNumber].(int64) != int64(0) {
|
||||
t.Error("StoreAttemptNumber set incorrectly.")
|
||||
}
|
||||
if _, ok := storer.Users["john@john.com"][StoreAttemptTime].(time.Time); !ok {
|
||||
t.Error("StoreAttemptTime not set.")
|
||||
if handled {
|
||||
t.Error("it shouldn't have been handled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAfterAuthFail_Lock(t *testing.T) {
|
||||
func TestBeforeAuthDisallow(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ab := authboss.New()
|
||||
var old, current time.Time
|
||||
var ok bool
|
||||
harness := testSetup()
|
||||
|
||||
ctx := ab.NewContext()
|
||||
storer := mocks.NewMockStorer()
|
||||
ab.Storage.Server = storer
|
||||
lock := Lock{ab}
|
||||
ab.LockWindow = 30 * time.Minute
|
||||
ab.LockDuration = 30 * time.Minute
|
||||
ab.LockAfter = 3
|
||||
|
||||
email := "john@john.com"
|
||||
|
||||
ctx.User = map[string]interface{}{ab.PrimaryID: email}
|
||||
|
||||
old = time.Now().UTC().Add(-1 * time.Hour)
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
if lockedIntf, ok := storer.Users["john@john.com"][StoreLocked]; ok && lockedIntf.(bool) {
|
||||
t.Errorf("%d: User should not be locked.", i)
|
||||
}
|
||||
|
||||
if err := lock.afterAuthFail(ctx); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if val := storer.Users[email][StoreAttemptNumber].(int64); val != int64(i+1) {
|
||||
t.Errorf("%d: StoreAttemptNumber set incorrectly: %v", i, val)
|
||||
}
|
||||
if current, ok = storer.Users[email][StoreAttemptTime].(time.Time); !ok || old.After(current) {
|
||||
t.Errorf("%d: StoreAttemptTime not set correctly: %v", i, current)
|
||||
}
|
||||
|
||||
current = old
|
||||
user := &mocks.User{
|
||||
Locked: time.Now().UTC().Add(time.Hour),
|
||||
}
|
||||
|
||||
if locked := storer.Users[email][StoreLocked].(time.Time); !locked.After(time.Now()) {
|
||||
t.Error("User should be locked for some duration:", locked)
|
||||
r := mocks.Request("GET")
|
||||
r = r.WithContext(context.WithValue(r.Context(), authboss.CTXKeyUser, user))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handled, err := harness.lock.BeforeAuth(w, r, false)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if val := storer.Users[email][StoreAttemptNumber].(int64); val != int64(3) {
|
||||
t.Error("StoreAttemptNumber set incorrectly:", val)
|
||||
if !handled {
|
||||
t.Error("it should have been handled")
|
||||
}
|
||||
if _, ok = storer.Users[email][StoreAttemptTime].(time.Time); !ok {
|
||||
t.Error("StoreAttemptTime not set correctly.")
|
||||
|
||||
if w.Code != http.StatusTemporaryRedirect {
|
||||
t.Error("code was wrong:", w.Code)
|
||||
}
|
||||
|
||||
opts := harness.redirector.Options
|
||||
if opts.RedirectPath != harness.ab.Paths.LockNotOK {
|
||||
t.Error("redir path was wrong:", opts.RedirectPath)
|
||||
}
|
||||
|
||||
if len(opts.Failure) == 0 {
|
||||
t.Error("expected a failure message")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAfterAuthFail_Reset(t *testing.T) {
|
||||
func TestAfterAuthSuccess(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ab := authboss.New()
|
||||
var old, current time.Time
|
||||
var ok bool
|
||||
harness := testSetup()
|
||||
|
||||
ctx := ab.NewContext()
|
||||
storer := mocks.NewMockStorer()
|
||||
lock := Lock{ab}
|
||||
ab.LockWindow = 30 * time.Minute
|
||||
ab.Storage.Server = storer
|
||||
|
||||
old = time.Now().UTC().Add(-time.Hour)
|
||||
|
||||
email := "john@john.com"
|
||||
ctx.User = map[string]interface{}{
|
||||
ab.PrimaryID: email,
|
||||
StoreAttemptNumber: int64(2),
|
||||
StoreAttemptTime: old,
|
||||
StoreLocked: old,
|
||||
last := time.Now().UTC().Add(-time.Hour)
|
||||
user := &mocks.User{
|
||||
Email: "test@test.com",
|
||||
AttemptCount: 45,
|
||||
LastAttempt: last,
|
||||
}
|
||||
|
||||
lock.afterAuthFail(ctx)
|
||||
if val := storer.Users[email][StoreAttemptNumber].(int64); val != int64(1) {
|
||||
t.Error("StoreAttemptNumber set incorrectly:", val)
|
||||
harness.storer.Users["test@test.com"] = user
|
||||
|
||||
r := mocks.Request("GET")
|
||||
r = r.WithContext(context.WithValue(r.Context(), authboss.CTXKeyUser, user))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handled, err := harness.lock.AfterAuthSuccess(w, r, false)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if current, ok = storer.Users[email][StoreAttemptTime].(time.Time); !ok || current.Before(old) {
|
||||
t.Error("StoreAttemptTime not set correctly.")
|
||||
if handled {
|
||||
t.Error("it should never be handled")
|
||||
}
|
||||
if locked := storer.Users[email][StoreLocked].(time.Time); locked.After(time.Now()) {
|
||||
t.Error("StoreLocked not set correctly:", locked)
|
||||
|
||||
user = harness.storer.Users["test@test.com"]
|
||||
if 0 != user.GetAttemptCount() {
|
||||
t.Error("attempt count wrong:", user.GetAttemptCount())
|
||||
}
|
||||
if !last.Before(user.GetLastAttempt()) {
|
||||
t.Errorf("last attempt should be more recent, old: %v new: %v", last, user.GetLastAttempt())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAfterAuthFail_Errors(t *testing.T) {
|
||||
func TestAfterAuthFailure(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ab := authboss.New()
|
||||
lock := Lock{ab}
|
||||
ctx := ab.NewContext()
|
||||
harness := testSetup()
|
||||
|
||||
lock.afterAuthFail(ctx)
|
||||
if _, ok := ctx.User[StoreAttemptNumber]; ok {
|
||||
t.Error("Expected nothing to be set, missing user.")
|
||||
user := &mocks.User{
|
||||
Email: "test@test.com",
|
||||
}
|
||||
harness.storer.Users["test@test.com"] = user
|
||||
|
||||
if IsLocked(harness.storer.Users["test@test.com"]) {
|
||||
t.Error("should not be locked")
|
||||
}
|
||||
|
||||
r := mocks.Request("GET")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
var handled bool
|
||||
var err error
|
||||
|
||||
for i := 1; i <= 3; i++ {
|
||||
if IsLocked(harness.storer.Users["test@test.com"]) {
|
||||
t.Error("should not be locked")
|
||||
}
|
||||
|
||||
r := r.WithContext(context.WithValue(r.Context(), authboss.CTXKeyUser, user))
|
||||
handled, err = harness.lock.AfterAuthFail(w, r, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if i < 3 {
|
||||
if handled {
|
||||
t.Errorf("%d) should not be handled until lock occurs", i)
|
||||
}
|
||||
|
||||
user := harness.storer.Users["test@test.com"]
|
||||
if user.GetAttemptCount() != i {
|
||||
t.Errorf("attempt count wrong, want: %d, got: %d", i, user.GetAttemptCount())
|
||||
}
|
||||
if IsLocked(user) {
|
||||
t.Error("should not be locked")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !handled {
|
||||
t.Error("should have been handled at the end")
|
||||
}
|
||||
|
||||
if !IsLocked(harness.storer.Users["test@test.com"]) {
|
||||
t.Error("should be locked at the end")
|
||||
}
|
||||
|
||||
if w.Code != http.StatusTemporaryRedirect {
|
||||
t.Error("code was wrong:", w.Code)
|
||||
}
|
||||
|
||||
opts := harness.redirector.Options
|
||||
if opts.RedirectPath != harness.ab.Paths.LockNotOK {
|
||||
t.Error("redir path was wrong:", opts.RedirectPath)
|
||||
}
|
||||
|
||||
if len(opts.Failure) == 0 {
|
||||
t.Error("expected a failure message")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLock(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ab := authboss.New()
|
||||
storer := mocks.NewMockStorer()
|
||||
ab.Storage.Server = storer
|
||||
lock := Lock{ab}
|
||||
harness := testSetup()
|
||||
|
||||
email := "john@john.com"
|
||||
storer.Users[email] = map[string]interface{}{
|
||||
ab.PrimaryID: email,
|
||||
"password": "password",
|
||||
user := &mocks.User{
|
||||
Email: "test@test.com",
|
||||
}
|
||||
harness.storer.Users["test@test.com"] = user
|
||||
|
||||
if IsLocked(harness.storer.Users["test@test.com"]) {
|
||||
t.Error("should not be locked")
|
||||
}
|
||||
|
||||
err := lock.Lock(email)
|
||||
if err != nil {
|
||||
if err := harness.lock.Lock(context.Background(), "test@test.com"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if locked := storer.Users[email][StoreLocked].(time.Time); !locked.After(time.Now()) {
|
||||
t.Error("User should be locked.")
|
||||
if !IsLocked(harness.storer.Users["test@test.com"]) {
|
||||
t.Error("should be locked")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnlock(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ab := authboss.New()
|
||||
storer := mocks.NewMockStorer()
|
||||
ab.Storage.Server = storer
|
||||
lock := Lock{ab}
|
||||
ab.LockWindow = 1 * time.Hour
|
||||
harness := testSetup()
|
||||
|
||||
email := "john@john.com"
|
||||
storer.Users[email] = map[string]interface{}{
|
||||
ab.PrimaryID: email,
|
||||
"password": "password",
|
||||
"locked": true,
|
||||
user := &mocks.User{
|
||||
Email: "test@test.com",
|
||||
Locked: time.Now().UTC().Add(time.Hour),
|
||||
}
|
||||
harness.storer.Users["test@test.com"] = user
|
||||
|
||||
if !IsLocked(harness.storer.Users["test@test.com"]) {
|
||||
t.Error("should be locked")
|
||||
}
|
||||
|
||||
err := lock.Unlock(email)
|
||||
if err != nil {
|
||||
if err := harness.lock.Unlock(context.Background(), "test@test.com"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
attemptTime := storer.Users[email][StoreAttemptTime].(time.Time)
|
||||
if attemptTime.After(time.Now().UTC().Add(-ab.LockWindow)) {
|
||||
t.Error("StoreLocked not set correctly:", attemptTime)
|
||||
}
|
||||
if number := storer.Users[email][StoreAttemptNumber].(int64); number != int64(0) {
|
||||
t.Error("StoreLocked not set correctly:", number)
|
||||
}
|
||||
if locked := storer.Users[email][StoreLocked].(time.Time); locked.After(time.Now()) {
|
||||
t.Error("User should not be locked.")
|
||||
if IsLocked(harness.storer.Users["test@test.com"]) {
|
||||
t.Error("should no longer be locked")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiddlewareAllow(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ab := authboss.New()
|
||||
called := false
|
||||
server := Middleware(ab, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
called = true
|
||||
}))
|
||||
|
||||
user := &mocks.User{
|
||||
Locked: time.Now().UTC().Add(time.Hour),
|
||||
}
|
||||
|
||||
r := mocks.Request("GET")
|
||||
r = r.WithContext(context.WithValue(r.Context(), authboss.CTXKeyUser, user))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
server.ServeHTTP(w, r)
|
||||
|
||||
if !called {
|
||||
t.Error("The user should have been allowed through")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiddlewareDisallow(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ab := authboss.New()
|
||||
redirector := &mocks.Redirector{}
|
||||
ab.Config.Paths.LockNotOK = "/lock/not/ok"
|
||||
ab.Config.Core.Logger = mocks.Logger{}
|
||||
ab.Config.Core.Redirector = redirector
|
||||
|
||||
called := false
|
||||
server := Middleware(ab, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
called = true
|
||||
}))
|
||||
|
||||
user := &mocks.User{
|
||||
Locked: time.Now().UTC().Add(-time.Hour),
|
||||
}
|
||||
|
||||
r := mocks.Request("GET")
|
||||
r = r.WithContext(context.WithValue(r.Context(), authboss.CTXKeyUser, user))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
server.ServeHTTP(w, r)
|
||||
|
||||
if called {
|
||||
t.Error("The user should not have been allowed through")
|
||||
}
|
||||
if redirector.Options.Code != http.StatusTemporaryRedirect {
|
||||
t.Error("expected a redirect, but got:", redirector.Options.Code)
|
||||
}
|
||||
if p := redirector.Options.RedirectPath; p != "/lock/not/ok" {
|
||||
t.Error("redirect path wrong:", p)
|
||||
}
|
||||
}
|
||||
|
21
user.go
21
user.go
@ -37,6 +37,19 @@ type ConfirmableUser interface {
|
||||
PutEmail(email string)
|
||||
}
|
||||
|
||||
// LockableUser is a user that can be locked
|
||||
type LockableUser interface {
|
||||
User
|
||||
|
||||
GetAttemptCount() (attempts int)
|
||||
GetLastAttempt() (last time.Time)
|
||||
GetLocked() (locked time.Time)
|
||||
|
||||
PutAttemptCount(attempts int)
|
||||
PutLastAttempt(last time.Time)
|
||||
PutLocked(locked time.Time)
|
||||
}
|
||||
|
||||
// ArbitraryUser allows arbitrary data from the web form through. You should
|
||||
// definitely only pull the keys you want from the map, since this is unfiltered
|
||||
// input from a web request and is an attack vector.
|
||||
@ -87,3 +100,11 @@ func MustBeConfirmable(u User) ConfirmableUser {
|
||||
}
|
||||
panic("could not upgrade user to a confirmable user, check your user struct")
|
||||
}
|
||||
|
||||
// MustBeLockable forces an upgrade to a Lockable user or panic.
|
||||
func MustBeLockable(u User) LockableUser {
|
||||
if lu, ok := u.(LockableUser); ok {
|
||||
return lu
|
||||
}
|
||||
panic("could not upgrade user to a lockable user, check your user struct")
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user