Skip to content

feat(shell): sign in through the system auth session when a provider refuses the WebView - #190

Draft
hotlong wants to merge 1 commit into
mainfrom
claude/issue-148-system-auth-session
Draft

hotlong wants to merge 1 commit into
mainfrom
claude/issue-148-system-auth-session

Conversation

@hotlong

@hotlong hotlong commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Refs #148. This does not close it: the card's acceptance needs device builds, and the three platform cards below.

Clause-②: no
Metadata-protocol: none

维护者速读

What changes

When a navigation would reach a provider that refuses embedded WebViews (the check sign-in-excursion.ts already had), the shell no longer stops at a notice:

  1. Is it offered? An anonymous GET /api/v1/auth/one-time-token/handoff with this app's callback. The route checks the scheme before the session, so 401 means registered. 400 (unregistered) or 404 (no route) keeps today's notice, and no browser opens.
  2. Auth session. lib/shell/system-sign-in.ts makes an RFC 7636 verifier (32 random bytes, kept in memory) and opens <server>/_console/login?redirect=<hand-off with the S256 challenge> through openAuthSessionAsync, ephemeral on iOS.
  3. Callback. ai.objectos.mobile://auth/callback?token=T, parsed strictly: exactly one token, in the token's alphabet. redirectIncomingPath drops this link, so on Android the token never becomes a router path and the Console underneath stays put.
  4. Redeem. ConsoleWebView holds the token until a Console is ready, then injects a script that POSTs T.verifier to /one-time-token/verify only if location.origin is the server's. The page moves to the Console, or to /login?error=handoff_failed, whose banner already explains a failed sign-in. A token whose connection was switched away from is dropped.

Also: expo-crypto for the random bytes and digest, ai.objectos.mobile registered as a second scheme because TrustedAppSchemeSchema refuses objectos, and docs (HYBRID-SHELL.md "System sign-in", DEPLOYMENT.md, DEVICE-TESTING.md).

The server enforces single use, the one-minute expiry, the interception refusal, the five-minute freshness rule and the cross-site refusal (objectstack one-time-token-handoff.ts). The shell does not repeat them.

Waiting on

Tests

  • pnpm exec jest: 50 suites, 1097 tests passed, before the last formatting-only revert; the ConsoleWebView and lib/shell suites were re-run after it (14 suites, 532 tests passed).
  • tsc --noEmit clean. ESLint clean on the changed source directories and the new test file.
  • New: system-sign-in.test.ts covers the RFC 7636 appendix B vectors, the entry URL, callback refusals, the redeem script run against a stubbed location and fetch (including that it sends nothing from another origin), and the probe. ConsoleWebView.test.tsx covers the session start, one session at a time, a token arriving before and after ready, a connection switch, and each outcome.
  • An iOS Release build compiles and links with the new scheme and module.

Run on a simulator

iOS Release build on an iPhone 17 Pro simulator, fresh install, against a local @objectstack/cli@18.3.0 with the scheme patched in and fake Google client values, so the Google button appears and its navigation is refused. A local proxy stands in for objectstack-ai/objectstack#29971: it answers the Console's mis-prefixed redirect with a 302 to the real hand-off route. The sign-in inside the auth session used the local dev seed admin, not Google.

Case Result
Tap Google in the WebView The navigation is blocked and the auth session opens on the server's sign-in page, with no notice
Sign in there The session closes and the WebView opens the Console as admin@objectos.ai
Close the session The sign-in page stays, with no message
Redemption refused (proxy answers 400) Back on /login, with the Console's banner: 单点登录未能完成…(handoff_failed)
Retry after that failure Signs in
huayan.objectos.ai (18.2.0, route answers 404), Cloud excursion then Google The 应用暂不支持 notice shows and no session opens

Against the same server over HTTP, statuses only: signed-out hand-off 401; hand-off 302 to the app scheme; Sec-Fetch-Site: cross-site 400; T alone 400; T with a guessed verifier 400; then the real T.verifier 200 with a cookie; replay 400.

Not verified

🤖 Generated with Claude Code

…refuses the WebView

Google answers an embedded WebView with disallowed_useragent, so the shell
has been telling those users the sign-in method is not supported. It now opens
the server's sign-in page in the OS auth session and brings the session back
through the server's one-time-token hand-off, bound to a PKCE verifier kept in
memory. The token is redeemed from inside the WebView, on the server's origin
only, so the cookie lands in the WebView's store and the shell keeps nothing.

A server that does not register ai.objectos.mobile keeps the notice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hotlong

hotlong commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

Maintainer ruling (2026-11-04, in chat): the shell redeems the token with the injected script. No Console consumer is added. This settles the first of the three choices listed on #148.

Checked against origin/main for how this holds as the Console upgrades:

  • The script's request uses a published contract: AuthEndpointPaths.oneTimeTokenVerify with its request and response schemas in packages/spec/src/api/auth-endpoints.zod.ts. The spec's docblock already says the WebView redeems.
  • It knows only two Console URLs, /_console/ and /_console/login?error=. The shell already depends on that base path and sign-in route everywhere.
  • Landing on /_console/ passes through the Console's route guards (RequireOrganization), so guards added at route level apply to this sign-in too. Steps that exist only in LoginPage do not run here.

Still open, not ruled: the auth session's entry URL, /_console/login?redirect=<hand-off>, is the part of this flow that does depend on Console behaviour.

@hotlong

hotlong commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

Maintainer ruling (2026-11-04, in chat): the entry moves to the server. This settles the point left open in the comment above. Filed as objectstack-ai/objectstack#29997.

The app will open only the hand-off route in the auth session, and the server sends a signed-out page navigation to its own sign-in page. So signInEntryUrl no longer names the Console's login route or its redirect parameter.

This PR's code does not change yet. It keeps today's entry until #29997 has shipped, because an older server answers the new entry with raw JSON. Then this PR needs:

  • signInEntryUrl drops to the hand-off URL, with its tests and the "System sign-in" sections of docs/HYBRID-SHELL.md and docs/DEPLOYMENT.md.
  • A new simulator run, since the results above went through the old entry and a proxy.

The redeem script and the probe stay as they are.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant