Skip to content

External IdP sign-in from the shell (ASWebAuthenticationSession / Custom Tabs) for providers that refuse embedded WebViews #148

Open

Description

@hotlong

repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv

Ruled: 6593861138 · letter B · 2026-11-03T02:26Z
Blocked-by: objectstack-ai/objectstack#29714
Blocked-by: objectstack-ai/objectstack#29971
Blocked-by: objectstack-ai/objectstack#29972
Blocked-by: objectstack-ai/objectstack#29997
Blocked-by: objectstack-ai/cloud#3549
Blocked-by: #144

Context

Sign-in happens in the Console inside the WebView. An IdP host listed in EXPO_PUBLIC_TRUSTED_HOSTS can load there, and the session cookie lands in the WebView. Some providers refuse embedded WebViews: Google returns disallowed_useragent, and enterprise IdPs may enforce the same. Today those sign-ins open in the system browser, where the resulting cookie never reaches the WebView.

Ask

Design and implement the flow:

  1. The Console starts the social / SSO sign-in.
  2. The shell opens it in ASWebAuthenticationSession / Custom Tabs (expo-web-browser openAuthSessionAsync).
  3. The server redirects to an app-scheme callback carrying a one-time code.
  4. The shell hands the code to the Console.
  5. The Console exchanges the code for a WebView session.

This needs a server endpoint for the one-time code. Check the existing auth plugin routes first, and file the objectstack-ai/objectstack side as its own card if a route is missing. The code must be single-use, short-lived and bound to the initiating session (PKCE-style), so an intercepted callback cannot be replayed.

Acceptance

  • Google sign-in works on iOS and Android device builds.
  • Replaying a callback fails.
  • docs/HYBRID-SHELL.md (Session and auth) is updated.

Note for triage: this touches a security boundary (session transfer). The flow shape belongs in front of the maintainer before dispatch.

