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
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
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:
- Declare what the provider can supply in
profileFields. - Return those values from
fetchUserasavatarUrl,firstNameandlastName.
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. avatarUrlmust behttps. 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.