Social Auth (OAuth)
CruzJS supports social authentication via the SocialAuthModule, which is included in StartModule. Seven providers are supported out of the box: GitHub, Google, Discord, Twitter (X), LinkedIn, Microsoft, and Apple.
The SocialAuthModule is included in StartModule, so no additional registration is needed if you use StartModule:
import { StartModule } from '@cruzjs/start/start.module';
export default createCruzApp({ modules: [StartModule],});Supported Providers
Section titled “Supported Providers”| Provider | PKCE | Env Vars Required |
|---|---|---|
| GitHub | No | GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET |
| Yes | GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET | |
| Discord | No | DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET |
| Twitter (X) | Yes | TWITTER_CLIENT_ID, TWITTER_CLIENT_SECRET |
| No | LINKEDIN_CLIENT_ID, LINKEDIN_CLIENT_SECRET | |
| Microsoft | No | MICROSOFT_CLIENT_ID, MICROSOFT_CLIENT_SECRET |
| Apple | No | APPLE_CLIENT_ID, APPLE_TEAM_ID, APPLE_KEY_ID, APPLE_PRIVATE_KEY |
Provider Configuration
Section titled “Provider Configuration”Set environment variables for each provider you want to enable. Only providers with configured credentials will appear in the login UI.
# GitHubGITHUB_CLIENT_ID=your-github-client-idGITHUB_CLIENT_SECRET=your-github-client-secret
# GoogleGOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.comGOOGLE_CLIENT_SECRET=your-google-client-secret
# DiscordDISCORD_CLIENT_ID=your-discord-client-idDISCORD_CLIENT_SECRET=your-discord-client-secret
# Twitter (X)TWITTER_CLIENT_ID=your-twitter-client-idTWITTER_CLIENT_SECRET=your-twitter-client-secret
# LinkedInLINKEDIN_CLIENT_ID=your-linkedin-client-idLINKEDIN_CLIENT_SECRET=your-linkedin-client-secret
# MicrosoftMICROSOFT_CLIENT_ID=your-microsoft-client-idMICROSOFT_CLIENT_SECRET=your-microsoft-client-secret
# AppleAPPLE_CLIENT_ID=your-apple-client-idAPPLE_TEAM_ID=your-apple-team-idAPPLE_KEY_ID=your-apple-key-idAPPLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"Multi-Binding Pattern
Section titled “Multi-Binding Pattern”Providers use the OAUTH_PROVIDER multi-injection token. Each provider is bound separately, and the framework collects all bindings at runtime:
import { OAUTH_PROVIDER } from '@cruzjs/start/social-auth';
// In a custom module, you can add additional providers:@Module({ providers: [ { provide: OAUTH_PROVIDER, useClass: CustomOAuthProvider, multi: true, }, ],})export class CustomAuthModule {}OAuth Flow
Section titled “OAuth Flow”1. Redirect to Provider
Section titled “1. Redirect to Provider”The user clicks a social login button, which redirects to:
/auth/:providerFor example, /auth/github redirects to GitHub’s OAuth consent screen.
2. Provider Callback
Section titled “2. Provider Callback”After the user authorizes, the provider redirects back to:
/auth/:provider/callback3. Callback Processing
Section titled “3. Callback Processing”The callback handler performs:
- CSRF validation — verifies the
stateparameter (nonce-basedOAuthState) - Code exchange — exchanges the authorization code for access tokens
- User info fetch — retrieves the user’s email, name, and avatar from the provider
- Account resolution — one of three paths:
- Existing account: Provider account already linked. Load the identity and update tokens.
- Email match: No linked account, but an identity exists with the same email. Link the provider and set
accountLinked: true. - New user: Create a new identity (email is auto-verified via OAuth), create the provider link, and dispatch
IdentityCreatedEvent.
- Session creation — create a session and return the token
Callback Result
Section titled “Callback Result”type SocialAuthResult = { user: { id: string; email: string; name: string; emailVerified: boolean }; session: { token: string; expiresAt: string }; isNewUser: boolean; accountLinked: boolean;};OAuthState (CSRF Protection)
Section titled “OAuthState (CSRF Protection)”Each authorization URL includes a cryptographically random state nonce. The nonce is stored server-side with a short TTL and validated on callback. This prevents CSRF attacks where an attacker could trick a user into linking the attacker’s OAuth account.
Connected Accounts
Section titled “Connected Accounts”Users can view and manage their connected social accounts.
List Connected Accounts
Section titled “List Connected Accounts”const { data } = trpc.socialAuth.getConnectedAccounts.useQuery();// data: [{ provider: 'github', email: 'user@github.com', connectedAt: '...' }, ...]Disconnect an Account
Section titled “Disconnect an Account”trpc.socialAuth.disconnectAccount.useMutation().mutate({ provider: 'github',});tRPC Procedures
Section titled “tRPC Procedures”| Procedure | Type | Auth | Description |
|---|---|---|---|
socialAuth.getAvailableProviders | query | Public | List configured providers |
socialAuth.getAuthUrl | mutation | Public | Generate authorization URL for a provider |
socialAuth.getConnectedAccounts | query | Protected | List connected OAuth accounts |
socialAuth.disconnectAccount | mutation | Protected | Remove a connected OAuth provider |
Example: Social Login Buttons
Section titled “Example: Social Login Buttons”function SocialLoginButtons() { const { data: providers } = trpc.socialAuth.getAvailableProviders.useQuery();
return ( <div className="flex flex-col gap-2"> {providers?.map((provider) => ( <a key={provider} href={`/auth/${provider}`} className="btn btn-outline" > Sign in with {provider.charAt(0).toUpperCase() + provider.slice(1)} </a> ))} </div> );}Example: Connected Accounts Settings
Section titled “Example: Connected Accounts Settings”function ConnectedAccounts() { const { data: accounts } = trpc.socialAuth.getConnectedAccounts.useQuery(); const { data: providers } = trpc.socialAuth.getAvailableProviders.useQuery(); const disconnect = trpc.socialAuth.disconnectAccount.useMutation();
const connected = new Set(accounts?.map((a) => a.provider));
return ( <div> <h3>Connected Accounts</h3> {providers?.map((provider) => ( <div key={provider} className="flex items-center justify-between py-2"> <span>{provider}</span> {connected.has(provider) ? ( <button onClick={() => disconnect.mutate({ provider })}> Disconnect </button> ) : ( <a href={`/auth/${provider}`}>Connect</a> )} </div> ))} </div> );}