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 <SessionProvider>{children}</SessionProvider>;
}
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 (
<div className="space-y-4">
<button
onClick={() => handleSocialLogin("google")}
className="w-full flex items-center justify-center gap-2 bg-white text-gray-700 border border-gray-300 rounded-lg px-4 py-2 hover:bg-gray-50"
>
<FaGoogle className="text-red-500" />
Continue with Google
</button>
<button
onClick={() => handleSocialLogin("facebook")}
className="w-full flex items-center justify-center gap-2 bg-blue-600 text-white rounded-lg px-4 py-2 hover:bg-blue-700"
>
<FaFacebook />
Continue with Facebook
</button>
</div>
);
};
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 (
<div className="min-h-screen flex items-center justify-center">
<div className="bg-white p-8 rounded-lg shadow-md">
<h1 className="text-2xl font-bold text-red-600 mb-4">
Authentication Error
</h1>
<p className="text-gray-600">
{error || "An error occurred during authentication"}
</p>
</div>
</div>
);
}
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(<SocialLoginButtons />);
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.
Top comments (0)