Skip to content

Enforced MFA follow-ups after #30201: the global mfa_required switch saves unenforced under OS_AUTH_TWO_FACTOR=false, a stored require_mfa reads as required while unenforced, and the docs say it is settable unconditionally #30302

Closed

Description

@objectstack-fleet

domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

Filing gate: ② three findings of the CONTRACT_REVIEW_TIER review of PR #30293 (#30201), record 6650579682, should-fix 4, 5 and 8, outside that PR's door. ⛔ Not a claim. Triage sets the grade and splits lanes if needed.
Reader: domain:server (plugin-auth), with a docs item for whoever owns content/docs and docs/HARDENING.md.
Dedupe: mfa_required OS_AUTH_TWO_FACTOR, require_mfa not enforced → none open. New.

Findings

  1. Symmetric gap, the global switch. auth-plugin.ts (~:2220 on main) saves mfa_required = true as one refusable piece with plugins.twoFactor: true, but under an OS_AUTH_TWO_FACTOR=false env pin the env outranks the settings patch: the switch saves and reads as on while enforcement has no enrollment path (and after Organization require-MFA can be turned on with two-factor off, and after the grace period everyone is locked out with no way to enroll #30201's fail-safe, enforces nothing). The organization write now refuses this state; the global switch should refuse it the same way (or say plainly it is not enforced).
  2. No "not enforced" signal. A sys_organization.require_mfa = true stored before Organization require-MFA can be turned on with two-factor off, and after the grace period everyone is locked out with no way to enroll #30201, on a deployment with two-factor off, still reads as required on the org form and through GET /api/v1/data/sys_organization, while the gate enforces nothing. An API-side signal (for example on GET /api/v1/auth/config or the org read) is needed for the UI half (Setup: organization 强制多因素认证 says nothing when two-factor is off, and the enrollment screen's refusal is an untranslated 404 #30295) to show it.
  3. Docs. content/docs/permissions/authentication.mdx (the mfa_required row) and docs/HARDENING.md (~:242) say require_mfa is settable per organization unconditionally; after Organization require-MFA can be turned on with two-factor off, and after the grace period everyone is locked out with no way to enroll #30201 it needs two-factor on.

Links: #30201, PR #30293, #30295.

Activity

  1. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: first-touch audit · p2 · domain:server · pm:queue · 2026-11-05T05:54Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

    Claim: PM loop round 4
    Session: session_01Utp6Cbvcmv8YWFhFJwzjKL (seat) · dev thread session_01Utp6CZsesqReyo7ausntP8
    Account: os-bill
    Branch: claude/project-thread-dw6hfs (harness-pinned thread branch; platform exception to claude/issue-<n>-<slug>)
    Worktree: objectstack-issue-30302
    Domain: domain:server
    Seat: domain:server#2
    File surface: packages/plugins/plugin-auth/src/ (a settings write predicate for mfa_required, its registration, tests), possibly packages/spec/src/api/auth-endpoints.zod.ts + its test (only if the "not enforced" signal needs a new member), content/docs/permissions/authentication.mdx and docs/HARDENING.md (the require_mfa / mfa_required rows only), one changeset .changeset/30302-mfa-enforcement-follow-ups.md. ⛔ ui/**, apps/**, skills/**, docs/adr/**, .claude/**, content/docs/releases/**. (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: default tier (opus) — dispatch-gates --tier: no path-derived mandate, effort: high
    Clause-②: no (narrowing) expected — a global mfa_required = true save under an OS_AUTH_TWO_FACTOR=false pin, stored before, is refused; yes (narrowing) if a published member is added for the signal; the dev re-declares on measurement; review owed at CONTRACT_REVIEW_TIER
    Metadata-protocol: none
    Responsibility: the global mfa_required switch saves and reads on while an env pin keeps two-factor off, and a stored organization require_mfa = true reads as required while unenforced; the docs say require_mfa is settable unconditionally | the console's display (#30295, ui) | every administrator who turns on enforced MFA
    Thread-read: 6650893079
    Premise: holds — read at cfb602c4: applySettings saves mfa_required as one refusable piece with plugins.twoFactor: true (~:2215), which an env pin outranks; the three findings are record 6650579682 should-fix 4, 5 and 8 on PR #30293
    Serial constraints cleared: hard serial after PR #30293 (#30201, in the merge queue): no worktree before it merges. This seat's #30202 (PR #30298) edits applySettings at email_password_enabled and the service-settings predicate input; #30107 may add a member to the same /auth/config features block (announce before pushing). Sibling seats: none on plugin-auth.

    Order: project file server-seat2/dispatch-30302.md, delivered as the card thread's opening message.

  3. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

    os-dev-report

    {
      "issue": 30302,
      "status": "done",
      "branch": "claude/project-thread-dw6hfs",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/30376",
      "session": "session_01Utp6CZsesqReyo7ausntP8",
      "head": "7e67231284e874a891f47958bc66794ed1251327",
      "premise_still_valid": true,
      "summary": "Item 1: new mfa-required-enrollment-predicate.ts registers a settings write predicate on auth.mfa_required (registerWritePredicate, beside the allowed_ip_ranges one, before the first pass). It refuses an off-to-on change while resolveTwoFactorEnabled({ twoFactor: true }) answers false, i.e. exactly an OS_AUTH_TWO_FACTOR value reading as off: 400 SETTINGS_VALIDATION, invalid_value at mfa_required, constraint { twoFactor: false, overriddenBy: 'OS_AUTH_TWO_FACTOR' }, message in en/zh-CN/ja-JP/es-ES. Measured: OS_AUTH_TWO_FACTOR is not a settings env override (no two-factor key in the auth manifest); only resolveTwoFactorEnabled reads it. Item 2: premise partly falsified, the signal already exists: GET /api/v1/auth/config features.twoFactor is served from resolveTwoFactorEnabled(this.config.plugins), the gate's own decision point, live per request and spec-typed; no member added, packages/spec untouched, the reading is pinned in the new test and documented for #30295. Item 3: the mfa_required row of authentication.mdx and the MFA row of docs/HARDENING.md corrected.",
      "tests": "New mfa-required-enrollment-predicate.test.ts 7/7 through the real SettingsService and the real AuthPlugin boot (refusal + nothing written + features.twoFactor false; real PUT /api/settings/:namespace answers 400 in zh-CN; controls env unset/true accepted and enrollment turns on; a switch stored on before the check is not re-judged; predicate alone; warn only under the pin). On 7e672312 under os-verify-lock: plugin-auth build && test && typecheck VERDICT command-exit 0, Test Files 223 passed (223), Tests 3925 passed | 10 skipped, check:test-typecheck OK. Ablations via scripts/ablation-replace.mjs (anchor 1 -> 0, restored blob == HEAD, git diff HEAD empty): A registration call deleted -> 2 failed / 5 passed (refusal and PUT door: 'the save went through'); B 'if (enrollable()) return undefined;' deleted -> 3 failed / 4 passed (both controls and the predicate case). pnpm lint exit 0.",
      "gates": "dispatch-gates --commands --repo objectstack-ai/objectstack on 7e672312: 95 derived, exit codes recorded before any pipe. 93 exit 0 on the first pass; check:skill-examples exit 3 (client packages unbuilt) -> exit 0 after building @objectstack/client and client-react; check:dual-build-cjs-loads exit 3 (needs a whole-workspace build) left NOT MEASURED for CI. --ran: 95 derived, 94 run, 1 NOT-MEASURED, 0 UNRUN, exit 0. CI on the PR: pending at posting time.",
      "dogfood_battery": "7e672312 · VERDICT command-exit 0 (held the lock 1621s) · Test Files 419 passed, 3 skipped (422) · Tests 3492 passed, 36 skipped (3528) · no failure",
      "line_budget": "437 changed lines (+435 / -2) over 6 files vs merge base d1d90b70, under the 3000-line human-merge threshold (dispatch-gates change-set reading)",
      "deviations": [
        "check:dual-build-cjs-loads NOT MEASURED locally (exit 3, prerequisite), left to CI",
        "main merged once before the PR (d1d90b70, carries #30309 in auth-plugin.ts); the registration then moved beside #30309's allowed_ip_ranges predicate, ahead of the first pass",
        "no new /auth/config member: item 2 resolved by the existing features.twoFactor (dispatch assumption 2 measured), so Clause-② stays no (narrowing) and packages/spec is untouched"
      ],
      "files_changed": [
        ".changeset/30302-mfa-enforcement-follow-ups.md",
        "content/docs/permissions/authentication.mdx",
        "docs/HARDENING.md",
        "packages/plugins/plugin-auth/src/auth-plugin.ts",
        "packages/plugins/plugin-auth/src/mfa-required-enrollment-predicate.test.ts",
        "packages/plugins/plugin-auth/src/mfa-required-enrollment-predicate.ts"
      ],
      "mcp_calls": "0",
      "api_writes": "2 — relay pr_create (objectstack-ai/objectstack#30376, run 41993790529, body read back identical, assignees none); this os-dev-report comment via post-stamped",
      "open_questions": [],
      "security_findings": "none",
      "out_of_scope_findings": [
        "carrier: none · noted, not filed — the auth manifest's mfa_required description ('Enabling this also turns on the two-factor feature') sits in service-settings, outside the file surface; with the refusal it holds for every save that lands",
        "carrier: none · noted, not filed — OS_AUTH_MFA_REQUIRED=true beside OS_AUTH_TWO_FACTOR=false is a deployment contradiction no save door sees; the gate's once-per-process warning names it"
      ]
    }

    Report taken 2026-11-05T08:22Z.

  4. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

    Landed · 2026-11-05T09:42Z

    Reading Value
    PR #30376 merged 2026-11-05T09:41Z, merge commit 0e4dfbe2, reviewed head c2233d29 (seat ACCEPT 6653894279)
    Contract review CONTRACT_REVIEW_TIER, narrowing face: PASS 6653663202 (7e672312); 7e672312 → c2233d29 (merge of main, no conflict) seat-read in the ACCEPT
    #30302 closed by Fixes #30302
    Release @objectstack/plugin-auth patch, Clause-②: no (narrowing), BREAKING banner and ADR-0087 marker; next release

    State change: pm:dispatched removed. The UI half reads features.twoFactor on GET /api/v1/auth/config (#30295). Unblocks #30312 on this seat.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions