You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(auth): last-login audit fields + ADR-0069 status (P1 completion) (#2570)
Fills the one missing ADR-0069 D7 identity field pair — the rest of P1
(complexity/history/expiry/HIBP/lockout/enforced-MFA) was already
implemented (this task was evaluate-first, don't re-develop).
- platform-objects: add sys_user.last_login_at (datetime) +
last_login_ip (text, 45 = IPv6 max), both readonly/Admin group.
- plugin-auth: AuthManager.stampLastLogin stamps them on every
successful /sign-in/email via the after-hook, independent of the
lockout threshold (which gates recordSignInOutcome). IP from the
trusted forwarded headers (x-forwarded-for → cf-connecting-ip →
x-real-ip), capped to the column width. Best-effort: never turns a
valid login into an error.
- 4 unit tests (Date stamp + IP, runs when lockout off, IP cap, never
throws on engine failure).
- ADR-0069 status Proposed → Accepted (P1/P2 implemented) with an
implementation-status matrix documenting what is landed and the
remaining P2 gaps (per-org sys_organization.allowed_ip_ranges,
shared-store rate limiting for multi-node).
Verified: plugin-auth 5 files / platform-objects 2 files green; live
showcase run shows last_login_at/ip populated after sign-in (IP from
x-forwarded-for), and the pre-existing D1 complexity + D2 lockout
enforced end-to-end when enabled via settings.
Claude-Session: https://claude.ai/code/session_014y5kiH3aPLWtRRRGcVrXcT
Co-authored-by: Claude <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: docs/adr/0069-enterprise-authentication-hardening.md
+12-1Lines changed: 12 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,6 +1,6 @@
1
1
# ADR-0069: Enterprise authentication hardening — password policy, enforced MFA, SSO, session controls, network gating, and anti-brute-force, all enforcement-wired
2
2
3
-
**Status**: Proposed (2026-06-24)
3
+
**Status**: Accepted — **P1 + P2 implemented**(2026-07-04); P3 partially landed (see Addendum). Original proposal 2026-06-24.
4
4
**Deciders**: ObjectStack Protocol Architects
5
5
**Builds on**: [ADR-0049](./0049-no-unenforced-security-properties.md) (**the governing constraint** — a security property that isn't enforced at runtime is forbidden; no toggle may be a "false surface"), [ADR-0007](./0007-settings-manifest-and-kv-store.md) (settings manifest + cascade KV store), [ADR-0057](./0057-erp-authorization-core-business-units-and-scope-depth.md) (`sys_role` is platform-native, decoupled from better-auth; org scoping), [ADR-0066](./0066-unified-authorization-model.md) (capability/assignment split), [ADR-0068](./0068-unified-user-context-and-built-in-identity-roles.md) (`current_user` contract, built-in roles)
@@ -112,6 +112,8 @@ Legend: **[native]** = a better-auth config/plugin does the enforcing; **[custom
112
112
113
113
Plus admin-facing surface: a `sys_user` list-view filter for locked accounts + an **Unlock** action; a "force password reset" action.
114
114
115
+
**Implementation status (2026-07-04):** all `sys_user` (incl. `last_login_at`/`last_login_ip`), `sys_session`, `sys_account`, and `sys_organization.require_mfa` fields are landed, plus the `unlock_user` admin action. `last_login_at`/`last_login_ip` are stamped on every successful `/sign-in/email` by `AuthManager.stampLastLogin` (after-hook, independent of lockout config). **Not landed:** a per-record `sys_organization.allowed_ip_ranges` (and optional `sys_user.allowed_ip_ranges` override) — the IP allow-list currently exists only as the **global**`auth.allowed_ip_ranges` setting (D5); per-org/per-user IP scoping is tracked as remaining P2 work.
116
+
115
117
---
116
118
117
119
## Enforcement seam summary (where the code hooks)
@@ -132,6 +134,15 @@ Each row in D1-D6 names exactly one of these seams. No setting is introduced wit
|**P2** (D4/D5) | 🟡 **mostly implemented**| Session idle/absolute/concurrent (`enforceSessionControls`/`enforceConcurrentCap`) and the **global** IP allow-list (`isClientIpAllowed`, `auth.allowed_ip_ranges`) are landed. **Remaining:** per-org `sys_organization.allowed_ip_ranges` (+ optional `sys_user.allowed_ip_ranges` override); a **shared/Redis rate-limit store** for multi-node (current store is in-memory). |
143
+
|**P2/P3** (D6) | 🟡 partial | Generic OIDC RP wired (`genericOAuth`/`sso`); admin OIDC **trust-list settings UI** still env/`sys_sso_provider`-only. |
144
+
|**P3** (SAML, broader social) | 🟡 partial |`@better-auth/sso` present (SAML now better-auth-native — see Addendum); broader settings-driven social providers pending. |
145
+
135
146
## Out of scope / deferred
136
147
137
148
-**SCIM / directory provisioning & bulk import** — separate ADR (provisioning, not authentication); tracked in #2246.
0 commit comments