Skip to main content

Add OAuth Providers to Your Boilerplate's Auth 2026

·StarterPick Team
oauthnextauthauthguide2026

TL;DR

Adding OAuth to NextAuth takes 30 minutes per provider. The work is: (1) create an OAuth app in the provider's developer console, (2) add the provider to NextAuth config, (3) handle account linking if users can have both email and OAuth. This guide covers the full pattern for the 5 most common providers.


NextAuth Provider Configuration

// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth';
import GitHubProvider from 'next-auth/providers/github';
import GoogleProvider from 'next-auth/providers/google';
import LinkedInProvider from 'next-auth/providers/linkedin';
import TwitterProvider from 'next-auth/providers/twitter';
import MicrosoftEntraIdProvider from 'next-auth/providers/microsoft-entra-id';
import { PrismaAdapter } from '@auth/prisma-adapter';
import { prisma } from '@/lib/prisma';

export const authOptions = {
  adapter: PrismaAdapter(prisma),
  providers: [
    GitHubProvider({
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    }),
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
    LinkedInProvider({
      clientId: process.env.LINKEDIN_CLIENT_ID!,
      clientSecret: process.env.LINKEDIN_CLIENT_SECRET!,
    }),
    TwitterProvider({
      clientId: process.env.TWITTER_CLIENT_ID!,
      clientSecret: process.env.TWITTER_CLIENT_SECRET!,
      version: '2.0',
    }),
    MicrosoftEntraIdProvider({
      clientId: process.env.AZURE_AD_CLIENT_ID!,
      clientSecret: process.env.AZURE_AD_CLIENT_SECRET!,
      tenantId: process.env.AZURE_AD_TENANT_ID!, // or 'common' for multi-tenant
    }),
  ],
  // ...
};

Provider Setup Guides

GitHub OAuth App

  1. GitHub → Settings → Developer settings → OAuth Apps → New OAuth App
  2. Authorization callback URL: https://yoursaas.com/api/auth/callback/github
  3. Copy Client ID and Client Secret
GITHUB_CLIENT_ID=Iv1.xxxxxxxxxxxxxxxx
GITHUB_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Google OAuth

  1. Google Cloud Console → APIs & Services → Credentials → Create OAuth 2.0 Client
  2. Authorized redirect URIs: https://yoursaas.com/api/auth/callback/google
  3. Add test users during development (required until verified)
GOOGLE_CLIENT_ID=xxxxx.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-xxxxx

Microsoft Entra ID (Azure AD)

For B2B SaaS targeting Microsoft-heavy enterprises:

  1. Azure Portal → Microsoft Entra ID → App Registrations → New Registration
  2. Redirect URI: https://yoursaas.com/api/auth/callback/microsoft-entra-id
  3. Certificates & Secrets → New client secret
AZURE_AD_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
AZURE_AD_CLIENT_SECRET=xxxxx~xxxxx
AZURE_AD_TENANT_ID=common  # Or specific tenant ID for single-org

Prisma Schema for Multiple Accounts

// NextAuth requires these models for account linking
model Account {
  id                String  @id @default(cuid())
  userId            String
  type              String
  provider          String
  providerAccountId String
  refresh_token     String? @db.Text
  access_token      String? @db.Text
  expires_at        Int?
  token_type        String?
  scope             String?
  id_token          String? @db.Text
  session_state     String?

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([provider, providerAccountId])
}

model User {
  id            String    @id @default(cuid())
  name          String?
  email         String?   @unique
  emailVerified DateTime?
  image         String?
  accounts      Account[]
  sessions      Session[]
}

Account Linking

Allow the same user to log in with multiple providers:

// authOptions configuration
export const authOptions = {
  // ...
  callbacks: {
    async signIn({ user, account, profile }) {
      // Check if email already exists with different provider
      if (account?.provider !== 'credentials') {
        const existingUser = await prisma.user.findUnique({
          where: { email: user.email! },
          include: { accounts: true },
        });

        if (existingUser) {
          // Check if this OAuth provider is already linked
          const linkedAccount = existingUser.accounts.find(
            a => a.provider === account?.provider
          );

          if (!linkedAccount) {
            // Link the new provider to the existing account
            await prisma.account.create({
              data: {
                userId: existingUser.id,
                type: account!.type,
                provider: account!.provider,
                providerAccountId: account!.providerAccountId,
                access_token: account!.access_token,
                token_type: account!.token_type,
                scope: account!.scope,
              },
            });
          }
        }
      }
      return true;
    },

    async session({ session, user }) {
      // Add user.id to session
      session.user.id = user.id;
      return session;
    },
  },
};

Sign In Page with OAuth Buttons

// app/auth/signin/page.tsx
import { getProviders, signIn } from 'next-auth/react';

const PROVIDER_ICONS: Record<string, string> = {
  github: '/icons/github.svg',
  google: '/icons/google.svg',
  linkedin: '/icons/linkedin.svg',
};

const PROVIDER_LABELS: Record<string, string> = {
  github: 'Continue with GitHub',
  google: 'Continue with Google',
  linkedin: 'Continue with LinkedIn',
  'microsoft-entra-id': 'Continue with Microsoft',
};

export default async function SignInPage() {
  const providers = await getProviders();

  return (
    <div className="max-w-sm mx-auto py-20">
      <h1 className="text-2xl font-bold text-center mb-8">Sign in</h1>
      <div className="space-y-3">
        {Object.values(providers ?? {}).map(provider => (
          <form
            key={provider.id}
            action={async () => {
              'use server';
              await signIn(provider.id, { redirectTo: '/dashboard' });
            }}
          >
            <button
              type="submit"
              className="w-full flex items-center justify-center gap-3 border border-gray-300 rounded-lg px-4 py-2.5 text-sm font-medium hover:bg-gray-50"
            >
              {PROVIDER_ICONS[provider.id] && (
                <img src={PROVIDER_ICONS[provider.id]} alt="" className="w-5 h-5" />
              )}
              {PROVIDER_LABELS[provider.id] ?? `Continue with ${provider.name}`}
            </button>
          </form>
        ))}
      </div>
    </div>
  );
}

Showing Linked Accounts in Settings

// app/settings/security/page.tsx
export default async function SecuritySettingsPage() {
  const session = await getServerSession();
  const user = await prisma.user.findUnique({
    where: { id: session!.user.id },
    include: { accounts: true },
  });

  const connectedProviders = user?.accounts.map(a => a.provider) ?? [];
  const availableProviders = ['github', 'google', 'linkedin'];

  return (
    <div>
      <h2 className="text-lg font-medium mb-4">Connected Accounts</h2>
      <div className="space-y-3">
        {availableProviders.map(provider => (
          <div key={provider} className="flex items-center justify-between py-3 border-b">
            <div className="flex items-center gap-3">
              <img src={`/icons/${provider}.svg`} alt="" className="w-6 h-6" />
              <span className="capitalize">{provider}</span>
            </div>
            {connectedProviders.includes(provider) ? (
              <span className="text-sm text-green-600 font-medium">Connected</span>
            ) : (
              <button
                onClick={() => signIn(provider)}
                className="text-sm text-indigo-600 hover:text-indigo-700"
              >
                Connect
              </button>
            )}
          </div>
        ))}
      </div>
    </div>
  );
}

Time Budget

TaskDuration
First OAuth provider (GitHub)30 min
Each additional provider15 min
Account linking logic1 hour
Sign-in page UI1 hour
Connected accounts settings1 hour
Total (3 providers)~4 hours

Compare boilerplates by their auth provider support on StarterPick.

Check out this boilerplate

View ShipFast on StarterPick →

Comments