Activity

  1. added
    enhancementNew feature or request
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; on a PR: awaiting the maintainer's review
    on Nov 2, 2026
  2. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    Contributor

    repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv

    决策:谷歌这类拒绝内嵌浏览器的登录,怎么把登录态交回 App?

    一句话问题: 用谷歌等账号登录时,对方拒绝在 App 内嵌页面里登录。今天这类用户根本登不进 App。要修,就得设计一条把登录结果从系统浏览器安全交回 App 的通道。卡面自己也注明:流程形状需先呈维护者。

    Governing text:

    • docs/HYBRID-SHELL.md 〈Session and auth〉:"Providers that refuse embedded WebViews (Google) need the ASWebAuthenticationSession flow — tracked as follow-up work"。
    • 车道章程:「会话转移 … 皆属安全边界,恒在人工地板」。
    • SKILL.md 升级条:「修复以猜测定安全边界 ⇒ 决策卡先比对声明式方案」。

    前提(带 re-check):

    • 平台认证插件基于 better-auth 1.7.12。今天它没有启用一次性码类插件(oneTimeToken)或 Expo 移动端插件,只常开 bearer()。re-check(objectstack):git grep -nE "oneTimeToken|@better-auth/expo" origin/main -- packages/plugins/plugin-auth,本席读数为 0 命中。
    • 1.7.12 是否自带上述两个插件:未验证(仅凭对该库的记忆,未读安装包)。
    • 本卡依赖平台先出端点,属跨仓:平台侧另立卡,mobile 侧挂 Blocked-by:。

    选项:

    选项 做什么 客户可感知后果 工作量
    A 照卡面:系统认证会话(ASWebAuthenticationSession / Custom Tabs)+ 平台新增一次性码端点(单次、短时、PKCE 式绑定发起会话),Console 用码换 WebView 会话 谷歌与企业 IdP 都能登录;平台多一个认证端点 objectstack server 车道 1 卡 + mobile 1 PR + Console 消费卡 · 无先例
    B 采用平台认证库(better-auth)的现成一次性码或移动端插件做码交换,不自研端点 同 A;安全参数由成熟实现定 需先实测 1.7.12 是否带该插件;有则 objectstack 一张启用卡 + mobile 1 PR
    C 暂不做:谷歌这类 IdP 在移动端不支持,登录页提示改用邮箱或企业 SSO(内嵌可用的) 部分用户登录受阻 零开发

    业务含义: A 是自己造一把一次性钥匙。B 是用锁匠现成的钥匙。C 是先关这扇门。

    四轴(业务立场):

    • 长远合理性(权重最高):系统认证会话加一次性码,是 iOS 与 Android 官方推荐、主流 App 的通用形态,终态必有。差别只在自研(A)还是复用成熟实现(B)。认证端点越少自研越好。
    • 业务拉动: 未上架,今天无用户撞上。但谷歌登录是常见企业需求,docs/DEPLOYMENT.md 的审核也看登录可用性。拉动中等,不急。
    • 防 AI 犯错: 安全边界最怕 AI 凭猜测定参数(过期时间、绑定方式)。B 的参数由库定,AI 只接线。A 下每个参数都要人审。
    • 创业阶段不扩散: C 零扩散。B 复用已有依赖,扩散最小。A 新增一个自研认证端点,是长期审计义务。
    os-decision-facets
    ① 长远:系统认证会话+一次性码是终态;B 复用成熟实现,A 自研
    ② 拉动:中等,未上架无人撞上,企业谷歌登录是常见需求
    ③ 防错:B 参数由库定,AI 只接线;A 每个安全参数需人审
    ④ 不扩散:C 零;B 复用依赖;A 新自研端点
    Prior rulings read: one-time code|WebAuthenticationSession → 2 hits (#148 itself, #144 rebuild PR); ADR none; thread: none
    

    推荐:B,以「认证库有现成移动端流程」为前提。

    • 前提证伪则回到 A,不许悄悄退回 C。
    • 置信缺口:本席未读安装包,未验证 better-auth 1.7.12 的插件清单。
    • 不取 A:自研认证端点,在有现成实现时不划算。
    • 不取 C:终态必做,推迟只会把同一个问题留给上架后的用户。
    • 自检:只看①选 B;②③④ 是否翻转:否。

    裁后执行: 席位派一个只读读数 dev 实测认证库插件。按结果在 objectstack 立平台端点卡(server 车道),mobile 本卡挂 pm:blocked 加 Blocked-by: 指向它。平台端点进可安装的发行版后,再派本卡。

    Analysis-tier: default (seat tier)

  3. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    Contributor

    repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv

    Ruling: batch #1 item 4 · letter B · maintainer 「同意」 2026-11-03T02:26Z

    The maintainer accepted the batch's recommendations in the session session_01QSeKYnW1Xf6HDSWz1eGAqv. The presentation (item 4) put B as the recommendation, and the reply 「同意」 applies to it.

    Option B (the system auth session plus a one-time code exchange) reuses the platform auth library's existing plugin (better-auth one-time-token or Expo plugin) rather than a hand-written endpoint.

    Premise of this ruling, falsifiable: better-auth 1.7.12, as installed in objectstack packages/plugins/plugin-auth, ships a plugin that issues a single-use, short-lived code a native app can exchange for a session.

    • The seat first dispatches a read-only measurement of that premise.
    • If it holds: an objectstack card enables the plugin (server lane), and this card waits on it with Blocked-by:.
    • ⛔ If it does not hold, report the fork back to the decision box. Do not fall back to C quietly. A is the stated fallback, and it needs its own confirmation.

    Effort for B: 1 read-only measurement, then 1 objectstack enable card and 1 mobile PR.

    This is a session-transfer flow, so the maintainer merges the PR.

  4. added
    pm:queueReady for the PM dispatch loop
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; on a PR: awaiting the maintainer's review
    pm:queueReady for the PM dispatch loop
    on Nov 3, 2026
  5. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    Contributor

    repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv

    Premise of the ruling measured, and it holds. better-auth@1.7.12 ships ./plugins/one-time-token (/one-time-token/generate, /one-time-token/verify, default 3-minute expiry, hashed storage available). It is not enabled in objectstack plugin-auth today.

    Platform card filed: objectstack-ai/objectstack#29714 (server lane; the maintainer merges, because the PR must close callback interception). This card moves pm:queue → pm:blocked with Blocked-by: objectstack-ai/objectstack#29714. It resumes once the plugin ships in an installable platform release.

  6. self-assigned this
    on Nov 3, 2026
  7. hotlong commented on Nov 3, 2026

    @hotlong
    ContributorAuthor

    Claim: maintainer direct order (not a PM loop round), 2026-11-03T16:50Z
    Session: Claude desktop session of the maintainer; order verbatim 「开始做 #148」, after 「148 要看看车道有没有认领」
    Account: hotlong
    Branch: claude/issue-148-system-auth-session
    Worktree: mobile-148
    File surface: lib/shell/, components/ or app/ for the console screen, app.config.ts (the reverse-domain scheme), docs/HYBRID-SHELL.md, locales/, tests, one changeset
    Clause-②: no
    Metadata-protocol: none
    Thread-read: 6593927768
    Premise: holds. @objectstack/plugin-auth@18.3.0 (npm, 2026-11-03T15:11Z) contains the hand-off route, so Blocked-by objectstack-ai/objectstack#29714 has shipped.
    Serial constraints cleared: none in flight on these files; #182 / PR #189 (os-justin) touches locales, so new locale keys rebase over it.

    Lane read before this claim: no assignee, no branch, no PR, and pm:blocked on this card. Lane seat #160 holds #182 only.

    pm:blocked stays on: this is a session-transfer flow, and the flow shape goes to the maintainer, who merges. A readout of the remaining prerequisites follows on this card.

  8. hotlong commented on Nov 3, 2026

    @hotlong
    ContributorAuthor

    Readout after the claim: the shell half is built (draft PR follows). Three platform pieces are still missing, so Google sign-in cannot work end to end yet.

    Blocker status. #29714 shipped in 18.3.0 (npm, 2026-11-03T15:11Z), but that release alone does not make the flow reachable:

    Card Gap How I know
    objectstack-ai/objectstack#29971 The Console's sign-in page cannot reach the hand-off: its redirect lands on 页面不存在 Measured on 18.3.0 in a browser
    objectstack-ai/objectstack#29972 os serve has no setting for trustedAppSchemes Measured: 0 occurrences in the 18.3.0 CLI
    objectstack-ai/cloud#3549 Hosted environments pass no scheme, and registerRoutes: false means the route is not mounted Read from source, not measured

    huayan.objectos.ai is on 18.2.0 and answers 404 on both routes.

    Flow shape, for the maintainer (this card asks for it before work lands):

    1. The WebView asks for a provider that refuses it. The existing check blocks the load and returns to /login.
    2. The shell asks anonymously whether this server registered the app (GET .../handoff: 401 yes, 400 or 404 no). If not, today's notice stays and no browser opens.
    3. It keeps a PKCE verifier in memory and opens <server>/_console/login?redirect=<hand-off with the S256 challenge> in the auth session, ephemeral on iOS. The user chooses the provider again there.
    4. After the sign-in the Console navigates to the hand-off, which answers 302 ai.objectos.mobile://auth/callback?token=T.
    5. The shell injects a script into the WebView that POSTs T.verifier to /one-time-token/verify, only if location.origin is the server's. The cookie lands in the WebView, and the page moves to the Console or to /login?error=handoff_failed.

    Three choices in that shape the card did not specify:

    • The shell redeems by injected script, not through the Console. The card's step 5 says the Console exchanges the code, and the lane charter says the shell does not call platform APIs. The server's docblock allows either. I chose the script because it needs no new bridge method and keeps the verifier out of URLs. The request is same-origin from the page, and the shell's native side holds no cookie or credential. The alternative is a bridge method plus a Console consumer card.
    • The user picks the provider twice, once in the WebView and again in the auth session. The provider's URL cannot be reused: its OAuth state cookie lives in the WebView.
    • A new URL scheme ai.objectos.mobile, because TrustedAppSchemeSchema refuses objectos. That and expo-crypto are native changes, so this needs a new binary.

    Known gap on Android. Custom Tabs share Chrome's cookies and have no ephemeral mode through expo-web-browser, so an old Chrome session reaches the hand-off and gets a 401. That is rule 4 on objectstack-ai/objectstack#29971.

    Not verified yet: the auth session on a simulator, Android, and Google itself. The server half is verified only by its own tests so far.

  9. hotlong commented on Nov 3, 2026

    @hotlong
    ContributorAuthor

    Draft PR: #190.

  10. hotlong commented on Nov 3, 2026

    @hotlong
    ContributorAuthor

    The shell half ran end to end on an iOS simulator, with a local stand-in for objectstack-ai/objectstack#29971. Results are in #190. Google itself, Android and a real device are still open, and the three Blocked-by cards still stand.

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 requestpm:blockedBlocked by another issue/PR — body carries Blocked-by: #N

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions