test(auth): cover the lock itself and its expiry, not just the counter (#3624 follow-up) - #3667
Merged
Merged
Conversation
#3624 follow-up) #3659 pinned the two ends of the 2FA failure budget — a wrong code is counted, a correct one resets it. It stopped short of the transitions that make it a LOCKOUT rather than a counter, which is the part that actually protects the account. Three more assertions: 1. **The lock engages.** Spending the budget stamps `locked_until` in the future. Reaching it means respecting two stacked budgets: better-auth allows 5 verifications per two-factor cookie (`beginAttempt(5)`) and 10 consecutive failures per ACCOUNT (`accountLockout.maxFailedAttempts`). Only the second touches the columns under test, so the loop takes a fresh challenge every 5 tries. The counter is asserted after every single failure, not just at the end. 2. **The lock bites before the code is checked.** A locked account refuses even a VALID TOTP with 429. Without this the "lockout" would just be a slower guess loop. 3. **An expired lock clears itself.** better-auth clears it through a GUARDED `incrementOne` — `where locked_until <= now`, `set { failedVerificationCount: 0, lockedUntil: null }`. That is the only place the ObjectQL adapter has to handle `lte` against a datetime, so this covers the operator as much as the columns: a driver mishandling it would leave the user locked out forever with no error anywhere. The test ages the lock through the data engine because no endpoint can — the default duration is 900s. Test-only, and `@objectstack/dogfood` is private, hence the empty changeset. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UYLC8TfjzHGwatNxZKdX7H
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
July 27, 2026 13:42
This was referenced Jul 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
接 #3659。那个 PR 钉住了 2FA 失败预算的两端 —— 输错会被计数、输对会清零 —— 但停在了让它成为锁定而非单纯计数器的那几步转移上,而那才是真正保护账户的部分。
补三条断言:
1. 锁真的会落。 预算耗尽后
locked_until被戳上一个未来时间。走到这一步要同时尊重两个叠在一起的预算:better-auth 每个 two-factor cookie 只允许 5 次验证(
beginAttempt(5)),每个账户允许 10 次连续失败(accountLockout.maxFailedAttempts)。只有后者碰得到本次要测的两个列,所以循环每 5 次就得换一个新 challenge。计数器在每一次失败后都断言,不是只在最后看一眼。2. 锁在验码之前就生效。 已锁定的账户连正确的 TOTP 也拒(429)。没有这条,所谓"锁定"不过是一个慢一点的爆破循环。
3. 过期的锁会自己清掉。 better-auth 清锁走的是一个带 guard 的
incrementOne——where locked_until <= now,set { failedVerificationCount: 0, lockedUntil: null }。这是整个适配器里唯一需要处理
lte对 datetime 的地方,所以这条断言覆盖的不只是列,还有那个操作符:驱动一旦处理错,用户会被永久锁死,而且任何地方都不会报错。测试通过数据引擎把锁提前到过期(默认时长 900 秒,没有端点能加速它)—— 这是全文件唯一一处伸手绕过 API 的地方,已在注释里写明原因。测试
two-factor-lockout.dogfood.test.ts4 passed;连同org-create-default-team.dogfood.test.ts一起 5 passed。纯测试改动,且
@objectstack/dogfood是 private 包,所以配的是空 changeset(仓库认可的"本 PR 不发布任何东西"声明)。Refs #3624, #3647, #3659。
Generated by Claude Code