Skip to content

Authentication settings: turning off email and password with no other sign-in method saves without warning, and everyone is locked out #30202

Closed

Description

@objectstack-fleet

epic:#26676#1 · zhuangjianguo · session_cdb7a717def74e719988f089fbaa18ff

From a real-user walkthrough of Setup, filed on the maintainer's word: 「以上都同意,立卡」 (2026-11-05). Measured on main b7075d9908, with the console in dev mode, unless a line says otherwise. Test app scaffolded with create-objectstack@18.3.0.

What a customer sees

An administrator plans to move to single sign-on and turns off 启用邮箱/密码登录 before SSO is configured. It saves. As soon as people sign out, no one can sign back in, the administrator included, and the fix needs an environment variable or a database edit.

Measured

Expected

The save is refused while it would leave no sign-in method, and names what to configure first. When email and password really are off, the login page does not offer the form.

Activity

  1. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: first-touch audit · p1 · domain:server · pm:queue · 2026-11-05T02:56Z

    ⛔ Not a claim.

    • Read fresh. Turning off email and password sign-in with no other sign-in method configured saves with no warning. Once people sign out, nobody can sign back in, the administrator included, and recovery needs an environment change. A whole-tenant lockout from one Setup toggle, so p1.
    • Fix site: the auth settings save door (plugin-auth) refuses a configuration with no working sign-in method. The Setup confirmation is the domain:ui half.

    Generated by Claude Code

  2. added
    domain:serverDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    priority:p1High: required for production / M2
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 5, 2026
  3. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

    Claim: PM loop round 3
    Session: session_01Utp6Cbvcmv8YWFhFJwzjKL (seat) · dev thread session_01Utp6CXhZcHuALPAjkDCiiT
    Account: os-bill
    Branch: claude/project-thread-bg54o3 (harness-pinned thread branch; platform exception to claude/issue-<n>-<slug>)
    Worktree: objectstack-issue-30202
    Domain: domain:server
    Seat: domain:server#2
    File surface: packages/plugins/plugin-auth/src/ (the auth settings apply / save door, tests), possibly manifest.ts (help text), one changeset .changeset/30202-keep-a-sign-in-method.md. ⛔ packages/spec/**, ui/**, apps/**, skills/**, docs/adr/**, .claude/**. (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: default tier (opus) — dispatch-gates --tier: no path-derived mandate, effort: xhigh (p1)
    Clause-②: no — a refused save / gate fail-safe on an existing door; the dev re-declares yes for a new published code, export or settings key, or no (narrowing) if a value an admin could save before is now refused (then CONTRACT_REVIEW_TIER review)
    Metadata-protocol: none
    Responsibility: the auth settings apply (auth-plugin.ts, email_password_enabled) persists email/password off with no other sign-in method | no other door checks that a sign-in method remains | every user of the deployment, the administrator included, once they sign out
    Thread-read: 6648434001
    Premise: holds — at 5fa9375e email_password_enabled is applied through emailAndPasswordField(...) with no check of social providers or SSO; the same apply already has a refusable path (mfa_required) and a refusal-isolation test
    Serial constraints cleared: #30201 (this seat) may edit auth-plugin.ts near mfa_required and the session-policy gate; disjoint hunks, each pulls main before PR and before landing. No other in-flight domain:server claim names plugin-auth (#29997 closed; sibling seats' cards read by title/surface)

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

  4. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-bill · session_01Utp6CXhZcHuALPAjkDCiiT
    os-dev-report

    {
      "issue": 30202,
      "status": "done",
      "branch": "claude/project-thread-bg54o3",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/30298",
      "session": "session_01Utp6CXhZcHuALPAjkDCiiT",
      "premise_still_valid": true,
      "summary": "Saving the auth settings now refuses, at the settings save door and before anything is written, turning email and password off while no other way in works, and (with email and password already off) the Google change that removes the last provider. It is one write predicate (SettingsService.registerWritePredicate, the ADR-0144 D4 seam) registered from AuthPlugin.bindAuthSettings on auth.email_password_enabled and auth.google_enabled / google_client_id / google_client_secret, answering the existing 400 SETTINGS_VALIDATION invalid_value field error with constraint.lastSignInMethod and a message in en / zh-CN / ja-JP / es-ES naming what to set up first. 'Another way in' = a social or OIDC provider in force; enterprise SSO switched on AND a sys_sso_provider row; phone-number sign-in; a declared hostSignInHandoff (ssoOnlyMode alone, magic link and passkeys do not count). Recovery for an already locked deployment is the existing OS_AUTH_EMAIL_PASSWORD_ENABLED=true override plus a restart, named in the changeset and pinned; no new variable.",
      "tests": "sign-in-method-floor.test.ts (new, 11 cases, real SettingsService + AuthPlugin + AuthManager): 11/11, re-run on 249ec4c4 (main 4be97342 merged). Whole plugin-auth suite (vitest --maxWorkers=2): Test Files 221 passed, Tests 3887 passed / 10 skipped, on deadfe9f and on 1474c9ec (main 959c26be merged). plugin-auth typecheck (both tsconfigs + check:test-typecheck): exit 0 on the merged head after rebuilding @objectstack/core's dist (stale soleWriter d.ts from main's DispatchLoop change). pnpm lint: exit 0. Ablations through scripts/ablation-replace.mjs, each anchor 1 -> 0 and restored to the HEAD blob with git diff HEAD empty (tests import src; service-settings aliased to src, so no dist leg): A email/password refusal returns nothing -> 3 red; B Google refusal returns nothing -> 1 red; C SSO switch counts without a provider row -> 1 red.",
      "gates": "node scripts/pm/dispatch-gates.mjs --commands: 72 derived families on db95d0dc, each exit code recorded before any pipe (/mnt/project-files/server-seat2/gates-30202-ran.txt): 72 exit 0; --ran: 72 derived, 72 run, 0 NOT-MEASURED, 0 UNRUN. First pass had two exit 3 for prerequisites (check:dual-build-cjs-loads: seven packages without dist, built; check-plugin-teardown-shape --self-test: git fetch of 621a4876), both then 0. The 18 families the changeset path derives re-run on deadfe9f (changeset-only commit): all 0. On the merged head the derived list is the same 72 (sorted diff empty); check:engine-double-contract re-run: 0. CI check-runs on PR #30298 (head 249ec4c4): not yet read at report time; the seat thread hears when they settle.",
      "dogfood_battery": "db95d0dc · os-verify-lock VERDICT command-exit 1 (held 1933s) · Test Files 418 passed / 1 failed / 3 skipped (422) · Tests 3491 passed / 1 failed / 36 skipped (3528) · failure: permission-set-clone-door.dogfood.test.ts > 'a platform set (the lock-and-clone rule's commonest source) copies into its shipping package's pair', 422 INVALID_METADATA on member_default's sys_comment / sys_activity grants. Control: registerSignInMethodFloor( neutralized (--hold, plugin-auth rebuilt, ablation-dist-preflight marker present in 2 built files) -> same file red the same way (1 failed / 10 passed); restored, rebuilt, --absent confirmed. It is main red #30255 (from #30222), reverted on main by #30262; after merging 4be97342 that file passes (11/11, VERDICT command-exit 0). Full battery not re-run after the merges.",
      "line_budget": "5 files, +641 / -0 against origin/main: sign-in-method-floor.ts +262, sign-in-method-floor.test.ts +333, auth-plugin.ts +22, .changeset/30202-keep-a-sign-in-method.md +19, scripts/engine-double-contract.pinned.json +5",
      "files_changed": [
        "packages/plugins/plugin-auth/src/sign-in-method-floor.ts",
        "packages/plugins/plugin-auth/src/sign-in-method-floor.test.ts",
        "packages/plugins/plugin-auth/src/auth-plugin.ts",
        ".changeset/30202-keep-a-sign-in-method.md",
        "scripts/engine-double-contract.pinned.json"
      ],
      "deviations": [
        "scripts/engine-double-contract.pinned.json is outside the claimed file surface: one ledger row for the new test's findOne double, written by check-engine-double-contract.mjs --write, which the gate requires for a new test file with an engine double.",
        "The refusal message names what to configure, not the recovery variable: it speaks to the administrator in Setup, who is still signed in. OS_AUTH_EMAIL_PASSWORD_ENABLED is named in the changeset and the PR body.",
        "The harness's assign / review-request step was skipped, as the order says.",
        "After the changeset-only commit deadfe9f, only the 18 changeset-derived families were re-run, not all 72."
      ],
      "mcp_calls": "5 — all reads: issue_read get #30202, issue_read get_comments #30202 (x2), issue_read get #29159, pull_request_read get #30298 (draft true, no assignees, body as sent). No MCP write.",
      "api_writes": "2 — relay pr_create (POST /repos/objectstack-ai/objectstack/pulls) and post-stamped (POST /repos/objectstack-ai/objectstack/issues/30202/comments); plus git push (not REST). Write budget: 3 of 3.",
      "open_questions": [
        {
          "question": "A provider filled in and email and password turned off in the same save is refused (the predicate judges one key against the running config). Is two saves acceptable?",
          "options": [
            "A keep two saves; the message says to save the other method first",
            "B judge the batch as a whole, which needs the batch on SettingsWritePredicateInput (a service-settings contract change, its own card)"
          ],
          "recommendation": "A, because it refuses nothing that leaves a way in once the method is saved, and B widens a shared contract beyond this card."
        }
      ],
      "security_findings": "none",
      "out_of_scope_findings": [
        "class: a · reach: Setup → Settings → Authentication shows no confirmation before turning off the last sign-in method, and shows the refusal only as the field error the console extractor reads; the card also saw the login page draw the email/password form under os dev with better-auth's English 'Email and password is not enabled' (LoginForm.tsx:351 has the no-method state at 797f9257, so what remains is what the console dev server draws there; not reproduced) · evidence: card #30202 body, ui/packages/auth/src/LoginForm.tsx · dedupe words: login page no sign-in method, setup auth confirmation, email password disabled form",
        "carrier: seat files to domain:ui (lane off duty) · noted, not filed",
        "class: b · reach: other doors to the same lockout that are not settings saves: deleting the last sys_sso_provider row, or a redeploy that drops the last stack-config provider or plugins.phoneNumber, while email and password is off · evidence: read, not measured · dedupe words: last sso provider delete lockout, redeploy drops provider lockout",
        "carrier: seat · noted, not filed"
      ]
    }

    Report taken 2026-11-05T05:28Z.


    Generated by Claude Code

  5. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

    Claim correction (supersedes the Clause-② line and widens the file surface of claim 6648660209; every other line stands)
    Clause-②: no (narrowing) — measured by the dev: the auth settings save door refuses, as a whole batch (400 SETTINGS_VALIDATION), a save that turns off the last sign-in method (email and password with no other method, or the last Google provider while email and password are off), a value an admin could save before. Recovery for a deployment already locked: the existing OS_AUTH_EMAIL_PASSWORD_ENABLED=true and a restart. Review owed: CONTRACT_REVIEW_TIER over the non-prose diff (narrowing face).
    File surface (added): scripts/engine-double-contract.pinned.json (one row the gate requires for the new module). Accepted by the seat, subject to the contract review.

  6. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-bill · session_01Utp6CXhZcHuALPAjkDCiiT
    os-dev-report

    {
      "issue": 30202,
      "status": "done",
      "branch": "claude/project-thread-bg54o3",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/30298",
      "session": "session_01Utp6CXhZcHuALPAjkDCiiT",
      "premise_still_valid": true,
      "summary": "Rework after contract review 6650756385 (seat ruling B, same PR). service-settings: SettingsWritePredicateInput gains optional batch (every key the write sets, frozen, null = clear), built once in refuseByWritePredicates and passed to every predicate. plugin-auth: the sign-in floor judges the state after the whole save, so { email_password_enabled: false, google_enabled: false } (or the Google secret cleared) on email+password+Google is refused at both keys, and Google filled in, or switched on over a stored pair, in the same save as email and password off is accepted. Should-fix: a social or OIDC provider counts only when both clientId and clientSecret are non-blank in the boot configuration it came from (providerCredentialIndex); Google added from the settings has a pair by construction. The one-key two-save limitation of the first head is gone.",
      "tests": "sign-in-method-floor.test.ts 17/17 (6 new: both-keys refusal x2 batches, Google filled in the same save, switch over a stored pair, half a pair refused, blank-secret stack provider, providerCredentialIndex). settings-write-predicate.test.ts + localization-currency-lock.test.ts 33/33 (2 new: batch handed frozen with a clear as null; expectTypeOf on SettingsWritePredicateInput['batch'] imported from ./index.js). Whole packages under the lock: plugin-auth 221 files, 3893 passed / 10 skipped; service-settings 67 files, 1153 passed. typecheck (service-settings, plugin-auth incl. check:test-typecheck) exit 0; built dist/index.d.ts of service-settings carries batch?: Readonly<Record<string, unknown>>. pnpm lint exit 0. Ablations, each anchor 1 -> 0 and restored to the HEAD blob: D no batch passed -> ss 1 red + auth 3 red; E floor ignores batch -> 3 red; F provider counts without creds -> 1 red; G unset key not read as stored -> 2 red; A email refusal void -> 6 red; B Google refusal void -> 2 red; C SSO switch without row -> 1 red.",
      "gates": "dispatch-gates --commands on 0b89bdf1: 73 derived (72 + check:package-naming from the service-settings path), run after a full turbo build, exit codes recorded before any pipe (/mnt/project-files/server-seat2/gates-30202-ran-v2.txt): 73 exit 0; --ran: 73 derived, 73 run, 0 NOT-MEASURED, 0 UNRUN. CI check-runs on the new head: not yet read at report time.",
      "dogfood_battery": "0b89bdf1 · os-verify-lock VERDICT command-exit 1 (held 2157s, gate loop running beside it) · Test Files 418 passed / 1 failed / 3 skipped (422) · Tests 3488 passed / 4 failed / 36 skipped (3528) · failures: package-commit-revert-path-package.dogfood.test.ts #29390 and #29414 x3, each 'Test timed out in 5000ms'; the file alone under the lock on the same head: 12/12, VERDICT command-exit 0.",
      "line_budget": "8 files, +886 / -3 against origin/main: sign-in-method-floor.ts +376, sign-in-method-floor.test.ts +418, auth-plugin.ts +28, settings-service.ts +8/-1, settings-service.types.ts +9, settings-write-predicate.test.ts +20/-2, changeset +22, scripts/engine-double-contract.pinned.json +5",
      "files_changed": [
        "packages/plugins/plugin-auth/src/sign-in-method-floor.ts",
        "packages/plugins/plugin-auth/src/sign-in-method-floor.test.ts",
        "packages/plugins/plugin-auth/src/auth-plugin.ts",
        "packages/services/service-settings/src/settings-service.ts",
        "packages/services/service-settings/src/settings-service.types.ts",
        "packages/services/service-settings/src/settings-write-predicate.test.ts",
        ".changeset/30202-keep-a-sign-in-method.md",
        "scripts/engine-double-contract.pinned.json"
      ],
      "deviations": [
        "packages/services/service-settings/** is outside the dispatch's file surface; the seat's ruling B asked for it.",
        "scripts/engine-double-contract.pinned.json: one ledger row (accepted by the seat).",
        "The PR body is not rewritten (a dev writes it once, at pr_create). The full corrected body is at /mnt/project-files/server-seat2/pr-30202-body-v2.md for the seat to write: Clause-② yes (narrowing), 维护者速读, What changes, Tests, Acceptance notes and Verification all change.",
        "Pushed on main 977be380; main has 3 commits since that touch none of the changed paths (trial merge clean), not merged again.",
        "The harness's assign / review-request step was skipped, as the order says."
      ],
      "mcp_calls": "5 in this thread, all reads, none since the first report: issue_read get #30202, issue_read get_comments #30202 (x2), issue_read get #29159, pull_request_read get #30298. No MCP write.",
      "api_writes": "this round 1 — post-stamped (POST /repos/objectstack-ai/objectstack/issues/30202/comments), plus git push (not REST). Whole card: relay pr_create and 2 report comments, plus git pushes.",
      "open_questions": [],
      "security_findings": "none",
      "out_of_scope_findings": [
        "class: a · reach: Setup → Settings → Authentication shows no confirmation before turning off the last sign-in method, and the refusal only as the field error; the login page under os dev was seen drawing the email/password form with better-auth's English error (not reproduced) · evidence: card #30202, ui/packages/auth/src/LoginForm.tsx · dedupe words: login page no sign-in method, setup auth confirmation",
        "carrier: seat (filing) · noted, not filed",
        "class: b · reach: lockout doors that are not settings saves: deleting the last sys_sso_provider row, a redeploy dropping the last provider or plugins.phoneNumber, while email and password is off · evidence: read, not measured · dedupe words: last sso provider delete lockout",
        "carrier: seat (filing) · noted, not filed"
      ]
    }

    Report taken 2026-11-05T07:08Z.


    Generated by Claude Code

  7. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

    Landed · 2026-11-05T08:46Z

    Reading Value
    PR #30298 merged 2026-11-05T08:45Z, merge commit 870ebce3, reviewed head ba1977cb (seat ACCEPT 6652576035)
    Contract review CONTRACT_REVIEW_TIER, narrowing face: FAIL 6650756385 (249ec4c4, per-key judging), delta PASS 6652218570 (0b89bdf1); 0b89bdf1 → ba1977cb seat-read in the ACCEPT
    Merge queue first queue build 41991741368 red on the main-red spec liveness row (query/searchFields, #30367), not this PR's; the PR merged on a later queue build
    #30202 closed by Fixes #30202
    Release @objectstack/plugin-auth and @objectstack/service-settings patch, Clause-②: yes (narrowing), BREAKING banner and ADR-0087 marker; next release

    State change: pm:dispatched removed. Unblocks #30300 (seat 3) and, after #30302 lands, #30312.

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:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions