Skip to content

feat: Add server-backed Google One Tap - #473

Open
workos-tars[bot] wants to merge 2 commits into
mainfrom
tars/google-one-tap
Open

feat: Add server-backed Google One Tap#473
workos-tars[bot] wants to merge 2 commits into
mainfrom
tars/google-one-tap

Conversation

@workos-tars

@workos-tars workos-tars Bot commented Aug 25, 2026

Copy link
Copy Markdown

Summary

  • Add a GoogleOneTap presentation component that configures Google Identity Services for a server POST.
  • Add handleGoogleOneTap to validate Google's double-submit CSRF token, exchange the credential server-side, and persist the standard encrypted AuthKit session.
  • Fall back to Hosted AuthKit when One Tap cannot complete an interactive requirement, without putting pending tokens in URLs.
  • Document customer-owned Google credentials, HTTPS origins, identity-only limitations, and the standard sign-in fallback.
  • Add a runnable example route plus component and handler coverage.

Dependencies

  • WorkOS API support: workos/workos#70242.
  • Merge blocker: publish @workos-inc/node 10.12 or newer, then bump this repository's dev dependency and replace the temporary typed compatibility guard with the published SDK method.
  • The Node SDK and standalone OpenAPI commits are complete locally; their task-scoped transports still return HTTP 403.

Requested by mg@workos.com via TARS

Posting Google's credential to the application server keeps bearer tokens
out of browser JavaScript and lets AuthKit persist sessions through its
existing encrypted cookie path. Hosted AuthKit remains the safe fallback
when another authentication step is required.
@workos-tars
workos-tars Bot requested a review from a team as a code owner August 25, 2026 06:25
@greptile-apps

greptile-apps Bot commented Aug 25, 2026

Copy link
Copy Markdown

Greptile Summary

Adds a server-backed Google One Tap flow that validates Google’s CSRF token, exchanges the ID token through WorkOS, saves the encrypted AuthKit session, and falls back to Hosted AuthKit when necessary.

  • Adds the GoogleOneTap client presentation component and public exports.
  • Adds the handleGoogleOneTap route handler with configurable public redirect origin, hooks, session persistence, and PKCE fallback.
  • Adds tests, documentation, and runnable Next.js example wiring.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the previously reported proxy redirect issue is addressed by the new validated baseURL option and corresponding documentation.

Important Files Changed

Filename Overview
src/google-one-tap-route.ts Implements CSRF validation, server-side token exchange, session persistence, Hosted AuthKit fallback, and a baseURL override for proxy-safe redirects.
src/components/google-one-tap.tsx Renders the Google Identity Services script and HTML API configuration for server POST delivery.
src/google-one-tap-route.spec.ts Covers successful authentication, public-origin redirects, malformed requests, fallback behavior, hooks, and unsupported SDK handling.
examples/next/src/app/auth/google-one-tap/route.ts Provides minimal example route wiring for the new One Tap handler.
README.md Documents Google configuration, proxy-safe baseURL usage, CSP support, flow limitations, and fallback behavior.

Sequence Diagram

sequenceDiagram
  participant B as Browser
  participant G as Google Identity Services
  participant A as Application One Tap Route
  participant W as WorkOS
  participant S as AuthKit Session
  B->>G: Select Google identity
  G->>A: POST credential and CSRF token
  A->>A: Validate double-submit CSRF token
  A->>W: Exchange Google ID token
  alt Authentication succeeds
    W-->>A: Authentication response
    A->>S: Save encrypted session
    A-->>B: 303 redirect to application
  else Interactive authentication required
    A->>A: Create PKCE state
    A-->>B: 303 redirect to Hosted AuthKit
  end
Loading

Reviews (2): Last reviewed commit: "fix: Complete One Tap fallback flow" | Re-trigger Greptile

Comment thread src/google-one-tap-route.ts Outdated
@gjtorikian

Copy link
Copy Markdown
Contributor

SDK PR Review

Findings

🔴 [CRITICAL] Hosted AuthKit fallback is broken end-to-end — PKCE cookie never set

