diff --git a/pkg/user/totp.go b/pkg/user/totp.go index 2a270deb1..31235cce5 100644 --- a/pkg/user/totp.go +++ b/pkg/user/totp.go @@ -17,7 +17,9 @@ package user import ( + "fmt" "image" + "strconv" "code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/log" @@ -136,6 +138,22 @@ func ValidateTOTPPasscode(s *xorm.Session, passcode *TOTPPasscode) (t *TOTP, err return nil, ErrInvalidTOTPPasscode{Passcode: passcode.Passcode} } + // Prevent passcode reuse: check if this passcode was already used + usedKey := fmt.Sprintf("totp_used_%s_%s", strconv.FormatInt(passcode.User.ID, 10), passcode.Passcode) + _, exists, err := keyvalue.Get(usedKey) + if err != nil { + return nil, err + } + if exists { + return nil, ErrTOTPPasscodeUsed{} + } + + // Mark this passcode as used + err = keyvalue.Put(usedKey, true) + if err != nil { + return nil, err + } + return } diff --git a/pkg/user/totp_test.go b/pkg/user/totp_test.go index be3279adb..0a4722ca9 100644 --- a/pkg/user/totp_test.go +++ b/pkg/user/totp_test.go @@ -33,7 +33,7 @@ func TestTOTPPasscodeCannotBeReused(t *testing.T) { defer s.Close() // Generate a valid TOTP passcode for user1's secret from the fixture - secret := "JBSWY3DPEHPK3PXP" + secret := "JBSWY3DPEHPK3PXP" //nolint:gosec passcode, err := totp.GenerateCode(secret, time.Now()) require.NoError(t, err)