Skip to content

fix(android): sign in with Google through a backend-verified ticket - #295

Merged
arogan178 merged 2 commits into
masterfrom
fix/android-google-signin-ticket
Oct 21, 2026
Merged

arogan178 merged 2 commits into
masterfrom
fix/android-google-signin-ticket

Conversation

@arogan178

Copy link
Copy Markdown
Owner

Why

Google sign-in in the Android app has never worked. Clerk refuses every ID token the Google picker returns with authorization_invalid, for sign-in and sign-up, because the token's azp names our Android OAuth client. Android's picker cannot mint a token without it.

Confirmed on a Pixel 8 Pro against production Clerk on 2026-10-21:

  • A forged token gets google_one_tap_token_invalid. A real one passes and is then refused, so the problem isn't the token.
  • A second Google account's sign-up is refused the same way, so it isn't account-specific. Swapping the account's primary email changed nothing.
  • Everything in the dashboards is correct: Native API on, Android app registered with both SHA-256s, custom credentials, redirect allowlist.

Kallb123/async-games#663 hit the identical refusal and fixed it the same way.

What changes

Backend: POST /api/auth/google-native (public, strict auth rate limit)

  1. Verifies the ID token with Google's published keys:
    • RS256 only.
    • Issuer is Google, audience is our web client, and azp is one of our Android clients.
    • Not expired, and email_verified is true.
  2. Finds the Clerk user who has this exact Google account (sub) linked and verified through Clerk's own OAuth. Email only narrows the search. An admin-asserted address on a Clerk user never proves ownership: this is the takeover hole called out in the async-games PR.
  3. Returns a 60-second Clerk sign-in ticket, or { ticket: null } when no account has that Google login linked.

App (signInWithNativeGoogle, shared by the sign-in and sign-up forms)

  • Redeems the ticket with signIn.create({ strategy: "ticket" }).
  • Falls back to Clerk's web OAuth when no account is linked (sign-up happens there), when the picker fails, or when the backend route is switched off (404).
  • Replaces the four google_one_tap attempts, none of which could succeed.

Picker bugs fixed along the way

  • The app opened the web login on top of the picker after a 10-second timeout, or when the user cancelled. It now waits, and cancelling stops.
  • The plugin reused the last Google account, so nobody could switch accounts. It now forgets it first, so the account list always shows.

Deploy

The managed backend env (MANAGED_BACKEND_ENV_FILE in the infra repo) needs:

GOOGLE_WEB_CLIENT_ID=880247591600-g42kbb95b131mcjfrn838ruj89pe0mp5.apps.googleusercontent.com
GOOGLE_ANDROID_CLIENT_IDS=880247591600-8bmdasnntm6i80kfau7f84pjue61t1m8.apps.googleusercontent.com,880247591600-oj5mhd3k9g5e2cp8pm1nd2c0hj8r9odr.apps.googleusercontent.com

These are client IDs, not secrets. Until both are set, the route 404s and the app uses web OAuth as before.

Testing

  • google-native.test.ts signs real RS256 tokens with throwaway keys. It covers a foreign signing key, wrong audience, a foreign or missing azp, an unverified email, a foreign issuer, an expired token, alg: none and an unknown key ID. On the lookup side it covers an email-only match, a different Google account on the same address, and an unverified link. Each guard was mutation-checked: removing it fails a test.
  • Route tests: ticket issued with a 60-second expiry, no ticket for an unlinked account, 401 on an unverified token, 404 until configured.
  • App flow tests: signed in, cancelled, the three fallbacks to web, and the two failures.
  • Backend 499/499 and frontend 1293/1293 tests pass. Typecheck and lint pass on both (warnings only, none new).
  • Not yet tested on the device against this route. That needs the env above deployed first.

Clerk refuses every ID token the Android Google picker returns, for sign-in
and sign-up alike, because the token's azp names the Android OAuth client.
The picker cannot mint one without it, so native Google sign-in never worked.

The backend now verifies the token itself (Google's keys, our web client as
audience, our Android clients as azp, verified email), finds the Clerk user
who linked that exact Google account, and returns a 60-second sign-in ticket
the app redeems. Email only narrows the lookup; an admin-asserted address
never proves ownership. With no linked account the app continues to Clerk's
web OAuth, which is where sign-up happens.

Also fixes two picker bugs: the app opened the web login on top of the
picker after 10 seconds or on cancel, and the plugin reused the last Google
account so nobody could switch.

Needs GOOGLE_WEB_CLIENT_ID and GOOGLE_ANDROID_CLIENT_IDS in the managed
backend env; until both are set the route 404s and the app uses web OAuth.
…actor

Also time out the Google certificates request after five seconds.
@arogan178
arogan178 merged commit a3d9fa7 into master Oct 21, 2026
7 checks passed
@arogan178
arogan178 deleted the fix/android-google-signin-ticket branch October 21, 2026 08:46
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