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

vercelBot commented Jun 27, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
specReadyReadyPreview, CommentJun 27, 2026 2:07pm

Request Review

@github-actionsgithub-actionsBot 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-zhuang@github-advanced-security