Some checks are pending
Security Checks / security (push) Waiting to run
统一 frontend、backend、test、test-log 和 out 目录,更新构建、部署及测试入口;纳入配置管理兼容与首次安全引导修复;排除本机配置、密钥和测试产物。
182 lines
6.1 KiB
Go
182 lines
6.1 KiB
Go
package verification
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestStoreIssuesAndConsumesCodeOnlyOnce(t *testing.T) {
|
|
store, err := NewStore(4)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var issuedCode string
|
|
challenge, err := store.Issue(IssueRequest{Purpose: PurposeLogin, Account: "smail", Email: "User@Example.com"}, func(challenge Challenge, code string) error {
|
|
issuedCode = code
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if issuedCode == "" || store.Size() != 1 {
|
|
t.Fatal("challenge was not retained in memory")
|
|
}
|
|
if err := store.Verify(challenge.ID, "smail", "user@example.com", issuedCode, PurposeLogin); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if store.Size() != 0 {
|
|
t.Fatal("verified challenge was not consumed")
|
|
}
|
|
if err := store.Verify(challenge.ID, "smail", "user@example.com", issuedCode, PurposeLogin); !errors.Is(err, ErrInvalidChallenge) {
|
|
t.Fatalf("replayed challenge was accepted: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestStoreLimitsAttemptsAndExpires(t *testing.T) {
|
|
store, err := NewStore(4)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var code string
|
|
challenge, err := store.Issue(IssueRequest{Purpose: PurposePasswordRecover, Account: "smail", Email: "user@example.com", TTL: time.Second, MaxAttempts: 2}, func(_ Challenge, issued string) error {
|
|
code = issued
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wrongCode := "000000"
|
|
if code == wrongCode {
|
|
wrongCode = "111111"
|
|
}
|
|
if err := store.Verify(challenge.ID, "smail", "user@example.com", wrongCode, PurposePasswordRecover); err == nil {
|
|
t.Fatal("wrong code was accepted")
|
|
}
|
|
if err := store.Verify(challenge.ID, "smail", "user@example.com", wrongCode, PurposePasswordRecover); !errors.Is(err, ErrTooManyAttempts) {
|
|
t.Fatalf("wrong code result = %v", err)
|
|
}
|
|
if err := store.Verify(challenge.ID, "smail", "user@example.com", code, PurposePasswordRecover); !errors.Is(err, ErrInvalidChallenge) {
|
|
t.Fatalf("replayed exhausted challenge result = %v", err)
|
|
}
|
|
challenge, err = store.Issue(IssueRequest{Purpose: PurposeLogin, Account: "smail", Email: "user@example.com", TTL: time.Nanosecond}, func(_ Challenge, _ string) error { return nil })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
if err := store.Verify(challenge.ID, "smail", "user@example.com", code, PurposeLogin); !errors.Is(err, ErrInvalidChallenge) && !errors.Is(err, ErrExpiredChallenge) {
|
|
t.Fatalf("expired challenge result = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestStoreRestartInvalidatesAllChallenges(t *testing.T) {
|
|
store, err := NewStore(4)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
challenge, err := store.Issue(IssueRequest{Purpose: PurposeLogin, Account: "smail", Email: "user@example.com"}, func(_ Challenge, _ string) error { return nil })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
store.ResetForProcessRestart()
|
|
if store.Size() != 0 {
|
|
t.Fatal("restart did not clear in-memory challenges")
|
|
}
|
|
if err := store.Verify(challenge.ID, "smail", "user@example.com", "000000", PurposeLogin); err == nil {
|
|
t.Fatal("challenge survived process restart")
|
|
}
|
|
}
|
|
|
|
func TestInvalidateAccountOnlyRemovesTargetChallenges(t *testing.T) {
|
|
store, err := NewStore(4)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
codes := make(map[string]string)
|
|
delivery := func(challenge Challenge, code string) error {
|
|
codes[challenge.Account] = code
|
|
return nil
|
|
}
|
|
target, err := store.Issue(IssueRequest{Purpose: PurposeLogin, Account: "root", Email: "root@example.com"}, delivery)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
other, err := store.Issue(IssueRequest{Purpose: PurposeLogin, Account: "alice", Email: "alice@example.com"}, delivery)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if removed := store.InvalidateAccount("root"); removed != 1 {
|
|
t.Fatalf("removed challenges = %d", removed)
|
|
}
|
|
if err := store.Verify(target.ID, "root", "root@example.com", codes["root"], PurposeLogin); !errors.Is(err, ErrInvalidChallenge) {
|
|
t.Fatalf("target challenge survived invalidation: %v", err)
|
|
}
|
|
if err := store.Verify(other.ID, "alice", "alice@example.com", codes["alice"], PurposeLogin); err != nil {
|
|
t.Fatalf("other challenge was invalidated: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestStoreUsesEightDigitCodesAndReissueInvalidatesPrevious(t *testing.T) {
|
|
store, err := NewStore(8)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var firstCode string
|
|
first, err := store.Issue(IssueRequest{Purpose: PurposeLogin, Account: "smail", Email: "user@example.com"}, func(_ Challenge, code string) error {
|
|
firstCode = code
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(firstCode) != DefaultCodeLength {
|
|
t.Fatalf("code length = %d, want %d", len(firstCode), DefaultCodeLength)
|
|
}
|
|
var secondCode string
|
|
second, err := store.Issue(IssueRequest{Purpose: PurposeLogin, Account: "smail", Email: "user@example.com"}, func(_ Challenge, code string) error {
|
|
secondCode = code
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if first.ID == second.ID || store.Size() != 1 {
|
|
t.Fatal("reissue did not replace the previous challenge")
|
|
}
|
|
if err := store.Verify(first.ID, "smail", "user@example.com", firstCode, PurposeLogin); !errors.Is(err, ErrInvalidChallenge) {
|
|
t.Fatalf("old challenge result = %v", err)
|
|
}
|
|
if err := store.Verify(second.ID, "smail", "user@example.com", secondCode, PurposeLogin); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestStoreRejectsDifferentBrowserContextWithoutConsumingChallenge(t *testing.T) {
|
|
store, err := NewStore(4)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var code string
|
|
challenge, err := store.Issue(IssueRequest{
|
|
Purpose: PurposePasswordRecover,
|
|
Account: "smail",
|
|
Email: "user@example.com",
|
|
BrowserTransaction: "browser-a",
|
|
}, func(_ Challenge, issued string) error {
|
|
code = issued
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := store.VerifyBoundWithContext(challenge.ID, "smail", "user@example.com", code, PurposePasswordRecover, 0, "browser-b"); !errors.Is(err, ErrInvalidChallenge) {
|
|
t.Fatalf("different browser context result = %v", err)
|
|
}
|
|
if store.Size() != 1 {
|
|
t.Fatal("context mismatch consumed the challenge")
|
|
}
|
|
if err := store.VerifyBoundWithContext(challenge.ID, "smail", "user@example.com", code, PurposePasswordRecover, 0, "browser-a"); err != nil {
|
|
t.Fatalf("matching browser context result = %v", err)
|
|
}
|
|
}
|