User Authentication with Google & GitHub in Next.js Using Prisma and NextAuth
Add to your RSS feed30 October 202417 min readTable of Contents
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
- Basic knowledge of Next.js - Familiarity with Next.js and its folder structure will help.
- Node.js and npm installed - You’ll need both installed on your local machine.
- 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.
- Docker: Install Docker to easily manage your PostgreSQL database in a containerized environment. Refer to the Docker documentation for installation instructions.
- PostgreSQL: Familiarity with PostgreSQL is beneficial, as we will use it as our database.
- 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.
1 Need to install the following packages:2 create-next-app@15.0.23 Ok to proceed? (y) y45 √ Would you like to use TypeScript? ... No / Yes6 √ Would you like to use ESLint? ... No / Yes7 √ Would you like to use Tailwind CSS? ... No / Yes8 √ Would you like your code inside a `src/` directory? ... No / Yes9 √ Would you like to use App Router? (recommended) ... No / Yes10 √ Would you like to use Turbopack for next dev? ... No / Yes11 √ Would you like to customize the import alias (@/* by default)? ... No / Yes12 Creating a new Next.js app in C:\Users\useR\Desktop\NextJS\next-auth-prisma.1314 Using npm.1516 Initializing project with template: app-tw171819 Installing dependencies:20 - react21 - react-dom22 - next2324 Installing devDependencies:25 - typescript26 - @types/node27 - @types/react28 - @types/react-dom29 - postcss30 - tailwindcss31 - eslint32 - eslint-config-next
then
Once your project is created, install dependencies:
- 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:
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:
1 version: '3.8'23 services:4 db:5 image: postgres:latest6 container_name: next-oauth-prisma7 environment:8 POSTGRES_USER: postgres_user9 POSTGRES_PASSWORD: postgres_password10 POSTGRES_DB: postgres_auth11 ports:12 - '5432:5432'13 volumes:14 - postgres_data:/var/lib/postgresql/data15 - ./init.sql:/docker-entrypoint-initdb.d/init.sql # Optional: initialize with SQL script1617 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
Running docker-compose up -d
is a way to start the containers defined in your docker-compose.yml
file with some specific advantages:
- 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. - 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. - 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
. - 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:
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.
1 // This is your Prisma schema file,2 // learn more about it in the docs: https://pris.ly/d/prisma-schema34 // 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-init67 generator client {8 provider = "prisma-client-js"9 }1011 datasource db {12 provider = "postgresql"13 url = env("DATABASE_URL")14 }1516 model User {17 id String @id @default(cuid())18 name String?19 email String @unique20 emailVerified DateTime?21 image String?22 role String?23 accounts Account[]24 sessions Session[]2526 createdAt DateTime @default(now())27 updatedAt DateTime @updatedAt2829 @@map("users")30 }3132 model Account {33 userId String34 type String35 provider String36 providerAccountId String37 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?4445 createdAt DateTime @default(now())46 updatedAt DateTime @updatedAt4748 user User @relation(fields: [userId], references: [id], onDelete: Cascade)4950 @@id([provider, providerAccountId])51 @@map("accounts")52 }5354 model Session {55 sessionToken String @unique56 userId String57 expires DateTime58 user User @relation(fields: [userId], references: [id], onDelete: Cascade)5960 createdAt DateTime @default(now())61 updatedAt DateTime @updatedAt6263 @@map("sessions")64 }6566 model VerificationToken {67 identifier String68 token String69 expires DateTime7071 @@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
andSession
) 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 theUser
table. - Other fields, like
access_token
,refresh_token
, andexpires_at
, store authentication data. - @@id([provider, providerAccountId]): Composite primary key across
provider
andproviderAccountId
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 theUser
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
andtoken
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:
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:
1 Which style would you like to use? › New York2 Which color would you like to use as base color? › Zinc3 Do you want to use CSS variables for colors? › no / yes
Add ShadcnUI Components
and
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:
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:
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:
1 import Link from 'next/link';23 const Logo = () => {4 return (5 <Link href='/'>6 <div className='flex gap-2 items-center'>7 <span className='p-2 bg-main'>8 <svg9 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
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';1718 interface UserButtonProps {19 user: User;20 }2122 export default function UserButton({ user }: UserButtonProps) {23 return (24 <DropdownMenu>25 <DropdownMenuTrigger asChild>26 <Button size='icon' className='flex-none rounded-full'>27 <Image28 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 Admin51 </Link>52 </DropdownMenuItem>53 )}54 </DropdownMenuGroup>55 <DropdownMenuSeparator />56 <DropdownMenuItem asChild>57 <button58 onClick={() => signOut({ callbackUrl: '/' })}59 className='flex w-full items-center'60 >61 <LogOut className='mr-2 h-4 w-4' /> Sign Out62 </button>63 </DropdownMenuItem>64 </DropdownMenuContent>65 </DropdownMenu>66 );67 }
Navbar Component
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:
Here's how to do it:
1 // navbar.tsx23 'use client';45 import { signIn, useSession } from 'next-auth/react';6 import UserButton from './user-button';7 import { Button } from './ui/button';89 const Navbar = () => {10 const session = useSession();11 const user = session.data?.user;1213 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 tosession
.- The
user
variable extracts theuser
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
1 // layout.tsx23 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';89 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 });1920 export const metadata: Metadata = {21 title: "Create Next App",22 description: "Generated by create next app",23 };2425 export default function RootLayout({26 children,27 }: Readonly<{28 children: React.ReactNode;29 }>) {30 return (31 <html lang="en">32 <body33 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
1 import { DefaultSession } from "next-auth";23 declare module "next-auth" {4 interface Session {5 user: User & DefaultSession["user"];6 }78 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
1 import { PrismaClient } from '@prisma/client';23 const prismaClientSingleton = () => {4 return new PrismaClient();5 };67 declare const globalThis: {8 prismaGlobal: ReturnType<typeof prismaClientSingleton>;9 } & typeof global;1011 const prisma = globalThis.prismaGlobal ?? prismaClientSingleton();1213 export default prisma;1415 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
1 // header.tsx23 import Navbar from './navbar';4 import Logo from './ui/logo';56 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
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
.
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
1 // auth.ts23 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';910 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
1 export { auth as middleware } from '@/auth';
Add your Google and GitHub OAuth credentials to the .env file:
1 GOOGLE_CLIENT_ID=your-google-client-id2 GOOGLE_CLIENT_SECRET=your-google-client-secret3 GITHUB_CLIENT_ID=your-github-client-id4 GITHUB_CLIENT_SECRET=your-github-client-secret5 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:
- Visit Google Cloud Console and log in with your Google account.
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:
1 // @type {import('next').NextConfig}2 const nextConfig = {3 images: {4 remotePatterns: [5 {6 protocol: 'https',7 hostname: 'lh3.googleusercontent.com', // Google profile images8 },9 {10 protocol: 'https',11 hostname: 'avatars.githubusercontent.com', // GitHub profile avatars12 },13 ],14 },15 };1617 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!