Skip to content

feat(auth): password expiry via the session-validation gate (ADR-0069 D1, P1)#2388

Merged
os-zhuang merged 2 commits into
mainfrom
auth/adr-0069-session-gate
Jun 27, 2026
Merged

feat(auth): password expiry via the session-validation gate (ADR-0069 D1, P1)#2388
os-zhuang merged 2 commits into
mainfrom
auth/adr-0069-session-gate

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

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 (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 so they can recover.

Package Change
@objectstack/core new pure evaluateAuthGate / isAuthGateAllowlisted — one source of truth for the allow-list (auth endpoints, change-password, health, UI-bootstrap reads)
@objectstack/plugin-auth customSession computes the gate posture once → user.authGate; computeAuthGate compares sys_user.password_changed_at vs the window; password_changed_at stamped on sign-up / change / reset; isAuthGateActive() keeps it zero-overhead when off
@objectstack/platform-objects new sys_user.password_changed_at column
@objectstack/rest resolveExecCtx carries authGate; enforceAuth blocks gated sessions (independent of requireAuth)
@objectstack/service-settings new password_expiry_days setting

Design notes

  • Seam choice (found via dogfood): the live data path is @objectstack/rest's rest-server (not the hono-plugin CRUD routes nor the dispatcher). The gate hooks enforceAuth, 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.
  • Dispatcher/MCP path is a tracked follow-up ([Auth] ADR-0069 remaining — session-validation gate (password expiry + enforced MFA), session controls, IP allowlist, OIDC/SAML #2375). The Console's REST surface is fully gated here.
  • Null password_changed_at never expires (upgrade-safe). Default-off / additive. ADR-0049 (enforcement ships with the setting); Date timestamps (ADR-0074).

Verification

  • Unit: core 299 + rest 140 + plugin-auth 182 + service-settings 129 + platform-objects 63 green; full 60-package build incl. strict DTS green. New tests: the pure core allow-list/evaluate, the enforceAuth gate, computeAuthGate/stampPasswordChangedAt, and the settings bind.
  • Live dogfood (showcase, password_expiry_days=30, backdated password):
    • expired user: GET and POST /api/v1/data/*403 PASSWORD_EXPIRED; change-password (allow-listed) → 200
    • admin + within-window users → not gated (no false positive)
    • after change-password the gate lifts (normal RLS resumes) → remediation loop closes

Next

Enforced MFA (D3) reuses this authGate seam — a second posture branch (mfa_required + grace + per-org) + an objectui follow-up for the enrollment prompt.

🤖 Generated with Claude Code

… 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>
@vercel

vercel Bot commented Jun 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jun 27, 2026 4:11pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Jun 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/core, @objectstack/platform-objects, @objectstack/plugin-auth, @objectstack/rest, packages/services.

30 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/core/index.mdx (via @objectstack/core)
  • content/docs/concepts/core/plugins.mdx (via @objectstack/core)
  • content/docs/concepts/core/services.mdx (via @objectstack/core)
  • content/docs/concepts/implementation-status.mdx (via @objectstack/core, @objectstack/plugin-auth, @objectstack/rest)
  • content/docs/concepts/north-star.mdx (via packages/core)
  • content/docs/concepts/packages.mdx (via @objectstack/core, @objectstack/platform-objects, @objectstack/plugin-auth, @objectstack/rest)
  • content/docs/concepts/setup-app.mdx (via @objectstack/platform-objects)
  • content/docs/concepts/webhook-delivery.mdx (via @objectstack/core)
  • content/docs/getting-started/cli.mdx (via @objectstack/plugin-auth)
  • content/docs/guides/ai-capabilities.mdx (via @objectstack/core)
  • content/docs/guides/api-reference.mdx (via @objectstack/rest)
  • content/docs/guides/auth-sso.mdx (via @objectstack/plugin-auth)
  • content/docs/guides/authentication.mdx (via @objectstack/core, @objectstack/plugin-auth)
  • content/docs/guides/contracts/index.mdx (via @objectstack/core)
  • content/docs/guides/kernel-services.mdx (via @objectstack/core, @objectstack/plugin-auth)
  • content/docs/guides/objectql-migration.mdx (via @objectstack/core)
  • content/docs/guides/packages.mdx (via @objectstack/core, @objectstack/platform-objects, @objectstack/plugin-auth, @objectstack/rest, packages/services)
  • content/docs/guides/plugin-development.mdx (via @objectstack/core)
  • content/docs/guides/plugins.mdx (via @objectstack/core, @objectstack/plugin-auth, @objectstack/rest)
  • 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/examples.mdx (via @objectstack/core)
  • 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/config-resolution.mdx (via @objectstack/core)
  • content/docs/protocol/objectos/i18n-standard.mdx (via packages/services)
  • content/docs/protocol/objectos/index.mdx (via @objectstack/core)
  • content/docs/protocol/objectos/lifecycle.mdx (via @objectstack/core)
  • content/docs/protocol/objectos/plugin-spec.mdx (via @objectstack/core)
  • 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.

Comment thread packages/core/src/security/auth-gate.ts Fixed
Comment thread packages/plugins/plugin-auth/src/auth-manager.ts Fixed
Comment thread packages/plugins/plugin-auth/src/auth-manager.ts Fixed
Comment thread packages/rest/src/rest-server.ts Fixed
- 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>
@os-zhuang
os-zhuang merged commit ce0b4f6 into main Jun 27, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the auth/adr-0069-session-gate branch June 27, 2026 16:17
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants