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

Jan 16, 2025 - 21:06
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

  1. Initial Setup
  2. Configuration
  3. Database Integration
  4. Implementation
  5. Error Handling
  6. Security Considerations
  7. Testing
  8. 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

  1. Environment Variables

    • Never commit sensitive credentials
    • Use different OAuth credentials for development and production
  2. Error Handling

    • Implement comprehensive error logging
    • Provide user-friendly error messages
  3. Security

    • Implement rate limiting
    • Use HTTPS in production
    • Keep dependencies updated
  4. 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:

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.