Skip to main content
Scripts

Google Sign-In

Google Sign-In supports One Tap, personalized buttons, and automatic sign-in with a Google Account.

useScriptGoogleSignIn() loads Google Identity Services and adds helpers for initialization, buttons, and One Tap prompts.

Google Sign-In

View source

Nuxt Config Setup

Add this to your nuxt.config.ts to load Google Sign-In globally. Alternatively you can use the useScriptGoogleSignIn composable for more control.

export default defineNuxtConfig({
  scripts: {
    registry: {
      googleSignIn: {
        clientId: '123456789.apps.googleusercontent.com',
        trigger: 'onNuxtReady',
      }
    }
  }
})

useScriptGoogleSignIn()

The useScriptGoogleSignIn composable lets you have fine-grain control over when and how Google Sign-In is loaded on your site.

const { proxy } = useScriptGoogleSignIn()

proxy.accounts.id.prompt()

Please follow the Registry Scripts guide to learn more about advanced usage.

Live demo

Live DemoawaitingLoad

Sign in with your Google account:

Or

Composable API

useScriptGoogleSignIn() returns the standard script context (status, proxy, onLoaded, …) plus three helpers that wrap the most common flows. Every helper call merges schema options passed to the composable, so you don't have to repeat clientId, loginUri, uxMode, and related options.

const { initialize, renderButton, prompt, status, onLoaded, proxy } = useScriptGoogleSignIn({
  clientId: 'YOUR_CLIENT_ID',
  context: 'signin',
})

initialize(config?)

Calls google.accounts.id.initialize() with schema options merged with config. The helper only forwards the first call, which follows Google's guidance to initialize once per page and keeps remounts from resetting the active configuration.

initialize({
  callback: (response) => {
    // verify response.credential server-side
  }
})

renderButton(parent, config?)

Renders the personalized button and is safe to re-render on locale change or navigation. If Google Identity Services has not initialized yet, the helper tries to initialize it from the configured options. Popup mode requires a callback, supplied either to the composable or through initialize({ callback }); without one, renderButton() returns without rendering. Redirect mode can initialize without a callback.

<script setup lang="ts">
const { initialize, renderButton } = useScriptGoogleSignIn()
const buttonRef = useTemplateRef<HTMLDivElement>('buttonRef')

initialize({
  callback: response => console.log('Credential received', response),
})

watch(buttonRef, (el) => {
  if (el)
    renderButton(el, { text: 'continue_with' })
}, { immediate: true })
</script>

<template>
  <div ref="buttonRef" />
</template>

prompt(listener?)

Shows the One Tap prompt. In popup mode, call initialize({ callback }) first (or pass the callback to the composable); otherwise initialization is deferred and prompt() returns without showing One Tap.

prompt()

Switching locales

The button locale is a renderButton option, not an initialize one. To change the language, clear the container and re-render:

watch([locale, buttonRef], ([newLocale, el]) => {
  if (!el)
    return
  el.innerHTML = ''
  renderButton(el, { locale: newLocale })
}, { immediate: true })

Redirect UX mode

With uxMode: 'redirect', Google POSTs the credential to your loginUri server endpoint as application/x-www-form-urlencoded (fields: credential, g_csrf_token, select_by, …). The credential does not appear as a URL fragment after the redirect; it travels in the POST body, which your server handles before redirecting the browser. Validate the double-submit CSRF token before accepting the credential.

If you need the credential client-side (e.g. SPA with a separate API), use uxMode: 'popup' with a callback instead.

const { initialize, renderButton } = useScriptGoogleSignIn({
  uxMode: 'redirect',
  loginUri: 'https://your-server.com/auth/google',
})

initialize() // no callback needed in redirect mode

Moment notifications

With FedCM, Google removes display-moment notifications and the detailed skipped reason. Google also warns that the prompt callback might not receive every moment notification, so do not make application flow depend on it. If you inspect the remaining skipped and dismissed moments, avoid the removed methods:

const { initialize, prompt } = useScriptGoogleSignIn()

initialize({
  callback: response => console.log('Credential received', response),
})
prompt((notification) => {
  if (notification.isSkippedMoment()) {
    console.log('One Tap skipped')
  }

  if (notification.isDismissedMoment()) {
    console.log('Dismissed:', notification.getDismissedReason())
  }
})

See Google's FedCM migration guide for the removed methods and notification limitations.

Server-side verification

Always verify the credential token on your server. Google's Node.js authentication library checks the signature and the aud, iss, and exp claims:

pnpm add google-auth-library
server/api/auth/google.post.ts
import { OAuth2Client } from 'google-auth-library'

const client = new OAuth2Client()

export default defineEventHandler(async (event) => {
  const { credential } = await readBody(event)

  const ticket = await client.verifyIdToken({
    idToken: credential,
    audience: 'YOUR_CLIENT_ID',
  })
  const payload = ticket.getPayload()
  if (!payload) {
    throw createError({ statusCode: 401, message: 'Invalid token' })
  }

  const user = {
    email: payload.email,
    name: payload.name,
    picture: payload.picture,
    sub: payload.sub,
  }

  return { user }
})

