DEV Community

Cover image for Implementing Secure Social Login Authentication in Next.js 13+ NextAuth.js
Emaani Dev
Emaani Dev

Posted on

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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 };
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

4. Implementation

Authentication

"use client";
import { SessionProvider } from "next-auth/react";

export function AuthProvider({ children }: { children: React.ReactNode }) {
  return <SessionProvider>{children}</SessionProvider>;
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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,
      });
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

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.

Heroku

Simplify your DevOps and maximize your time.

Since 2007, Heroku has been the go-to platform for developers as it monitors uptime, performance, and infrastructure concerns, allowing you to focus on writing code.

Learn More

Top comments (0)