Documentation

Single Sign-On (SSO)

Single Sign-On (SSO)

Enable social authentication with Google, Discord, Facebook, or custom OAuth2 providers in VitNode.

VitNode provides built-in OAuth2 single sign-on. Register an adapter in your API configuration, and authentication buttons automatically appear on /login and /register.

Quick start

Register an SSO adapter in apps/api/src/vitnode.api.config.ts:

apps/api/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({
  authorization: {
    ssoAdapters: [
      GoogleSSOApiPlugin({
        clientId: process.env.GOOGLE_CLIENT_ID!,
        clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
      }),
    ],
  },
})

A Google button automatically renders on your login and registration forms, four-color G included. The built-in adapters ship their own marks and colors - Discord and Facebook buttons are filled with their brand color - and a custom adapter can add its own.

On the login page

The provider buttons sit in their own column next to the email form, under one Continue with heading, so each button only needs the provider's name (screen readers still hear "Continue with Google"). On small screens the columns stack: providers first, then the form.

  • /login packs the providers into a two-column grid, so six of them take three rows instead of six. The passkey button goes right below.
  • /register stacks them full width, because the sign-up form next to them is taller anyway.
  • The method someone signed in with last - a provider, a passkey or a password - gets a Last used badge, and a last-used provider moves to the top of the grid. It's remembered in that browser only after a successful sign-in, so a visitor who gives up halfway through Discord's consent screen won't see it.
  • Only one kind of method enabled? The card shrinks back to a single column.

Supported Providers


Callback URLs

When configuring OAuth2 applications in provider developer consoles, use the following redirect URI pattern:

https://your-domain.com/login/sso/{provider_id}

For example: https://your-domain.com/login/sso/google.


When a user signs in via SSO:

  1. Known Identity: The provider account was linked before, so the user is signed straight into it.
  2. Existing Email Match: An account already uses that address. The visitor is asked for that account's password; once it checks out, the provider identity is linked to the account and they are signed in. From then on that provider signs them straight in (case 1). A social login can never take over an account on its own - the password is what proves ownership.
  3. New Visitor: A new user is created with their social display name and email. Avatar and real name stay empty until the member imports them from Connected accounts.

Signed-in members can also connect more providers, import their avatar and name, and disconnect accounts in Settings → Connected accounts - see Connected Accounts.

SSO confirms the email

A provider only hands VitNode an address it has verified itself (the built-in adapters refuse anything else), so a social sign-in doubles as email confirmation:

  • a new visitor starts out with emailVerified: true, even when an email adapter is configured and password sign-ups would have to confirm first;
  • a known identity or a freshly linked account whose address is still unconfirmed is marked confirmed - provided the address the provider returned is the one on the account. An account whose email was changed since the link was made is left alone.

Linking an existing account

The callback answers 409 with a short-lived, signed link offer - the account's email, whether it has a password, and a token good for ten minutes. The login page turns that into a "Connect Provider to your account" form: the email is shown read-only, the visitor types their password, and POST /users/sso/{providerId}/link verifies both before it writes the core_users_sso row and mints a session. The token carries the provider account id, so nothing about which identity gets linked is taken from the browser.

An account created by another provider has no password to confirm with. When an email adapter is configured the form offers Set a password (the ordinary reset flow) and asks them to try the provider again afterwards; without one it points back to the login page.

Every successful link emits user.sso.linked.

The address the provider returns is matched in its canonical form, so a Google account that reports jan.kowalski@gmail.com finds the member who registered as jankowalski@gmail.com instead of quietly becoming a second account. See One Mailbox, One Account.

Learn More