Single Sign-On (SSO)

Google

Add Google sign-in to VitNode - create an OAuth client, set the exact redirect URI and register the Google SSO adapter in your API config.

Google is the provider most people want first. The whole job is: create an OAuth client in the Google Auth Platform, give it the redirect URI VitNode already decided on, and pass the two credentials to GoogleSSOApiPlugin.

Quick start

If you already have a client ID and secret, this is the entire integration.

.env
GOOGLE_CLIENT_ID=1234567890-abc.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxxxxx
src/vitnode.api.config.ts
import { GoogleSSOApiPlugin } from '@vitnode/core/api/adapters/sso/google'
import { buildApiConfig } from '@vitnode/core/vitnode.config'

export const vitNodeApiConfig = buildApiConfig({
  plugins: [],
  authorization: {
    ssoAdapters: [
      GoogleSSOApiPlugin({
        clientId: process.env.GOOGLE_CLIENT_ID,
        clientSecret: process.env.GOOGLE_CLIENT_SECRET,
      }),
    ],
  },
})

Restart the API and a Google button appears on /login. The rest of this page is how to get those two values.

Set up the OAuth client

Sign in to Google Cloud

Go to the Google Cloud Console and sign in with the account that should own the OAuth client. It does not have to be the account you sign in to your own site with.

Pick or create a project

An OAuth client belongs to a project, so select one from the project picker in the top bar - or create one if this is a fresh account. Name it after your site; the name is internal and never shown to visitors.

Open the Google Auth Platform

Type Google Auth Platform into the console's search bar and open the first result. This is the section that used to be called "OAuth consent screen", and it is where both the consent details and the clients live.

Google will not issue a client until it knows what to show on the consent screen. Provide the App name - this is the name visitors read in "Sign in to continue to ..." - and a Support email.

Then, under Audience, choose External so anyone with a Google account can sign in. Internal restricts sign-in to a single Google Workspace organisation, which is almost never what a public site wants.

Create the client and set the redirect URI

Go to Clients in the left navigation and press Create client.

  1. Application type: Web application.
  2. Name: anything - this one is internal too.
  3. Under Authorized redirect URIs, add the URI for every origin you run.

VitNode builds this URI itself, from NEXT_PUBLIC_WEB_URL, so it has to match character for character - no trailing slash, no locale prefix.

EnvironmentAuthorized redirect URI
Development (NEXT_PUBLIC_WEB_URL unset)http://localhost:3000/login/sso/google
Productionhttps://your-domain.com/login/sso/google

Add both. Google allows several redirect URIs per client, and one client for dev and prod is fine while you are getting started.

redirect_uri_mismatch

This is the error you will get if the URI differs by so much as a trailing slash. It comes from Google, not from VitNode, and it is always this field.

Copy the client ID and secret

Open the client you just created from the Clients list. The Client ID and Client secret are on the right-hand side of the detail panel. Copy both - Google will let you see the secret again, but you may as well paste it straight into .env.

Set the environment variables

These names are not magic - they are whatever you read in your config on the next step. What matters is that they are server-side variables, so no NEXT_PUBLIC_ prefix: that prefix is what marks a value for the browser bundle, and a client secret in the browser bundle is a client secret you have published.

.env
GOOGLE_CLIENT_ID=1234567890-abc.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxxxxx

Register the adapter

Add GoogleSSOApiPlugin to authorization.ssoAdapters in your API config. Note that it is a plain factory function, not a class - there is no new.

src/vitnode.api.config.ts
import { GoogleSSOApiPlugin } from '@vitnode/core/api/adapters/sso/google'
import { buildApiConfig } from '@vitnode/core/vitnode.config'

export const vitNodeApiConfig = buildApiConfig({
  plugins: [],
  authorization: {
    ssoAdapters: [
      GoogleSSOApiPlugin({
        clientId: process.env.GOOGLE_CLIENT_ID,
        clientSecret: process.env.GOOGLE_CLIENT_SECRET,
      }),
    ],
  },
})

The redirect URI is computed when this file is evaluated, so restart the API rather than relying on a hot reload:

bun dev
pnpm dev
npm run dev

Verify it works

Ask the API what it thinks it supports:

curl http://localhost:3000/api/@vitnode/core/middleware
# {"isEmail":false,"sso":[{"id":"google","name":"Google"}]}

Then open /login and press Google. You should be sent to Google's consent screen, bounced back to /login/sso/google?code=...&state=..., and land on the front page signed in. Your account now has a row in core_users_sso with providerId google.

Publish the app

While the client is unpublished, Google restricts sign-in to the test users you listed and shows an "unverified app" warning. Once the round trip works, go to Google Auth Platform → Audience and press Publish app.

What the adapter asks Google for

Useful when you are debugging a consent screen that shows more or fewer permissions than you expected.

SettingValue
Authorize URLhttps://accounts.google.com/o/oauth2/auth
Token URLhttps://oauth2.googleapis.com/token
Profile URLhttps://www.googleapis.com/oauth2/v1/userinfo
Scopesopenid profile email
Provider idgoogle
Fields readid, email, name, verified_email

The account name VitNode creates comes from Google's name field, stripped of characters that cannot appear in a profile URL.

Gotchas

Unverified Google emails are rejected

The adapter reads verified_email and throws a 400 when it is false, so a Google account with an unconfirmed address cannot sign in at all. The visitor sees the generic error screen rather than an explanation, which is the one place this is worth knowing about in advance.

A missing key fails on click, not on boot

A GoogleSSOApiPlugin whose clientId is undefined registers happily - the button renders and the provider is listed - and throws Missing Google client ID the moment somebody presses it. If the button is there but does nothing except raise an error toast, check the environment before you check the console.

An email that already has a password account is a dead end

If someone registered with the same address using the password form, the callback answers 409 and offers them the login page instead. There is no link-an-existing-account screen yet - see how the callback handles accounts.

Next