Implementing Secure Social Login Authentication in Next.js 13+ NextAuth.js
Introduction Social login authentication has become a standard feature in modern web applications. This article will guide you through implementing secure social login using Next.js 13+, NextAuth.js, and Prisma, with Google and Facebook as authentication providers. Prerequisites Node.js 16+ Next.js 13+ PostgreSQL database Google and Facebook Developer accounts Table of Contents Initial Setup Configuration Database Integration Implementation Error Handling Security Considerations Testing Best Practices 1. Initial Setup First, install the required dependencies: npm install next-auth @auth/prisma-adapter prisma @prisma/client 2. Configuration Environment Variables Create a .env file: # OAuth Configuration GOOGLE_CLIENT_ID=your_google_client_id GOOGLE_CLIENT_SECRET=your_google_client_secret FACEBOOK_CLIENT_ID=your_facebook_client_id FACEBOOK_CLIENT_SECRET=your_facebook_client_secret NEXTAUTH_URL=http://localhost:3000 NEXTAUTH_SECRET=your_secure_secret # Database DATABASE_URL="postgresql://user:password@localhost:5432/dbname" NextAuth Configuration Create the authentication configuration file: import NextAuth, { DefaultSession, NextAuthOptions } from "next-auth"; import GoogleProvider from "next-auth/providers/google"; import FacebookProvider from "next-auth/providers/facebook"; import { PrismaAdapter } from "@auth/prisma-adapter"; import prisma from "@/app/lib/prisma"; declare module "next-auth" { interface Session extends DefaultSession { user: { id: string; email: string; name?: string | null; } & DefaultSession["user"] } } export const authOptions: NextAuthOptions = { adapter: PrismaAdapter(prisma), providers: [ GoogleProvider({ clientId: process.env.GOOGLE_CLIENT_ID ?? "", clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? "", }), FacebookProvider({ clientId: process.env.FACEBOOK_CLIENT_ID ?? "", clientSecret: process.env.FACEBOOK_CLIENT_SECRET ?? "", }), ], callbacks: { async session({ session, user }) { if (session.user) { session.user.id = user.id; } return session; }, async signIn({ user, account, profile }) { try { if (!user.email) return false; const existingUser = await prisma.user.findUnique({ where: { email: user.email }, include: { profile: true }, }); if (!existingUser) { await prisma.user.create({ data: { email: user.email, name: user.name || "", profile: { create: { firstName: (profile as any)?.given_name || "", lastName: (profile as any)?.family_name || "", } } }, }); } return true; } catch (error) { console.error("Error in signIn callback:", error); return false; } }, }, pages: { signIn: '/login', error: '/auth/error', }, }; const handler = NextAuth(authOptions); export { handler as GET, handler as POST }; 3. Database Integration Prisma Schema model User { id String @id @default(cuid()) email String @unique name String? profile Profile? accounts Account[] sessions Session[] } model Profile { id String @id @default(cuid()) userId String @unique firstName String lastName String user User @relation(fields: [userId], references: [id]) } // NextAuth.js required models 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 Session { id String @id @default(cuid()) sessionToken String @unique userId String expires DateTime user User @relation(fields: [userId], references: [id], onDelete: Cascade) } 4. Implementation Authentication "use client"; import { SessionProvider } from "next-auth/react"; export function AuthProvider({ children }: { children: React.ReactNode }) { return {children}; } 4.2 Social Login Buttons Component "use client"; import { signIn } from "next-auth/react"; import { FaGoogle, FaFacebook } from "react-icons/fa"; export const SocialLoginButtons = () => { const handleSocialLogin = async (provider: "google" | "facebook") => { try { const result = await signIn(provider, { callbackUrl: '/dashboard', r
Introduction
Social login authentication has become a standard feature in modern web applications. This article will guide you through implementing secure social login using Next.js 13+, NextAuth.js, and Prisma, with Google and Facebook as authentication providers.
Prerequisites
- Node.js 16+
- Next.js 13+
- PostgreSQL database
- Google and Facebook Developer accounts
Table of Contents
- Initial Setup
- Configuration
- Database Integration
- Implementation
- Error Handling
- Security Considerations
- Testing
- Best Practices
1. Initial Setup
First, install the required dependencies:
npm install next-auth @auth/prisma-adapter prisma @prisma/client
2. Configuration
Environment Variables
Create a .env
file:
# OAuth Configuration
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
FACEBOOK_CLIENT_ID=your_facebook_client_id
FACEBOOK_CLIENT_SECRET=your_facebook_client_secret
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your_secure_secret
# Database
DATABASE_URL="postgresql://user:password@localhost:5432/dbname"
NextAuth Configuration
Create the authentication configuration file:
import NextAuth, { DefaultSession, NextAuthOptions } from "next-auth";
import GoogleProvider from "next-auth/providers/google";
import FacebookProvider from "next-auth/providers/facebook";
import { PrismaAdapter } from "@auth/prisma-adapter";
import prisma from "@/app/lib/prisma";
declare module "next-auth" {
interface Session extends DefaultSession {
user: {
id: string;
email: string;
name?: string | null;
} & DefaultSession["user"]
}
}
export const authOptions: NextAuthOptions = {
adapter: PrismaAdapter(prisma),
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID ?? "",
clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? "",
}),
FacebookProvider({
clientId: process.env.FACEBOOK_CLIENT_ID ?? "",
clientSecret: process.env.FACEBOOK_CLIENT_SECRET ?? "",
}),
],
callbacks: {
async session({ session, user }) {
if (session.user) {
session.user.id = user.id;
}
return session;
},
async signIn({ user, account, profile }) {
try {
if (!user.email) return false;
const existingUser = await prisma.user.findUnique({
where: { email: user.email },
include: { profile: true },
});
if (!existingUser) {
await prisma.user.create({
data: {
email: user.email,
name: user.name || "",
profile: {
create: {
firstName: (profile as any)?.given_name || "",
lastName: (profile as any)?.family_name || "",
}
}
},
});
}
return true;
} catch (error) {
console.error("Error in signIn callback:", error);
return false;
}
},
},
pages: {
signIn: '/login',
error: '/auth/error',
},
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
3. Database Integration
Prisma Schema
model User {
id String @id @default(cuid())
email String @unique
name String?
profile Profile?
accounts Account[]
sessions Session[]
}
model Profile {
id String @id @default(cuid())
userId String @unique
firstName String
lastName String
user User @relation(fields: [userId], references: [id])
}
// NextAuth.js required models
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 Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
4. Implementation
Authentication
"use client";
import { SessionProvider } from "next-auth/react";
export function AuthProvider({ children }: { children: React.ReactNode }) {
return {children} ;
}
4.2 Social Login Buttons Component
"use client";
import { signIn } from "next-auth/react";
import { FaGoogle, FaFacebook } from "react-icons/fa";
export const SocialLoginButtons = () => {
const handleSocialLogin = async (provider: "google" | "facebook") => {
try {
const result = await signIn(provider, {
callbackUrl: '/dashboard',
redirect: false,
});
if (result?.error) {
console.error('Social login error:', result.error);
}
} catch (error) {
console.error(`${provider} login error:`, error);
}
};
return (
);
};
5. Error Handling
Create a custom error page:
"use client";
import { useSearchParams } from "next/navigation";
export default function AuthError() {
const searchParams = useSearchParams();
const error = searchParams.get("error");
return (
Authentication Error
{error || "An error occurred during authentication"}
);
}
6. Security Considerations
CORS Configuration
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/api/')) {
const response = NextResponse.next();
response.headers.set('Access-Control-Allow-Credentials', 'true');
response.headers.set('Access-Control-Allow-Origin', '*');
response.headers.set('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
return response;
}
return NextResponse.next();
}
7. Testing
Test Authentication Flow
import { render, fireEvent, waitFor } from '@testing-library/react';
import { SocialLoginButtons } from '@/components/SocialLoginButtons';
import { signIn } from 'next-auth/react';
jest.mock('next-auth/react');
describe('SocialLoginButtons', () => {
it('handles Google login correctly', async () => {
const { getByText } = render( );
const googleButton = getByText('Continue with Google');
fireEvent.click(googleButton);
await waitFor(() => {
expect(signIn).toHaveBeenCalledWith('google', {
callbackUrl: '/dashboard',
redirect: false,
});
});
});
});
8. Best Practices
-
Environment Variables
- Never commit sensitive credentials
- Use different OAuth credentials for development and production
-
Error Handling
- Implement comprehensive error logging
- Provide user-friendly error messages
-
Security
- Implement rate limiting
- Use HTTPS in production
- Keep dependencies updated
-
User Experience
- Add loading states
- Provide clear feedback
- Handle offline scenarios
Conclusion
This implementation provides a secure and user-friendly social login system. Remember to:
- Regularly update dependencies
- Monitor authentication logs
- Test thoroughly across different browsers
Handle edge cases appropriately
This article provides a solid foundation for implementing social login in your Next.js application. For production deployment, ensure you follow security best practices and thoroughly test the implementation.