Logo VitNode
Documentation

Single Sign-On (SSO)

Custom SSO Adapter

Build a custom OAuth2 Single Sign-On adapter for any provider with the SSOApiPlugin interface.

VitNode includes built-in SSO adapters for Google, Discord, and Facebook. You can connect any other OAuth2 provider (e.g. GitHub, Apple, Slack) by implementing the SSOApiPlugin interface. State cookie generation, user session linking, and login buttons are handled automatically.

Quick start: GitHub SSO Adapter

Implement the 5 interface methods and register the adapter in vitnode.api.config.ts:

1. Build the Adapter

apps/api/src/utils/sso/github.ts
import type { SSOApiPlugin } from "@vitnode/core/api/models/sso"
import { getRedirectUri } from "@vitnode/core/api/models/sso"

export const GitHubSSOApiPlugin = ({
  clientId,
  clientSecret,
}: {
  clientId: string
  clientSecret: string
}): SSOApiPlugin => {
  const id = "github"
  const redirectUri = getRedirectUri(id)

  return {
    id,
    name: "GitHub",
    profileFields: ["avatar"],
    // 1. Build Authorization URL
    getUrl: ({ state }) =>
      `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=read:user,user:email&state=${state}`,

    // 2. Exchange Authorization Code for Token
    fetchToken: async (code) => {
      const response = await fetch("https://github.com/login/oauth/access_token", {
        method: "POST",
        headers: {
          Accept: "application/json",
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          client_id: clientId,
          client_secret: clientSecret,
          code,
          redirect_uri: redirectUri,
        }),
      })

      return await response.json()
    },

    // 3. Retrieve User Profile & Email
    fetchUser: async ({ access_token, token_type }) => {
      const headers = { Authorization: `${token_type} ${access_token}` }

      // Fetch Profile
      const userRes = await fetch("https://api.github.com/user", { headers })
      const user = await userRes.json()

      // Fetch Primary Email
      const emailRes = await fetch("https://api.github.com/user/emails", { headers })
      const emails: Array<{ email: string; primary: boolean; verified: boolean }> =
        await emailRes.json()
      const primaryEmail = emails.find((e) => e.primary && e.verified)?.email
      if (!primaryEmail) throw new Error("GitHub account has no verified email")

      return {
        id: String(user.id),
        email: primaryEmail,
        username: user.login,
        avatarUrl: user.avatar_url,
      }
    },
  }
}

2. Register in API Configuration

apps/api/src/vitnode.api.config.ts
import { buildApiConfig } from "@vitnode/core/vitnode.config"
import { GitHubSSOApiPlugin } from "./utils/sso/github"

export const vitNodeApiConfig = buildApiConfig({
  authorization: {
    ssoAdapters: [
      GitHubSSOApiPlugin({
        clientId: process.env.GITHUB_CLIENT_ID!,
        clientSecret: process.env.GITHUB_CLIENT_SECRET!,
      }),
    ],
  },
})

A GitHub login button automatically renders on /login and /register, and /login/sso/github routes incoming authentication requests. It is a neutral, text-only button until you give the adapter an icon and a brand color.


SSOApiPlugin Interface Reference

Prop

Type

See Provider Icons and Colors for the accepted icon and brandColor formats, and for registering a React component instead.

Profile data

Connected accounts can import a member's avatar, first name and last name from your provider. Two steps:

  1. Declare what the provider can supply in profileFields.
  2. Return those values from fetchUser as avatarUrl, firstName and lastName.
fetchUser: async ({ access_token, token_type }) => {
  // ...
  return {
    id: String(user.id),
    email: primaryEmail,
    username: user.login,
    avatarUrl: user.avatar_url ?? null,
  }
},
profileFields: ["avatar"],

All three are optional, so adapters written before this still work - they just have nothing to import. A few rules keep imports honest:

  • Return null (or leave it out) when the provider has no value. VitNode never erases a member's value with an empty one.
  • Only send separate first and last names the provider actually has. Don't split a full name - "Mary Ann Smith" has no safe answer.
  • avatarUrl must be https. VitNode downloads and re-stores it itself, with SSRF protection and size and type checks, so you don't have to.
  • Don't add scopes just for profile data. If a field needs extra permission, leave it out of profileFields.

Built-in SSO Adapters