Disable the user account after 10 failed password attempts
This commit is contained in:
parent
3572ac4b82
commit
5cfc9bf2f9
7 changed files with 122 additions and 10 deletions
|
@ -40,6 +40,8 @@ This document describes the different errors Vikunja can return.
|
|||
| 1016 | 412 | Totp is not enabled for this user. |
|
||||
| 1017 | 412 | The provided Totp passcode is invalid. |
|
||||
| 1018 | 412 | The provided user avatar provider type setting is invalid. |
|
||||
| 1019 | 412 | No openid email address was provided. |
|
||||
| 1020 | 412 | This user account is disabled. |
|
||||
|
||||
## Validation
|
||||
|
||||
|
|
|
@ -20,10 +20,8 @@ import (
|
|||
"net/http"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/modules/auth"
|
||||
"code.vikunja.io/api/pkg/notifications"
|
||||
user2 "code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web/handler"
|
||||
|
||||
|
@ -59,6 +57,11 @@ func Login(c echo.Context) error {
|
|||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
if user.Status == user2.StatusDisabled {
|
||||
_ = s.Rollback()
|
||||
return handler.HandleHTTPError(&user2.ErrAccountDisabled{UserID: user.ID}, c)
|
||||
}
|
||||
|
||||
totpEnabled, err := user2.TOTPEnabledForUser(s, user)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
|
@ -67,6 +70,7 @@ func Login(c echo.Context) error {
|
|||
|
||||
if totpEnabled {
|
||||
if u.TOTPPasscode == "" {
|
||||
_ = s.Rollback()
|
||||
return handler.HandleHTTPError(user2.ErrInvalidTOTPPasscode{}, c)
|
||||
}
|
||||
|
||||
|
@ -76,12 +80,7 @@ func Login(c echo.Context) error {
|
|||
})
|
||||
if err != nil {
|
||||
if user2.IsErrInvalidTOTPPasscode(err) {
|
||||
log.Errorf("Invalid TOTP credentials provided for user %d", user.ID)
|
||||
|
||||
err2 := notifications.Notify(user, &user2.InvalidTOTPNotification{User: user})
|
||||
if err2 != nil {
|
||||
log.Errorf("Could not send failed TOTP notification to user %d: %s", user.ID, err2)
|
||||
}
|
||||
user2.HandleFailedTOTPAuth(s, user)
|
||||
}
|
||||
_ = s.Rollback()
|
||||
return handler.HandleHTTPError(err, c)
|
||||
|
|
|
@ -425,3 +425,30 @@ func (err *ErrNoOpenIDEmailProvided) HTTPError() web.HTTPError {
|
|||
Message: "No email address available. Please make sure the openid provider publicly provides an email address for your account.",
|
||||
}
|
||||
}
|
||||
|
||||
// ErrAccountDisabled represents a "AccountDisabled" kind of error.
|
||||
type ErrAccountDisabled struct {
|
||||
UserID int64
|
||||
}
|
||||
|
||||
// IsErrAccountDisabled checks if an error is a ErrAccountDisabled.
|
||||
func IsErrAccountDisabled(err error) bool {
|
||||
_, ok := err.(*ErrAccountDisabled)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err *ErrAccountDisabled) Error() string {
|
||||
return "Account is disabled"
|
||||
}
|
||||
|
||||
// ErrCodeAccountDisabled holds the unique world-error code of this error
|
||||
const ErrCodeAccountDisabled = 1020
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err *ErrAccountDisabled) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{
|
||||
HTTPCode: http.StatusPreconditionFailed,
|
||||
Code: ErrCodeAccountDisabled,
|
||||
Message: "This account is disabled. Check your emails or ask your administrator.",
|
||||
}
|
||||
}
|
||||
|
|
|
@ -135,3 +135,28 @@ func (n *InvalidTOTPNotification) ToDB() interface{} {
|
|||
func (n *InvalidTOTPNotification) Name() string {
|
||||
return "totp.invalid"
|
||||
}
|
||||
|
||||
// PasswordAccountLockedAfterInvalidTOTOPNotification represents a PasswordAccountLockedAfterInvalidTOTOPNotification notification
|
||||
type PasswordAccountLockedAfterInvalidTOTOPNotification struct {
|
||||
User *User
|
||||
}
|
||||
|
||||
// ToMail returns the mail notification for PasswordAccountLockedAfterInvalidTOTOPNotification
|
||||
func (n *PasswordAccountLockedAfterInvalidTOTOPNotification) ToMail() *notifications.Mail {
|
||||
return notifications.NewMail().
|
||||
Subject("We've disabled your account on Vikunja").
|
||||
Greeting("Hi " + n.User.GetName() + ",").
|
||||
Line("Someone tried to log in with your credentials but failed to provide a valid TOTP passcode.").
|
||||
Line("After 10 failed attempts, we've disabled your account and reset your password. To set a new one, follow the instructions in the reset email we just sent you.").
|
||||
Line("If you did not receive an email with reset instructions, you can always request a new one at [" + config.ServiceFrontendurl.GetString() + "get-password-reset](" + config.ServiceFrontendurl.GetString() + "get-password-reset).")
|
||||
}
|
||||
|
||||
// ToDB returns the PasswordAccountLockedAfterInvalidTOTOPNotification notification in a format which can be saved in the db
|
||||
func (n *PasswordAccountLockedAfterInvalidTOTOPNotification) ToDB() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Name returns the name of the notification
|
||||
func (n *PasswordAccountLockedAfterInvalidTOTOPNotification) Name() string {
|
||||
return "password.account.locked.after.invalid.totop"
|
||||
}
|
||||
|
|
|
@ -18,12 +18,17 @@ package user
|
|||
|
||||
import (
|
||||
"image"
|
||||
"strconv"
|
||||
|
||||
"xorm.io/xorm"
|
||||
"code.vikunja.io/api/pkg/modules/keyvalue"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/notifications"
|
||||
|
||||
"github.com/pquerna/otp"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// TOTP holds a user's totp setting in the database.
|
||||
|
@ -149,3 +154,46 @@ func GetTOTPQrCodeForUser(s *xorm.Session, user *User) (qrcode image.Image, err
|
|||
}
|
||||
return key.Image(300, 300)
|
||||
}
|
||||
|
||||
func (u *User) GetFailedTOTPAttemptsKey() string {
|
||||
return "failed_totp_attempts_" + strconv.FormatInt(u.ID, 10)
|
||||
}
|
||||
|
||||
// HandleFailedTOTPAuth handles informing the user of failed TOTP attempts and blocking the account after 10 attempts
|
||||
func HandleFailedTOTPAuth(s *xorm.Session, user *User) {
|
||||
log.Errorf("Invalid TOTP credentials provided for user %d", user.ID)
|
||||
|
||||
err := notifications.Notify(user, &InvalidTOTPNotification{User: user})
|
||||
if err != nil {
|
||||
log.Errorf("Could not send failed TOTP notification to user %d: %s", user.ID, err)
|
||||
}
|
||||
|
||||
key := user.GetFailedTOTPAttemptsKey()
|
||||
err = keyvalue.IncrBy(key, 1)
|
||||
if err != nil {
|
||||
log.Errorf("Could not increase failed TOTP attempts for user %d: %s", user.ID, err)
|
||||
}
|
||||
|
||||
a, _, err := keyvalue.Get(key)
|
||||
if err != nil {
|
||||
log.Errorf("Could get failed TOTP attempts for user %d: %s", user.ID, err)
|
||||
}
|
||||
attempts := a.(int64)
|
||||
if attempts > 10 {
|
||||
log.Infof("Blocking user account %d after 10 failed TOTP password attempts", user.ID)
|
||||
err = RequestUserPasswordResetToken(s, user)
|
||||
if err != nil {
|
||||
log.Errorf("Could not reset password of user %d after 10 failed TOTP attempts: %s", user.ID, err)
|
||||
}
|
||||
err = notifications.Notify(user, &PasswordAccountLockedAfterInvalidTOTOPNotification{
|
||||
User: user,
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("Could send password information mail to user %d after 10 failed TOTP attempts: %s", user.ID, err)
|
||||
}
|
||||
err = user.SetStatus(s, StatusDisabled)
|
||||
if err != nil {
|
||||
log.Errorf("Could not disable user %d: %s", user.ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -458,3 +458,13 @@ func UpdateUserPassword(s *xorm.Session, user *User, newPassword string) (err er
|
|||
|
||||
return err
|
||||
}
|
||||
|
||||
// SetStatus sets a users status in the database
|
||||
func (u *User) SetStatus(s *xorm.Session, status Status) (err error) {
|
||||
u.Status = status
|
||||
_, err = s.
|
||||
Where("id = ?", u.ID).
|
||||
Cols("status").
|
||||
Update(u)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -67,8 +67,9 @@ func ResetPassword(s *xorm.Session, reset *PasswordReset) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
user.Status = StatusActive
|
||||
_, err = s.
|
||||
Cols("password").
|
||||
Cols("password", "status").
|
||||
Where("id = ?", user.ID).
|
||||
Update(user)
|
||||
if err != nil {
|
||||
|
|
Loading…
Reference in a new issue