mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-08-17 16:07:18 +02:00
fix totp code not replayable
Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
@@ -112,6 +112,7 @@ func NewServices(
|
||||
RoleRepository: repositories.Role,
|
||||
CompanyRepository: repositories.Company,
|
||||
PasswordHasher: utilities.PasswordHasher,
|
||||
TOTPReplayCache: service.NewTOTPReplayCache(),
|
||||
}
|
||||
recipient := &service.Recipient{
|
||||
Common: common,
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// totpWindow is the lifetime of a used-token entry.
|
||||
// pquerna/otp totp.Validate uses Skew:1 (current ±1 period × 30s = 90s total window).
|
||||
const totpWindow = 90 * time.Second
|
||||
|
||||
type TOTPReplayCache struct {
|
||||
mu sync.Mutex
|
||||
entries map[string]time.Time // "userID:token" -> usedAt
|
||||
}
|
||||
|
||||
func NewTOTPReplayCache() *TOTPReplayCache {
|
||||
c := &TOTPReplayCache{entries: make(map[string]time.Time)}
|
||||
go c.runCleanup()
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *TOTPReplayCache) key(userID, token string) string {
|
||||
return userID + ":" + token
|
||||
}
|
||||
|
||||
// isUsed reports whether token has already been consumed for userID within the window.
|
||||
func (c *TOTPReplayCache) isUsed(userID, token string) bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
usedAt, ok := c.entries[c.key(userID, token)]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if time.Since(usedAt) > totpWindow {
|
||||
delete(c.entries, c.key(userID, token))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// markUsed records token as consumed for userID.
|
||||
func (c *TOTPReplayCache) markUsed(userID, token string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.entries[c.key(userID, token)] = time.Now()
|
||||
}
|
||||
|
||||
func (c *TOTPReplayCache) runCleanup() {
|
||||
ticker := time.NewTicker(totpWindow)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
c.mu.Lock()
|
||||
now := time.Now()
|
||||
for k, usedAt := range c.entries {
|
||||
if now.Sub(usedAt) > totpWindow {
|
||||
delete(c.entries, k)
|
||||
}
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
}
|
||||
+13
-2
@@ -37,8 +37,9 @@ type User struct {
|
||||
UserRepository *repository.User
|
||||
RoleRepository *repository.Role
|
||||
CompanyRepository *repository.Company
|
||||
PasswordVerifier *password.Argon2Verifier
|
||||
PasswordHasher *password.Argon2Hasher
|
||||
PasswordVerifier *password.Argon2Verifier
|
||||
PasswordHasher *password.Argon2Hasher
|
||||
TOTPReplayCache *TOTPReplayCache
|
||||
}
|
||||
|
||||
// Create creates a new user
|
||||
@@ -708,11 +709,16 @@ func (u *User) SetupCheckTOTP(
|
||||
}
|
||||
// verify the token
|
||||
u.Logger.Debug("verifying TOTP")
|
||||
if u.TOTPReplayCache.isUsed(userID.String(), token.String()) {
|
||||
u.Logger.Debug("failed to verify TOTP - token already used")
|
||||
return errs.ErrUserWrongTOTP
|
||||
}
|
||||
valid := totp.Validate(token.String(), secret)
|
||||
if !valid {
|
||||
u.Logger.Debug("failed to verify TOTP - invalid token")
|
||||
return errs.ErrUserWrongTOTP
|
||||
}
|
||||
u.TOTPReplayCache.markUsed(userID.String(), token.String())
|
||||
u.Logger.Debugw("Enabling MFA TOTP for user", "userID", userID)
|
||||
// enable TOTP
|
||||
err = u.UserRepository.EnableTOTP(
|
||||
@@ -807,6 +813,10 @@ func (u *User) CheckTOTP(
|
||||
userID *uuid.UUID,
|
||||
token *vo.String64,
|
||||
) error {
|
||||
if u.TOTPReplayCache.isUsed(userID.String(), token.String()) {
|
||||
u.Logger.Debug("failed to verify TOTP - token already used")
|
||||
return errs.ErrUserWrongTOTP
|
||||
}
|
||||
// get the secret
|
||||
secret, _, err := u.UserRepository.GetTOTP(
|
||||
ctx,
|
||||
@@ -822,6 +832,7 @@ func (u *User) CheckTOTP(
|
||||
u.Logger.Debug("failed to verify TOTP - invalid token")
|
||||
return errs.ErrUserWrongTOTP
|
||||
}
|
||||
u.TOTPReplayCache.markUsed(userID.String(), token.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user