Skip to content

feat(plugin-auth): account lockout + rate-limit tuning (ADR-0069 D2, P1)#2365

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

feat(plugin-auth): account lockout + rate-limit tuning (ADR-0069 D2, P1)#2365
os-zhuang merged 1 commit into
mainfrom
auth/adr-0069-lockout

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Second slice of ADR-0069 (enterprise authentication hardening) — tracking issue #2281 — implementing D2: per-identity account lockout + native rate-limit tuning. Reuses the setting→enforcement-seam pattern established by the HIBP PR (#2361).

What

Account lockout [custom][field]

  • New identity columns sys_user.failed_login_count + sys_user.locked_until.
  • New auth settings lockout_threshold (0 = off, default) and lockout_duration_minutes (default 15), in a new Anti-abuse settings group.
  • Enforced in the /sign-in/email better-auth before/after hooks: failures increment the counter; crossing the threshold stamps locked_until; a locked account is rejected even with the correct password (per-identity — survives IP rotation, unlike rate limiting). A successful sign-in resets both fields.
  • Admin Unlock: admin-guarded POST /api/v1/auth/admin/unlock-user route + an unlock_user action on sys_user.

Rate-limit tuning [native]

  • New auth settings rate_limit_max / rate_limit_window_seconds wire better-auth's core rateLimit with stricter customRules for /sign-in/email, /sign-up/email, /request-password-reset, /reset-password.

ADR-0049 / ADR-0074 compliance

Every setting ships with its enforcement (no false surface). All default off / to safe values and are additive — no behavior change on upgrade. locked_until is written as a Date, never epoch-ms (ADR-0074).

Verification

  • Unit: 158 plugin-auth + 129 service-settings + 63 platform-objects green; builds incl. strict DTS green.
  • Live dogfood (showcase): 3 failed sign-ins lock the account → the correct password is then rejected 403 ACCOUNT_LOCKED while other users (incl. the admin) sign in normally → admin unlock restores access → unauthenticated unlock 401 → a rate-limit burst trips 429 after the cap.

Dogfood caught a real bug the mocked unit tests had masked: the lockout helpers initially used filter: (copied from an adjacent code path) but the ObjectQL engine's query key is where: — so lookups silently returned the first user (the admin) instead of the target. Fixed to where:, and the test mock now honours the where clause so the regression can't recur.

Out of scope (follow-up ADR-0069)

D3 enforced MFA; remaining D1 [custom] (complexity / expiry / history); P2 (sessions / IP allowlist / OIDC trust-list); P3 (SAML). A locked-accounts list-view filter is a small follow-up.

🤖 Generated with Claude Code

Per-identity brute-force protection, the second ADR-0069 slice (after HIBP),
reusing the setting -> enforcement-seam pattern.

- Account lockout [custom][field]: sys_user.failed_login_count / locked_until
  columns; `lockout_threshold` (0=off) + `lockout_duration_minutes` settings.
  Enforced in the /sign-in/email before/after hooks: failures increment,
  crossing the threshold stamps locked_until, and a locked account is rejected
  even with the correct password (survives IP rotation). Success resets both.
- Admin Unlock: admin-guarded POST /api/v1/auth/admin/unlock-user + sys_user
  `unlock_user` action.
- Rate-limit tuning [native]: `rate_limit_max` / `rate_limit_window_seconds`
  wire better-auth core `rateLimit` with stricter customRules on the auth
  endpoints.

Default-off / additive (no upgrade behavior change); ADR-0049 (enforcement
ships with the setting); timestamps written as Date, never epoch-ms (ADR-0074).

Verified live (dogfood): 3 failed sign-ins lock the account; the correct
password is then rejected with ACCOUNT_LOCKED while OTHER users (incl. admin)
sign in normally; admin unlock restores access; unauthenticated unlock 401;
rate-limit burst trips 429 after the cap. Unit: 158 plugin-auth + 129
service-settings + 63 platform-objects green; the lockout mock honours the
ObjectQL `where` key so the filter/where bug dogfood caught cannot regress.

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 1:06pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/l 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.

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/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant