Skip to content

External sign-in bar: let enterprise IdP pages load in the WebView during sign-in, behind a native bar showing the IdP's domain #155

Closed

Description

@hotlong

repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv

Blocked-by: #152

Context

Design screen 3. A private server's Console may sign in through an enterprise IdP (OIDC or SAML) whose host is not known to the shell in advance. Today the navigation policy sends any off-server top-frame page to the system browser, so the IdP session never reaches the WebView. EXPO_PUBLIC_TRUSTED_HOSTS only helps white-label builds that know the host at build time.

Ask

  1. Sign-in excursion mode.
    • A top-frame navigation away from the current server is admitted only when the excursion started from the server's sign-in surface: the Console /login route, or /api/v1/auth/*.
    • The excursion lasts until the WebView returns to the server's origin, or the user cancels.
    • Outside an excursion, the existing policy holds unchanged.
  2. Native bar. While on an off-server host, a native top bar shows a lock and the exact host, plus "正在外部页面登录 · " and a 取消 action. Cancel returns to the server's /login.
  3. Limits.
    • Only https: hosts are admitted.
    • Never admit hosts on the bridge's trust list; the bridge never answers an excursion page.
    • Cap the excursion: after N redirects, or after a time limit, end it and show an error. Choose N and the limit in the PR.
  4. IdPs that refuse embedded WebViews (e.g. Google) stay on the system-auth-session path of External IdP sign-in from the shell (ASWebAuthenticationSession / Custom Tabs) for providers that refuse embedded WebViews #148. Detect that refusal where feasible and route there.

Security boundary: this admits new top-frame origins. Per the charter it stays on the human floor; the maintainer merges and reviews the threat model in the PR (phishing via a crafted login redirect, an excursion that never returns).

Acceptance

  • Unit tests cover the excursion state machine: start only from the sign-in surface, end on return, cancel, http refused, cap reached, and the bridge never trusting an excursion origin.
  • Manual run against a test OIDC provider on a development build.

Activity

  1. added
    enhancementNew feature or request
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    on Nov 2, 2026
  2. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    Contributor

    repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv

    Gate #150 released. This card moves to pm:blocked on #152, type enhancement.

    When #152 lands, this card goes to the maintainer before any dispatch. Letting IdP pages load in the WebView admits a new origin to the navigation policy, which the lane charter puts on the human floor.

  3. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    on Nov 2, 2026
  4. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    Contributor

    repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv

    Unblocked: Blocked-by #152 merged (678fbe9).

    Claim: PM loop round 1
    Session: session_01QSeKYnW1Xf6HDSWz1eGAqv
    Account: os-justin
    Branch: claude/issue-155-idp-excursion
    Worktree: mobile-issue-155
    Domain: n/a — single-lane repo (repo:mobile)
    Seat: repo:mobile#1
    File surface: lib/shell/navigation-policy.ts, a new excursion state machine under lib/shell/, components/shell/ConsoleWebView.tsx (excursion wiring, through the load state machine), a native bar component under components/shell/, lib/shell/bridge/handler.ts (never trust an excursion origin), app/console.tsx, locales/*.json (all ten), __tests__/**, docs/HYBRID-SHELL.md, .changeset/155-idp-excursion.md (stop on breach; explain in the report)
    Container & model: L, mode:subagent, model: default tier (opus) — dispatch-gates --tier --repo objectstack-ai/mobile: no path-derived mandate, effort: xhigh
    Clause-②: no
    Metadata-protocol: none
    Responsibility: n/a — not a defect card
    Thread-read: 6576856193
    Premise: holds. main at 678fbe9 sends every off-server top-frame page to the system browser (lib/shell/navigation-policy.ts), and there is no excursion mode. Security boundary: the maintainer merges this PR and reviews its threat model; the seat does not land it.
    Serial constraints cleared: #174 may touch app/console.tsx (ordinary concurrency). #157 does not touch this surface.

  5. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    Contributor

    repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv
    os-dev-report

    {
      "issue": 155,
      "status": "done",
      "branch": "claude/issue-155-idp-excursion",
      "pr": "https://github.com/objectstack-ai/mobile/pull/176",
      "session": "session_01QSeKYnW1Xf6HDSWz1eGAqv",
      "premise_still_valid": true,
      "summary": "Sign-in excursion mode: when the current server's sign-in surface (Console /login, or /api/v1/auth/*) navigates the top frame off the server, the provider's https pages load in the WebView under a native bar (lock, exact host, 「正在外部页面登录 · SERVER_LABEL」, 取消); it ends on return to the server origin, on 取消 (new WebView at /login), or with a notice after 30 off-server navigations, 10 minutes (better-auth 1.7.12's OAuth state lifetime), an http page, or a provider that refuses embedded WebViews (accounts.google.com or a disallowed_useragent URL: 'not supported in the app yet'). The start is judged on the shell's own native record of the top-frame URL (admitted top-frame requests plus load start/end URLs), never on routeChanged; the bridge is closed during an excursion (handler inExcursion) and the excursion origin is never on its trust list; routes and push tokens are held through the load state machine's reload event and answers in flight are dropped; decideNavigation and console-load.ts are unchanged. PR body carries the Threat model (T1 phishing via crafted login redirect, T2 never-returning excursion, T3 stale reports, T4 same-origin script, T5 Android sub-frames) with mitigation and pinning test per row, plus two documented residuals the maintainer reviews at merge (OAuth-provider hand-off to a consent-skipped first-party client from /login loads under the bar; iOS back-swipe after a return opens the in-app browser). Deviation: the first WIP commit carried the harness's model-named trailer; I amended it to the AGENTS.md model-free pair and pushed with --force-with-lease=claude/issue-155-idp-excursion:381aef1 (all five section 3 conditions held, no PR existed yet).",
      "tests": "Head 0c0706a (objectstack-ai/mobile). npx jest --ci --coverage: exit 0, Test Suites 41 passed / 41, Tests 729 passed / 729 (baseline 678fbe9: 39 / 669). Cold variant npx jest --ci --no-cache __tests__/components/SignInExcursionBar.test.tsx __tests__/components/ConsoleWebView.test.tsx: exit 0, 2 suites, 82 tests. New: __tests__/lib/shell/sign-in-excursion.test.ts, __tests__/components/SignInExcursionBar.test.tsx; extended: navigation-policy.test.ts (isSignInSurface), bridge-handler.test.ts (during a sign-in excursion), ConsoleWebView.test.tsx (sign-in excursion, 21 tests). Ablations at f015c25 (same code, 728 tests), each via objectstack scripts/ablation-replace.mjs WRAP mode (anchor 1 -> 0, blob changed) plus a shell EXIT/INT/TERM trap, restored with git checkout HEAD and proven by blob == HEAD, git diff HEAD empty, porcelain empty. Leg A sign-in-surface start check (drop the isSignInSurface clause): jest exit 1, 7 failed / 728: the-start ×3 (any other server page, another origin, record only from WebView reports), the-end ×2 (no restart after return, current connection only), ConsoleWebView 'sends the same page to the system browser from any other Console page', pre-existing 'applies the navigation policy'. Leg B1 bridge closure (drop if inExcursion return null): 2 failed: bridge-handler 'answers no call and takes no event, even one naming the server's origin', ConsoleWebView 'answers no call while an excursion page is on screen, whatever origin it names'. Leg B2 in-flight answer drop (epoch check): 1 failed: 'drops the answer to a call the Console made before it left for the provider'. Leg B3 trust list widened to any origin: 12 failed incl. new 'never trusts the excursion page's origin, in an excursion or after it' + 11 pre-existing origin tests. Device check on a development build against a test OIDC provider: NOT RUN, NOT MEASURED (no device here), handed to the maintainer in 需要你做的.",
      "gates": "Local, head 0c0706a, exit codes captured before any pipe (recorded in scratchpad issue-155/gate-exit-codes.txt): npx tsc --noEmit exit 0; npx eslint . --ext .ts,.tsx exit 0; npx jest --ci --coverage exit 0 (lock VERDICT command-exit 0); cold component run exit 0. CI on PR #176 head 0c0706a, check-runs read once: 'Code Quality' (ci.yml, runs jest --ci --coverage) completed · success; 'Lint & Type Check' (lint-typecheck.yml, tsc + eslint) completed · success. dispatch-gates.mjs not run: it answers for the objectstack tree only; the mobile gate list is hand-derived from its .github/workflows, as os-dev.md directs for a sister repo. Control-byte self-scan of the 23 changed files: no hits.",
      "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm (Clause-②: no)",
      "mcp_calls": "0 — no MCP GitHub calls of any kind",
      "api_writes": "2 relay/REST writes: (1) pr_create through scripts/pm/fleet-write/dispatch.mjs (relay run 41550124706, POST /repos/objectstack-ai/mobile/pulls, draft, no labels, no assignees; read-back 22014 bytes sent = stored); (2) this os-dev-report comment through scripts/pm/post-stamped.mjs (POST /repos/objectstack-ai/mobile/issues/155/comments). Not REST: git pushes of claude/issue-155-idp-excursion: empty probe, 5 commit pushes, 1 --force-with-lease (trailer amend, see summary).",
      "open_questions": [],
      "security_findings": "none",
      "out_of_scope_findings": [
        "carrier: seat · two pre-existing navigation-policy observations, reported to the maintainer privately"
      ]
    }
  6. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    Contributor

    repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv

    ACCEPT → PR #176 (head 0c0706a). This is a security boundary, so the maintainer reviews the Threat model and merges; the seat does not land it. Next after merge: #158 (docs) waits only on #153.

  7. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    Contributor

    repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv

    Landed. #176 merged through the merge queue. os-zhuang enqueued it after the Threat-model review. The merge_group run passed CI and Lint & Type Check. The card closed completed via Fixes #155, and pm:dispatched is removed.

    Not covered here: IdPs that refuse embedded WebViews still wait for the ruling on #148. #158 (docs) now waits only on #153.

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

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions