Google One Tap Integration#
The oneTapClient plugin integrates Google's Identity Services (GIS) library into better-auth, enabling passwordless sign-in via the One Tap prompt or a rendered "Sign in with Google" button. The plugin is split into a client (packages/better-auth/src/plugins/one-tap/client.ts) and a server side that exposes a single POST /one-tap/callback endpoint for ID-token exchange .
Architecture Overview#
Browser GIS Library better-auth
│ │ │
│── oneTap() called ──────────────>│ │
│ loadGoogleScript() │ │
│── google.accounts.id.initialize()│ │
│── google.accounts.id.prompt() ──>│ (browser/FedCM UI) │
│<──────────────── credential ─────│ │
│── POST /one-tap/callback ────────────────────────────────>│
│<────────────────── session cookie ──────────────────────── │
The GIS script (https://accounts.google.com/gsi/client) is lazy-loaded on first call and deduplicated via a window.googleScriptInitialized flag. A module-level boolean guard (isRequestInProgress) prevents overlapping prompt calls.
Key Client Options (GoogleOneTapOptions)#
Defined at lines 86–145:
| Option | Default | Notes |
|---|---|---|
clientId | (required) | Google OAuth Client ID |
autoSelect | — | Auto-select if one account already signed in |
cancelOnTapOutside | — | Disabled by FedCM — see below |
uxMode | "popup" | "popup" or "redirect" |
context | "signin" | "signin" |
additionalOptions | — | Passed directly to google.accounts.id.initialize() |
promptOptions.fedCM | true | Enables FedCM mode; set false to revert to JS prompts |
promptOptions.baseDelay | 1000 ms | Base for exponential backoff retry |
promptOptions.maxAttempts | 5 | Max retries before calling onPromptNotification |
The per-call oneTap() action also accepts GoogleOneTapActionOptions which adds callbackURL, onPromptNotification, nonce, and button (for rendering a GSI button instead of the prompt).
FedCM Transition and API Incompatibilities#
Background: Google is migrating One Tap from JavaScript-controlled prompts (rendered inside the page) to FedCM-controlled prompts (rendered by the browser). FedCM is now enabled by default in better-auth .
How it's passed to GIS:
use_fedcm_for_prompt: useFedCM is included in the google.accounts.id.initialize() call , where useFedCM = options.promptOptions?.fedCM !== false. This opts the app into FedCM and suppresses Google's GSI deprecation warnings .
API methods affected by FedCM (see prompt lifecycle handler):
| Method | FedCM behavior |
|---|---|
notification.isDismissedMoment?.() | Still available; use optional chaining |
notification.isSkippedMoment?.() | Still available; use optional chaining |
notification.getSkippedReason?.() | Unavailable under FedCM — missing reason is treated as no-retry |
notification.isNotDisplayed?.() | Deprecated under FedCM — handled only as non-FedCM fallback |
cancelOnTapOutside | Ignored — browser controls dismissal, not JavaScript |
All notification callbacks use optional chaining (?.()) defensively since FedCM may not populate them .
Sign-out: When FedCM is active and the browser supports IdentityCredential, navigator.credentials.preventSilentAccess() is called on every /sign-out response to clear cached credentials. Without this, users can appear logged out in the app but remain silently credentialed in the browser.
What the GIS Library Manages Internally#
A direct navigator.credentials.get() call was attempted in PR #6380 but removed in PR #7928 because the GIS library already invokes FedCM internally when use_fedcm_for_prompt is set. The direct call caused double prompts and runtime errors. Do not add direct FedCM API calls alongside GIS initialization.
Server-Side: /one-tap/callback#
The callback endpoint accepts { idToken, callbackURL }. Validation (verifyGoogleIdToken) fetches Google's public keys from https://www.googleapis.com/oauth2/v3/certs, verifies the JWT signature via jose, and checks issuer, audience (client ID), expiration, and optionally hd (hosted domain). On success the handler calls handleOAuthUserInfo — the same path used by the standard Google redirect flow — to create or link accounts and issue a session cookie.
Server-side plugin options (OneTapOptions):
disableSignup— reject new users, allow only existing accountsclientId— override the client ID used for audience validation if not set via the social provider config
Disabling FedCM#
Set promptOptions.fedCM: false to revert to the legacy JavaScript-controlled prompt. This restores cancelOnTapOutside behavior and re-enables getSkippedReason(), but will trigger GSI deprecation warnings from Google's library .
Primary Source Files#
- Client plugin:
packages/better-auth/src/plugins/one-tap/client.ts - Server plugin:
packages/better-auth/src/plugins/one-tap/index.ts - Token verification:
verifyGoogleIdToken