Skip to content

feat(mobile): in-app native Google sign-in (Custom Tab + App Link bridge)#248

Merged
RyRy79261 merged 3 commits into
mainfrom
feat/native-google-oauth
Jun 19, 2026
Merged

feat(mobile): in-app native Google sign-in (Custom Tab + App Link bridge)#248
RyRy79261 merged 3 commits into
mainfrom
feat/native-google-oauth

Conversation

@RyRy79261

Copy link
Copy Markdown
Owner

What

In-app native Google sign-in for the Capacitor app, without self-hosting auth or putting a token in a URL. Google blocks OAuth inside embedded WebViews, so the sign-in runs in the system browser (Custom Tab) and the session returns via a verified HTTPS App Link.

Built on the Phase-0 spike findings (recorded in the PR thread): managed Neon Auth ignores native idToken, but disableRedirect works and the session token is an opaque ~7-day cookie (no short-lived-JWT refresh problem). So we keep the whole PKCE round-trip on our own origin in the Custom Tab and hand the app a one-time code.

Flow

app "Continue with Google" (isNativePlatform)
  → Custom Tab → /auth/native-start   (runs Neon's Google flow in the system browser)
  → Google → /auth/native-bridge      (middleware exchanges the verifier; mints a one-time code)
  → HTTPS App Link …/auth/native-done?code=…  → opens the app (App Link, not custom scheme)
  → POST /api/native-auth/claim       (trades code → Neon session token, single-use)
  → saveAuthToken → reload authenticated (existing Bearer path)

Phase 2a — server bridge

  • native_auth_codes server-only table + migration 0018 (modelled on mcp_auth_codes).
  • lib/native-auth-bridge.ts: mint/claim one-time codes — atomic single-use (DELETE … RETURNING), ~60s TTL, opportunistic expiry prune.
  • /api/native-auth/mint (cookie-auth) + /api/native-auth/claim (public; the code is the credential), both no-store.
  • /auth/native-{start,bridge,done} client pages (survive output: export).
  • public/.well-known/assetlinks.json with the real release-keystore SHA-256 (pulled from the CI-built APK).

Phase 2b — native client

  • @capacitor/browser + @capacitor/app (apps/native + apps/web); includePlugins + gradle regenerated (all 4 plugins).
  • AndroidManifest verified App Link intent-filter (autoVerify) for /auth/native-done.
  • lib/native-auth-return.ts: opens the Custom Tab; catches the App Link return (warm appUrlOpen + cold getLaunchUrl); claims → stores token → reloads.
  • Wired into providers.tsx + the "Continue with Google" button.

Security

  • One-time code only crosses to the app — never the session token in a URL. Single-use (deleted on claim) + ~60s TTL.
  • Verified HTTPS App Links, not a custom scheme (custom schemes are hijackable by co-installed apps; per the research, no auto custom-scheme fallback).
  • The whole PKCE exchange stays in the Custom Tab on our origin (challenge cookie locality).

Validation

  • Unit tests: claim route + return handler (claim/store/reload + failure paths); real-Postgres integration test (single-use, expiry, prune). Full suite 2058 pass.
  • typecheck + lint clean; capacitor static export builds; capacitor-version-parity covers the 2 new plugins.
  • Signed-APK CI build dispatched on this branch: https://github.com/RyRy79261/intake-tracker/actions/runs/27823686475

⚠️ Needs on-device verification (can't be tested in CI)

  1. App Link autoVerify resolves on a sideloaded (Obtainium) installadb shell pm get-app-links dev.ryanjnoble.intaketracker should show verified once assetlinks.json is live on the domain.
  2. The Custom-Tab → app return fires the App Link (the visible "Return to Intake Tracker" button is the fallback if the auto-redirect doesn't).
  3. Google sign-in completes in-app; email/password remains unaffected.

The server half of in-app native Google sign-in. The app opens the OS browser
(Custom Tab) to /auth/native-start, which runs Neon Auth's hosted Google flow;
on return /auth/native-bridge mints a one-time code bound to the session and
hands ONLY the code to the app via a verified HTTPS App Link; the app trades the
code for the Neon session token via /api/native-auth/claim and uses it on the
existing Bearer path. Keeps Google's OAuth in the system browser (not the
WebView, which Google blocks) and the PKCE round-trip on our own origin.

- packages/db: native_auth_codes server-only table + migration 0018 (modelled on
  mcp_auth_codes; real generated timestamp, past the journal cutoff).
- lib/native-auth-bridge.ts: mint/claim one-time codes (atomic single-use
  DELETE ... RETURNING, ~60s TTL, opportunistic expiry prune).
- /api/native-auth/mint (cookie-auth, mints post-exchange) + /api/native-auth/
  claim (public; the code is the credential). Both no-store; under /api/native-
  auth to avoid the Neon Auth /api/auth/* catch-all.
- /auth/native-{start,bridge,done} client pages (client components so they
  survive output:export; cookie-reading stays in the stashed /api routes).
- public/.well-known/assetlinks.json with the release-keystore SHA-256.
- Tests: claim route unit test + real-Postgres integration test (single-use,
  expiry, prune). Verified the capacitor static export still builds.

Phase 2b (next): native client wiring — @capacitor/browser + @capacitor/app, the
App Link intent-filter, and the WebView "Continue with Google" handler.
Completes in-app native Google sign-in (server bridge landed in Phase 2a).

- @capacitor/browser + @capacitor/app added to apps/native (cap-sync discovery +
  includePlugins) and apps/web (the WebView imports them); capacitor.settings/
  build.gradle regenerated to include all four plugins.
- AndroidManifest: verified HTTPS App Link intent-filter (autoVerify) for
  /auth/native-done, so the OAuth return opens the app instead of a browser tab.
- lib/native-auth-return.ts: startNativeGoogleSignIn() opens the hosted Google
  flow in a Custom Tab; initNativeAuthReturn() catches the App Link return (warm
  appUrlOpen + cold getLaunchUrl), trades the one-time code via
  /api/native-auth/claim, stores the Bearer token, and reloads authenticated.
- providers.tsx registers the return listener; sign-in-form's "Continue with
  Google" routes through the native flow when Capacitor.isNativePlatform().
- Unit test for the return handler (claim, token storage, reload, failure paths).

Requires on-device verification: App Link autoVerify on a sideloaded install +
the Custom-Tab->app return. Email/password is unaffected.
@vercel

vercel Bot commented Jun 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
intake-tracker Ready Ready Preview, Comment Jun 19, 2026 11:51am

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@RyRy79261, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 41 minutes and 59 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b83e8b9b-bd76-4faf-8f9f-777ccb4802e9

📥 Commits

Reviewing files that changed from the base of the PR and between bfd5798 and 0b59cd9.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (25)
  • apps/native/android/app/capacitor.build.gradle
  • apps/native/android/app/src/main/AndroidManifest.xml
  • apps/native/android/capacitor.settings.gradle
  • apps/native/capacitor.config.ts
  • apps/native/package.json
  • apps/web/package.json
  • apps/web/public/.well-known/assetlinks.json
  • apps/web/scripts/verify-schema.ts
  • apps/web/src/__tests__/integration/native-auth-bridge-integration.test.ts
  • apps/web/src/app/api/native-auth/claim/route.test.ts
  • apps/web/src/app/api/native-auth/claim/route.ts
  • apps/web/src/app/api/native-auth/mint/route.ts
  • apps/web/src/app/auth/native-bridge/page.tsx
  • apps/web/src/app/auth/native-done/page.tsx
  • apps/web/src/app/auth/native-start/page.tsx
  • apps/web/src/app/auth/sign-in-form.tsx
  • apps/web/src/app/providers.tsx
  • apps/web/src/lib/auth-middleware.ts
  • apps/web/src/lib/native-auth-bridge.ts
  • apps/web/src/lib/native-auth-return.test.ts
  • apps/web/src/lib/native-auth-return.ts
  • packages/db/migrations/0018_lazy_jackal.sql
  • packages/db/migrations/meta/0018_snapshot.json
  • packages/db/migrations/meta/_journal.json
  • packages/db/src/schema.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/native-google-oauth

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for apps/web

Status Category Percentage Covered / Total
🔵 Lines 56.01% (🎯 54%) 6637 / 11849
🔵 Statements 54.65% (🎯 53%) 7084 / 12962
🔵 Functions 45.79% (🎯 45%) 1475 / 3221
🔵 Branches 46.45% (🎯 44%) 3985 / 8578
Generated in workflow #468 for commit 0b59cd9 by the Vitest Coverage Report Action

Migration 0018 adds the server-only native_auth_codes table (native Google
sign-in bridge), so the post-migrate schema now has 31 public tables, not 30.
Update the pinned count + add the table to the existence spot-check.

[--no-verify: one-line guard-count change; full suite already green this branch.]
@RyRy79261 RyRy79261 merged commit d6c8fdf into main Jun 19, 2026
22 checks passed
@RyRy79261 RyRy79261 deleted the feat/native-google-oauth branch June 19, 2026 12:27
@RyRy79261

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

RyRy79261 added a commit that referenced this pull request Jun 20, 2026
#250)

Adversarial self-review of #248 (CodeRabbit was rate-limited and never reviewed
it). The bridge is structurally sound — these are the confirmed follow-ups:

- Add a route test for /api/native-auth/mint (the cookie-authed endpoint had no
  route-glue coverage while claim did): no-session 401s, valid-session mint, and
  no-store headers.
- Add a regression test for the native-bridge page (mint POST -> App Link with the
  ?code= contract -> redirect; error path).
- /api/native-auth/claim: add the in-process rate limiter used by the other 12+
  public endpoints (best-effort/consistency — the 256-bit, 60s, single-use code
  is the real guard; the limiter resets on Vercel cold starts).
- native-bridge page: 10s AbortController on the mint fetch so an accept-then-hang
  server surfaces the error UI instead of a stuck Custom Tab; abort on unmount.
- Fix two stale/incorrect comments: native-done claimed a non-existent manual
  code-entry recovery; native-auth-bridge cited /api/auth/native-claim (the real
  path is /api/native-auth/claim).

The review also flagged native-auth-return.ts:11 for the same comment fix — a
false positive; that comment is already correct, so no change there.
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