Socialite
OAuth-based social authentication for Rudder. Built-in providers for GitHub, Google, Facebook, and Apple, plus an extension point for custom OAuth providers. Use it to add "Sign in with ..." buttons to your app without hand-writing OAuth flows.
Install
pnpm add @rudderjs/socialite// config/socialite.ts
import { Env } from '@rudderjs/support'
export default {
github: {
clientId: Env.get('GITHUB_CLIENT_ID', ''),
clientSecret: Env.get('GITHUB_CLIENT_SECRET', ''),
redirectUrl: Env.get('GITHUB_REDIRECT_URL', 'http://localhost:3000/auth/github/callback'),
},
google: {
clientId: Env.get('GOOGLE_CLIENT_ID', ''),
clientSecret: Env.get('GOOGLE_CLIENT_SECRET', ''),
redirectUrl: Env.get('GOOGLE_REDIRECT_URL', 'http://localhost:3000/auth/google/callback'),
scopes: ['openid', 'profile', 'email'],
},
}The provider is auto-discovered. No external dependencies — every built-in provider uses native fetch.
Routes
The OAuth dance is two routes per provider — one to start, one to receive the callback:
// routes/web.ts
import { Socialite } from '@rudderjs/socialite'
Route.get('/auth/github', () => Socialite.driver('github').redirect())
Route.get('/auth/github/callback', async (req) => {
const user = await Socialite.driver('github').user(req)
// Find or create your User by user.getEmail() / user.getId()
const local = await User.firstOrCreate({ email: user.getEmail() }, {
name: user.getName() ?? user.getNickname(),
avatar: user.getAvatar(),
githubId: user.getId(),
})
await Auth.login(local)
return Response.redirect('/')
})Socialite.driver(name).redirect() returns a 302 Response. Socialite.driver(name).user(req) exchanges the authorization code for tokens and fetches the user profile.
The user object
const user = await Socialite.driver('github').user(req)
user.getId() // provider-specific unique ID
user.getName() // full name (may be null)
user.getEmail() // email (may be null — depends on provider + scopes)
user.getAvatar() // avatar URL (may be null)
user.getNickname() // username / handle (may be null)
user.token // OAuth access token
user.refreshToken // OAuth refresh token (may be null)
user.expiresIn // seconds until expiry (may be null)
user.getRaw() // raw provider responseFor mobile apps or other flows where you already have an access token, getUserByToken(token) skips the code-exchange step:
const user = await Socialite.driver('github').getUserByToken(accessToken)If you only need the tokens (no user fetch):
const { accessToken, refreshToken, expiresIn } =
await Socialite.driver('github').getAccessToken(code)Scopes
Each provider has sensible defaults. Override per-call:
Socialite.driver('google').setScopes(['openid', 'email']) // replace
Socialite.driver('github').withScopes(['repo', 'gist']) // append| Provider | Default scopes |
|---|---|
github |
read:user, user:email |
google |
openid, profile, email |
facebook |
email, public_profile |
apple |
name, email |
Sign-in-with-Apple
Apple's OAuth flow has two non-standard requirements the driver handles automatically — but you have to provide extra config.
client_secret must be a freshly-signed ES256 JWT (not a static string), so add three Apple-specific fields:
// config/socialite.ts
import { readFileSync } from 'node:fs'
import type { AppleSocialiteConfig } from '@rudderjs/socialite'
export default {
apple: {
clientId: Env.get('APPLE_CLIENT_ID', ''), // Service ID, e.g. com.example.app
redirectUrl: Env.get('APPLE_REDIRECT_URL', ''),
teamId: Env.get('APPLE_TEAM_ID', ''), // 10-char Team ID
keyId: Env.get('APPLE_KEY_ID', ''), // 10-char Key ID for the .p8
privateKey: readFileSync(Env.get('APPLE_PRIVATE_KEY_PATH', ''), 'utf8'),
clientSecret: '', // unused; left for type compat
} satisfies AppleSocialiteConfig,
}Download the .p8 file once from the Apple Developer portal. The driver mints a fresh JWT per token exchange (5-minute lifetime by default; override with clientSecretTtl).
id_token returned from Apple is verified end-to-end before any user data is trusted: signature against Apple's JWKS (https://appleid.apple.com/auth/keys, cached for 1h), iss === https://appleid.apple.com, aud matches your clientId, exp in the future, and sub non-empty.
State parameter
Pass a state string for CSRF protection — generate it before redirect, store it in the session, verify on callback:
const state = crypto.randomUUID()
await Session.put('oauth.state', state)
const url = Socialite.driver('github').getRedirectUrl(state)
return Response.redirect(url)// callback
const expected = await Session.pull('oauth.state')
if (req.query.state !== expected) abort(400, 'State mismatch')State isn't generated automatically — apps that need CSRF protection on the OAuth flow should follow this pattern.
Custom providers
Extend SocialiteDriver to wire any OAuth 2.0 service (the built-in GitHubProvider, GoogleProvider, etc. extend it too):
import { Socialite, SocialiteDriver, SocialUser } from '@rudderjs/socialite'
class GitLabProvider extends SocialiteDriver {
protected defaultScopes() { return ['read_user'] }
protected authUrl() { return 'https://gitlab.com/oauth/authorize' }
protected tokenUrl() { return 'https://gitlab.com/oauth/token' }
protected userUrl() { return 'https://gitlab.com/api/v4/user' }
protected mapToUser(data, token, refreshToken) {
return new SocialUser({
id: String(data.id),
name: data.name,
email: data.email,
avatar: data.avatar_url,
nickname: data.username,
token, refreshToken, raw: data,
})
}
}
Socialite.extend('gitlab', (config) => new GitLabProvider(config))Add the matching config block to config/services.ts and you're done.
Provider config shape
interface SocialiteProviderConfig {
clientId: string
clientSecret: string
redirectUrl: string
scopes?: string[]
}Pitfalls
- Email missing. GitHub doesn't expose primary email when only
user:emailis granted and the user marks all emails as private. Handle thenullcase in your sign-up flow — fall back to nickname-based account creation, or prompt for email. - Apple's first-login quirk. Apple sends
nameonly on the first authorization. Persist it on first login; subsequent logins won't include it. - No state parameter. Without CSRF state, an attacker can complete an OAuth flow and bind a victim's social identity to their own session. Always generate, store, and verify state.
- Driver instance caching.
Socialite.driver('github')returns the same instance on repeat calls —setScopes()mutates that shared instance. For per-route customization, capture the instance per call site.