2015-01-24 02:25:12 +02:00
|
|
|
// Package lock implements user locking after N bad sign-in attempts.
|
|
|
|
package lock
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"gopkg.in/authboss.v0"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2015-02-16 23:31:26 +02:00
|
|
|
StoreAttemptNumber = "attempt_number"
|
|
|
|
StoreAttemptTime = "attempt_time"
|
|
|
|
StoreLocked = "locked"
|
2015-01-24 02:25:12 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
ErrLocked = errors.New("Account is locked.")
|
|
|
|
)
|
|
|
|
|
|
|
|
// L is the singleton instance of the lock module which will have been
|
|
|
|
// configured and ready to use after authboss.Init()
|
|
|
|
var L *Lock
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
L = &Lock{}
|
|
|
|
authboss.RegisterModule("lock", L)
|
|
|
|
}
|
|
|
|
|
|
|
|
type Lock struct {
|
|
|
|
}
|
|
|
|
|
2015-02-16 06:07:36 +02:00
|
|
|
func (l *Lock) Initialize() error {
|
|
|
|
if authboss.Cfg.Storer == nil {
|
2015-01-24 02:25:12 +02:00
|
|
|
return errors.New("lock: Need a Storer.")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Events
|
2015-02-16 06:07:36 +02:00
|
|
|
authboss.Cfg.Callbacks.Before(authboss.EventGet, l.BeforeAuth)
|
|
|
|
authboss.Cfg.Callbacks.Before(authboss.EventAuth, l.BeforeAuth)
|
|
|
|
authboss.Cfg.Callbacks.After(authboss.EventAuth, l.AfterAuth)
|
|
|
|
authboss.Cfg.Callbacks.After(authboss.EventAuthFail, l.AfterAuthFail)
|
2015-01-24 02:25:12 +02:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (l *Lock) Routes() authboss.RouteTable {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (l *Lock) Storage() authboss.StorageOptions {
|
|
|
|
return authboss.StorageOptions{
|
2015-02-16 23:31:26 +02:00
|
|
|
StoreAttemptNumber: authboss.Integer,
|
|
|
|
StoreAttemptTime: authboss.DateTime,
|
|
|
|
StoreLocked: authboss.Bool,
|
2015-01-24 02:25:12 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// BeforeAuth ensures the account is not locked.
|
|
|
|
func (l *Lock) BeforeAuth(ctx *authboss.Context) error {
|
|
|
|
if ctx.User == nil {
|
2015-02-07 14:27:12 +02:00
|
|
|
return errors.New("lock: user not loaded in before auth callback")
|
2015-01-24 02:25:12 +02:00
|
|
|
}
|
|
|
|
|
2015-02-16 23:31:26 +02:00
|
|
|
if intf, ok := ctx.User[StoreLocked]; ok {
|
2015-01-24 02:25:12 +02:00
|
|
|
if locked, ok := intf.(bool); ok && locked {
|
|
|
|
return ErrLocked
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// AfterAuth resets the attempt number field.
|
|
|
|
func (l *Lock) AfterAuth(ctx *authboss.Context) {
|
|
|
|
if ctx.User == nil {
|
2015-02-16 06:07:36 +02:00
|
|
|
fmt.Fprintln(authboss.Cfg.LogWriter, "lock: user not loaded in after auth callback")
|
2015-01-24 02:25:12 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2015-02-16 23:31:26 +02:00
|
|
|
ctx.User[StoreAttemptNumber] = 0
|
|
|
|
ctx.User[StoreAttemptTime] = time.Now().UTC()
|
2015-01-24 02:25:12 +02:00
|
|
|
|
2015-02-18 18:57:50 +02:00
|
|
|
if err := ctx.SaveUser(); err != nil {
|
2015-02-16 06:07:36 +02:00
|
|
|
fmt.Fprintf(authboss.Cfg.LogWriter, "lock: saving user failed %v", err)
|
2015-01-24 02:25:12 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// AfterAuthFail adjusts the attempt number and time.
|
|
|
|
func (l *Lock) AfterAuthFail(ctx *authboss.Context) {
|
|
|
|
if ctx.User == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
lastAttempt := time.Now().UTC()
|
2015-02-16 23:31:26 +02:00
|
|
|
if attemptTimeIntf, ok := ctx.User[StoreAttemptTime]; ok {
|
2015-01-24 02:25:12 +02:00
|
|
|
if attemptTime, ok := attemptTimeIntf.(time.Time); ok {
|
|
|
|
lastAttempt = attemptTime
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
nAttempts := 0
|
2015-02-16 23:31:26 +02:00
|
|
|
if attemptsIntf, ok := ctx.User[StoreAttemptNumber]; ok {
|
2015-01-24 02:25:12 +02:00
|
|
|
if attempts, ok := attemptsIntf.(int); ok {
|
|
|
|
nAttempts = attempts
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
nAttempts++
|
|
|
|
|
2015-02-16 06:07:36 +02:00
|
|
|
if time.Now().UTC().Sub(lastAttempt) <= authboss.Cfg.LockWindow {
|
|
|
|
if nAttempts >= authboss.Cfg.LockAfter {
|
2015-02-16 23:31:26 +02:00
|
|
|
ctx.User[StoreLocked] = true
|
2015-01-24 02:25:12 +02:00
|
|
|
}
|
|
|
|
|
2015-02-16 23:31:26 +02:00
|
|
|
ctx.User[StoreAttemptNumber] = nAttempts
|
2015-01-25 08:19:22 +02:00
|
|
|
} else {
|
2015-02-16 23:31:26 +02:00
|
|
|
ctx.User[StoreAttemptNumber] = 0
|
2015-01-24 02:25:12 +02:00
|
|
|
}
|
2015-02-16 23:31:26 +02:00
|
|
|
ctx.User[StoreAttemptTime] = time.Now().UTC()
|
2015-01-24 02:25:12 +02:00
|
|
|
|
2015-02-18 18:57:50 +02:00
|
|
|
if err := ctx.SaveUser(); err != nil {
|
2015-02-16 06:07:36 +02:00
|
|
|
fmt.Fprintf(authboss.Cfg.LogWriter, "lock: saving user failed %v", err)
|
2015-01-24 02:25:12 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Lock a user manually.
|
|
|
|
func (l *Lock) Lock(key string, storer authboss.Storer) error {
|
|
|
|
user, err := storer.Get(key, authboss.ModuleAttrMeta)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
attr := authboss.Unbind(user)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2015-02-16 23:31:26 +02:00
|
|
|
attr[StoreLocked] = true
|
2015-01-24 02:25:12 +02:00
|
|
|
|
|
|
|
return storer.Put(key, attr)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Unlock a user that was locked by this module.
|
|
|
|
func (l *Lock) Unlock(key string, storer authboss.Storer) error {
|
|
|
|
user, err := storer.Get(key, authboss.ModuleAttrMeta)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
attr := authboss.Unbind(user)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2015-01-25 08:19:22 +02:00
|
|
|
// Set the last attempt to be -window*2 to avoid immediately
|
|
|
|
// giving another login failure.
|
2015-02-16 23:31:26 +02:00
|
|
|
attr[StoreAttemptTime] = time.Now().UTC().Add(-authboss.Cfg.LockWindow * 2)
|
|
|
|
attr[StoreAttemptNumber] = 0
|
|
|
|
attr[StoreLocked] = false
|
2015-01-24 02:25:12 +02:00
|
|
|
|
|
|
|
return storer.Put(key, attr)
|
|
|
|
}
|