Add OAuth Providers to Your Boilerplate's Auth 2026
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.
Choosing Which OAuth Providers to Support
The right providers depend on your target user base. Different markets strongly prefer different identity providers, and adding 7 providers when your users mostly use Google wastes implementation time and clutters your sign-in page.
Google OAuth: Covers the largest share of consumer and professional users globally. If you support only one provider, make it Google. Most SaaS products see 60-70% of OAuth signups through Google.
GitHub OAuth: Essential for developer tools and developer-facing products. Developers prefer GitHub OAuth because it signals you understand their workflow. For non-developer products, GitHub is irrelevant.
LinkedIn OAuth: Best for B2B SaaS targeting professionals. Users who sign up with LinkedIn are more likely to be decision-makers and less likely to use disposable emails. Higher conversion signal quality.
Microsoft (Azure AD / Entra ID): Required for enterprise B2B SaaS. Large companies run Microsoft 365, and enterprise procurement often requires SSO through Entra ID. If you're targeting enterprise customers, this is non-negotiable — but it's overkill for consumer or SMB products.
Apple Sign In: Required for any iOS app (Apple mandates it). For web-only SaaS, the tradeoff is: Apple hides user email addresses by default (they provide a relay address), which complicates email-based communication. Add it when you have a meaningful portion of Apple ecosystem users who prefer it.
The two-provider sweet spot for most SaaS: Google + one domain-specific provider (GitHub for dev tools, LinkedIn for B2B professional tools, Microsoft for enterprise).
OAuth vs Passwordless vs Passwords
Before adding OAuth providers, it's worth understanding how they fit into your overall auth strategy. Most modern SaaS products offer multiple sign-in methods, and each has tradeoffs:
OAuth only: Simplest to implement and maintain. No password storage, no reset flows, no MFA implementation needed — the OAuth provider handles it. The downside: if a user loses access to their Google or GitHub account, they lose access to your product. For consumer SaaS, this is a rare edge case. For enterprise, it can be a dealbreaker.
Email + password: Users are in full control of their credentials. Requires password hashing, reset flows, breach monitoring, and MFA implementation. The overhead is significant for a solo founder to implement well. NextAuth's email provider (magic link / OTP) offers a passwordless middle ground: no password storage, but also not dependent on a third-party OAuth provider.
OAuth + email backup: The recommended pattern for most SaaS in 2026. Let users sign in with Google or GitHub, but also allow account linking to an email/OTP for users who want independence from their OAuth provider. The Account linking section below covers how to implement this.
The session management strategy affects all of these: NextAuth's database sessions (storing session tokens in Postgres) are appropriate for SaaS apps with per-request auth checks. JWT sessions (stored in cookies) are appropriate for stateless APIs. For a full-stack Next.js SaaS app with a dashboard, database sessions are the safer default — you can revoke them on demand.
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>
);
}
Security Considerations for OAuth
OAuth is a robust protocol but has implementation pitfalls that introduce security vulnerabilities:
State parameter: Always verify the state parameter in OAuth flows to prevent CSRF attacks. NextAuth handles this automatically — don't use a custom OAuth flow that skips it.
Scope minimization: Request only the OAuth scopes you actually need. scope: 'read:user user:email' for GitHub gives you profile and email. Requesting additional scopes (repos, organizations) that your product doesn't use reduces user trust at the consent screen and creates unnecessary data liability.
Email verification: Some OAuth providers return unverified email addresses. GitHub allows users to hide their primary email and return a no-reply address. Handle the case where user.email is null or is a no-reply relay address — don't treat an unverified email as a verified one.
Token storage: NextAuth stores refresh tokens in the database (via the Account model) rather than in cookies. Never store OAuth access tokens in localStorage — they're accessible to JavaScript running on your page and are a high-value phishing target.
OAuth Token Refresh and Expiry
For providers that issue short-lived access tokens (Google refreshes every hour; GitHub tokens don't expire by default), you need to handle token refresh. This matters when your product calls provider APIs on behalf of the user — reading their Google Calendar, accessing GitHub repositories, etc.
NextAuth with the Prisma adapter stores tokens in the Account model. The refresh token pattern:
// lib/auth-helpers.ts — refresh expired Google token
export async function getValidGoogleToken(userId: string): Promise<string | null> {
const account = await prisma.account.findFirst({
where: { userId, provider: 'google' },
});
if (!account) return null;
// Check if token is expired
const isExpired = account.expires_at && account.expires_at * 1000 < Date.now();
if (!isExpired && account.access_token) {
return account.access_token;
}
// Refresh using stored refresh token
if (!account.refresh_token) return null;
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: process.env.GOOGLE_CLIENT_ID!,
client_secret: process.env.GOOGLE_CLIENT_SECRET!,
grant_type: 'refresh_token',
refresh_token: account.refresh_token,
}),
});
const tokens = await response.json();
if (tokens.error) {
// Refresh token revoked — user needs to re-authorize
await prisma.account.update({
where: { id: account.id },
data: { access_token: null, refresh_token: null },
});
return null;
}
// Store updated tokens
await prisma.account.update({
where: { id: account.id },
data: {
access_token: tokens.access_token,
expires_at: Math.floor(Date.now() / 1000) + tokens.expires_in,
refresh_token: tokens.refresh_token ?? account.refresh_token,
},
});
return tokens.access_token;
}
Token refresh only matters if you're making API calls to provider services. If you're only using OAuth for identity (email + profile), you don't need to refresh tokens — the session token is what matters, and NextAuth manages that separately from the OAuth tokens.
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>
);
}
Customizing the Sign-In Experience
Default NextAuth sign-in pages are functional but minimal. Most production products customize the auth page to match their brand and improve conversion:
Separate sign-in and sign-up flows: The default NextAuth page shows all OAuth providers on a single page. Many products benefit from separating "Sign In" (for existing users) and "Create Account" (for new users) into distinct flows with different copy. This is a UI-only distinction — the underlying OAuth flow is identical — but it reduces friction by setting the right expectations.
Conditional provider rendering: Show only the providers that make sense for your product category. A developer tool shows GitHub prominently. A B2B product shows Google and Microsoft. Render based on product context rather than defaulting to the full list:
// Reorder providers based on your audience
const PROVIDER_ORDER = {
developer: ['github', 'google'],
enterprise: ['microsoft-entra-id', 'google'],
consumer: ['google', 'apple'],
};
const audience = process.env.NEXT_PUBLIC_PRODUCT_AUDIENCE ?? 'developer';
const orderedProviders = PROVIDER_ORDER[audience as keyof typeof PROVIDER_ORDER]
.map(id => Object.values(providers ?? {}).find(p => p.id === id))
.filter(Boolean);
Post-auth redirect: After OAuth sign-in, redirect users to the page they were trying to access (not always the dashboard home). Store the intended URL in the callbackUrl parameter:
// In protected route — save the current URL before redirecting to sign-in
redirect(`/auth/signin?callbackUrl=${encodeURIComponent(request.url)}`);
// NextAuth uses callbackUrl automatically if you pass it
await signIn(provider.id, { callbackUrl: searchParams.callbackUrl || '/dashboard' });
Enterprise SSO: Beyond OAuth
For enterprise customers, standard OAuth providers are often insufficient. Enterprise IT departments want SAML 2.0 or OIDC-based SSO through their identity provider (Okta, Ping Identity, Active Directory). This allows them to enforce security policies: MFA requirements, session timeouts, and instant account revocation when an employee leaves.
NextAuth supports this through the microsoft-entra-id provider (for Microsoft 365 organizations) and through community providers for Okta and Auth0. For full SAML support, consider migrating auth to Auth0, Okta, or WorkOS — these managed services handle the complexity of enterprise SSO identity provider connections at a per-user cost.
WorkOS is the most developer-friendly enterprise auth solution in 2026: it handles SAML, SCIM (user provisioning), Directory Sync, and audit logs through a single API, with pricing that starts at $0 for your first enterprise customer.
Testing OAuth Flows in Development
OAuth flows require browser redirects through external provider domains, which makes automated testing difficult. The practical testing approach:
Local development with localhost: All major providers support localhost callback URLs for development. The configuration:
# .env.local — use separate OAuth apps for dev and prod
GITHUB_CLIENT_ID=Iv1.dev_app_id # Dev-only OAuth app
GITHUB_CLIENT_SECRET=dev_secret
# Callback URL registered in GitHub: http://localhost:3000/api/auth/callback/github
Create separate OAuth applications for development and production. Dev apps use http://localhost:3000 callbacks; prod apps use your live domain. Never use production OAuth credentials in local development — a misconfigured redirect URI in dev could leak tokens.
Testing the account linking flow: Account linking edge cases are the most common source of auth bugs. Write integration tests that simulate the scenarios:
// test/auth/account-linking.test.ts
test('links OAuth account to existing email/password user', async () => {
// Create a user who signed up with email
const user = await createUserWithPassword('test@example.com', 'password123');
// Simulate OAuth sign-in with the same email via GitHub
const result = await simulateOAuthSignIn({
provider: 'github',
email: 'test@example.com',
providerAccountId: 'github-user-123',
});
// Should link to existing account, not create a new one
expect(result.userId).toBe(user.id);
// Should have both the original account and the GitHub account
const accounts = await prisma.account.findMany({ where: { userId: user.id } });
expect(accounts).toHaveLength(2); // credentials + github
});
Handling provider downtime: If GitHub's OAuth endpoint is down, your sign-in page breaks. Implement graceful error handling in the auth flow:
// authOptions callbacks
async signIn({ user, account, profile }) {
try {
// ... your sign-in logic
return true;
} catch (error) {
console.error('OAuth sign-in error:', error);
return '/auth/error?reason=provider_error'; // Redirect to error page
}
}
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 |
Related Resources
For the full auth library comparison — NextAuth vs Clerk vs Better Auth vs Auth0, Better Auth vs Clerk vs NextAuth covers when each library's approach to OAuth makes sense. For boilerplates that pre-configure multiple OAuth providers including the Prisma schema, how to customize ShipFast covers swapping auth providers in the most popular boilerplate. For adding team organizations on top of OAuth auth, team and org management guide covers the multi-member org structure.
Methodology
Provider setup instructions verified against each provider's current OAuth documentation: GitHub (2026), Google Cloud Console (2026), Microsoft Entra ID (formerly Azure AD). Security recommendations based on OWASP OAuth 2.0 Security Best Practices.
Compare boilerplates by their auth provider support on StarterPick.
Check out this boilerplate
View ShipFaston StarterPick →