Skip to content

SSO end-user UX: every refusal says "the sign-in link expired", internal terms reach end users, a refused IdP-initiated sign-in is silent, and Log out leaves the IdP session so the next click signs straight back in #32087

Open

Description

@os-bob

Edition / version

ObjectOS Enterprise, cloud main de8d979 (framework pin e24136c2). Live Okta (OIDC and SAML), browser walk-through for objectstack-ai/cloud#3666.

Findings, in the order a user meets them

  1. "Sign in with SSO" with an empty email shows the raw email, organizationSlug, domain or providerId is required. It should say "Enter your work email to sign in with SSO" (the button reads the password form's email field).
  2. Every SSO refusal starts with the wrong sentence. The login page shows "Single sign-on could not be completed — the sign-in link expired or was already used. Please try again." followed by the cause in parentheses, for:
    • SELF_REGISTRATION_CLOSED (uninvited user, JIT off): the cause text is right but aimed at admins: "(audience posture invite_only) … turn on just-in-time provisioning (allowJit)";
    • account_not_linked;
    • saml_error / Invalid SAML response (certificate mismatch);
    • state_not_found.
      None of these is an expired link, and "try again" only repeats the refusal. Expected: a per-cause sentence for the end user ("Your account isn't set up for this system yet. Ask your administrator to invite you." etc.), with the technical cause kept for admins (logs or a details toggle).
  3. A refused SAML IdP-initiated sign-in is silent. With allowIdpInitiated off, the Okta tile lands on /_console/login with no message. The server logs "SAML IdP-initiated SSO rejected: InResponseTo missing and allowIdpInitiated is false". The page should say that sign-in has to start from this system's login page, or that the admin must allow IdP-initiated sign-in.
  4. Log out does not end the IdP session. After Log out, "Sign in with SSO" signed the same user straight back in with no password prompt (Okta session alive). On a shared computer the next person gets the previous user's account. Expected: RP-initiated logout to the IdP's end_session_endpoint for OIDC (Okta and Entra both publish one) and SAML SLO where configured, or at least an admin setting for it.
  5. IdP-provisioned users: the user record shows "Email Verified: No" although the IdP asserted the email, and offers "Set Password", which gives an SSO-only user a local password that bypasses the IdP. That may be intended; enterprise customers usually want SSO-provisioned users to sign in only through the IdP, or at least a warning on that button.
  6. Edit Sign-In Options renders its two independent checkboxes (Allow IdP-Initiated, Allow JIT) as round, radio-style controls, which read as either/or.

What works well: the IdP-provisioned users are labelled "IdP-Provisioned" in the Users list; JIT, group → permission-set projection and OIDC signing-key rotation (Okta keyRotate) all worked live; the delete dialog explains the consequence for the domain's users.

Activity

  1. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Nov 10, 2026
  2. objectstack-fleet commented on Nov 10, 2026

    @objectstack-fleet
    Contributor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: finding promoted → domain:ui · p1 · pm:queue · 2026-11-10T07:55Z

    ⛔ Not a claim.


    Generated by Claude Code

  3. added
    bugSomething isn't working
    domain:uiDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    priority:p1High: required for production / M2
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Nov 10, 2026
  4. objectstack-fleet commented on Nov 10, 2026

    @objectstack-fleet
    Contributor

    domain:ui#2 · os-support-ai · session_01WGufzge87Pae2h7uSQzX8H

    Reserved by domain:ui#2 · 2026-11-10T08:10Z · SSO end-user UX on the login page and Setup · ⛔ the Claim: follows

    • Taken from pm:queue (triage 6762098073: domain:ui, p1, Enterprise order under pm-dispatch:企业版相关卡的优先级以 cloud ROADMAP.md 为准,席位不另行排序;总监席每周复读并把不一致之处呈维护者(cloud#3835 子项 6) #31574) under the maintainer's 「各开一个 spec ui devx 座位」 (this account's project, 2026-11-09).
    • Scope of this dispatch: items 1, 2, 3 and 6 (the empty-email message, per-cause end-user sentences, the silent IdP-initiated refusal, the checkbox controls). Items 4 and 5 are not built here: triage referred this card to security#1 by number, and whether they need private handling is that lane's call; the seat files no child for them and says nothing more about them in public. If a UX item turns out to need a server signal, the seat files a domain:server child at that point.
    • The card thread is being opened; the Claim: naming its branch follows within the hour.
    • Thread-read: 6762098073.
  5. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 10, 2026
  6. objectstack-fleet commented on Nov 10, 2026

    @objectstack-fleet
    Contributor

    domain:ui#2 · os-support-ai · session_01WGufzge87Pae2h7uSQzX8H

    Claim: PM loop round 1 · 2026-11-10T08:11Z
    Session: session_01WGufzfoxBMQZZCvgX8SGhb (card thread, dev) · seat session_01WGufzge87Pae2h7uSQzX8H
    Account: os-support-ai
    Branch: claude/project-thread-m23m40
    Worktree: objectstack-issue-32087
    Domain: domain:ui
    Seat: domain:ui#2
    File surface: ui/apps/console/src/pages/auth/LoginPage.tsx and its tests; ui/packages/app-shell/src/console/auth/** (the package login page and its refusal mapping) as far as the shared mapping needs; ui/packages/auth/src/LoginForm.tsx and its tests; ui/packages/i18n/src/locales/**; the renderer that draws the Edit Sign-In Options booleans (located by the dev, reported before building if outside ui/); root .changeset/; ⛔ no server code, no packages/spec/**
    Container & model: M, mode:thread, model: default tier (dispatch-gates --tier: "no path-derived mandate"), effort: high
    Clause-②: yes (widening): new keys in the published @object-ui/i18n catalogue
    Metadata-protocol: none
    Responsibility: ui/apps/console/src/pages/auth/LoginPage.tsx:366-374 (objectstack) prints one "link expired" sentence plus the raw cause for every SSO refusal | no platform path covers it: the server sends codes, the wording is the console's | every end user of an SSO-enabled Enterprise deployment, today
    Thread-read: 6762098073
    Premise: holds at origin/main fe1249b7: auth.login.errors.oauthCallbackFailed is the only SSO callback sentence on the console login page
    Serial constraints cleared: PR #32050 (#32021, this seat) edits the console and package LoginPage.tsx, signInRefusalMessages.ts and console LoginPage.* tests; this card builds on main away from its regions and merges main after #32050 lands (landed first). No other open PR names these files.

    Scope: items 1, 2, 3 and 6 of #32087; Part of #32087. Items 4 and 5 stay with the lane triage referred them to. Order: /mnt/project-files/ui-orders/32087.md. Summons: the maintainer's 「各开一个 spec ui devx 座位」 (this project, 2026-11-09).

  7. objectstack-fleet commented on Nov 10, 2026

    @objectstack-fleet
    Contributor

    domain:ui#2 · os-support-ai · session_01WGufzfoxBMQZZCvgX8SGhb
    os-dev-report
    {
    "issue": 32087,
    "status": "done",
    "branch": "claude/project-thread-m23m40",
    "pr": "#32129",
    "head": "dc0a73f3",
    "session": "session_01WGufzfoxBMQZZCvgX8SGhb",
    "premise_still_valid": true,
    "summary": "Items 1, 2 and 6 ship whole; item 3 ships for OIDC (state_not_found) and is a reported deviation for SAML, so the PR says Part of #32087. (1) LoginForm refuses an empty or whitespace-only email before any SSO request with the new optional label ssoEmailRequiredText, which both login pages pass. (2) ssoCallbackRefusal.ts maps SELF_REGISTRATION_CLOSED, account_not_linked, saml_error / invalid_saml_response and state_not_found to their own sentences, the generic oauthCallbackFailed only for anything else; code and description sit behind a Technical details toggle. DefaultLoginPage reads no ?error, so one reader. (6) The 16px Radix checkbox drew rounded-sm at 10px (theme --radius 0.875rem); console index.css sets --radius-sm 0.25rem on [role=checkbox] only, measured live 10px before, 4px after. No packages/** metadata moved.",
    "line_budget": "22 files, +494 / -19 against origin/main (code: LoginForm.tsx, ssoCallbackRefusal.ts new, two LoginPage.tsx, index.css; 11 locale packs; 5 test files; 1 changeset)",
    "files_changed": [
    ".changeset/32087-sso-login-refusal-sentences.md",
    "ui/apps/console/src/index-css-scan-excludes-tests.test.ts",
    "ui/apps/console/src/index.css",
    "ui/apps/console/src/pages/auth/LoginPage.tsx",
    "ui/apps/console/src/pages/auth/tests/LoginPage.error-banner.test.tsx",
    "ui/apps/console/src/pages/auth/ssoCallbackRefusal.ts",
    "ui/packages/app-shell/src/console/auth/LoginPage.tsx",
    "ui/packages/app-shell/src/views/ActionParamDialog.test.tsx",
    "ui/packages/auth/src/LoginForm.tsx",
    "ui/packages/auth/src/tests/LoginForm.test.tsx",
    "ui/packages/i18n/src/tests/de-quote-pairing-3876.test.ts",
    "ui/packages/i18n/src/locales/{ar,de,en,es,fr,ja,ko,pt-BR,ru,zh-Hans,zh-Hant}.ts"
    ],
    "tests": "Under os-verify-lock at dc0a73f: vitest over packages/auth, packages/i18n, console pages/auth, console index-css test, app-shell console/auth, app-shell ActionParamDialog and the ADR-0060 product-neutral-renderer ratchet: 213 files passed, 2871 tests passed, 13 skipped, exit 0. type-check auth, i18n, app-shell, console: all exit 0. eslint over every changed file exit 0. Ablations via scripts/ablation-replace.mjs, each restored with git diff HEAD empty: item 2 generic banner back 9 failed / 1 passed; item 1 empty-email guard removed 4 failed / 27 passed; item 6 --radius-sm line deleted 1 failed / 3 passed. Browser: dev boot with OS_SSO_ENABLED=true and one SAML provider, en and zh-CN, 11 shots plus measured landings in /mnt/project-files/ui-orders/32087-shots/.",
    "gates": "dispatch-gates --commands at dc0a73f: 38 lines, all under os-verify-lock, 38 exit 0; --ran: 38 derived, 38 run, 0 NOT-MEASURED. check-sdui-manifest exit 0 (in the 38). pnpm --dir ui run check:spec-symbols exit 0; check:new-line-citations exit 0; check:nul-bytes exit 0. First sweep on be5bf7c caught 2 i18n pins (zh-Hant generator output, de quote-pair count), fixed in dc0a73f. CI check-runs on #32129 not yet read (PR just opened).",
    "dogfood_battery": "n/a: no ! in the PR title and Clause-② is widening",
    "deviations": [
    "item 3 SAML: an IdP-initiated SAML refusal answers 302 to the site root with ?error=unsolicited_response, and the root redirect (packages/cli/src/utils/console.ts) sends /_console/ without the query, so the login page gets no signal. Per the order no server code changed; seat to file a domain:server child (land the refusal on the login page, or keep the query through the root redirect). UI side is then one row for unsolicited_response in ssoCallbackRefusal.ts.",
    "harness asked for a PR assignee and reviewer via MCP; skipped per os-dev rule 2 and the order (no assignee, no reviewer, no MCP writes)."
    ],
    "mcp_calls": "read-only only (issue and claim reads on #32087); no write tool.",
    "api_writes": "2: POST dispatches (relay pr_create, read-back identical, #32129) and this os-dev-report comment via scripts/pm/post-stamped.mjs. Plus 3 git pushes of the branch (52043d2, be5bf7c merge of main, dc0a73f).",
    "open_questions": [],
    "security_findings": "none",
    "out_of_scope_findings": [
    "class: a · reach: exception: maintainer (the order names this domain:server child) · a refused IdP-initiated SAML sign-in lands on / with ?error=unsolicited_response and the root redirect to /_console/ drops the query, so the user sees no message; ask: land it on the login page like the other SAML refusals, or keep the query through the redirect · dedupe: SAML IdP-initiated refusal landing, root redirect query drop"
    ]
    }

    Stamp: 2026-11-10T09:50Z


    Generated by Claude Code

  8. objectstack-fleet commented on Nov 10, 2026

    @objectstack-fleet
    Contributor

    domain:ui#2 · os-support-ai · session_01WGufzge87Pae2h7uSQzX8H

    Landed (part) · 2026-11-10T11:01Z

    Blocked-by: #32132

    Reading Value
    PR #32129 MERGED as cc8c4b0f4e67fcff96a6d685096c2003a460167d (Part of #32087)
    Delivered item 1 (empty email + "Sign in with SSO" asks for the email, no request), item 2 (per-cause end-user sentences, technical cause behind a toggle), item 3 for OIDC (state_not_found), item 6 (checkboxes keep a small corner)

    Remaining, and whose:

    pm:dispatched → pm:blocked, assignee cleared.

  9. 25 remaining items

  10. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    security#2 · os-tesla · session_01L9qibwMmGJKaQ52CgHqNu2

    待维护者裁决:第 4、5 项 · 2026-11-13T06:10Z

    本卡余下第 4、5 项,两项都要改产品行为,呈维护者裁决。分析经两轮隔离契约档复核(第 4 项 PASS;第 5 项的字母未被推翻,复核意见已吸收)。读数基于 main 83bd36de,静态阅读,未实机。

    第 4 项:退出本平台后,再点 SSO 登录会被直接放行

    Governing text:none found(ADR-0135 与 content/docs/permissions/sso.mdx 无退出条款);相关裁决 #30967 评论 6718355623(按提供方开关、默认关)。

    选项 做什么 客户可感知后果 工作量
    A(推荐) 按提供方开关「退出时同时退出公司登录系统」,默认关 SAML 单点登出 + OIDC 登出跳转;新增 IdP 登出地址字段;接收 IdP 登出须验签 管理员按需开;开了之后退出本应用会同时退出公司登录系统下的其它应用 server + ui + spec 描述,≥ 先例 #31448(+1196/−25)· 2–3 PR
    B 同 A,默认开 同 A 现有客户每次退出都被踢出整个公司会话 同 A
    C 只写文档 说明共用电脑须同时退出公司登录系统 风险留给客户 IT docs · 1 PR
    D 按提供方开关「每次登录都重新输密码」 SAML ForceAuthn / OIDC prompt=login 共用电脑问题解决,但每次都要输密码 server + ui · 1–2 PR

    四棱:① 长远:主流服务方的单点登出是按连接、可选、默认关(Salesforce SAML 设置的 Single Logout),也有不少 SaaS 不提供(C 是常见选择);两年后形态是 A。② 业务:拉动仅一次内部对接测试,弱。③ 防错:开了必须两种协议都兑现,缺登出端点/地址时保存即拒;接收登出必须验签。④ 不扩散:A 工作量约为 C 的 3 倍以上,可先 C 后 A。
    只看①选 A;②③④ 是否翻转:否(只影响时序)。

    第 5 项:公司登录系统开通的员工,仍可能拿到本地密码

    Governing text:ADR-0135 D5.2(受管身份不持有本地凭据;break-glass 条款保留初始密码与管理员代设)、spec AuthConfigSchema.ssoOnlyMode 描述(SSO-only 下受管用户不持有本地凭据,环境所有者保留应急密码)。

    选项 做什么 客户可感知后果 工作量
    A 现状 + 管理员设密码按钮加警告 ui 文案 只有提示,行为不变 ui · 1 PR
    D(推荐) 兑现已有的「仅 SSO 登录」开关 开了 SSO-only 时,服务端在每一处拒绝给受管员工生成本地密码,唯受管的平台管理员可给自己留应急密码(与云端现行一致);没开则维持现状并加 A 的警告 客户自己决定;开了就真的生效 server + ui · 1–2 PR(先例 #25399,+799/−7)+ spec 描述 + ADR-0135 D5.2 补注(Tier H,同步 cloud ADR-0024)
    E 云托管规则搬到所有部署 受管员工不能被代设或自设密码,平台管理员除外 所有部署同规则,不论是否 SSO-only server · 1 PR
    B 任何部署、任何入口一律不许 比云端更严,应急只走环境原生管理员 全员受管的环境须先建本地管理员 server + ui + 两份 ADR 修订

    四棱:① 长远:主流是「可选设置 + 按用户权限」(Salesforce「禁止用平台凭据登录」+「Is Single Sign-On Enabled」,管理员通常保留密码);D 就是这个形状,并把 spec 已声明的开关变成强制。② 业务:拉动仅一次内部测试,报告人最低诉求是警告,弱。③ 防错:D/E/B 都是服务端响亮拒绝,A 只是界面提示。④ 不扩散:D 不新增配置面。
    只看①选 D;②③④ 是否翻转:否。
    置信缺口:SSO-only 默认关,默认部署仍是现状(只多警告);D 不追溯已持有本地密码的员工。

    Related: #31448, #25399, #30967, #32669, #32188, #32115.

  11. added
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Nov 13, 2026
  12. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    security#2 · os-tesla · session_01L9qibwMmGJKaQ52CgHqNu2

    Maintainer rulings: items 4 and 5 · 2026-11-13T22:19Z

    The maintainer answered both decision cards of 6845651657 in the seat thread (2026-11-13 22:19Z).

    Item 4 → A: a per-provider "sign out of the company sign-in too" switch, off by default.

    • Each SSO provider gets its own switch; nothing changes until an administrator turns it on.
    • With it on, signing out of the platform also ends the company sign-in session, for SAML and OIDC alike.
    • SAML: the provider stores its identity provider's logout address, and logout messages coming back from the identity provider are accepted only when signed.
    • Saving the switch on a provider that cannot honour it (an OIDC provider with no logout endpoint, a SAML provider with no logout address) is refused loudly.
    • Work: a server card (switch, SAML and OIDC wiring, signature requirement, logout address field, spec description) and a ui card (form item, sign-out redirect).

    Item 5 → D: honour the existing SSO-only switch.

    • When an environment has SSO-only mode on, the server refuses to give an IdP-provisioned user a local password at every door. The one exception is a managed platform administrator setting their own break-glass password, the same exception the cloud-hosted rule already has.
    • When SSO-only mode is off, behaviour is unchanged, and the administrator's set-password action warns that it gives a managed user a way to sign in that bypasses the company sign-in.
    • Not retroactive: managed users who already hold a local password are a separate question.
    • Work: a server card (the refusal, the exception, pins, the ssoOnlyMode description), a governed ADR-0135 D5.2 note (kept in step with cloud ADR-0024 D5.2), and a ui card (the warning).

    The security seat orders the server cards first; the ui cards follow each server half.

  13. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    security#2 · os-tesla · session_01L9qibwMmGJKaQ52CgHqNu2

    Claim: PM loop round 4
    Session: session_01L9qibwgMDk9yn3MZdGMqEM (card thread), dispatched by session_01L9qibwMmGJKaQ52CgHqNu2
    Account: os-tesla
    Branch: claude/project-thread-zvdozk (objectstack-ai/objectstack)
    Worktree: objectstack-sso-only-local-password
    Domain: none (security lane; ruling 6866334319 on this card, item 5)
    Seat: security#2
    File surface: to be named after the read-only measurement (every door that can give a user a local password, the SSO-only check, the ssoOnlyMode description); amended by the seat before any edit
    Container & model: M (to measure), mode:thread, model: default tier, effort: xhigh (security, fleet rule 2)
    Clause-②: no (expected; the ssoOnlyMode description text changes, so the seat reviews it)
    Metadata-protocol: none
    Responsibility: an environment in SSO-only mode gives a managed user no local password | every local-password door, with the managed platform administrator's own break-glass exception | every SSO-only deployment
    Thread-read: 6866334319
    Premise: the maintainer ruled items 4 and 5 (6866334319); this is the server half of item 5, and the ui half follows it
    Serial constraints cleared: none (item 4 touches sign-out, not password doors)

  14. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Nov 13, 2026
  15. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    security#2 · os-tesla · session_01L9qibwMmGJKaQ52CgHqNu2

    Claim: PM loop round 5
    Session: session_01L9qibz44EJYWFvzJTmGHkg (card thread), dispatched by session_01L9qibwMmGJKaQ52CgHqNu2
    Account: os-tesla
    Branch: claude/project-thread-ur0eh0 (objectstack-ai/objectstack)
    Worktree: objectstack-sso-provider-sign-out
    Domain: none (security lane; ruling 6866334319 on this card, item 4)
    Seat: security#2
    File surface: to be named after the read-only measurement (the sign-out path, SSO provider config and its schema); amended by the seat before any edit
    Container & model: L (to measure), mode:thread, model: default tier, effort: xhigh (security, fleet rule 2)
    Clause-②: yes (expected; widening: a per-provider switch and a logout address; the seat gives the go on the field shapes and runs the contract review)
    Metadata-protocol: to measure
    Responsibility: signing out can also end the company sign-in, per provider, off by default | SSO provider config and sign-out for SAML and OIDC | deployments that turn the switch on
    Thread-read: 6866334319
    Premise: the maintainer ruled items 4 and 5 (6866334319); this is the server half of item 4, and the ui half follows it
    Serial constraints cleared: none (item 5 touches password doors, not sign-out)

  16. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    security#2 · os-tesla · session_01L9qibwMmGJKaQ52CgHqNu2

    Claim 6866398944 amended · 2026-11-13T22:26Z: surface named from the card thread's measurement (objectstack 84c66d36). No packages/spec change.

    File surface: packages/platform-objects/src/identity/sys-sso-provider.object.ts (a per-provider sign-out switch column, an action param, the list column) and the SSO provider actions that take the SAML logout address; packages/plugins/plugin-auth/src/ (SSO sign-in options, SAML and OIDC provider bridges, sign-out, OIDC callback) and their tests; generated references; root .changeset/
    Clause-②: yes (widening: a per-provider switch, off by default, and an optional logout address param)
    Metadata-protocol: added

  17. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    security#2 · os-tesla · session_01L9qibwMmGJKaQ52CgHqNu2

    Claim 6866387589 amended · 2026-11-13T22:27Z: surface named from the card thread's measurement (objectstack 84c66d36): four doors that can give a user a local password.

    File surface: packages/plugins/plugin-auth/src/ (the set-initial-password, administrator set-password and both reset-password doors, one shared SSO-only judge beside the cloud-managed one) and their tests; packages/spec/src/system/auth-config.zod.ts (the ssoOnlyMode description text only) and its generated reference; root .changeset/
    Clause-②: no
    Metadata-protocol: none

  18. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    security#2 · os-tesla · session_01L9qibz44EJYWFvzJTmGHkg
    os-dev-report

    {
      "issue": 32087,
      "status": "done",
      "branch": "claude/project-thread-ur0eh0",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/33170",
      "session": "session_01L9qibz44EJYWFvzJTmGHkg",
      "premise_still_valid": true,
      "summary": "Item 4: a per-provider switch sys_sso_provider.sign_out_of_idp (off by default, trackHistory), taken by the vendor's /sso/register and /sso/update-provider as signOutOfIdp, with the Edit Sign-In Options param and the list column. On, sign-out also ends the identity provider session: SAML through the vendor's single logout, with enableSingleLogout read per provider and logout messages accepted only when signed; the IdP logout address is the new logoutUrl on the register and edit SAML doors, stored as idpMetadata.singleLogoutService (no new column). OIDC through an RP-initiated logout URL carrying id_token_hint, the end_session_endpoint read from discovery when the switch is saved on and kept in oidc_config. Saving it on is refused 400 VALIDATION_FAILED on signOutOfIdp for a provider that cannot honour it. Surface grep for ruling|Ruling-ref|maintainer|⛔: 14 added lines, every one a code comment citing ruling record 6866334319 or the existing 6718355623 (the file convention); no maintainer or ⛔ hit.",
      "tests": "New pins: sso-saml-refusal-landing.test.ts (6: switch off keeps the IdP logout endpoint closed and the session stands; on, sign-out answers the IdP logout URL and the session ends; unsigned LogoutRequest and unsigned LogoutResponse each refused; signed redirect-binding message refused; a signed LogoutRequest ends only the named session; SP metadata advertises SingleLogoutService only when on), sso-oidc-refusal-cause.test.ts (3: control off, on with id_token_hint/client_id/post_logout_redirect_uri/state and Location, untrusted callbackURL not passed), sso-provider-lifecycle.test.ts (5: save-time refusals through /sso/update-provider for SAML and OIDC, saving off never refused, logoutUrl via edit and register doors, OIDC endpoint kept), sso-sign-out-of-idp.test.ts (14 unit). Full suites: plugin-auth Test Files 267 passed, Tests 5072 passed | 10 skipped; platform-objects Test Files 121 passed, Tests 1955 passed. Ablation (tests import src): scripts/ablation-replace.mjs set wantLogoutRequestSigned/wantLogoutResponseSigned false in ssoSamlOptions(), anchor x1 -> x0 and blob 21d3b866 -> 1fa01a29 on disk; the unsigned-message pin and the redirect-binding pin went red (2 failed | 4 passed); restored, blob == HEAD and git diff HEAD empty; green on the final run. typecheck (both packages) exit 0; eslint on changed files exit 0.",
      "gates": "dispatch-gates --commands derived 77; each run with </dev/null under os-verify-lock, exit codes recorded before any pipe. First pass had 4 non-zero: check-plugin-teardown-shape --self-test exit 3 (shallow clone; fixture commit fetched, then 0), check-tenant-audit-census and its --self-test exit 1 (an anonymous adapter type on the new provider-row write; named it SsoProviderRowAdapter, then 0), check:dual-build-cjs-loads exit 3 (dist missing, built by a later gate in the run, then 0). dispatch-gates --ran: 77 derived, 77 run, 0 NOT-MEASURED, 0 UNRUN. CI check-runs on the PR not yet read (draft just opened).",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "mcp_calls": "read-only only: mcp__github__issue_read and mcp__github__pull_request_read (an exact count was not kept across a context reset); no MCP write tool",
      "api_writes": "1 — the relay dispatch POST /repos/objectstack-ai/objectstack/dispatches for pr_create (run 43529268119, read-back identical, assignee os-tesla), plus this comment through post-stamped.mjs; pushes to claude/project-thread-ur0eh0 only",
      "open_questions": [],
      "security_findings": "none",
      "out_of_scope_findings": [
        "carrier: PR #33170 Acceptance notes · noted, not filed — the Console sign-out following the answered url, and any Edit Sign-In Options / SAML form item the form does not render by itself (the later ui card)",
        "carrier: PR #33170 Acceptance notes · noted, not filed — @better-auth/sso 1.7.12 limits measured: a signed HTTP-Redirect logout message is refused (fail closed), the outgoing LogoutRequest carries no RelayState, only sessions started while the switch is on are covered, the shared OIDC callback without a provider id is not recorded, an IdP-started logout is not written as a logout audit row"
      ]
    }

    Generated by Claude Code

  19. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    Contributor

    security#2 · os-tesla · session_01L9qibwgMDk9yn3MZdGMqEM
    os-dev-report

    Stamp: 2026-11-14T00:07Z

    {
      "issue": 32087,
      "status": "done",
      "branch": "claude/project-thread-zvdozk",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/33177",
      "head": "f3393c48",
      "session": "session_01L9qibwgMDk9yn3MZdGMqEM",
      "premise_still_valid": true,
      "summary": "Item 5 server half (ruling record 6866334319, D). With SSO-only mode on, four doors give an idp_provisioned user no local password: set-initial-password, admin/set-user-password (sys_user#set_user_password), /reset-password and /phone-number/reset-password; each answers 409 RESOURCE_CONFLICT naming SSO-only mode and writes nothing. Exception: a platform administrator setting their own (session isPlatformAdminUser on set-initial; actor = target behind gateAdmin on set-user-password; isPlatformAdminUserId on the resets). One shared judge in plugin-auth sso-only-local-password.ts reuses the cloud-managed source read (readUserSource, moved there) and fails closed as 503. The reset doors judge only a live token or a matching in-budget code, read without spending it. ssoOnlyMode TSDoc + describe say what the server enforces (generated reference regenerated); authentication.mdx gains a short section. Not doors (unchanged): change-password, sign-up, create/import, SCIM, unmounted email-OTP reset.",
      "door_exception_table": [
        "set-initial-password: SSO-only + idp_provisioned caller -> 409; own platform admin (session isPlatformAdminUser) -> allowed; env_native/anonymous -> unchanged",
        "admin/set-user-password: SSO-only + idp_provisioned target -> 409 (another administrator as target included); target = caller (gateAdmin admits platform admins only) -> allowed; env_native -> unchanged; cloud-managed answers first",
        "reset-password (emailed): SSO-only + idp_provisioned (holder included) -> 409; platform admin own (isPlatformAdminUserId) -> allowed; unmatched token -> same answer as SSO-only off",
        "phone-number/reset-password: same as emailed; wrong code -> same answer as SSO-only off"
      ],
      "tests": "At f3393c48 under os-verify-lock: plugin-auth whole package vitest 267/267 files, 5072 passed, 10 skipped, exit 0; pins in sso-only-local-password.test.ts (new), cloud-managed.test.ts and password-reset-ends-sessions.test.ts (new top-level describes in the shared harnesses), 77 tests; plus api-error-code.pin + request-json-body-refusal, 157 total in those 5 files. plugin-auth typecheck (with check:test-typecheck) and spec typecheck exit 0 after a full declaration build. Ablation via scripts/ablation-replace.mjs on the committed judge (refuse verdict -> allow, anchor 1 -> 0): 9 failed / 68 passed across the three pin files, restored by the tool, blob equals HEAD, git diff HEAD empty. One existing pin reversed by the ruling (cloud-managed.test.ts 'unmanaged with SSO-only on, both doors answer the idp user as before') now asserts the SSO-only refusal answers there, with a new SSO-only-off control leg.",
      "gates": "dispatch-gates --commands at f3393c48: 120 derived, all run with </dev/null and exit codes recorded first, 120 exit 0; --ran: 120 derived, 120 run, 0 NOT-MEASURED, 0 UNRUN (list: /mnt/project-files/security-seat-2/r2026-11-13/out/32087-i5-gates-ran-f3393c48.list). spec check:generated: check:docs stale, regenerated with --fix. First sweep found check:dispatcher-error-vocabulary unresolved-constant on the refusal code alias (fixed: literal + equality pin) and api-error-code.pin on a non-literal APIError body (fixed). eslint over changed .ts exit 0. CI check-runs on #33177 not yet read (PR just opened).",
      "dogfood_battery": "n/a: no ! in the PR title and Clause-② is no with no (narrowing) arm",
      "ruling_grep": "Surface grep for ruling|Ruling-ref|maintainer|⛔ in the touched files: cloud-managed.ts:7 (cloud ADR-0024 D5.2 / ADR-0135 D5.2 mirror; consistent, untouched); admin-user-endpoints.ts:1073-1075 (ruling C-prime item 2, the forced reset's revocation default; the SSO-only refusal answers before it, untouched); auth-manager-before-hook.ts:420-424, 538, 553, 678 (other doors' rulings, untouched); auth-plugin.ts and auth-manager.ts hits none adjacent to the edits; auth-config.zod.ts hits concern audience posture, not ssoOnlyMode. No hit contradicts this change.",
      "deviations": [
        "The card's newest Claim: line is the sibling item-4 claim (branch ur0eh0), not 6866387589 naming this branch; the seat confirmed in the seat thread that this branch may push.",
        "The harness asked for an assignee and reviewer via MCP; skipped per the frame and os-dev rule 2. The only assignee write is assignees [os-tesla] on the relay pr_create; read-back lists os-tesla.",
        "Merged origin/main once before opening (one docs-only commit); no rebuild needed for it."
      ],
      "remainders": [
        "ui card: the administrator set-password warning",
        "governed ADR-0135 D5.2 note, kept in step with cloud ADR-0024 D5.2 (seat)",
        "the #32115 local-password remainder and the question of managed users who already hold a local password"
      ],
      "mcp_calls": "0",
      "api_writes": "2: POST relay pr_create (#33177, read-back identical, 11649 bytes) and this os-dev-report comment via post-stamped. Plus git pushes of the branch.",
      "open_questions": [],
      "security_findings": "withheld",
      "out_of_scope_findings": []
    }
  20. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    Contributor

    security#2 · os-tesla · session_01L9qibwMmGJKaQ52CgHqNu2

    Claim 6866387589 amended again · 2026-11-14T01:15Z: the declaration follows contract review 6868317289. With SSO-only on, the local-password doors now refuse a managed user where they answered before, and two additive members widen the surface. The surface is unchanged.

    Clause-②: yes (narrowing)
    Metadata-protocol: none

  21. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    Contributor

    director#1 · hotlong · session_f4c42f815d5e47b39de5e9fb0d6d7864

    2026-11-14T04:35Z One more measured case: EMAIL_DOMAIN_NOT_ALLOWED still shows the generic "link expired" headline, on 18.4.0.

    Where. Staging test1016 os-4e4qa4, Enterprise plan, cloud PR #4415 head 153d10ca (objectstack 18.4.0). The provider is a real Okta OIDC app (okta, domain objectlab.ai verified, JIT on).

    What happened. An Okta user whose email is qa.sub@dev.objectlab.ai (a subdomain, so unlisted) used the login page's 「Sign in with SSO」.

    • The server refused it correctly. The container logged [sso] account creation refused: the address is not at a domain the provider has proven {"code":"EMAIL_DOMAIN_NOT_ALLOWED","providerId":"okta","method":"sso-oidc"}, and no account was created.
    • The callback lands on /_console/login?error=EMAIL_DOMAIN_NOT_ALLOWED&error_description=With SSO domain verification on, an identity provider creates an account only for an email address at a domain it has proven. Nothing was created.
    • The banner reads "Single sign-on could not be completed — the sign-in link expired or was already used. Please try again." The real code appears only behind 「Technical details」.

    Why. ui/apps/console/src/pages/auth/ssoCallbackRefusal.ts has its own sentences for SELF_REGISTRATION_CLOSED, account_not_linked, saml_error / invalid_saml_response and state_not_found. Every other code falls back to oauthCallbackFailed. EMAIL_DOMAIN_NOT_ALLOWED already has an end-user sentence on the sign-up side (signUpRefusalMessages.ts:65, auth.register.errors.emailDomainNotAllowed), but the SSO callback mapping does not use it.

    Impact. "Please try again" sends the person into a loop that can never succeed. The refusal is about their email domain, which only their administrator can change.

    Suggested sentence (the decision on wording stays with this card): something like "Your email address isn't at a domain this sign-in is set up for. Ask your administrator."

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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions