Skip to content

feat(plugin-auth): password history / no-reuse (ADR-0069 D1, P1)#2371

Merged
os-zhuang merged 1 commit into
mainfrom
auth/adr-0069-pw-history
Jun 27, 2026
Merged

feat(plugin-auth): password history / no-reuse (ADR-0069 D1, P1)#2371
os-zhuang merged 1 commit into
mainfrom
auth/adr-0069-pw-history

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

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

  • New auth setting password_history_count (0–24, default 0 = off).
  • 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.
  • New bounded sys_account.previous_password_hashes column (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 native revokeSessionsOnPasswordReset.

ADR-0049

Default-off / additive (no upgrade behavior change); the setting ships with its enforcement.

Verification

  • Unit: 135 plugin-auth (parse / reuse-reject / ring-trim helpers with a stub verify + where-aware sys_account mock) + service-settings + platform-objects green; builds incl. strict DTS green.
  • 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.

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

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>
@vercel

vercel Bot commented Jun 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jun 27, 2026 2:07pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Jun 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/platform-objects, @objectstack/plugin-auth, packages/services.

15 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/implementation-status.mdx (via @objectstack/plugin-auth)
  • content/docs/concepts/packages.mdx (via @objectstack/platform-objects, @objectstack/plugin-auth)
  • content/docs/concepts/setup-app.mdx (via @objectstack/platform-objects)
  • content/docs/getting-started/cli.mdx (via @objectstack/plugin-auth)
  • content/docs/guides/auth-sso.mdx (via @objectstack/plugin-auth)
  • content/docs/guides/authentication.mdx (via @objectstack/plugin-auth)
  • content/docs/guides/kernel-services.mdx (via @objectstack/plugin-auth)
  • content/docs/guides/packages.mdx (via @objectstack/platform-objects, @objectstack/plugin-auth, packages/services)
  • content/docs/guides/plugins.mdx (via @objectstack/plugin-auth)
  • content/docs/guides/production-readiness.mdx (via @objectstack/plugin-auth)
  • content/docs/guides/runtime-services/audit-service.mdx (via packages/services)
  • content/docs/guides/runtime-services/index.mdx (via packages/services)
  • content/docs/guides/runtime-services/settings-service.mdx (via packages/services)
  • content/docs/protocol/objectos/i18n-standard.mdx (via packages/services)
  • content/docs/releases/v9.mdx (via @objectstack/plugin-auth)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

if (ctx?.path === '/change-password' || ctx?.path === '/reset-password') {
const stash = ctx?.context?.__osPwHistory;
if (stash?.userId) {
let succeeded = true;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants