1
0
mirror of https://github.com/volatiletech/authboss.git synced 2024-11-28 08:58:38 +02:00
authboss/lock/lock.go
2018-03-09 14:46:33 -08:00

187 lines
5.1 KiB
Go

// Package lock implements user locking after N bad sign-in attempts.
package lock
import (
"context"
"net/http"
"time"
"github.com/pkg/errors"
"github.com/volatiletech/authboss"
)
// Storage key constants
const (
StoreAttemptNumber = "attempt_number"
StoreAttemptTime = "attempt_time"
StoreLocked = "locked"
)
var (
errUserMissing = errors.New("user not loaded in BeforeAuth callback")
)
func init() {
authboss.RegisterModule("lock", &Lock{})
}
// Lock module
type Lock struct {
*authboss.Authboss
}
// Init the module
func (l *Lock) Init(ab *authboss.Authboss) error {
l.Authboss = ab
l.Events.Before(authboss.EventAuth, l.BeforeAuth)
l.Events.Before(authboss.EventOAuth2, l.BeforeAuth)
l.Events.After(authboss.EventAuth, l.AfterAuthSuccess)
l.Events.After(authboss.EventAuthFail, l.AfterAuthFail)
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(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)
}
// 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(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)
}
// 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(r)
if err != nil {
return false, err
}
lu := authboss.MustBeLockable(user)
last := lu.GetLastAttempt()
attempts := lu.GetAttemptCount()
attempts++
nowLocked := false
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
}
lu.PutAttemptCount(attempts)
} else {
lu.PutAttemptCount(1)
}
lu.PutLastAttempt(time.Now().UTC())
if err := l.Authboss.Config.Storage.Server.Save(r.Context(), lu); err != nil {
return false, err
}
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(ctx context.Context, key string) error {
user, err := l.Authboss.Config.Storage.Server.Load(ctx, key)
if err != nil {
return err
}
lu := authboss.MustBeLockable(user)
lu.PutLocked(time.Now().UTC().Add(l.Authboss.Config.Modules.LockDuration))
return l.Authboss.Config.Storage.Server.Save(ctx, lu)
}
// Unlock a user that was locked by this module.
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
}
lu := authboss.MustBeLockable(user)
// Set the last attempt to be -window*2 to avoid immediately
// 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.Authboss.Config.Storage.Server.Save(ctx, lu)
}
// Middleware ensures that a user is not locked, or else it will intercept the request
// and send them to the configured LockNotOK page, this will load the user if he's not been loaded
// yet from the session. And panics if it cannot load the user.
func Middleware(ab *authboss.Authboss) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := ab.LoadCurrentUserP(&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())
}