feat(android): add Auth Tab login for self-managed front doors - #4536
feat(android): add Auth Tab login for self-managed front doors#4536btli wants to merge 32 commits into
Conversation
|
@btli This PR is a Bug fix, Feature, or UI / frontend change but the Demo section is missing or only contains a placeholder. These change types require a screenshot or screen recording so reviewers can see the new behaviour without checking out the branch. Please update the Demo section with:
Use |
Cross-vendor review complete — 7 roundsThis PR was implemented and reviewed by independent agents of different vendors (implementer never reviewed its own diff; reviewers received only the diff plus an acceptance contract, never the worktree). Final verdict: APPROVE, ready to merge after a real-device check. Blockers found and closed, by round
VerificationAll gates were re-run independently by the orchestrator at each round, not taken from worker reports. At the final head: Android 133 tests / 14 suites / 0 failures / 0 errors / 0 skipped (JDK 21.0.12), native-auth 43 passed, full The merge of current Scope — please read before mergingAuth Tab activates only on:
It does not activate on a direct Outstanding
|
Replace the retired WebView-interception approach for Databricks-hosted (front-door auth proxy) servers with the official androidx.browser Auth Tab surface: when an in-WebView-auth server bounces off-origin to its front door / IdP, the shell opens the server's new GET /auth/native-complete in an Auth Tab, the whole redirect chain runs in a real browser context (IdPs that refuse embedded user-agents, e.g. Google federation, work there), and the server completes back into the app with a fixed omnigent://auth-callback redirect carrying the app's state nonce plus a credential: the proxy-forwarded X-Forwarded-Access-Token in header mode, a minted session JWT in oidc/accounts mode. The app binds the result to the flow that launched it (state nonce + pinned origin) and bootstraps the WebView — Bearer on the main-frame load, or the session cookie via the existing inject path. Devices/browsers without Auth Tab support, dismissed tabs, and servers without the endpoint all fall back to the existing inline in-WebView flow. The credential-bearing redirect is only ever expected through the Auth Tab's Activity-result channel; the omnigent://auth-callback manifest filter (scoped to that host only) exists for the degraded Custom-Tab path, where an unmatched state nonce drops the intent. Also fixes onConfigurationChanged reading the stale cached configuration for system-bar icon polarity. Signed-off-by: Bryan Li <bryan.li@gmail.com> Co-authored-by: omnigent <noreply@omnigent.ai>
…allback Address cross-review findings on the Auth Tab login: - The completion redirect no longer carries a credential. An authenticated /auth/native-complete hit now creates a short-lived (120s), single-use flow record binding the app's state nonce and PKCE S256 challenge to the identity, and redirects only an opaque one-time code. The credential is released by /auth/native-exchange against code + state + code_verifier, with the record popped before any check so a failed guess burns the code; without a valid challenge no flow (and no code) is created at all, so an attacker-driven authenticated browser visit yields nothing exchangeable. There is deliberately no pre-registration endpoint: a front-door proxy 302s every unauthenticated native request, and a public registrar would not authenticate the initiator anyway — PKCE carries the binding. - The exchange has two transports, chosen by the server: a native POST (oidc/accounts — the credential never touches a URL) and a second, silently authenticated Auth Tab hop for header-mode servers behind a front door, the only transport such a proxy leaves open. - Removed the omnigent://auth-callback VIEW trampoline and manifest filter: callbacks are accepted only through the Auth Tab's Activity-result channel, so there is no squattable delivery surface and no VIEW path left to wedge a flow. On the launcher path an unmatched or malformed callback now always cancels the pending flow and takes the one-shot inline fallback (regression-tested), instead of leaving it armed and blocking every later login. - The inline downgrade is now sticky for the process (reset only on a server switch) rather than re-armed by any pinned-origin page load. Signed-off-by: Bryan Li <bryan.li@gmail.com> Co-authored-by: omnigent <noreply@omnigent.ai> Signed-off-by: Bryan Li <bryan.li@gmail.com>
Signed-off-by: Bryan Li <bryan.li@gmail.com>
Signed-off-by: Bryan Li <bryan.li@gmail.com>
Signed-off-by: Bryan Li <bryan.li@gmail.com>
Signed-off-by: Bryan Li <bryan.li@gmail.com>
Signed-off-by: Bryan Li <bryan.li@gmail.com>
Signed-off-by: Bryan Li <bryan.li@gmail.com>
On a tablet or unfolded foldable the right Workspace rail rendered under the status bar, leaving its tab icons un-tappable, and ran under the gesture-nav bar at the bottom. The rail is `md:m-2` and only renders at md+, but every native-shell inset rule lives inside `@media (width < 48rem)` — so none of them reach it — and it is absent from the shared panel selector lists on both the CSS and Android-injected sides. Add the safe-area margins outside the width gate, and mirror the same declaration into the Android injected sheet so shells pointed at an older web build get the fix too. Uses --omnigent-safe-*, not --omnigent-inset-*: the latter folds in the native bottom-bar footprint and would double-count. Signed-off-by: Bryan Li <bryan.li@gmail.com>
The panel docks as a full-height rail at md+ but was missing from both inset selector lists, so it had no safe-area padding at any width. It is reachable only in debug mode, which is why it went unnoticed. Pre-existing gap, adjacent to the Workspace rail fix rather than part of it. Signed-off-by: Bryan Li <bryan.li@gmail.com>
Signed-off-by: Bryan Li <bryan.li@gmail.com>
A media-gated override appearing later in index.css with the same selector previously slipped past the ancestry check, which only inspected the first matching rule. Assert over every rule that sets the Workspace rail's margins so a later override at md+ fails the suite instead of silently winning the cascade. Signed-off-by: Bryan Li <bryan.li@gmail.com>
The safe-area assertion read only the first matching rule, so a second top-level rule could reintroduce the composite inset vars — double counting the native bottom bar — while the suite stayed green. Signed-off-by: Bryan Li <bryan.li@gmail.com>
Only the longhand properties were matched, so a shorthand `margin` declaration set both edges without ever being checked for an enclosing at-rule. Signed-off-by: Bryan Li <bryan.li@gmail.com>
Matching the exact selector text let an equivalently-spelled one through, and checking only the first rule let a later override zero the margins without naming a banned variable. Match on the rail's aria-label and require the margin rule to be unique. Signed-off-by: Bryan Li <bryan.li@gmail.com>
margin-block and its start/end forms set the same edges as the rule being guarded, so an override written that way was never matched. Signed-off-by: Bryan Li <bryan.li@gmail.com>
The rule scan keyed on one literal spelling, so the same selector written with single quotes, no quotes, or extra spacing introduced a margin the uniqueness and safe-area checks never saw. Signed-off-by: Bryan Li <bryan.li@gmail.com>
The scan matched any spelling of the rail's attribute selector, but the ancestry check then re-found one exact literal and skipped the rule when it differed — two sources of truth for the same question, and a rule whose nesting was never actually checked. Signed-off-by: Bryan Li <bryan.li@gmail.com>
…tors The ancestry walk built named at-rule records that no assertion ever read — every use compared them to an empty list. Brace depth answers the same question in a third of the lines and is stricter: it also rejects the rule being nested inside a plain rule, which the ancestor form let through. Also stops `scroll-margin-top` and friends registering as margin overrides. Signed-off-by: Bryan Li <bryan.li@gmail.com>
The rail now pads content clear of the OS bars on all four edges instead of shrinking with top/bottom margins, so its background and divider stay flush. Lateral insets are folded into --omnigent-safe-left and -right and the published insets include the display cutout, which systemBars() alone omits. The injected fallback sheet mirrors the same rules. Co-authored-by: Isaac Signed-off-by: Bryan Li <bryan.li@gmail.com>
The Workspace-rail and full-height-panel checks each spelled out the same
four padding expectations and the same stylesheet append/remove dance, and
three call sites repeated the selector-extraction slice. They now go
through withStyle, expectSafeAreaPadding, and selectorOf. Also records why
the injected Android inset CSS must avoid backticks and `${`.
Co-authored-by: Isaac
Signed-off-by: Bryan Li <bryan.li@gmail.com>
The injected fallback sheet flattened index.css's media split, so at md+ widths panels docked inside the already-padded Workspace rail (frameless FileViewer, right-docked drawers) were safe-area padded twice. Mirror the (width < 48rem) guard around the :is(...) panel rule and pin it with a structural test. Also decouple the CSS layout test from WorkspacePanel's prop surface: a stub aside carries the selector contract, a source assertion pins the component's aria-label, and brace-depth scanning shrinks to a blanked- source count. Co-authored-by: Isaac Signed-off-by: Bryan Li <bryan.li@gmail.com>
A local uv invocation rewrote the lock's package URLs to an internal proxy; restore the normalized PyPI form the lock check expects. Co-authored-by: Isaac Signed-off-by: Bryan Li <bryan.li@gmail.com>
The rail only exists at md+, outside the mobile drawer rules, so its safe-area padding has its own rule; this pins the injected-bridge -> data-android-native -> inset-fold -> computed-padding chain end to end with distinct per-edge insets. Co-authored-by: Isaac Signed-off-by: Bryan Li <bryan.li@gmail.com>
Execution-logs / file-viewer / terminals panels replace the Workspace rail in the same right-edge slot at md+, so they slid under the status bar and cutout the rail was just fixed to avoid. Pad their top, bottom and right edges (content sits to the left), exempting panels nested inside the already-padded rail, in both index.css and the injected fallback sheet. Share the CSS block-extraction across the test file. Co-authored-by: Isaac Signed-off-by: Bryan Li <bryan.li@gmail.com>
The injected sheet is interpolated into a JS template literal, so a backtick or dollar-brace in the CSS breaks the script at runtime while the style-tag tests keep passing. Co-authored-by: Isaac Signed-off-by: Bryan Li <bryan.li@gmail.com>
The chat header's right edge clears the inline workspace rail via --workspace-panel-offset. The +16 covered the rail's old outer margins; the rail is flush now, so the offset is the rail width itself. Co-authored-by: Isaac Signed-off-by: Bryan Li <bryan.li@gmail.com>
…bars Scoping the injected panel inset rule to phone widths also stripped the conversations sidebar's insets at md+, where it is a pinned column at the left screen edge — unfolded, its header rendered under the status bar. Pad its exposed edges (top/bottom/left) at md+ in index.css and mirror the rule in the injected fallback sheet. Android-only: iOS md+ keeps its current layout. Signed-off-by: Bryan Li <bryan.li@gmail.com>
Co-authored-by: Isaac Signed-off-by: Bryan Li <bryan.li@gmail.com>
A server switch mid-login (server-switcher menu today, deep links next) could inject the old server's session token into the new origin's cookie store. Bind each login result to its originating origin and cancel any in-flight login when the pinned server changes. Co-authored-by: Isaac Signed-off-by: Bryan Li <bryan.li@gmail.com>
Signed-off-by: Bryan Li <bryan.li@gmail.com>
Signed-off-by: Bryan Li <bryan.li@gmail.com>
f3c47bd to
08f440b
Compare
Related issue
Partially addresses #3799 for operator-managed, same-origin front doors. It does not close #3799: direct
*.databricksapps.comorigins cannot expose Digital Asset Links anonymously and continue using the #4296 inline WebView fallback.Supersedes #3800's retired WebView-interception approach.
Summary
This PR adds Android Auth Tab login for deployments that can publish a verified Android app association on the exact server origin. Round 6 fixes two Android regressions found after the server-side flow was approved:
handle_all_urls, namespaceandroid_app, the runningcontext.packageName, and the SHA-256 fingerprint of the running APK's signing certificate. A non-empty but unrelated/wrong-keystoreassetlinks.jsonno longer launches Auth Tab.ELI5
The app asks, “Does this server explicitly name this exact installed APK, and is there a browser that supports Auth Tab?” If yes, it may try Auth Tab. If that attempt cannot run or fails, the app goes back to the login method that already worked for that server instead of forcing login into a WebView.
Supported deployment reality
headermode + anonymous matching DAL + forwarded per-user tokentaboidc/accountsorigin + matching DALpostoidc/accountsserverpost*.databricksapps.comoriginHuman verification recipe
OMNIGENT_AUTH_PROVIDER=header. Configure the front door to forward a per-user token inX-Forwarded-Access-Token, or setOMNIGENT_FORWARDED_TOKEN_HEADERto the actual protected header name.https://omnigent.example.comto Omnigent and exempt onlyGET/HEAD /.well-known/assetlinks.jsonfrom front-door authentication../gradlew assembleDebug; use the debug variant SHA-256 from./gradlew signingReportinOMNIGENT_ANDROID_AUTH_TAB_APPSwithOMNIGENT_NATIVE_AUTH_BASE_URL.web/android/README.md.adb logcat -s OmnigentAuthreportsasset links probe ... -> availableandproxy login -> auth tab; complete login and verify the SPA loads authenticated in the WebView.[], remove the anonymous exemption, or use a non-matching fingerprint; choose Reload, retry, and verify the established fallback runs without launching Auth Tab.Residuals
Test Plan
cd web/android && JAVA_HOME="$(brew --prefix openjdk@21)/libexec/openjdk.jdk/Contents/Home" ./gradlew testDebugUnitTest --rerun-tasksuv run --frozen python -m pytest tests/server/test_native_auth_complete.py -qgit diff --exit-code uv.lockuv.lockis unchanged.uv run --frozen --extra dev pre-commit run --files <11 changed files>.worktrees, including Pyrefly, ruff, web Prettier, Android ktlint format/check, and file-hygiene hooks. The repository's.worktreesignore causes Pyrefly to see no configured sources when run in-place.Demo
N/A from this environment. The user-visible Auth Tab flow requires the unchecked physical-device/self-managed-front-door verification above.
Type of change
Test coverage
Coverage notes
Android tests cover exact DAL matching, malformed/unrelated/wrong-keystore associations, cache invalidation, probe coalescing, direct-Databricks probe avoidance, capable-origin Auth Tab selection, and the required capable-origin/no-provider RFC 8252 fallback. Server tests cover both exchange transports, single-use redemption, retry-safe validation failures, forwarded-token behavior, callback-origin binding, and configuration validation. Live browser/front-door/device behavior remains the explicit unchecked item above.
Changelog
Android can sign in through Auth Tab on verified self-managed front doors while preserving the existing inline or system-browser fallback for every other deployment.