mirror of
https://github.com/go-vikunja/vikunja.git
synced 2026-05-08 21:08:04 -05:00
fix: add TTL-based expiry and cleanup for used TOTP passcode entries
Store a unix timestamp instead of a boolean, and treat entries older than 90 seconds as expired. A background goroutine lazily cleans up expired keys after each successful validation to prevent unbounded growth in the keyvalue store.
This commit is contained in:
@@ -20,6 +20,7 @@ import (
|
||||
"fmt"
|
||||
"image"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
@@ -138,25 +139,50 @@ 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
|
||||
// Prevent passcode reuse within the validity window.
|
||||
// Store the timestamp when the passcode was used; treat entries older than
|
||||
// 90 seconds (30s TOTP window + clock skew) as expired.
|
||||
const totpUsedTTL = 90 * time.Second
|
||||
usedKey := fmt.Sprintf("totp_used_%s_%s", strconv.FormatInt(passcode.User.ID, 10), passcode.Passcode)
|
||||
_, exists, err := keyvalue.Get(usedKey)
|
||||
val, exists, err := keyvalue.Get(usedKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exists {
|
||||
return nil, ErrTOTPPasscodeUsed{}
|
||||
if usedAt, ok := val.(int64); ok && time.Since(time.Unix(usedAt, 0)) < totpUsedTTL {
|
||||
return nil, ErrTOTPPasscodeUsed{}
|
||||
}
|
||||
// Entry expired — allow reuse, overwrite below
|
||||
}
|
||||
|
||||
// Mark this passcode as used
|
||||
err = keyvalue.Put(usedKey, true)
|
||||
// Mark this passcode as used with the current timestamp
|
||||
err = keyvalue.Put(usedKey, time.Now().Unix())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Lazily clean up expired entries to prevent unbounded growth
|
||||
go cleanupExpiredTOTPKeys(totpUsedTTL)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func cleanupExpiredTOTPKeys(ttl time.Duration) {
|
||||
keys, err := keyvalue.ListKeys("totp_used_")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, key := range keys {
|
||||
val, exists, err := keyvalue.Get(key)
|
||||
if err != nil || !exists {
|
||||
continue
|
||||
}
|
||||
if usedAt, ok := val.(int64); ok && time.Since(time.Unix(usedAt, 0)) >= ttl {
|
||||
_ = keyvalue.Del(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetTOTPQrCodeForUser returns a qrcode for a user's totp setting
|
||||
func GetTOTPQrCodeForUser(s *xorm.Session, user *User) (qrcode image.Image, err error) {
|
||||
t, err := GetTOTPForUser(s, user)
|
||||
|
||||
Reference in New Issue
Block a user