From 0abd751799df695d6b83c6f65149b3d5a48310f9 Mon Sep 17 00:00:00 2001 From: "workos-tars[bot]" <269013284+workos-tars[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 06:21:08 +0000 Subject: [PATCH 1/2] feat: Add server-backed Google One Tap 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. --- README.md | 29 +++++ examples/next/.env.local.example | 4 +- examples/next/README.md | 6 +- .../next/src/app/auth/google-one-tap/route.ts | 3 + examples/next/src/app/page.tsx | 4 + src/components/google-one-tap.spec.tsx | 26 +++++ src/components/google-one-tap.tsx | 26 +++++ src/components/index.ts | 4 +- src/google-one-tap-route.spec.ts | 106 ++++++++++++++++++ src/google-one-tap-route.ts | 75 +++++++++++++ src/index.ts | 3 + 11 files changed, 283 insertions(+), 3 deletions(-) create mode 100644 examples/next/src/app/auth/google-one-tap/route.ts create mode 100644 src/components/google-one-tap.spec.tsx create mode 100644 src/components/google-one-tap.tsx create mode 100644 src/google-one-tap-route.spec.ts create mode 100644 src/google-one-tap-route.ts diff --git a/README.md b/README.md index bdab4c0..31fd1b2 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,35 @@ In the [WorkOS dashboard](https://dashboard.workos.com), go to **Redirects** and > [!IMPORTANT] > The Sign-in URL is required for features like [impersonation](https://workos.com/docs/user-management/impersonation) to work correctly. Without it, WorkOS-initiated flows (such as impersonating a user from the dashboard) will fail because they cannot complete the PKCE/CSRF verification that this library enforces on every callback. +### Google One Tap + +Google One Tap posts its signed ID token to your application so the token and the resulting WorkOS session stay server-side. + +Configure Google OAuth with your own credentials in the WorkOS Dashboard. In Google Cloud, add your application origin to **Authorized JavaScript origins** and the handler URL below to **Authorized redirect URIs**. WorkOS sandbox demo credentials cannot be used for One Tap. This flow requires `@workos-inc/node` 10.12 or newer. + +```tsx +// app/page.tsx +import { GoogleOneTap } from '@workos-inc/authkit-nextjs/components'; + +export default function Page() { + return ( + + ); +} +``` + +```ts +// app/auth/google-one-tap/route.ts +import { handleGoogleOneTap } from '@workos-inc/authkit-nextjs'; + +export const POST = handleGoogleOneTap({ returnPathname: '/dashboard' }); +``` + +`handleGoogleOneTap` verifies Google's double-submit CSRF token, exchanges the ID token through WorkOS, and stores the normal encrypted AuthKit session. The token never enters a URL. If One Tap cannot complete—for example, because another authentication step is required—the handler falls back to Hosted AuthKit. Keep the standard sign-in button visible because browsers can suppress the prompt, and this flow does not return Google access or refresh tokens for additional scopes. + ### Proxy / Middleware This library relies on Next.js proxy (called "middleware" in Next.js ≤15) to provide session management for routes. diff --git a/examples/next/.env.local.example b/examples/next/.env.local.example index d742eb2..2c1d901 100644 --- a/examples/next/.env.local.example +++ b/examples/next/.env.local.example @@ -1,4 +1,6 @@ WORKOS_CLIENT_ID= WORKOS_API_KEY= -NEXT_PUBLIC_WORKOS_REDIRECT_URI=http://localhost:3000/callback WORKOS_COOKIE_PASSWORD= +NEXT_PUBLIC_WORKOS_REDIRECT_URI=http://localhost:3000/callback +NEXT_PUBLIC_APP_URL=https://your-app.example +NEXT_PUBLIC_GOOGLE_CLIENT_ID= diff --git a/examples/next/README.md b/examples/next/README.md index 8d0b4a1..f1d3f98 100644 --- a/examples/next/README.md +++ b/examples/next/README.md @@ -26,9 +26,13 @@ You will need a [WorkOS account](https://dashboard.workos.com/signup). WORKOS_COOKIE_PASSWORD= NEXT_PUBLIC_WORKOS_REDIRECT_URI=http://localhost:3000/callback + NEXT_PUBLIC_APP_URL=https://your-app.example + NEXT_PUBLIC_GOOGLE_CLIENT_ID= ``` -5. Run the following command and navigate to [http://localhost:3000](http://localhost:3000). +5. To enable Google One Tap, configure Google OAuth with your own credentials in the WorkOS Dashboard. In Google Cloud, add your HTTPS application origin as an **Authorized JavaScript origin** and `/auth/google-one-tap` as an **Authorized redirect URI**. Copy the same Google client ID into `NEXT_PUBLIC_GOOGLE_CLIENT_ID`. For local testing, expose the application through an HTTPS development tunnel and use that origin for `NEXT_PUBLIC_APP_URL`. + +6. Run the following command and navigate to [http://localhost:3000](http://localhost:3000). ```bash pnpm dev diff --git a/examples/next/src/app/auth/google-one-tap/route.ts b/examples/next/src/app/auth/google-one-tap/route.ts new file mode 100644 index 0000000..11c3afa --- /dev/null +++ b/examples/next/src/app/auth/google-one-tap/route.ts @@ -0,0 +1,3 @@ +import { handleGoogleOneTap } from '@workos-inc/authkit-nextjs'; + +export const POST = handleGoogleOneTap({ returnPathname: '/account' }); diff --git a/examples/next/src/app/page.tsx b/examples/next/src/app/page.tsx index 276568d..b69bdd6 100644 --- a/examples/next/src/app/page.tsx +++ b/examples/next/src/app/page.tsx @@ -1,10 +1,13 @@ import NextLink from 'next/link'; import { withAuth } from '@workos-inc/authkit-nextjs'; import { Button, Flex, Heading, Text } from '@radix-ui/themes'; +import { GoogleOneTap } from '@workos-inc/authkit-nextjs/components'; import { SignInButton } from './components/sign-in-button'; export default async function HomePage() { const { user } = await withAuth(); + const googleClientId = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID; + const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000'; return ( {user ? ( @@ -27,6 +30,7 @@ export default async function HomePage() { Sign in to view your account details + {googleClientId && } )} diff --git a/src/components/google-one-tap.spec.tsx b/src/components/google-one-tap.spec.tsx new file mode 100644 index 0000000..d64e0d0 --- /dev/null +++ b/src/components/google-one-tap.spec.tsx @@ -0,0 +1,26 @@ +import '@testing-library/jest-dom'; +import { render } from '@testing-library/react'; +import React from 'react'; +import { GoogleOneTap } from './google-one-tap.js'; + +describe('GoogleOneTap', () => { + it('renders the Google Identity Services configuration', () => { + const { container } = render( + , + ); + + expect(document.querySelector('script')).toHaveAttribute('src', 'https://accounts.google.com/gsi/client'); + expect(container.querySelector('#g_id_onload')).toHaveAttribute('data-client_id', 'google-client-id'); + expect(container.querySelector('#g_id_onload')).toHaveAttribute( + 'data-login_uri', + 'https://example.com/auth/google-one-tap', + ); + expect(container.querySelector('#g_id_onload')).toHaveAttribute('data-context', 'signup'); + expect(container.querySelector('#g_id_onload')).toHaveAttribute('data-cancel_on_tap_outside', 'false'); + }); +}); diff --git a/src/components/google-one-tap.tsx b/src/components/google-one-tap.tsx new file mode 100644 index 0000000..b039037 --- /dev/null +++ b/src/components/google-one-tap.tsx @@ -0,0 +1,26 @@ +'use client'; + +import React from 'react'; + +export interface GoogleOneTapProps { + clientId: string; + loginUri: string; + context?: 'signin' | 'signup' | 'use'; + cancelOnTapOutside?: boolean; +} + +export function GoogleOneTap({ clientId, loginUri, context = 'signin', cancelOnTapOutside = true }: GoogleOneTapProps) { + return ( + <> +