JavaScript Development Space

User Authentication with Google & GitHub in Next.js Using Prisma and NextAuth

Add to your RSS feed30 October 202417 min read
User Authentication with Google & GitHub in Next.js Using Prisma and NextAuth

Implementing user authentication is a foundational part of building secure and dynamic web applications. In this article, we’ll walk through how to set up authentication in a Next.js application, using NextAuth for seamless integration with Google and GitHub OAuth providers, and Prisma to manage our user data in a database. By the end of this guide, you’ll have a solid authentication setup with external OAuth providers that store user data efficiently.

We’ll use the newly released Next.js version 15, PostgreSQL set up via Docker, Prisma for type safety, and ShadcnUI for fast UI development. By the end, we'll have an application where users can authenticate via Google or GitHub, with their data instantly stored in our database.

Prerequisites

  1. Basic knowledge of Next.js - Familiarity with Next.js and its folder structure will help.
  2. Node.js and npm installed - You’ll need both installed on your local machine.
  3. A GitHub and Google account - For testing the OAuth flow. Prisma CLI - Make sure you have the Prisma CLI installed, as we’ll use it to set up and migrate the database.
  4. Docker: Install Docker to easily manage your PostgreSQL database in a containerized environment. Refer to the Docker documentation for installation instructions.
  5. PostgreSQL: Familiarity with PostgreSQL is beneficial, as we will use it as our database.
  6. Familiarity with Prisma: Basic knowledge of Prisma ORM for managing database operations will be useful.

Steps to Create User Authentication

Let’s dive into setting up the project and adding user authentication.

Step 1: Initialize Next.js Project

Start by creating a new Next.js project.

