OAuth Integration

Patterns for OAuth authentication with multiple providers.

Overview#

OAuth enables secure third-party authentication:

  • Social login (Google, GitHub, Apple)
  • Enterprise SSO integration
  • Account linking across providers
  • Custom OAuth provider support

Prerequisites:

  • OAuth credentials from each provider
  • NextAuth.js or custom implementation

Installation#

npm install next-auth
1# .env.local 2GOOGLE_CLIENT_ID=your-google-client-id 3GOOGLE_CLIENT_SECRET=your-google-client-secret 4GITHUB_CLIENT_ID=your-github-client-id 5GITHUB_CLIENT_SECRET=your-github-client-secret 6APPLE_CLIENT_ID=your-apple-client-id 7APPLE_CLIENT_SECRET=your-apple-client-secret

Implementation#

Multi-Provider Setup#

1// auth.ts 2import NextAuth from 'next-auth' 3import Google from 'next-auth/providers/google' 4import GitHub from 'next-auth/providers/github' 5import Apple from 'next-auth/providers/apple' 6import { PrismaAdapter } from '@auth/prisma-adapter' 7import { prisma } from '@/lib/db' 8 9export const { handlers, auth, signIn, signOut } = NextAuth({ 10 adapter: PrismaAdapter(prisma), 11 providers: [ 12 Google({ 13 clientId: process.env.GOOGLE_CLIENT_ID!, 14 clientSecret: process.env.GOOGLE_CLIENT_SECRET! 15 }), 16 GitHub({ 17 clientId: process.env.GITHUB_CLIENT_ID!, 18 clientSecret: process.env.GITHUB_CLIENT_SECRET! 19 }), 20 Apple({ 21 clientId: process.env.APPLE_CLIENT_ID!, 22 clientSecret: process.env.APPLE_CLIENT_SECRET! 23 }) 24 ], 25 callbacks: { 26 async signIn({ user, account, profile }) { 27 // Custom validation 28 if (account?.provider === 'google') { 29 return profile?.email_verified === true 30 } 31 return true 32 }, 33 async session({ session, user }) { 34 session.user.id = user.id 35 return session 36 } 37 }, 38 pages: { 39 signIn: '/login', 40 error: '/auth/error' 41 } 42})

Social Login Buttons#

1// components/auth/SocialButtons.tsx 2'use client' 3 4import { signIn } from 'next-auth/react' 5import { Github } from 'lucide-react' 6 7export function SocialButtons() { 8 return ( 9 <div className="space-y-3"> 10 <button 11 onClick={() => signIn('google', { callbackUrl: '/dashboard' })} 12 className="flex w-full items-center justify-center gap-2 rounded-lg border py-2 hover:bg-gray-50" 13 > 14 <GoogleIcon className="h-5 w-5" /> 15 Continue with Google 16 </button> 17 18 <button 19 onClick={() => signIn('github', { callbackUrl: '/dashboard' })} 20 className="flex w-full items-center justify-center gap-2 rounded-lg border py-2 hover:bg-gray-50" 21 > 22 <Github className="h-5 w-5" /> 23 Continue with GitHub 24 </button> 25 26 <button 27 onClick={() => signIn('apple', { callbackUrl: '/dashboard' })} 28 className="flex w-full items-center justify-center gap-2 rounded-lg bg-black py-2 text-white" 29 > 30 <AppleIcon className="h-5 w-5" /> 31 Continue with Apple 32 </button> 33 </div> 34 ) 35} 36 37function GoogleIcon({ className }: { className?: string }) { 38 return ( 39 <svg className={className} viewBox="0 0 24 24"> 40 <path 41 fill="currentColor" 42 d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" 43 /> 44 <path 45 fill="currentColor" 46 d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" 47 /> 48 </svg> 49 ) 50}

Account Linking#

1// auth.ts 2export const { handlers, auth } = NextAuth({ 3 callbacks: { 4 async signIn({ user, account }) { 5 // Check if user exists with different provider 6 const existingUser = await prisma.user.findUnique({ 7 where: { email: user.email! }, 8 include: { accounts: true } 9 }) 10 11 if (existingUser) { 12 // Check if this provider is already linked 13 const hasProvider = existingUser.accounts.some( 14 a => a.provider === account?.provider 15 ) 16 17 if (!hasProvider && account) { 18 // Link new provider to existing account 19 await prisma.account.create({ 20 data: { 21 userId: existingUser.id, 22 type: account.type, 23 provider: account.provider, 24 providerAccountId: account.providerAccountId, 25 access_token: account.access_token, 26 refresh_token: account.refresh_token, 27 expires_at: account.expires_at 28 } 29 }) 30 } 31 } 32 33 return true 34 } 35 } 36})

Custom Provider#

1// lib/custom-provider.ts 2import type { OAuthConfig } from 'next-auth/providers' 3 4interface CustomProfile { 5 id: string 6 email: string 7 name: string 8 avatar_url: string 9} 10 11export function CustomProvider(): OAuthConfig<CustomProfile> { 12 return { 13 id: 'custom', 14 name: 'Custom Provider', 15 type: 'oauth', 16 authorization: { 17 url: 'https://custom.com/oauth/authorize', 18 params: { scope: 'user:email' } 19 }, 20 token: 'https://custom.com/oauth/token', 21 userinfo: 'https://custom.com/api/user', 22 profile(profile) { 23 return { 24 id: profile.id, 25 email: profile.email, 26 name: profile.name, 27 image: profile.avatar_url 28 } 29 }, 30 clientId: process.env.CUSTOM_CLIENT_ID, 31 clientSecret: process.env.CUSTOM_CLIENT_SECRET 32 } 33}

OAuth Error Handling#

1// app/auth/error/page.tsx 2import Link from 'next/link' 3 4export default function AuthError({ 5 searchParams 6}: { 7 searchParams: { error?: string } 8}) { 9 const errorMessages: Record<string, string> = { 10 OAuthSignin: 'Error starting OAuth flow', 11 OAuthCallback: 'Error in OAuth callback', 12 OAuthCreateAccount: 'Could not create account', 13 EmailCreateAccount: 'Could not create account', 14 Callback: 'Error in callback', 15 OAuthAccountNotLinked: 'Email already registered with different provider', 16 EmailSignin: 'Error sending email', 17 CredentialsSignin: 'Invalid credentials', 18 SessionRequired: 'Please sign in to continue', 19 default: 'An error occurred' 20 } 21 22 const message = errorMessages[searchParams.error ?? 'default'] 23 24 return ( 25 <div className="mx-auto max-w-md py-12 text-center"> 26 <h1 className="mb-4 text-2xl font-bold">Authentication Error</h1> 27 <p className="mb-6 text-gray-600">{message}</p> 28 <Link href="/login" className="text-blue-600 hover:underline"> 29 Try again 30 </Link> 31 </div> 32 ) 33}

Best Practices#

  1. Verify email addresses - Only allow verified emails from providers
  2. Handle account linking - Allow users to connect multiple providers
  3. Display friendly errors - Map OAuth error codes to user-friendly messages
  4. Store refresh tokens - Enable token refresh for long-lived access
  5. Configure redirect URIs - Set proper callback URLs in provider dashboards