1
0
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:
Aaron L 2018-02-27 21:20:55 -08:00
parent 62dd36b71e
commit 9ef2a06dcb
5 changed files with 404 additions and 254 deletions

View File

@ -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

View File

@ -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 }

View File

@ -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())
}

View File

@ -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
View File

@ -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")
}