Cross-Origin-Opener-Policy

Non-FedCM popup flows may require a compatible Cross-Origin-Opener-Policy so the popup can communicate with your page.

If you set COOP at all, use same-origin-allow-popups:

nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    '/login/**': {
      headers: { 'Cross-Origin-Opener-Policy': 'same-origin-allow-popups' },
    },
  },
})

Google's COOP setup guidance applies this header when you disable FedCM. Browser-rendered FedCM popups and redirect mode do not need it.

FedCM API support

Google now marks use_fedcm_for_prompt as deprecated and ignored. The registry's useFedcmForPrompt option still maps to that field, so changing it has no effect. Button FedCM uses use_fedcm_for_button, which this registry does not currently map.

Cross-origin iframes

For supported same-site cross-origin iframe integrations, add the allow attribute to every parent iframe:

<iframe src="https://your-app.com/login" allow="identity-credentials-get"></iframe>
With FedCM enabled, customizing the One Tap prompt position with prompt_parent_id is not supported. Google does not support One Tap in cross-site iframes.

Use an email address or Google user ID as the hint when revoking Sign in with Google consent:

const { onLoaded } = useScriptGoogleSignIn()

function revokeAccess(hint: string) {
  onLoaded(({ accounts }) => {
    accounts.id.revoke(hint, (response) => {
      if (response.successful) {
        console.log('Access revoked')
      }
      else {
        console.error('Revocation failed:', response.error)
      }
    })
  })
}

Best practices

Logout handling

If you enable automatic sign-in, call disableAutoSelect() when the user signs out. This prevents the same account from immediately signing in again:

function signOut() {
  // Clear your app's session
  user.value = null

  // Prevent One Tap from auto-selecting this account
  onLoaded(({ accounts }) => {
    accounts.id.disableAutoSelect()
  })
}

Hosted domain restriction

Use hd to optimize the sign-in flow for a Google Workspace domain:

const { initialize } = useScriptGoogleSignIn({
  hd: 'your-company.com',
})

initialize({ callback: handleCredentialResponse })

The client-side hd option is not an authorization check. Verify the ID token's hd claim on your server before restricting access to the domain.

Local development setup

To test Google Sign-In locally:

  1. Go to Google Cloud Console → Credentials
  2. Create or select an OAuth 2.0 Client ID (Web application type)
  3. Under Authorized JavaScript origins, add:
    • http://localhost:3000 (or your exact dev server origin)
  4. Save and copy your Client ID
Add the exact development origin, including its scheme and port. You don't need a redirect URI when using popup mode.

Then configure your environment:

.env
NUXT_PUBLIC_SCRIPTS_GOOGLE_SIGN_IN_CLIENT_ID=your-client-id.apps.googleusercontent.com

Guides

See Google's setup guide to create a client ID and configure the OAuth consent screen.
clientIdstring required

Your Google API client ID.

autoSelectboolean

Auto-select credentials when only one Google account is available.

context'signin' | 'signup' | 'use'

The context text for the One Tap prompt.

useFedcmForPromptboolean

Enable FedCM (Federated Credential Management) API support. Mandatory from August 2025.

cancelOnTapOutsideboolean = true

Cancel the One Tap prompt if the user clicks outside.

uxMode'popup' | 'redirect'

The UX mode for the sign-in flow.

loginUristring

The URI to redirect to after sign-in when using redirect UX mode.

itpSupportboolean

Enable Intelligent Tracking Prevention (ITP) support for Safari.

allowedParentOriginstring | string[]

Allowed parent origin(s) for iframe embedding.

hdstring

Restrict sign-in to a specific Google Workspace hosted domain.

Example

One Tap sign-in

Initialize Google Identity Services, then open the One Tap prompt:

<script setup lang="ts">
const { initialize, prompt } = useScriptGoogleSignIn({
  context: 'signin',
})

async function handleCredentialResponse(response: CredentialResponse) {
  await $fetch('/api/auth/google', {
    method: 'POST',
    body: { credential: response.credential }
  })
}

initialize({ callback: handleCredentialResponse })
onMounted(() => prompt())
</script>

Personalized Button

Render a personalized Sign in with Google button:

<script setup lang="ts">
const { initialize, renderButton } = useScriptGoogleSignIn()
const buttonRef = useTemplateRef<HTMLDivElement>('buttonRef')

function handleCredentialResponse(response: CredentialResponse) {
  console.log('Signed in!', response.credential)
}

initialize({ callback: handleCredentialResponse })

watch(buttonRef, (el) => {
  if (el) {
    renderButton(el, {
      type: 'standard',
      theme: 'outline',
      size: 'large',
      text: 'signin_with',
      shape: 'rectangular',
      logo_alignment: 'left',
    })
  }
}, { immediate: true })
</script>

<template>
  <div ref="buttonRef" />
</template>