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): password expiry via the session-validation gate (ADR-0069 D1, P1) (#2388)
* feat(auth): password expiry via the session-validation gate (ADR-0069 D1, P1)
Builds the authentication-policy session gate ADR-0069 needs (the seam shared
by password expiry and enforced MFA) and wires it for password expiry.
- core: pure evaluateAuthGate / isAuthGateAllowlisted helper — one source of
truth for the allow-list (auth + remediation + health + UI-bootstrap reads).
- plugin-auth: customSession computes posture once -> user.authGate;
computeAuthGate compares sys_user.password_changed_at vs password_expiry_days;
password_changed_at stamped on sign-up / change / reset; isAuthGateActive()
keeps it zero-overhead when off.
- platform-objects: sys_user.password_changed_at column.
- rest: resolveExecCtx carries authGate; enforceAuth blocks gated sessions
(independent of requireAuth) with 403 { code: PASSWORD_EXPIRED }, allow-list
keeps auth/remediation reachable.
- service-settings: password_expiry_days setting (0 = off).
Default-off / additive; null password_changed_at never expires (upgrade-safe);
ADR-0049 (enforcement ships with the setting); Date timestamps (ADR-0074).
Verified live (dogfood): expiry=30 + backdated password -> data GET/POST 403
PASSWORD_EXPIRED while change-password (allow-listed) works; admin + within-
window users NOT gated; after change-password the gate lifts (normal RLS
resumes) -> remediation loop closes. Unit: core 299 + rest 140 + plugin-auth
182 + service-settings 129 + platform-objects 63 green; full 60-pkg build
(incl. strict DTS) green.
The dispatcher/MCP path is a follow-up (#2375); the Console's REST surface is
fully gated here. Enforced MFA (D3) reuses this seam next.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(auth): address CodeQL — ReDoS-safe path strip + dead-code cleanup
- core/auth-gate: replace the trailing-slash regex (`/\/+$/`, flagged as
polynomial ReDoS on uncontrolled path input) with an O(n) char-scan strip.
- plugin-auth: declare `succeeded`/`signupOk` without the always-overwritten
initial value (useless-assignment warnings).
- rest-server: drop the redundant `authService &&` (already non-null here).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Auth: password expiry — the session-validation gate (ADR-0069 D1, P1)
10
+
11
+
Builds the **authentication-policy session gate** ADR-0069 needs and uses it for password expiry. When `password_expiry_days` (new `auth` setting, 0 = off) is exceeded, an authenticated user is blocked from protected REST resources with `403 PASSWORD_EXPIRED` until they change their password — while auth + remediation paths stay reachable.
12
+
13
+
-**core**: new pure `evaluateAuthGate` / `isAuthGateAllowlisted` helper (`@objectstack/core/security`) — single source of truth for the allow-list (auth endpoints, change-password, health, UI-bootstrap reads).
14
+
-**plugin-auth**: `customSession` computes the gate posture once and attaches `user.authGate`; `computeAuthGate` reads `sys_user.password_changed_at` vs the configured window; `password_changed_at` is stamped on sign-up / change / reset; `isAuthGateActive()` keeps the gate **zero-overhead** when off.
15
+
-**platform-objects**: new `sys_user.password_changed_at` column.
16
+
-**rest**: `resolveExecCtx` carries `authGate`; `enforceAuth` blocks gated sessions (independent of `requireAuth`) using the core allow-list.
17
+
-**service-settings**: new `password_expiry_days` field.
18
+
19
+
Default-off / additive (no upgrade behavior change); a null `password_changed_at` never expires (existing users). Per ADR-0049 the setting ships with its enforcement; timestamps written as `Date` (ADR-0074).
20
+
21
+
This gate is the shared seam for **enforced MFA** (ADR-0069 D3), which lands next as a small addition (a second `authGate` branch). The dispatcher/MCP path is a follow-up (tracked in #2375); the REST surface the Console uses is fully gated here.
description: 'When set and in the future, sign-in is rejected (brute-force lockout). Auto-clears past this time; an admin can clear it early via Unlock.',
448
448
}),
449
449
450
+
// ADR-0069 D1 — last password change; drives password-expiry enforcement.
451
+
// Stamped on sign-up / change-password / reset-password. Null = never
452
+
// expires (until the user next changes their password).
453
+
password_changed_at: Field.datetime({
454
+
label: 'Password Changed At',
455
+
required: false,
456
+
readonly: true,
457
+
group: 'Admin',
458
+
description: 'Timestamp of the last password change. Backs password_expiry_days; system-managed.',
0 commit comments