Skip to main content

How Social Login Works

Add Google, Apple, or Facebook sign-in to your app. nauth-toolkit uses a redirect-first OAuth flow: the backend owns the entire OAuth lifecycle. The frontend just opens a URL and handles the redirect back.

Supported Providers

ProviderPackageNotes
Google@nauth-toolkit/social-googleWeb OAuth + native mobile token verification
Apple@nauth-toolkit/social-appleJWT client secret auto-managed, form_post callback
Facebook@nauth-toolkit/social-facebookStandard OAuth 2.0
Sample apps

Social login is implemented in the nauth example apps — see the NestJS, Express, and Fastify examples for social routes, and the React/Angular examples for frontend callback handling.

Prerequisites

You have completed the Basic Auth Flows guide and have working signup, login, and challenge endpoints. You also need:

  • OAuth credentials from at least one provider (Google Cloud Console, Apple Developer, or Facebook Developer)
  • A decision on Token Delivery mode: cookies (web), json (mobile/SPA), or hybrid

How the Flow Works

Cookies mode: Tokens are set as httpOnly cookies during the redirect. No further action needed.

JSON mode: The redirect includes an exchangeToken query parameter. The frontend calls POST /auth/social/exchange to receive tokens in the response body.

Challenges after social login: If MFA or phone verification is required, the redirect includes an exchangeToken regardless of delivery mode. The frontend calls /auth/social/exchange to receive the challenge, completes it, then receives tokens.

Configuration

config/auth.config.ts
social: {
redirect: {
frontendBaseUrl: 'https://app.example.com',

// Prevent open redirects — only allow relative returnTo paths
allowAbsoluteReturnTo: false,
allowedReturnToOrigins: ['https://app.example.com'],
},

google: {
enabled: true,
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackUrl: 'https://api.example.com/auth/social/google/callback',
scopes: ['openid', 'email', 'profile'],
autoLink: true, // Auto-link if email matches existing account
allowSignup: true, // Allow new user creation via social login
},

apple: {
enabled: true,
clientId: process.env.APPLE_CLIENT_ID,
teamId: process.env.APPLE_TEAM_ID,
keyId: process.env.APPLE_KEY_ID,
privateKeyPem: process.env.APPLE_PRIVATE_KEY_PEM,
callbackUrl: 'https://api.example.com/auth/social/apple/callback',
scopes: ['name', 'email'],
},

facebook: {
enabled: true,
clientId: process.env.FACEBOOK_CLIENT_ID,
clientSecret: process.env.FACEBOOK_CLIENT_SECRET,
callbackUrl: 'https://api.example.com/auth/social/facebook/callback',
scopes: ['email', 'public_profile'],
},
},

See Configuration > Social Login for the full reference including OAuth parameters, native mobile settings, and advanced options.

OAuth Parameters

Control provider behavior on a per-request basis:

ProviderKey Parameters
Googleprompt (select_account, consent, none), hd (restrict to Workspace domain), login_hint
Facebookauth_type (rerequest, reauthenticate), display (page, popup, touch)
Applenonce (ID token validation)

Parameters can be set as defaults in config (oauthParams) or per-request from the frontend.

Shared Routes

All providers use the same route structure. The SocialRedirectHandler (from @nauth-toolkit/core) handles the logic — your routes just delegate to it.

EndpointMethodAuthPurpose
/auth/social/:provider/redirectGETPublicStart OAuth flow (redirects to provider)
/auth/social/:provider/callbackGETPublicProvider callback (Google/Facebook)
/auth/social/:provider/callbackPOSTPublicProvider callback (Apple form_post)
/auth/social/exchangePOSTPublicExchange exchangeToken for tokens or challenge
/auth/social/:provider/verifyPOSTPublicNative mobile token verification

Web OAuth Routes

