* fix: purge authentication data on hard user deletion * fix: fail closed when 2FA status lookup fails * fix: reject stale Telegram login callbacks * fix(twofa): prevent concurrent backup code and lockout bypasses * fix(auth): harden user deletion and Telegram verification
131 lines
3.5 KiB
Go
131 lines
3.5 KiB
Go
package model
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
"github.com/go-redis/redis/v8"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestHardDeleteUserPurgesAuthenticationDataWhenRedisFails(t *testing.T) {
|
|
truncateTables(t)
|
|
|
|
user := User{Username: "hard-delete-user", Password: "password"}
|
|
require.NoError(t, DB.Create(&user).Error)
|
|
require.NoError(t, DB.Create(&Token{UserId: user.Id, Key: "hard-delete-token"}).Error)
|
|
require.NoError(t, DB.Create(&TwoFA{UserId: user.Id, Secret: "secret", IsEnabled: true}).Error)
|
|
require.NoError(t, DB.Create(&TwoFABackupCode{UserId: user.Id, CodeHash: "hash"}).Error)
|
|
require.NoError(t, DB.Create(&PasskeyCredential{UserID: user.Id, CredentialID: "credential", PublicKey: "public-key"}).Error)
|
|
require.NoError(t, DB.Create(&UserOAuthBinding{UserId: user.Id, ProviderId: 1, ProviderUserId: "provider-user"}).Error)
|
|
|
|
oldRedisEnabled, oldRDB := common.RedisEnabled, common.RDB
|
|
common.RedisEnabled = true
|
|
var cacheInvalidatedAfterCommit atomic.Bool
|
|
common.RDB = redis.NewClient(&redis.Options{
|
|
Dialer: func(context.Context, string, string) (net.Conn, error) {
|
|
var count int64
|
|
if err := DB.Unscoped().Model(&User{}).Where("id = ?", user.Id).Count(&count).Error; err == nil && count == 0 {
|
|
cacheInvalidatedAfterCommit.Store(true)
|
|
}
|
|
return nil, errors.New("forced redis failure")
|
|
},
|
|
MaxRetries: -1,
|
|
})
|
|
t.Cleanup(func() {
|
|
_ = common.RDB.Close()
|
|
common.RedisEnabled, common.RDB = oldRedisEnabled, oldRDB
|
|
})
|
|
|
|
require.NoError(t, HardDeleteUserById(user.Id))
|
|
assert.True(t, cacheInvalidatedAfterCommit.Load())
|
|
|
|
var count int64
|
|
require.NoError(t, DB.Unscoped().Model(&User{}).Where("id = ?", user.Id).Count(&count).Error)
|
|
assert.Zero(t, count)
|
|
for _, record := range []any{
|
|
&Token{},
|
|
&TwoFA{},
|
|
&TwoFABackupCode{},
|
|
&PasskeyCredential{},
|
|
&UserOAuthBinding{},
|
|
} {
|
|
require.NoError(t, DB.Unscoped().Model(record).Where("user_id = ?", user.Id).Count(&count).Error)
|
|
assert.Zero(t, count)
|
|
}
|
|
}
|
|
|
|
func TestIncrementFailedAttemptsCountsConcurrentFailures(t *testing.T) {
|
|
truncateTables(t)
|
|
|
|
user := User{Username: "twofa-cas-user", Password: "password"}
|
|
require.NoError(t, DB.Create(&user).Error)
|
|
twoFA := TwoFA{UserId: user.Id, Secret: "secret", IsEnabled: true}
|
|
require.NoError(t, DB.Create(&twoFA).Error)
|
|
|
|
const attempts = 4
|
|
errs := make(chan error, attempts)
|
|
var wg sync.WaitGroup
|
|
for range attempts {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
errs <- (&TwoFA{Id: twoFA.Id}).IncrementFailedAttempts()
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
close(errs)
|
|
for err := range errs {
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
var reloaded TwoFA
|
|
require.NoError(t, DB.First(&reloaded, twoFA.Id).Error)
|
|
assert.Equal(t, attempts, reloaded.FailedAttempts)
|
|
}
|
|
|
|
func TestValidateBackupCodeCanOnlySucceedOnce(t *testing.T) {
|
|
truncateTables(t)
|
|
|
|
const code = "ABCD-1234"
|
|
require.NoError(t, CreateBackupCodes(123, []string{code}))
|
|
|
|
const attempts = 2
|
|
results := make(chan bool, attempts)
|
|
errs := make(chan error, attempts)
|
|
var wg sync.WaitGroup
|
|
for range attempts {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
valid, err := ValidateBackupCode(123, code)
|
|
results <- valid
|
|
errs <- err
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
close(results)
|
|
close(errs)
|
|
|
|
for err := range errs {
|
|
require.NoError(t, err)
|
|
}
|
|
wins := 0
|
|
for valid := range results {
|
|
if valid {
|
|
wins++
|
|
}
|
|
}
|
|
assert.Equal(t, 1, wins)
|
|
|
|
remaining, err := GetUnusedBackupCodeCount(123)
|
|
require.NoError(t, err)
|
|
assert.Zero(t, remaining)
|
|
}
|