feat: 实现 argon2 哈希池、令牌与登录锁定计数
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPoolLimitsConcurrency(t *testing.T) {
|
||||
pool := NewPoolSize(1)
|
||||
errCh := make(chan error, 3)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(3)
|
||||
for i := 0; i < 3; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_, err := pool.Hash(context.Background(), PasswordLogin, "concurrency-test-password")
|
||||
errCh <- err
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
close(errCh)
|
||||
for err := range errCh {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if pool.MaxActive() > 1 {
|
||||
t.Fatalf("max active=%d want <=1", pool.MaxActive())
|
||||
}
|
||||
if pool.MaxActive() < 1 {
|
||||
t.Fatal("expected at least one active hash")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPoolHashVerifyRoundTrip(t *testing.T) {
|
||||
pool := NewPoolSize(2)
|
||||
ctx := context.Background()
|
||||
phc, err := pool.Hash(ctx, PasswordAdmin, "round-trip-password-1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.HasPrefix(phc, "$argon2id$") {
|
||||
t.Fatalf("phc=%q", phc)
|
||||
}
|
||||
ok, err := pool.Verify(ctx, PasswordAdmin, "round-trip-password-1", phc)
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("ok=%v err=%v", ok, err)
|
||||
}
|
||||
ok, err = pool.Verify(ctx, PasswordAdmin, "wrong-password-xxxxx", phc)
|
||||
if err != nil || ok {
|
||||
t.Fatalf("mismatch ok=%v err=%v", ok, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionAndAPITokens(t *testing.T) {
|
||||
s := NewSessionTokens()
|
||||
tok, hash, err := s.Issue(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.HasPrefix(tok, "nst_") {
|
||||
t.Fatalf("tok=%q", tok)
|
||||
}
|
||||
if !s.LooksLikeSessionToken(tok) {
|
||||
t.Fatal("LooksLikeSessionToken")
|
||||
}
|
||||
if !EqualHash(hash, s.HashToken(tok)) {
|
||||
t.Fatal("hash mismatch")
|
||||
}
|
||||
|
||||
a := NewAPITokens()
|
||||
atok, ahash, err := a.Issue(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.HasPrefix(atok, "nxm_") {
|
||||
t.Fatalf("atok=%q", atok)
|
||||
}
|
||||
if !a.LooksLikeAPIToken(atok) || !EqualHash(ahash, a.HashToken(atok)) {
|
||||
t.Fatal("api token hash")
|
||||
}
|
||||
if EqualHash(hash, ahash) {
|
||||
t.Fatal("session and api hashes should differ")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginLocksWindowAndExpiry(t *testing.T) {
|
||||
locks := NewLoginLocks()
|
||||
now := time.Date(2026, 9, 30, 12, 0, 0, 0, time.UTC)
|
||||
locks.SetClock(func() time.Time { return now })
|
||||
|
||||
key := LockKey{Kind: LockLoginEndpointIP, EndpointID: "e1", IP: "1.2.3.4"}
|
||||
for i := 0; i < 9; i++ {
|
||||
locked, _ := locks.Fail(key)
|
||||
if locked {
|
||||
t.Fatalf("locked early at %d", i+1)
|
||||
}
|
||||
now = now.Add(time.Second)
|
||||
}
|
||||
locked, retry := locks.Fail(key)
|
||||
if !locked || retry <= 0 {
|
||||
t.Fatalf("want lock, locked=%v retry=%v", locked, retry)
|
||||
}
|
||||
locked, _ = locks.Check(key)
|
||||
if !locked {
|
||||
t.Fatal("check should be locked")
|
||||
}
|
||||
|
||||
now = now.Add(5*time.Minute + time.Second)
|
||||
locked, _ = locks.Check(key)
|
||||
if locked {
|
||||
t.Fatal("should unlock after window")
|
||||
}
|
||||
locked, _ = locks.Fail(key)
|
||||
if locked {
|
||||
t.Fatal("after expiry should not still be locked on first fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginLocksClearEndpoint(t *testing.T) {
|
||||
locks := NewLoginLocks()
|
||||
k1 := LockKey{Kind: LockLoginEndpointIP, EndpointID: "e1", IP: "1.1.1.1"}
|
||||
k2 := LockKey{Kind: LockLoginEndpoint, EndpointID: "e1"}
|
||||
k3 := LockKey{Kind: LockLoginEndpointIP, EndpointID: "e2", IP: "1.1.1.1"}
|
||||
for i := 0; i < 10; i++ {
|
||||
locks.Fail(k1)
|
||||
locks.Fail(k2)
|
||||
}
|
||||
locks.Fail(k3)
|
||||
locks.ClearEndpoint("e1")
|
||||
if locked, _ := locks.Check(k1); locked {
|
||||
t.Fatal("e1 ip lock should clear")
|
||||
}
|
||||
if locked, _ := locks.Check(k2); locked {
|
||||
t.Fatal("e1 total lock should clear")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user