Skip to content

feat(auth): enforced MFA via the session-validation gate (ADR-0069 D3, P1)#2390

Merged
os-zhuang merged 1 commit into
mainfrom
auth/adr-0069-enforced-mfa
Jun 27, 2026
Merged

feat(auth): enforced MFA via the session-validation gate (ADR-0069 D3, P1)#2390
os-zhuang merged 1 commit into
mainfrom
auth/adr-0069-enforced-mfa

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

ADR-0069 P1 (tracking #2281, #2375): enforced MFA (D3) — completes the session-validation gate, reusing the authGate seam shipped in #2388 (password expiry).

What

When mfa_required (new auth toggle) is on, an authenticated user without TOTP enrolled is blocked from protected REST resources with 403 MFA_REQUIRED once their mfa_grace_period_days (default 7) window elapses — while /two-factor/* enrollment stays reachable so they can comply.

  • plugin-auth: computeAuthGate gains an MFA branch (un-enrolled + grace elapsed → MFA_REQUIRED); mfa_required_at (the grace clock) is stamped lazily on the first required-but-unenrolled session; isAuthGateActive() also trips on mfa_required.
  • bind: mfa_required + mfa_grace_period_days; enabling mfa_required force-enables the twoFactor plugin so the enrollment endpoints exist (otherwise gated users couldn't comply).
  • platform-objects: new sys_user.mfa_required_at column.
  • service-settings: new Multi-factor settings group.

Default-off / additive; ADR-0049 (enforcement ships with the setting). The /two-factor/* + change-password paths are already allow-listed by the core gate helper.

Verification

  • Unit: plugin-auth 191 + service-settings 129 + platform-objects 63 + core 299 + rest 140 green; full build incl. strict DTS green. New tests: MFA gate (grace elapsed/within/null-stamp/enrolled/grace-0) + bind (force-twoFactor, clamp).
  • 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 — remediation open
    • after enrollment (two_factor_enabled) the gate lifts; native better-auth 2FA verification takes over
    • admin within grace → not falsely blocked

Follow-ups (tracked in #2375)

  • objectui: Console must render a TOTP-enroll prompt on 403 MFA_REQUIRED (otherwise flipping the toggle on shows errors instead of a guided enroll). Backend is default-off so existing envs are unaffected.
  • Per-org sys_organization.require_mfa; the dispatcher/MCP gate path.

ADR-0069 P1 — now complete

✅ D1 HIBP (#2361) · ✅ D2 lockout+rate-limit (#2365) · ✅ D1 complexity (#2368) · ✅ D1 history (#2371) · ✅ D1 expiry + gate (#2388) · ✅ D3 enforced MFA (this PR).

🤖 Generated with Claude Code

…, P1)

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: 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 Building Building Preview, Comment Jun 27, 2026 4:25pm

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 3 package(s): @objectstack/platform-objects, @objectstack/plugin-auth, packages/services.

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

  • content/docs/concepts/implementation-status.mdx (via @objectstack/plugin-auth)
  • content/docs/concepts/packages.mdx (via @objectstack/platform-objects, @objectstack/plugin-auth)
  • content/docs/concepts/setup-app.mdx (via @objectstack/platform-objects)
  • content/docs/getting-started/cli.mdx (via @objectstack/plugin-auth)
  • content/docs/guides/auth-sso.mdx (via @objectstack/plugin-auth)
  • content/docs/guides/authentication.mdx (via @objectstack/plugin-auth)
  • content/docs/guides/kernel-services.mdx (via @objectstack/plugin-auth)
  • content/docs/guides/packages.mdx (via @objectstack/platform-objects, @objectstack/plugin-auth, packages/services)
  • content/docs/guides/plugins.mdx (via @objectstack/plugin-auth)
  • 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/index.mdx (via packages/services)
  • content/docs/guides/runtime-services/settings-service.mdx (via packages/services)
  • content/docs/protocol/objectos/i18n-standard.mdx (via packages/services)
  • 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.

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.

1 participant