feat(plugin-auth): password history / no-reuse (ADR-0069 D1, P1)#2371
Merged
Conversation
Adds `password_history_count` (0-24, 0=off) auth setting. On /change-password and /reset-password a new password matching the current password or any of the last N hashes is rejected with PASSWORD_REUSE. A bounded `sys_account.previous_password_hashes` JSON ring (system-managed, hidden) backs the check, maintained by before/after hooks (capture old hash, append on success). Reuses better-auth's native `password.verify` (no bespoke crypto) and resolves the reset-flow user via better-auth's own reset-token verification lookup. Default-off / additive (no upgrade behavior change); ADR-0049 (enforcement ships with the setting). Verified live (dogfood, history_count=3): change P1->P2 ok; change back to P1 (historical) -> 400 PASSWORD_REUSE; change P2->P2 (current) -> 400 PASSWORD_REUSE; change to a fresh password ok; the ring column persists bounded real hashes. Unit: 135 plugin-auth (incl. parse/reuse/ring helpers with a stub verify + where-aware sys_account mock) + service-settings + platform-objects green; builds incl. strict DTS green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 3 package(s): 15 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
| if (ctx?.path === '/change-password' || ctx?.path === '/reset-password') { | ||
| const stash = ctx?.context?.__osPwHistory; | ||
| if (stash?.userId) { | ||
| let succeeded = true; |
This was referenced Jun 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.
ADR-0069 P1 (tracking #2281): D1 password history / no-reuse. Continues the password-policy work (after HIBP #2361 and complexity #2368), reusing the setting→before/after-hook pattern.
What
authsettingpassword_history_count(0–24, default 0 = off)./change-passwordand/reset-password, a new password matching the current password or any of the last N hashes is rejected withPASSWORD_REUSE.sys_account.previous_password_hashescolumn (JSON ring, system-managed, hidden) backs the check; before/after hooks capture the old hash and append it on success.Reuses better-auth (no duplication)
Per a "don't reinvent" review of the installed better-auth 1.6.20: password history is not native, so it's custom — but the check reuses better-auth's native
password.verify(no bespoke crypto), and the reset-flow user is resolved via the same reset-token verification lookup better-auth's own handler uses. Session revocation on reset stays on the nativerevokeSessionsOnPasswordReset.ADR-0049
Default-off / additive (no upgrade behavior change); the setting ships with its enforcement.
Verification
verify+ where-awaresys_accountmock) + service-settings + platform-objects green; builds incl. strict DTS green.400 PASSWORD_REUSE; change P2→P2 (current) →400 PASSWORD_REUSE; change to a fresh password ok; the ring column persists bounded real hashes.ADR-0069 P1 status
✅ D1 HIBP (#2361) · ✅ D2 lockout + rate-limit (#2365) · ✅ D1 complexity (#2368) · ✅ D1 history (this PR). Remaining P1: D1 expiry + D3 enforced MFA — both need the session-validation gate (core-runtime, higher blast radius); planned as a focused follow-up.
🤖 Generated with Claude Code