src/auth/social-redirect.controller.ts
import { Controller, Get, Post, Param, Query, Body, Redirect } from '@nestjs/common';
import {
Public,
SocialRedirectHandler,
AuthResponseDTO,
SocialCallbackFormDTO,
SocialCallbackQueryDTO,
SocialRedirectCallbackResponseDTO,
SocialExchangeDTO,
StartSocialRedirectQueryDTO,
StartSocialRedirectResponseDTO,
TokenDelivery,
} from '@nauth-toolkit/nestjs';

@Controller('auth/social')
export class SocialRedirectController {
constructor(private readonly socialRedirect: SocialRedirectHandler) {}

@Public()
@Redirect()
@Get(':provider/redirect')
async start(
@Param('provider') provider: string,
@Query() dto: StartSocialRedirectQueryDTO,
): Promise<StartSocialRedirectResponseDTO> {
return await this.socialRedirect.start(provider, dto);
}

@Public()
@Redirect()
@Get(':provider/callback')
async callbackGet(
@Param('provider') provider: string,
@Query() dto: SocialCallbackQueryDTO,
): Promise<SocialRedirectCallbackResponseDTO> {
return await this.socialRedirect.callback(provider, dto);
}

// Apple uses POST with form_post response mode
@Public()
@Redirect()
@Post(':provider/callback')
async callbackPost(
@Param('provider') provider: string,
@Body() dto: SocialCallbackFormDTO,
): Promise<SocialRedirectCallbackResponseDTO> {
return await this.socialRedirect.callback(provider, dto);
}

@Public()
@Post('exchange')
@TokenDelivery('json')
async exchange(@Body() dto: SocialExchangeDTO): Promise<AuthResponseDTO> {
return await this.socialRedirect.exchange(dto.exchangeToken);
}
}

Native Mobile Verification

For Capacitor/React Native apps that receive tokens directly from native SDKs (Google Sign-In SDK, Facebook SDK, etc.):

src/auth/social-redirect.controller.ts
import { Inject, BadRequestException, Optional } from '@nestjs/common';
import { VerifyTokenDTO } from '@nauth-toolkit/nestjs';
import { GoogleSocialAuthService } from '@nauth-toolkit/social-google/nestjs';
import { AppleSocialAuthService } from '@nauth-toolkit/social-apple/nestjs';
import { FacebookSocialAuthService } from '@nauth-toolkit/social-facebook/nestjs';

// Inside SocialRedirectController:
constructor(
private readonly socialRedirect: SocialRedirectHandler,
@Optional() @Inject(GoogleSocialAuthService)
private readonly googleAuth?: GoogleSocialAuthService,
@Optional() @Inject(AppleSocialAuthService)
private readonly appleAuth?: AppleSocialAuthService,
@Optional() @Inject(FacebookSocialAuthService)
private readonly facebookAuth?: FacebookSocialAuthService,
) {}

@Public()
@Post(':provider/verify')
async verifyNative(@Body() dto: VerifyTokenDTO): Promise<AuthResponseDTO> {
const provider = dto.provider;

if (provider === 'google') {
if (!this.googleAuth) throw new BadRequestException('Google OAuth is not configured');
return await this.googleAuth.verifyToken(dto);
}

if (provider === 'apple') {
if (!this.appleAuth) throw new BadRequestException('Apple OAuth is not configured');
return await this.appleAuth.verifyToken(dto);
}

if (provider === 'facebook') {
if (!this.facebookAuth) throw new BadRequestException('Facebook OAuth is not configured');
return await this.facebookAuth.verifyToken(dto);
}

throw new BadRequestException(`Unsupported provider: ${provider}`);
}

Native mobile apps always use JSON mode — tokens are returned in the response body.

Web vs Native Comparison

AspectWeb (Redirect-First)Native Mobile (Token Verify)
FlowBackend redirects to provider, then callbackNative SDK authenticates, app sends tokens
Endpoints/redirect, /callback, /exchange/verify
Token DeliveryCookies or JSON (via exchange)JSON (always)
Exchange NeededOnly when challenge pending (cookies mode)Never — tokens from verify directly
Frontend SDKloginWithSocial(), exchangeSocialRedirect()verifyNativeSocial()

