feat(auth): password expiry via the session-validation gate (ADR-0069 D1, P1)#2388
Merged
Conversation
… 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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 5 package(s): 30 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
- 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>
This was referenced Jun 27, 2026
os-zhuang
added a commit
that referenced
this pull request
Jun 27, 2026
…, P1) (#2390) Completes the session gate (after password expiry #2388) by reusing its authGate seam for enforced MFA. - plugin-auth: computeAuthGate gains an MFA branch — when mfaRequired and the user has no TOTP enrolled (sys_user.two_factor_enabled), block with MFA_REQUIRED once the grace window (mfa_required_at + mfaGracePeriodDays) elapses. mfa_required_at is stamped lazily on first required-but-unenrolled session. isAuthGateActive() also trips on mfaRequired. - bindAuthSettings: mfa_required + mfa_grace_period_days; enabling mfa_required force-enables the twoFactor plugin so /two-factor/* enrollment exists. - platform-objects: sys_user.mfa_required_at column. - service-settings: Multi-factor settings group. Default-off / additive; ADR-0049 (enforcement ships with the setting). Verified live (dogfood): mfa_required + grace 7, un-enrolled user — within grace NOT gated (mfa_required_at stamped); past grace -> 403 MFA_REQUIRED; /two-factor/enable reachable (200, totpURI) while blocked; after enrollment the gate lifts (native 2FA verification takes over); admin within grace NOT blocked. Unit: plugin-auth 191 + service-settings 129 + platform-objects 63 + core 299 + rest 140 green; full build (incl. strict DTS) green. objectui follow-up: Console must render a TOTP-enroll prompt on 403 MFA_REQUIRED. Per-org require_mfa + dispatcher/MCP gate remain follow-ups (#2375). Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This was referenced Jun 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ADR-0069 P1 (tracking #2281, follow-up #2375): builds the authentication-policy session gate and wires it for password expiry (D1). This is the shared seam that enforced MFA (D3) will reuse.
What
When
password_expiry_days(newauthsetting, 0 = off) is exceeded, an authenticated user is blocked from protected REST resources with403 PASSWORD_EXPIREDuntil they change their password — while auth + remediation paths stay reachable so they can recover.@objectstack/coreevaluateAuthGate/isAuthGateAllowlisted— one source of truth for the allow-list (auth endpoints, change-password, health, UI-bootstrap reads)@objectstack/plugin-authcustomSessioncomputes the gate posture once →user.authGate;computeAuthGatecomparessys_user.password_changed_atvs the window;password_changed_atstamped on sign-up / change / reset;isAuthGateActive()keeps it zero-overhead when off@objectstack/platform-objectssys_user.password_changed_atcolumn@objectstack/restresolveExecCtxcarriesauthGate;enforceAuthblocks gated sessions (independent ofrequireAuth)@objectstack/service-settingspassword_expiry_dayssettingDesign notes
@objectstack/rest'srest-server(not the hono-plugin CRUD routes nor the dispatcher). The gate hooksenforceAuth, which 39 protected routes already call. Metadata / discovery /me/*stay reachable so the Console can still render the remediation screen — blocking those would brick the UI; data (read + write) is the protected resource and is gated.password_changed_atnever expires (upgrade-safe). Default-off / additive. ADR-0049 (enforcement ships with the setting);Datetimestamps (ADR-0074).Verification
enforceAuthgate,computeAuthGate/stampPasswordChangedAt, and the settings bind.password_expiry_days=30, backdated password):GETandPOST /api/v1/data/*→403 PASSWORD_EXPIRED;change-password(allow-listed) → 200Next
Enforced MFA (D3) reuses this
authGateseam — a second posture branch (mfa_required+ grace + per-org) + an objectui follow-up for the enrollment prompt.🤖 Generated with Claude Code