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

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

Request Review

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

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@os-zhuang