File: src/google-one-tap-route.ts:69-70

The catch block calls getAuthorizationUrl({ returnPathname }) and redirects, but discards the returned sealedState. Every other caller of getAuthorizationUrl persists that state as a PKCE cookie (auth.ts:25-26 calls setPKCECookie(sealedState); the middleware appends it via appendPKCESetCookieHeader). Without it, when the user finishes hosted sign-in and lands on handleAuth, the callback throws missing_pkce_cookie (authkit-callback-route.ts:62-68) and the user gets an error page. The fallback path — a headline feature of this PR — can never succeed. The test suite misses this because getAuthorizationUrl is mocked and the test only asserts the redirect Location.

Suggestion:

const { url, sealedState } = await getAuthorizationUrl({ returnPathname });
await setPKCECookie(sealedState);
return noStore(redirectWithFallback(url));

Add a test asserting the PKCE cookie is set on the fallback response.

🟠 [HIGH] Ships against an unpublished @workos-inc/node API — sequencing blocker

File: src/google-one-tap-route.ts:16-20, package.json:48

authenticateWithGoogleIdToken doesn't exist in any published Node SDK (devDependency is ^10.7.0; peer range still allows ^9.0.0 || ^10.0.0). The PR body itself notes the API, Node SDK, and OpenAPI changes couldn't be pushed. Merged and released today, the documented feature silently falls back to hosted AuthKit for 100% of users — and since this repo uses release-please, merging triggers a release PR immediately.

Suggestion: Hold this PR until the Node SDK 10.12 release lands. Then bump the devDependency, note the minimum version in the peer-dep docs, and replace the reflection hack (next finding) with a typed call.

🟠 [HIGH] Errors are silently swallowed — the version-check error never surfaces

File: src/google-one-tap-route.ts:63-69

The catch block redirects with no logging. The deliberately thrown '@workos-inc/node 10.12 or newer is required' error (line 19) is dead code in practice — a developer on an old SDK just sees One Tap bounce to hosted sign-in with zero console output, indistinguishable from a legitimate MFA-required fallback. handleAuth logs console.error('[AuthKit callback error]', error) in the same situation (authkit-callback-route.ts:135).

Suggestion: console.error('[AuthKit Google One Tap error]', error) before the fallback, matching the existing convention. Consider rethrowing (not falling back) for the version-check error specifically, since that's a developer misconfiguration, not a user condition.

🟡 [MEDIUM] No baseURL override — request-derived redirect breaks proxy/container deployments

File: src/google-one-tap-route.ts:55-59

The success redirect is built from request.url, which behind a reverse proxy or in Docker can carry an internal hostname. handleAuth grew a baseURL option for exactly this (authkit-callback-route.ts:92-95). This was also flagged by Greptile and is still unresolved — independent analysis reaches the same conclusion.

Suggestion: Add baseURL?: string to HandleGoogleOneTapOptions with the same early validation handleAuth does, and use it as the redirect origin when provided.

🟡 [MEDIUM] Reflect.get/Reflect.apply bypasses TypeScript entirely

File: src/google-one-tap-route.ts:16-29

The reflection dance exists only because the current typings lack the method. The argument shape (clientId, token, ipAddress, userAgent) is completely unchecked — a rename or signature change in the SDK compiles fine and fails at runtime. It also hides the dependency from tooling that scans for API usage.

Suggestion: Once the SDK ships, make this a direct typed call: getWorkOS().userManagement.authenticateWithGoogleIdToken({...}). If graceful degradation for old SDKs must stay, keep a typeof guard but type the call site.

🟡 [MEDIUM] Test coverage gaps on the paths most likely to break

File: src/google-one-tap-route.spec.ts

No test for: the old-SDK path (authenticateWithGoogleIdToken missing), the onError callback, a missing (vs mismatched) CSRF cookie, a missing credential field, or the PKCE cookie on fallback (which would have caught the CRITICAL finding). The fallback test can't detect the cookie bug because both getAuthorizationUrl and the cookie write are mocked away.

