2020-04-17 21:25:35 +02:00
|
|
|
// Vikunja is a to-do list application to facilitate your life.
|
2021-02-02 20:19:13 +01:00
|
|
|
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
|
2020-04-17 21:25:35 +02:00
|
|
|
//
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
2020-12-23 16:41:52 +01:00
|
|
|
// it under the terms of the GNU Affero General Public Licensee as published by
|
2020-04-17 21:25:35 +02:00
|
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
|
|
// (at your option) any later version.
|
|
|
|
//
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
2020-12-23 16:41:52 +01:00
|
|
|
// GNU Affero General Public Licensee for more details.
|
2020-04-17 21:25:35 +02:00
|
|
|
//
|
2020-12-23 16:41:52 +01:00
|
|
|
// You should have received a copy of the GNU Affero General Public Licensee
|
2020-04-17 21:25:35 +02:00
|
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
package user
|
|
|
|
|
|
|
|
import (
|
2020-10-11 22:10:03 +02:00
|
|
|
"image"
|
2020-12-23 16:32:28 +01:00
|
|
|
|
2020-05-29 17:15:59 +02:00
|
|
|
"code.vikunja.io/api/pkg/config"
|
2021-07-29 18:45:22 +02:00
|
|
|
"code.vikunja.io/api/pkg/log"
|
2021-07-30 15:01:04 +02:00
|
|
|
"code.vikunja.io/api/pkg/modules/keyvalue"
|
2021-07-29 18:45:22 +02:00
|
|
|
"code.vikunja.io/api/pkg/notifications"
|
|
|
|
|
2020-04-17 21:25:35 +02:00
|
|
|
"github.com/pquerna/otp"
|
|
|
|
"github.com/pquerna/otp/totp"
|
2021-07-29 18:45:22 +02:00
|
|
|
"xorm.io/xorm"
|
2020-04-17 21:25:35 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
// TOTP holds a user's totp setting in the database.
|
|
|
|
type TOTP struct {
|
2020-12-18 17:51:22 +01:00
|
|
|
ID int64 `xorm:"bigint autoincr not null unique pk" json:"-"`
|
|
|
|
UserID int64 `xorm:"bigint not null" json:"-"`
|
2020-05-09 14:45:57 +02:00
|
|
|
Secret string `xorm:"text not null" json:"secret"`
|
2020-04-17 21:25:35 +02:00
|
|
|
// The totp entry will only be enabled after the user verified they have a working totp setup.
|
|
|
|
Enabled bool `xorm:"null" json:"enabled"`
|
|
|
|
// The totp url used to be able to enroll the user later
|
|
|
|
URL string `xorm:"text null" json:"url"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// TableName holds the table name for totp secrets
|
2020-10-11 22:10:03 +02:00
|
|
|
func (t *TOTP) TableName() string {
|
2020-04-17 21:25:35 +02:00
|
|
|
return "totp"
|
|
|
|
}
|
|
|
|
|
|
|
|
// TOTPPasscode is used to validate a users totp passcode
|
|
|
|
type TOTPPasscode struct {
|
|
|
|
User *User `json:"-"`
|
|
|
|
Passcode string `json:"passcode"`
|
|
|
|
}
|
|
|
|
|
2020-04-18 00:22:59 +02:00
|
|
|
// TOTPEnabledForUser checks if totp is enabled for a user - not if it is activated, use GetTOTPForUser to check that.
|
2020-12-23 16:32:28 +01:00
|
|
|
func TOTPEnabledForUser(s *xorm.Session, user *User) (bool, error) {
|
2020-05-29 17:15:59 +02:00
|
|
|
if !config.ServiceEnableTotp.GetBool() {
|
|
|
|
return false, nil
|
|
|
|
}
|
2020-08-07 16:41:35 +02:00
|
|
|
t := &TOTP{}
|
2020-12-23 16:32:28 +01:00
|
|
|
_, err := s.Where("user_id = ?", user.ID).Get(t)
|
2020-08-07 16:41:35 +02:00
|
|
|
return t.Enabled, err
|
2020-04-17 21:25:35 +02:00
|
|
|
}
|
|
|
|
|
2020-04-18 00:22:59 +02:00
|
|
|
// GetTOTPForUser returns the current state of totp settings for the user.
|
2020-12-23 16:32:28 +01:00
|
|
|
func GetTOTPForUser(s *xorm.Session, user *User) (t *TOTP, err error) {
|
2020-04-17 21:25:35 +02:00
|
|
|
t = &TOTP{}
|
2020-12-23 16:32:28 +01:00
|
|
|
exists, err := s.Where("user_id = ?", user.ID).Get(t)
|
2020-04-17 21:25:35 +02:00
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if !exists {
|
|
|
|
return nil, ErrTOTPNotEnabled{}
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// EnrollTOTP creates a new TOTP entry for the user - it does not enable it yet.
|
2020-12-23 16:32:28 +01:00
|
|
|
func EnrollTOTP(s *xorm.Session, user *User) (t *TOTP, err error) {
|
|
|
|
isEnrolled, err := s.Where("user_id = ?", user.ID).Exist(&TOTP{})
|
2020-04-17 21:25:35 +02:00
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if isEnrolled {
|
|
|
|
return nil, ErrTOTPAlreadyEnabled{}
|
|
|
|
}
|
|
|
|
|
|
|
|
key, err := totp.Generate(totp.GenerateOpts{
|
|
|
|
Issuer: "Vikunja",
|
|
|
|
AccountName: user.Username,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
t = &TOTP{
|
|
|
|
UserID: user.ID,
|
|
|
|
Secret: key.Secret(),
|
|
|
|
Enabled: false,
|
|
|
|
URL: key.URL(),
|
|
|
|
}
|
2020-12-23 16:32:28 +01:00
|
|
|
_, err = s.Insert(t)
|
2020-04-17 21:25:35 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// EnableTOTP enables totp for a user. The provided passcode is used to verify the user has a working totp setup.
|
2020-12-23 16:32:28 +01:00
|
|
|
func EnableTOTP(s *xorm.Session, passcode *TOTPPasscode) (err error) {
|
|
|
|
t, err := ValidateTOTPPasscode(s, passcode)
|
2020-04-17 21:25:35 +02:00
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-12-23 16:32:28 +01:00
|
|
|
_, err = s.
|
2020-04-17 21:25:35 +02:00
|
|
|
Where("id = ?", t.ID).
|
|
|
|
Cols("enabled").
|
|
|
|
Update(&TOTP{Enabled: true})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-04-18 01:38:49 +02:00
|
|
|
// DisableTOTP removes all totp settings for a user.
|
2020-12-23 16:32:28 +01:00
|
|
|
func DisableTOTP(s *xorm.Session, user *User) (err error) {
|
|
|
|
_, err = s.
|
|
|
|
Where("user_id = ?", user.ID).
|
|
|
|
Delete(&TOTP{})
|
2020-04-18 01:38:49 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-04-17 21:25:35 +02:00
|
|
|
// ValidateTOTPPasscode validated totp codes of users.
|
2020-12-23 16:32:28 +01:00
|
|
|
func ValidateTOTPPasscode(s *xorm.Session, passcode *TOTPPasscode) (t *TOTP, err error) {
|
|
|
|
t, err = GetTOTPForUser(s, passcode.User)
|
2020-04-17 21:25:35 +02:00
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if !totp.Validate(passcode.Passcode, t.Secret) {
|
|
|
|
return nil, ErrInvalidTOTPPasscode{Passcode: passcode.Passcode}
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetTOTPQrCodeForUser returns a qrcode for a user's totp setting
|
2020-12-23 16:32:28 +01:00
|
|
|
func GetTOTPQrCodeForUser(s *xorm.Session, user *User) (qrcode image.Image, err error) {
|
|
|
|
t, err := GetTOTPForUser(s, user)
|
2020-04-17 21:25:35 +02:00
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
key, err := otp.NewKeyFromURL(t.URL)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
return key.Image(300, 300)
|
|
|
|
}
|
2021-07-29 18:45:22 +02:00
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
|
|
|
key := user.GetFailedTOTPAttemptsKey()
|
2021-07-30 14:42:03 +02:00
|
|
|
err := keyvalue.IncrBy(key, 1)
|
2021-07-29 18:45:22 +02:00
|
|
|
if err != nil {
|
|
|
|
log.Errorf("Could not increase failed TOTP attempts for user %d: %s", user.ID, err)
|
2021-07-30 15:01:04 +02:00
|
|
|
return
|
2021-07-29 18:45:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
a, _, err := keyvalue.Get(key)
|
|
|
|
if err != nil {
|
|
|
|
log.Errorf("Could get failed TOTP attempts for user %d: %s", user.ID, err)
|
2021-07-30 15:01:04 +02:00
|
|
|
return
|
2021-07-29 18:45:22 +02:00
|
|
|
}
|
|
|
|
attempts := a.(int64)
|
2021-07-30 14:42:03 +02:00
|
|
|
|
|
|
|
if attempts == 3 {
|
|
|
|
err = notifications.Notify(user, &InvalidTOTPNotification{User: user})
|
2021-07-29 18:45:22 +02:00
|
|
|
if err != nil {
|
2021-07-30 14:42:03 +02:00
|
|
|
log.Errorf("Could not send failed TOTP notification to user %d: %s", user.ID, err)
|
2021-07-30 15:01:04 +02:00
|
|
|
return
|
2021-07-29 18:45:22 +02:00
|
|
|
}
|
|
|
|
}
|
2021-07-30 14:42:03 +02:00
|
|
|
|
|
|
|
if attempts < 10 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
2021-07-30 15:01:04 +02:00
|
|
|
return
|
2021-07-30 14:42:03 +02:00
|
|
|
}
|
|
|
|
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)
|
2021-07-30 15:01:04 +02:00
|
|
|
return
|
2021-07-30 14:42:03 +02:00
|
|
|
}
|
|
|
|
err = user.SetStatus(s, StatusDisabled)
|
|
|
|
if err != nil {
|
|
|
|
log.Errorf("Could not disable user %d: %s", user.ID, err)
|
|
|
|
}
|
2021-07-29 18:45:22 +02:00
|
|
|
}
|