diff --git a/pkg/user/error.go b/pkg/user/error.go index 0d863a05b..0456f0a68 100644 --- a/pkg/user/error.go +++ b/pkg/user/error.go @@ -373,6 +373,32 @@ func (err ErrInvalidTOTPPasscode) HTTPError() web.HTTPError { } } +// ErrTOTPPasscodeUsed represents a "TOTPPasscodeUsed" kind of error. +// This is returned when a TOTP passcode has already been used within its validity window. +type ErrTOTPPasscodeUsed struct{} + +// IsErrTOTPPasscodeUsed checks if an error is a ErrTOTPPasscodeUsed. +func IsErrTOTPPasscodeUsed(err error) bool { + _, ok := err.(ErrTOTPPasscodeUsed) + return ok +} + +func (err ErrTOTPPasscodeUsed) Error() string { + return "This totp passcode has already been used" +} + +// ErrCodeTOTPPasscodeUsed holds the unique world-error code of this error +const ErrCodeTOTPPasscodeUsed = 1025 + +// HTTPError holds the http error description +func (err ErrTOTPPasscodeUsed) HTTPError() web.HTTPError { + return web.HTTPError{ + HTTPCode: http.StatusPreconditionFailed, + Code: ErrCodeTOTPPasscodeUsed, + Message: "This totp passcode has already been used.", + } +} + // ErrInvalidAvatarProvider represents a "InvalidAvatarProvider" kind of error. type ErrInvalidAvatarProvider struct { AvatarProvider string diff --git a/pkg/user/totp_test.go b/pkg/user/totp_test.go new file mode 100644 index 000000000..be3279adb --- /dev/null +++ b/pkg/user/totp_test.go @@ -0,0 +1,56 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// 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 +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package user + +import ( + "testing" + "time" + + "code.vikunja.io/api/pkg/db" + + "github.com/pquerna/otp/totp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTOTPPasscodeCannotBeReused(t *testing.T) { + db.LoadAndAssertFixtures(t) + s := db.NewSession() + defer s.Close() + + // Generate a valid TOTP passcode for user1's secret from the fixture + secret := "JBSWY3DPEHPK3PXP" + passcode, err := totp.GenerateCode(secret, time.Now()) + require.NoError(t, err) + + user := &User{ID: 1} + + // First use should succeed + _, err = ValidateTOTPPasscode(s, &TOTPPasscode{ + User: user, + Passcode: passcode, + }) + require.NoError(t, err) + + // Second use of the same passcode should fail + _, err = ValidateTOTPPasscode(s, &TOTPPasscode{ + User: user, + Passcode: passcode, + }) + require.Error(t, err) + assert.True(t, IsErrTOTPPasscodeUsed(err), "expected ErrTOTPPasscodeUsed, got: %v", err) +}