npx create-next-app@latest next-oauth-prisma
bash
1 Need to install the following packages:
2 create-next-app@15.0.2
3 Ok to proceed? (y) y
4
5 √ Would you like to use TypeScript? ... No / Yes
6 √ Would you like to use ESLint? ... No / Yes
7 √ Would you like to use Tailwind CSS? ... No / Yes
8 √ Would you like your code inside a `src/` directory? ... No / Yes
9 √ Would you like to use App Router? (recommended) ... No / Yes
10 √ Would you like to use Turbopack for next dev? ... No / Yes
11 √ Would you like to customize the import alias (@/* by default)? ... No / Yes
12 Creating a new Next.js app in C:\Users\useR\Desktop\NextJS\next-auth-prisma.
13
14 Using npm.
15
16 Initializing project with template: app-tw
17
18
19 Installing dependencies:
20 - react
21 - react-dom
22 - next
23
24 Installing devDependencies:
25 - typescript
26 - @types/node
27 - @types/react
28 - @types/react-dom
29 - postcss
30 - tailwindcss
31 - eslint
32 - eslint-config-next

then

cd next-oauth-prisma

Once your project is created, install dependencies:

npm install next-auth@beta @prisma/client prisma
  • next-auth: Handles authentication.
  • @prisma/client and prisma: Used to interact with the database and manage user data.

Now, initialize your Prisma ORM project by generating a Prisma schema file using this command:

npx prisma init --datasource-provider postgresql

Step 2: Setup a Database

Before connecting our database, we first need to create it. To understand Docker better, you can refer to our article, How to Set Up a Local PostgreSQL Using Docker Compose.

Open the Docker application, and if you have other PostgreSQL containers running, stop them to avoid port conflicts. Then, create a docker-compose.yml file and add the following code to it:

yml
1 version: '3.8'
2
3 services:
4 db:
5 image: postgres:latest
6 container_name: next-oauth-prisma
7 environment:
8 POSTGRES_USER: postgres_user
9 POSTGRES_PASSWORD: postgres_password
10 POSTGRES_DB: postgres_auth
11 ports:
12 - '5432:5432'
13 volumes:
14 - postgres_data:/var/lib/postgresql/data
15 - ./init.sql:/docker-entrypoint-initdb.d/init.sql # Optional: initialize with SQL script
16
17 volumes:
18 postgres_data:

This Docker Compose file is configured to set up a PostgreSQL database container for your Next.js project with Prisma. Here’s a breakdown:

  • image: postgres:latest pulls the latest PostgreSQL image from Docker Hub.
  • container_name: Names the container next-oauth-prisma, which makes it easy to reference.
  • environment: Sets PostgreSQL environment variables:
  • POSTGRES_USER: the username (set here to postgres_user).
  • POSTGRES_PASSWORD: the password (postgres_password).
  • POSTGRES_DB: the name of the initial database to be created (postgres_auth).
  • ports: Exposes the container’s PostgreSQL port (5432) to the host’s port 5432. This makes the database accessible from outside the container via localhost:5432.

volumes:

  • postgres_data:/var/lib/postgresql/data: Mounts a named volume postgres_data to persist database data on the host, so data is preserved between container restarts.
  • ./init.sql:/docker-entrypoint-initdb.d/init.sql: Mounts an optional SQL script (init.sql), which can initialize the database with predefined tables or values on first run. This script is run automatically by PostgreSQL at startup if present.

Run

docker-compose up -d

Running docker-compose up -d is a way to start the containers defined in your docker-compose.yml file with some specific advantages:

  1. Detached Mode (-d): The -d flag stands for "detached mode," meaning the containers will run in the background. This allows you to keep using your terminal for other commands while your Docker services are running.
  2. Automatic Setup: This command automatically builds and starts up all the services defined in your docker-compose.yml file. In this case, it will set up and start the PostgreSQL database container with the specified configuration.
  3. Persistent Services: Running the container in detached mode is ideal for databases and other services that need to run continuously, as they keep running even if you close the terminal. You can stop them later using docker-compose down.
  4. Networking and Dependencies: If there are multiple services in your docker-compose.yml file that depend on each other, docker-compose up -d will ensure they start in the correct order and are networked together as defined.

In short, using docker-compose up -d is a convenient way to start your database in the background, letting you continue development without an open Docker terminal session.

Step 3: Configure Prisma

Configure Prisma by creating a .env file in the root directory with the following:

env
1 DATABASE_URL="postgresql://postgres_user:postgres_password@localhost:5432/postgres_auth?schema=public"

Replace username, password, and postgres_auth with your actual credentials.

Define the User Model in Prisma

Open prisma/schema.prisma and define the User model for NextAuth. Also, configure Prisma to use PostgreSQL.

prisma
1 // This is your Prisma schema file,
2 // learn more about it in the docs: https://pris.ly/d/prisma-schema
3
4 // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
5 // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
6
7 generator client {
8 provider = "prisma-client-js"
9 }
10
11 datasource db {
12 provider = "postgresql"
13 url = env("DATABASE_URL")
14 }
15
16 model User {
17 id String @id @default(cuid())
18 name String?
19 email String @unique
20 emailVerified DateTime?
21 image String?
22 role String?
23 accounts Account[]
24 sessions Session[]
25
26 createdAt DateTime @default(now())
27 updatedAt DateTime @updatedAt
28
29 @@map("users")
30 }
31
32 model Account {
33 userId String
34 type String
35 provider String
36 providerAccountId String
37 refresh_token String?
38 access_token String?
39 expires_at Int?
40 token_type String?
41 scope String?
42 id_token String?
43 session_state String?
44
45 createdAt DateTime @default(now())
46 updatedAt DateTime @updatedAt
47
48 user User @relation(fields: [userId], references: [id], onDelete: Cascade)
49
50 @@id([provider, providerAccountId])
51 @@map("accounts")
52 }
53
54 model Session {
55 sessionToken String @unique
56 userId String
57 expires DateTime
58 user User @relation(fields: [userId], references: [id], onDelete: Cascade)
59
60 createdAt DateTime @default(now())
61 updatedAt DateTime @updatedAt
62
63 @@map("sessions")
64 }
65
66 model VerificationToken {
67 identifier String
68 token String
69 expires DateTime
70
71 @@id([identifier, token])
72 @@map("verification_tokens")
73 }

Each model represents a table in the database, with Prisma creating these tables based on the defined schema. Here’s a breakdown of each model:

User Model

Defines a table for user information with fields for basic attributes like name, email, and role. Key points include:

  • id: A unique identifier with a default value generated by cuid().
  • email: A unique email field.
  • createdAt and updatedAt: Timestamps to track creation and update times, with updatedAt auto-updating on changes.
  • Relationships with other models (Account and Session) are represented as arrays.
  • @@map("users"): Renames the table in the database to users.

Account Model

Stores information about users’ accounts for third-party providers like Google or GitHub:

  • userId: Links to the User model through a foreign key, creating a relationship with the User table.
  • Other fields, like access_token, refresh_token, and expires_at, store authentication data.
  • @@id([provider, providerAccountId]): Composite primary key across provider and providerAccountId fields, enforcing uniqueness for each account.
  • @@map("accounts"): Names the table accounts in the database.

Session Model

Tracks each login session for users:

  • sessionToken: A unique session identifier.
  • expires: Indicates session expiry time.
  • The user field defines a foreign key relationship to the User table, with sessions deleted if the associated user is removed.
  • @@map("sessions"): Names the table sessions in the database.

VerificationToken Model

Used for managing passwordless login or account verification tokens:

  • identifier and token act as composite primary keys, ensuring uniqueness.
  • expires: Expiration timestamp for the token.
  • @@map("verification_tokens"): Names the table verification_tokens.

This schema allows for efficient management of user data, third-party account integrations, user sessions, and account verifications for a complete authentication system.

Run the following commands to create and migrate your database:

npx prisma migrate dev --name init

Step 4: Install ShadcnUI Package

We won't be building the entire project; instead, we'll just create a navbar with a user dropdown menu. To do this, we only need two components from the Shadcn library: Button and Dropdown Menu.

Run the following command to install the required ShadcnUI components package:

npx shadcn-ui init
bash
1 Which style would you like to use? › New York
2 Which color would you like to use as base color? › Zinc
3 Do you want to use CSS variables for colors? › no / yes

Add ShadcnUI Components

npx shadcn@latest add button

and

npx shadcn@latest add dropdown-menu

Now we’ve added everything needed to our project. In the next section, we’ll start building our UI and connect to the database.

Try running the project with:

npm run dev

Note: The latest version of Next.js has a bug with webpack, causing the error: "Module parse failed: Bad character escape sequence." We've previously covered a solution for this issue — read more about it here.

Create UI

Here's what we aim to build:

OAuth header

To achieve this, we'll need the following components: logo, header, navbar, and user-button.

Logo component

Create logo.tsx file inside the ui folder:

tsx
1 import Link from 'next/link';
2
3 const Logo = () => {
4 return (
5 <Link href='/'>
6 <div className='flex gap-2 items-center'>
7 <span className='p-2 bg-main'>
8 <svg
9 width='27px'
10 height='27px'
11 viewBox='0 0 24 24'
12 xmlns='http://www.w3.org/2000/svg'
13 fill='none'
14 strokeWidth='1'
15 strokeLinecap='round'
16 strokeLinejoin='miter'
17 className='stroke-black dark:stroke-white'
18 >
19 <path d='M9,3H8A3,3,0,0,0,5,6V9a3,3,0,0,1-3,3H2a3,3,0,0,1,3,3v4a3,3,0,0,0,3,3H9'></path>
20 <path d='M15,3h1a3,3,0,0,1,3,3V9a3,3,0,0,0,3,3h0a3,3,0,0,0-3,3v4a3,3,0,0,1-3,3H15'></path>
21 </svg>
22 </span>
23 <div className='flex gap-1 text-lg'>
24 <span className='text-primary font-montserrat'>Logo</span>
25 </div>
26 </div>
27 </Link>
28 );
29 };
30 export default Logo;

User Button Component

Create user-button.tsx inside the components folder

tsx
1 import {
2 DropdownMenu,
3 DropdownMenuContent,
4 DropdownMenuGroup,
5 DropdownMenuItem,
6 DropdownMenuLabel,
7 DropdownMenuSeparator,
8 DropdownMenuTrigger,
9 } from './ui/dropdown-menu';
10 import avatarPlaceholder from '@/assets/images/avatar_placeholder.png';
11 import { Lock, LogOut, Settings } from 'lucide-react';
12 import { signOut } from 'next-auth/react';
13 import { Button } from './ui/button';
14 import { User } from 'next-auth';
15 import Image from 'next/image';
16 import Link from 'next/link';
17
18 interface UserButtonProps {
19 user: User;
20 }
21
22 export default function UserButton({ user }: UserButtonProps) {
23 return (
24 <DropdownMenu>
25 <DropdownMenuTrigger asChild>
26 <Button size='icon' className='flex-none rounded-full'>
27 <Image
28 src={user.image || avatarPlaceholder}
29 alt='User profile picture'
30 width={50}
31 height={50}
32 className='aspect-square rounded-full bg-background object-cover'
33 />
34 </Button>
35 </DropdownMenuTrigger>
36 <DropdownMenuContent className='w-56'>
37 <DropdownMenuLabel>{user.name || 'User'}</DropdownMenuLabel>
38 <DropdownMenuSeparator />
39 <DropdownMenuGroup>
40 <DropdownMenuItem asChild>
41 <Link href='/settings'>
42 <Settings className='mr-2 h-4 w-4' />
43 <span>Settings</span>
44 </Link>
45 </DropdownMenuItem>
46 {user.role === 'admin' && (
47 <DropdownMenuItem asChild>
48 <Link href='/admin'>
49 <Lock className='mr-2 h-4 w-4' />
50 Admin
51 </Link>
52 </DropdownMenuItem>
53 )}
54 </DropdownMenuGroup>
55 <DropdownMenuSeparator />
56 <DropdownMenuItem asChild>
57 <button
58 onClick={() => signOut({ callbackUrl: '/' })}
59 className='flex w-full items-center'
60 >
61 <LogOut className='mr-2 h-4 w-4' /> Sign Out
62 </button>
63 </DropdownMenuItem>
64 </DropdownMenuContent>
65 </DropdownMenu>
66 );
67 }

Our login component will have three states: loading, sign in, and an avatar with a dropdown menu. To set this up, we need to follow these steps:

login statuses

Here's how to do it:

tsx
1 // navbar.tsx
2
3 'use client';
4
5 import { signIn, useSession } from 'next-auth/react';
6 import UserButton from './user-button';
7 import { Button } from './ui/button';
8
9 const Navbar = () => {
10 const session = useSession();
11 const user = session.data?.user;
12
13 return (
14 <nav className='flex gap-2 flex-col sm:flex-row w-[60%] sm:w-fit max-sm:mt-8 items-center justify-center'>
15 {user && <UserButton user={user} />}
16 {!user && session.status === 'loading' && <span>Loading user...</span>}
17 {!user && session.status !== 'loading' && (
18 <Button onClick={() => signIn()}>Sign in</Button>
19 )}
20 </nav>
21 );
22 };
23 export default Navbar;
  • useSession hook fetches the current session’s state and data, assigning it to session.
  • The user variable extracts the user object from the session data.

Rendering Logic:

If the user is logged in (user exists):

  • The UserButton component is rendered, displaying user-related options (e.g., an avatar with a dropdown menu).

If the session is loading (user state is undetermined):

  • It displays "Loading user..." to inform users that the status is being fetched.

If no user is logged in (and session isn’t loading):

  • It shows a Sign in button, which triggers the signIn function when clicked.

To add the user variable to the session, we need to adjust our layout.tsx slightly by wrapping it with our session HOC (Higher Order Component).

Layout Component

tsx
1 // layout.tsx
2
3 import type { Metadata } from "next";
4 import localFont from "next/font/local";
5 import "./globals.css";
6 import { SessionProvider } from 'next-auth/react';
7 import Header from '@/components/header';
8
9 const geistSans = localFont({
10 src: "./fonts/GeistVF.woff",
11 variable: "--font-geist-sans",
12 weight: "100 900",
13 });
14 const geistMono = localFont({
15 src: "./fonts/GeistMonoVF.woff",
16 variable: "--font-geist-mono",
17 weight: "100 900",
18 });
19
20 export const metadata: Metadata = {
21 title: "Create Next App",
22 description: "Generated by create next app",
23 };
24
25 export default function RootLayout({
26 children,
27 }: Readonly<{
28 children: React.ReactNode;
29 }>) {
30 return (
31 <html lang="en">
32 <body
33 className={`${geistSans.variable} ${geistMono.variable} antialiased`}
34 >
35 <SessionProvider>
36 <Header />
37 {children}
38 </SessionProvider>
39 </body>
40 </html>
41 );
42 }

Declare a module augmentation for next-auth:

Create a file src/types/next-auth.d.ts

ts
1 import { DefaultSession } from "next-auth";
2
3 declare module "next-auth" {
4 interface Session {
5 user: User & DefaultSession["user"];
6 }
7
8 interface User {
9 role: string | null;
10 }
11 }

The Session interface is modified to add a user object that combines User and the default structure of user in DefaultSession. This helps TypeScript understand that your session's user now includes the additional properties you defined in the User interface.

A new User interface is defined, adding a role property (type string | null) to store additional user role information. This is useful when you want to store user roles in the session for access control.

By adding these properties, you gain strongly typed access to user.role when working with NextAuth sessions, which makes it easier to implement role-based features or access control in your app.

Declare globalThis.prismaGlobal

Create a file src/lib/prisma.ts

ts
1 import { PrismaClient } from '@prisma/client';
2
3 const prismaClientSingleton = () => {
4 return new PrismaClient();
5 };
6
7 declare const globalThis: {
8 prismaGlobal: ReturnType<typeof prismaClientSingleton>;
9 } & typeof global;
10
11 const prisma = globalThis.prismaGlobal ?? prismaClientSingleton();
12
13 export default prisma;
14
15 if (process.env.NODE_ENV !== 'production') globalThis.prismaGlobal = prisma;

In summary, this code helps avoid reinitializing Prisma in development by using a singleton pattern, optimizing both development and production performance.

All that’s left is to create the Header component, where we’ll combine our logo and navbar, and then integrate it into layout.tsx.

Header Component

tsx
1 // header.tsx
2
3 import Navbar from './navbar';
4 import Logo from './ui/logo';
5
6 const Header = () => {
7 return (
8 <div className='light-gradient dark:dark-gradient font-public-sans'>
9 <div className='flex items-center justify-between m-4 sm:mt-2 mt-9 mx-8 flex-col sm:flex-row'>
10 <Logo />
11 <div className='flex flex-row justify-end gap-14'>
12 <Navbar />
13 </div>
14 </div>
15 </div>
16 );
17 };
18 export default Header;

Test it

npm run dev

Set Up NextAuth

Now, we’ll set up NextAuth to authenticate users using Google and GitHub. In the app/api/auth/[...nextauth] directory, create a new folder called auth and a file called route.ts.

ts
1 import { handlers } from '@/auth';
2 export const { GET, POST } = handlers;

In the root directory create 2 files: _middleware.ts and auth.ts

auth.ts

tsx
1 // auth.ts
2
3 import { PrismaAdapter } from '@auth/prisma-adapter';
4 import GitHub from 'next-auth/providers/github';
5 import Google from 'next-auth/providers/google';
6 import { Adapter } from 'next-auth/adapters';
7 import prisma from './lib/prisma';
8 import NextAuth from 'next-auth';
9
10 export const { handlers, signIn, signOut, auth } = NextAuth({
11 trustHost: true,
12 theme: {
13 logo: '/logo.png',
14 },
15 adapter: PrismaAdapter(prisma) as Adapter,
16 callbacks: {
17 session({ session, user }) {
18 session.user.role = user.role;
19 return session;
20 },
21 },
22 providers: [Google, GitHub],
23 });

In this file:

  • We define Google and GitHub as OAuth providers.
  • We configure the PrismaAdapter to manage data in the database.

_middleware.ts

ts
1 export { auth as middleware } from '@/auth';

Add your Google and GitHub OAuth credentials to the .env file:

env
1 GOOGLE_CLIENT_ID=your-google-client-id
2 GOOGLE_CLIENT_SECRET=your-google-client-secret
3 GITHUB_CLIENT_ID=your-github-client-id
4 GITHUB_CLIENT_SECRET=your-github-client-secret
5 NEXTAUTH_URL=http://localhost:3000

GitHub OAuth App

Creating a new GitHub OAuth App involves a few steps within the GitHub developer settings. Here’s a quick guide to help you set it up:

1. Go to GitHub Developer Settings:

  • Log in to your GitHub account.
  • Navigate to GitHub Developer Settings by clicking on your profile icon, selecting Settings, and then Developer settings from the sidebar.

2. Create a New OAuth App:

  • In the OAuth Apps section, click on New OAuth App.

3. Fill in the OAuth App Details:

  • Application name: Enter a name for your app, which users will see when authorizing.
  • Homepage URL: Add the main URL for your application, such as https://localhost:3000.
  • Authorization callback URL: Enter the URL where GitHub should redirect users after they authorize, typically something like https://localhost:3000/api/auth/callback/github (depending on how you've set up your app to handle callbacks).

4. Register the Application:

  • Once all fields are filled in, click Register application. GitHub will generate a Client ID and Client Secret for your OAuth app.

5. Secure the Client Secret:

  • Copy the Client ID and Client Secret. Keep these safe, as you’ll need them to configure OAuth in your application, and don’t expose them publicly.

6. Use in Application:

In your application code, use the Client ID and Client Secret to configure GitHub as an authentication provider (for example, using NextAuth or a similar library).

Your GitHub OAuth App is now ready! You can use it to authenticate users through GitHub in your application.

Google OAuth App

To create a new Google OAuth App, follow these steps within the Google Cloud Console:

1. Go to the Google Cloud Console:

2. Create or Select a Project:

  • From the top menu, click Select a project.
  • Choose an existing project or click New Project to create a new one specifically for your app.

3. Enable the OAuth API:

  • In the left sidebar, go to APIs & Services > Library.
  • Search for Google Identity or OAuth and click Enable on the Google Identity API.

4. Configure the OAuth Consent Screen:

  • Go to APIs & Services > OAuth consent screen.
  • Select External if your app will be available to general users, or Internal if it’s restricted to your organization (G Suite users only).
  • Fill out the App name, User support email, and any other required fields.
  • In Scopes for Google APIs, add scopes relevant to your app (e.g., user profile data).
  • Save and continue to configure the consent screen.

5. Create OAuth Credentials:

  • Navigate to APIs & Services > Credentials and click on Create Credentials > OAuth Client ID.
  • For Application type, choose Web application.
  • Under Authorized redirect URIs, add the callback URL for your application. This is typically in the format: https://localhost:3000/api/auth/callback/google

Replace localhost:3000 with your app’s actual domain.

6. Save Your Client ID and Client Secret:

  • After creating the credentials, Google will provide a Client ID and Client Secret.
  • Copy these, as you’ll need them to configure Google authentication in your app.

Your Google OAuth App is now configured and ready for use in your application.

We have one last step: configuring remote image handling in Next.js. To load images from external sources like Google and GitHub in our Next.js app, we need to set trusted image sources in the next.config.ts configuration file. Here’s how:

js
1 // @type {import('next').NextConfig}
2 const nextConfig = {
3 images: {
4 remotePatterns: [
5 {
6 protocol: 'https',
7 hostname: 'lh3.googleusercontent.com', // Google profile images
8 },
9 {
10 protocol: 'https',
11 hostname: 'avatars.githubusercontent.com', // GitHub profile avatars
12 },
13 ],
14 },
15 };
16
17 export default nextConfig;

The remotePatterns array lists trusted sources for loading images from external domains. This is useful when users authenticate with Google or GitHub, allowing us to display their profile images securely.

Test and Verify Authentication

After setting up the database, NextAuth configurations, and UI, you can test the authentication workflow. When a user signs in with Google or GitHub, NextAuth will store their information in the database via Prisma, enabling a seamless authentication experience across sessions.

Wrapping Up

With this setup, you’ve established a robust user authentication system in Next.js using NextAuth and Prisma. This implementation is scalable, letting you expand user data management and customize the authentication experience as needed. Using NextAuth with Prisma offers flexibility, and the integration with OAuth providers like Google and GitHub provides a simple, secure way to handle authentication in modern web applications.

Happy coding!

JavaScript Development Space

© 2024 JavaScript Development Space - Master JS and NodeJS. All rights reserved.