Account Linking

When a user signs in with a social provider and their email matches an existing account:

SettingBehavior
autoLink: trueSocial account is automatically linked to the existing account
autoLink: falseReturns an error — user must explicitly link via security settings
allowSignup: trueNew users can create accounts via social login
allowSignup: falseOnly existing users can use social login

Users can manage linked accounts from their security settings:

EndpointMethodAuthPurpose
/auth/social/linkedGETProtectedList linked social accounts
/auth/social/linkPOSTProtectedLink a social account
/auth/social/unlinkPOSTProtectedUnlink a social account
/auth/social/can-set-passwordGETProtectedCheck if social-only user can set a password
/auth/social/set-passwordPOSTProtectedSet password for social-only user
Session auth method

After social login, authMethod is set to the provider name (e.g., google, apple, facebook) instead of password. This is available in the response and stored on the cached user as sessionAuthMethod.

Challenges After Social Login

Social login can return challenges (MFA, phone verification) just like password login. The challenge is returned via the exchange endpoint:

  1. Backend redirect includes exchangeToken in the callback URL
  2. Frontend calls POST /auth/social/exchange with the exchangeToken
  3. Exchange returns the challenge (e.g., MFA_REQUIRED)
  4. Frontend completes the challenge via /auth/respond-challenge
  5. Tokens are issued

In cookies mode without challenges, tokens are set as cookies during the redirect — no exchange call needed.

Frontend Integration

Start social login

// Using the frontend SDK:
await client.loginWithSocial('google', {
returnTo: '/auth/callback',
appState: 'invite-code-123', // Optional, returned after flow
});

// With OAuth parameter overrides:
await client.loginWithSocial('google', {
returnTo: '/dashboard',
oauthParams: { prompt: 'select_account', hd: 'company.com' },
});

Handle the callback

The backend redirects back to your returnTo URL with:

  • Cookies mode (no challenge): Just appState — tokens are already set as cookies
  • Cookies mode (challenge): exchangeToken + appState — call exchange, complete challenge, receive cookies
  • JSON mode: exchangeToken + appState — always call exchange to receive tokens

For Angular, use the socialRedirectCallbackGuard for a guard-only callback route. For React, handle the exchangeToken query parameter in your callback page.

The appState value you pass to loginWithSocial() is returned in the callback URL as a query parameter. It's also stored in the SDK (accessible via getLastOauthState()). Use it for invite codes, referral tracking, or deep-link targets.

See the Frontend Social Authentication Guide for complete implementation examples.

Security notes
  • appState is not secret — it appears in browser history and logs
  • Open redirect protection — keep allowAbsoluteReturnTo: false unless you truly need absolute return URLs
  • Cluster-safe — OAuth state is stored via transient StorageAdapter (Redis/DB), so the flow works across multiple containers
Apple form_post

Apple uses POST with form_post response mode for callbacks. Ensure your backend can parse application/x-www-form-urlencoded bodies. Express handles this by default; Fastify requires the @fastify/formbody plugin.

Error Codes

Error CodeReasonUser Action
SOCIAL_CONFIG_MISSINGProvider not enabled in configCheck backend configuration
SOCIAL_TOKEN_INVALIDOAuth flow failed (denied, expired)Try again
SOCIAL_EMAIL_REQUIREDProvider did not return a verified emailVerify email with provider first
SOCIAL_ACCOUNT_LINKEDSocial account linked to another userUnlink from other account first
SOCIAL_ACCOUNT_NOT_FOUNDSocial account not linked to any userLink the account or sign in differently
SOCIAL_ACCOUNT_EXISTSProvider and provider ID already registeredUse existing account or sign in with that account
SIGNUP_DISABLEDallowSignup: false and no matching accountContact admin or sign up with email
VALIDATION_FAILEDRequest payload failed validationCheck request parameters

What's Next

Choose a provider to set up: