Tracking issue for implementing ADR-0069: Enterprise authentication hardening (merged in #2275 ). Design is accepted-in-principle; no code yet — this issue tracks the phased build.
Parent: #2246 (first-run admin experience). Governing constraint: ADR-0049 — every setting ships with its runtime enforcement; no "false surface" toggles.
Each item = (setting → enforcement seam → mechanism). [native] = better-auth 1.6.x knob; [custom] = ObjectStack hook/middleware; [field] = new identity column.
P1 — security floor (first real customers)
Password: breached-password reject [native] — enable haveibeenpwned plugin (password_reject_breached). Lowest-risk first PR / sample for the pattern. ✅ shipped in feat(plugin-auth): reject breached passwords via HIBP (ADR-0069 D1, P1) #2361 (the enforcement-wired pattern template).
Password: complexity [custom] — password_require_complexity + password_min_classes, validated in before hooks on /sign-up/email, /reset-password, /change-password. ✅ shipped in feat(plugin-auth): password complexity policy (ADR-0069 D1, P1) #2368 .
Password: expiry + history [custom][field] — password_expiry_days (gate at session validation), password_history_count (reject reuse); fields sys_user.password_changed_at, sys_account.previous_password_hashes. ✅ history shipped in feat(plugin-auth): password history / no-reuse (ADR-0069 D1, P1) #2371 (no-reuse, password_history_count + sys_account.previous_password_hashes); expiry shipped in feat(auth): password expiry via the session-validation gate (ADR-0069 D1, P1) #2388 (the session-validation gate; REST surface).
Account lockout [custom][field] — lockout_threshold + lockout_duration_minutes; fields sys_user.failed_login_count, locked_until; admin Unlock action + locked-accounts filter. ✅ shipped in feat(plugin-auth): account lockout + rate-limit tuning (ADR-0069 D2, P1) #2365 (locked-accounts list filter is a small follow-up).
Rate-limit tuning [native] — enable/tune better-auth core rateLimit with stricter customRules for /sign-in|sign-up|reset; shared store for multi-node. ✅ shipped in feat(plugin-auth): account lockout + rate-limit tuning (ADR-0069 D2, P1) #2365 (multi-node shared store still TODO).
Enforced MFA [custom][field] — mfa_required + mfa_grace_period_days; gate password-only sessions at session validation until TOTP enrolled+verified; field sys_user.mfa_required_at. ✅ shipped in feat(auth): enforced MFA via the session-validation gate (ADR-0069 D3, P1) #2390 (global mfa_required + grace, reusing the feat(auth): password expiry via the session-validation gate (ADR-0069 D1, P1) #2388 gate; per-org require_mfa + objectui enroll prompt = follow-ups [Auth] ADR-0069 remaining — session-validation gate (password expiry + enforced MFA), session controls, IP allowlist, OIDC/SAML #2375 ).
P2 — defense in depth
P3 — federation breadth
Out of scope (separate issues/ADRs)
SCIM / directory provisioning & bulk import (separate ADR — provisioning ≠ auth).
Passkeys / WebAuthn.
Suggested first PR : haveibeenpwned (native, low-risk) as the enforcement-wired pattern template, then the [custom] items each with their hook + tests.
Tracking issue for implementing ADR-0069: Enterprise authentication hardening (merged in #2275). Design is accepted-in-principle; no code yet — this issue tracks the phased build.
Parent: #2246 (first-run admin experience). Governing constraint: ADR-0049 — every setting ships with its runtime enforcement; no "false surface" toggles.
Each item = (setting → enforcement seam → mechanism).
[native]= better-auth 1.6.x knob;[custom]= ObjectStack hook/middleware;[field]= new identity column.P1 — security floor (first real customers)
[native]— enablehaveibeenpwnedplugin (password_reject_breached). Lowest-risk first PR / sample for the pattern. ✅ shipped in feat(plugin-auth): reject breached passwords via HIBP (ADR-0069 D1, P1) #2361 (the enforcement-wired pattern template).[custom]—password_require_complexity+password_min_classes, validated inbeforehooks on/sign-up/email,/reset-password,/change-password. ✅ shipped in feat(plugin-auth): password complexity policy (ADR-0069 D1, P1) #2368.[custom][field]—password_expiry_days(gate at session validation),password_history_count(reject reuse); fieldssys_user.password_changed_at,sys_account.previous_password_hashes. ✅ history shipped in feat(plugin-auth): password history / no-reuse (ADR-0069 D1, P1) #2371 (no-reuse,password_history_count+sys_account.previous_password_hashes); expiry shipped in feat(auth): password expiry via the session-validation gate (ADR-0069 D1, P1) #2388 (the session-validation gate; REST surface).[custom][field]—lockout_threshold+lockout_duration_minutes; fieldssys_user.failed_login_count,locked_until; admin Unlock action + locked-accounts filter. ✅ shipped in feat(plugin-auth): account lockout + rate-limit tuning (ADR-0069 D2, P1) #2365 (locked-accounts list filter is a small follow-up).[native]— enable/tune better-auth corerateLimitwith strictercustomRulesfor/sign-in|sign-up|reset; shared store for multi-node. ✅ shipped in feat(plugin-auth): account lockout + rate-limit tuning (ADR-0069 D2, P1) #2365 (multi-node shared store still TODO).[custom][field]—mfa_required+mfa_grace_period_days; gate password-only sessions at session validation until TOTP enrolled+verified; fieldsys_user.mfa_required_at. ✅ shipped in feat(auth): enforced MFA via the session-validation gate (ADR-0069 D3, P1) #2390 (global mfa_required + grace, reusing the feat(auth): password expiry via the session-validation gate (ADR-0069 D1, P1) #2388 gate; per-org require_mfa + objectui enroll prompt = follow-ups [Auth] ADR-0069 remaining — session-validation gate (password expiry + enforced MFA), session controls, IP allowlist, OIDC/SAML #2375).P2 — defense in depth
[custom][field]— idle timeout, absolute max, concurrent cap; fieldssys_session.last_activity_at,revoked_at,revoke_reason.[custom][field]—sys_organization.allowed_ip_ranges(+ optional per-user); Hono auth-route middleware before the better-auth handler.[native]— surface admin-managed OIDC providers (issuer/client/scopes/allowed domains) instead of env-onlygenericOAuth.P3 — federation breadth
@better-auth/ssovs custom plugin. Do not ship a SAML settings surface before an enforcing impl (ADR-0049).Out of scope (separate issues/ADRs)
Suggested first PR:
haveibeenpwned(native, low-risk) as the enforcement-wired pattern template, then the[custom]items each with their hook + tests.