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
- GitHub → Settings → Developer settings → OAuth Apps → New OAuth App
- Authorization callback URL:
https://yoursaas.com/api/auth/callback/github - Copy Client ID and Client Secret
GITHUB_CLIENT_ID=Iv1.xxxxxxxxxxxxxxxx
GITHUB_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Google OAuth
- Google Cloud Console → APIs & Services → Credentials → Create OAuth 2.0 Client
- Authorized redirect URIs:
https://yoursaas.com/api/auth/callback/google - 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:
- Azure Portal → Microsoft Entra ID → App Registrations → New Registration
- Redirect URI:
https://yoursaas.com/api/auth/callback/microsoft-entra-id - 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
| Task | Duration |
|---|---|
| First OAuth provider (GitHub) | 30 min |
| Each additional provider | 15 min |
| Account linking logic | 1 hour |
| Sign-in page UI | 1 hour |
| Connected accounts settings | 1 hour |
| Total (3 providers) | ~4 hours |
Compare boilerplates by their auth provider support on StarterPick.
Check out this boilerplate
View ShipFast on StarterPick →