Suggestion: Add those five cases; for the fallback test, assert response.headers.get('set-cookie') contains the PKCE cookie.

🔵 [LOW] GoogleOneTap component: raw <script> tag and fixed element id limit reliability

File: src/components/google-one-tap.tsx:15-24

GSI scans for #g_id_onload when the script executes. On client-side navigation React 19 dedupes hoisted async scripts and won't re-execute them, so One Tap won't prompt when the component mounts after a soft navigation. The hardcoded id="g_id_onload" also means two instances collide, and there's no nonce prop for strict-CSP consumers.

Suggestion: Document the "render on initially-loaded pages" caveat at minimum; longer term, offer programmatic google.accounts.id.initialize/prompt() init. Add an optional nonce prop passed to the script tag.

⚪ [INFO] Minor API-surface inconsistencies with handleAuth

File: src/google-one-tap-route.ts:9-13

HandleGoogleOneTapOptions lives in the route file while HandleAuthOptions lives in interfaces.ts; onSuccess is typed via Awaited<ReturnType<typeof authenticate>> instead of AuthenticationResponse directly. If onSuccess throws after saveSession, the user (already signed in) gets bounced to hosted sign-in.

Suggestion: Move the interface to interfaces.ts, type onSuccess against AuthenticationResponse, and don't route onSuccess failures into the auth fallback.

Summary

  • Critical: 1 | High: 2 | Medium: 3 | Low: 1 | Info: 1
  • Recommended action: REQUEST_CHANGES

The CSRF handling, session persistence, and docs are solid, but the fallback path — the feature's main safety net — cannot complete because the PKCE cookie is never set, and the whole feature depends on a Node SDK method that hasn't shipped. Block on the dependency release and the fallback fix before merging.

Fallbacks redirected without verifier state, so their callbacks always
failed. They also used a 307 after Google's credential POST, which could
resubmit bearer form data. Preserve PKCE state and use a 303 while making
unsupported SDK and authentication errors visible.
@workos-tars

workos-tars Bot commented Aug 25, 2026

Copy link
Copy Markdown
Author

TARS addressed the actionable findings in 6b70aa6.

Fixed

  • Persist PKCE state on Hosted AuthKit fallback and assert the cookie in tests.
  • Use HTTP 303 after Google’s POST, preventing credential form data from being resubmitted to redirect targets.
  • Log authentication failures with [AuthKit Google One Tap error].
  • Surface unsupported Node SDK versions instead of silently falling back.
  • Add baseURL validation and proxy/container-safe redirects.
  • Replace Reflect.get/Reflect.apply with a typed compatibility guard.
  • Move HandleGoogleOneTapOptions to interfaces.ts and type onSuccess directly.
  • Keep onSuccess failures outside the authentication fallback.
  • Add coverage for missing/mismatched CSRF, missing credentials, onError, unsupported SDKs, PKCE fallback, and baseURL.
  • Add CSP nonce support and document the initial-page/single-instance GSI constraint.

Remaining blocker

The PR body now explicitly blocks merge until @workos-inc/node 10.12+ is published. At that point, the dev dependency should be bumped and the compatibility guard replaced with the published typed method.

Validation

  • 422 tests passed
  • Typecheck passed
  • Lint and formatting passed
  • Package build passed
  • Current-head CI passed on Node 22 and Node 24, including lint and Socket checks

PR remains open and review-required; it has not been merged.

@workos-tars

workos-tars Bot commented Aug 25, 2026

Copy link
Copy Markdown
Author

TARS confirms this finding was valid and is already fixed in 6b70aa6.

  • Added and validated baseURL in HandleGoogleOneTapOptions.
  • Success redirects now use baseURL instead of the internal request.url origin when configured.
  • Added coverage asserting an internal request redirects to https://public.example.com/dashboard.
  • Documented the proxy/container configuration.

Greptile reviewed the previous head, 0abd751; current PR head is 6b70aa6. All current-head checks pass with no pending or failed checks. The PR remains blocked on review and the Node SDK release dependency.

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

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant