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

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>

This branch was successfully deployed

1 active deployment
Preview — 71f115de Deployed Jun 27, 2026 by vercel[bot]
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