Authentication is a crucial feature in web applications, and it can also be challenging to implement correctly. In this article, you'll learn how to integrate authentication in Next.js using NextAuth (v4).
Let’s get started.
Prerequisites
To make the most out of this guide, here’s what you should have under your belt:
- A basic understanding of HTML, CSS, and JavaScript.
- Some familiarity with Next.js and TypeScript.
- Experience using Tailwind CSS, even if it’s just the basics.
- Node.js installed on your computer. If unsure, check by running
node -v
in your terminal!
Install Next.js
To get started you need a Next.js project. You can skip this part if you want to integrate NextAuth into your existing Next.js project.
Run this command to initialize a bare-bone next.js project.
npx create-next-app@latest
Follow the prompts and go with the default.
This will generate a Next.js project with all the necessary files/folders and dependencies.
After the installation cd into the project directory and run this command:
npm run dev
This will open your project on localhost:3000
Database Configuration
For authentication, you have to store the user data on a Database.
So, before configuring NextAuth configure the database.
In this guide, we will use Postgresql with Prisma.
Install all these dependencies for integrating NextAuth with a Database.
npm install prisma next-auth @next-auth/prisma-adapter bcrypt
Create the Database Schema
On the root of your project folder create the schema file prisma → schema.prisma
Now, copy this over to that file.
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id String @id @default(cuid())
name String?
email String @unique
emailVerified DateTime?
password String?
image String?
accounts Account[]
sessions Session[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Account {
userId String
type String
provider String
providerAccountId String
refresh_token String?
access_token String?
expires_at Int?
token_type String?
scope String?
id_token String?
session_state String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@id([provider, providerAccountId])
}
model Session {
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model VerificationToken {
identifier String
token String
expires DateTime
@@id([identifier, token])
}
The first two blocks of code connect the database with Prisma.
So, we need the DATABASE_URL to make it work.
There are a lot of PostgreSQL providers available for example:
- Neon
- Supabase
- Xata
After you generate the DATABASE_URL update the .env file
DATABASE_URL="YOUR_DATABASE_URL"
Migration commands
Now, run these commands to create the tables on the database.
npx prisma generate
npx prisma db push
Create the Prisma client
Now create the Prisma Client to perform all the database-related operations.
Inside the app directory create libs/prismaDB.ts file and paste this code.
import { PrismaClient } from "@prisma/client";
const globalForPrisma = global as unknown as { prisma: PrismaClient };
export const prisma =
globalForPrisma.prisma ||
new PrismaClient({
log: ["query"],
});
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
Now you can import prisma
from anywhere on the project and use it to run the Database queries.
Configure NextAuth
Now we have to create the authOptions config.
Create the authOptions object inside the libs/auth.ts file ****and paste this code.
import { prisma } from "@/app/libs/prismaDB";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import bcrypt from "bcrypt";
import { NextAuthOptions } from "next-auth";
export const authOptions: NextAuthOptions = {
pages: {
signIn: "/auth/signin",
},
session: {
strategy: "jwt",
},
adapter: PrismaAdapter(prisma),
secret: process.env.SECRET,
providers: [
// the providers goes here
],
// debug: process.env.NODE_ENV === "development",
};
In this code snippet, we imported the Prisma client, PrismaAdapter, and NextAuthOptions from next-auth.
And created authOptions object, which contains all the nextAuth-related configs.
Here’s the break-down:
pages: It defines the page for auth. Used for redirects and fallback.
session: Defines the session strategy. We will use JWT.
adapter: Defines the adapter to manage Database operations. We will use Prisma.
secret: A secret for the Session.
providers: An Array of all the providers. We will use the Credentials provider. ****
Configuring Credentials Provider
Now, let’s configure the Credentials provider.
Go to auth.ts file and import the Credentials provider from nextAuth
import CredentialsProvider from "next-auth/providers/credentials";
Now, add this code snippet inside the providers array:
CredentialsProvider({
name: "credentials",
id: "credentials"
credentials: {
email: { label: "Email", type: "text", placeholder: "jsmith" },
password: { label: "Password", type: "password" },
username: {
label: "Username",
type: "text",
placeholder: "John Smith",
},
},
async authorize(credentials) {
// check to see if email and password is there
if (!credentials?.email || !credentials?.password) {
throw new Error("Please enter an email and password");
}
// check to see if user exists
const user = await prisma.user.findUnique({
where: {
email: credentials?.email,
},
});
// if no user was found
if (!user || !user?.hashedPassword) {
throw new Error("No user found");
}
// check to see if password matches
const passwordMatch = await bcrypt.compare(
credentials.password,
user.hashedPassword
);
// if password does not match
if (!passwordMatch) {
throw new Error("Incorrect password");
}
return user;
},
}),
Here’s the breakdown of the credentials config:
Name: The name of the credentials provider. If only one Credentials provider is used, it is specified in the signIn
function.
Id: A unique identifier for the credentials provider. When you have more than one credentials provider, you must use this identifier in the signIn
function instead of the name.
Credentials: Specify the values required for signing in.
Authorize: A callback function that uses the provided credentials to perform the authorization operations.
Configure User Registration
You need a registered user on the Database to be able signIn
Let’s set up user registrations.
Create the register route in api→auth→register→route.ts and paste this code.
import bcrypt from "bcrypt";
import { NextResponse } from "next/server";
import { prisma } from "@/app/libs/prismaDB";
export async function POST(request: any) {
const body = await request.json();
const { name, email, password } = body;
if (!name || !email || !password) {
return new NextResponse("Missing Fields", { status: 400 });
}
const exist = await prisma.user.findUnique({
where: {
email,
},
});
if (exist) {
throw new Error("Email already exists");
}
const hashedPassword = await bcrypt.hash(password, 10);
const user = await prisma.user.create({
data: {
name,
email,
hashedPassword,
},
} as any);
return NextResponse.json(user);
}
To register users, create a POST route that gets the user data from the request body.
Check if any required fields are missing; if they are, send an error message.
Next, fetch the user with the provided email. If the user already exists, return an error message and prompt them to log in or try a different email. If no user is found, create a new user with the provided credentials and return the user.
Sing Up Form
Let’s create the sign-up form to register User.
Create the Sign-Up route in app → auth → signup → page.tsx and paste this code.
"use client";
import Link from "next/link";
import React from "react";
import axios from "axios";
import { useRouter } from "next/navigation";
import { useState } from "react";
import toast from "react-hot-toast";
const Signup = () => {
const router = useRouter();
const [data, setData] = useState({
name: "",
email: "",
password: "",
rePassword: "",
});
const { name, email, password, rePassword } = data;
const handleChange = (e: any) => {
setData({ ...data, [e.target.name]: e.target.value });
};
const registerUser = async (e: any) => {
e.preventDefault();
if (!name || !email || !password) {
return toast.error("Something went wrong!");
}
axios
.post("/api/auth/register", {
name,
email,
password,
})
.then(() => {
toast.success("User has been registered.");
router.push("/auth/signin");
setData({
name: "",
email: "",
password: "",
rePassword: "",
});
})
.catch(() => toast.error("Something went wrong"));
};
return (
<form
onSubmit={registerUser}
className="w-[300px] mx-auto flex flex-col justify-center h-screen"
>
<h1 className="text-3xl font-bold mb-5">Sign Up</h1>
<div className="mb-6">
<label
htmlFor="name"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
>
Name
</label>
<input
type="text"
id="name"
name="name"
value={data.name}
onChange={handleChange}
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="jhon doe"
required
/>
</div>
<div className="mb-6">
<label
htmlFor="email"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
>
Email
</label>
<input
type="email"
id="email"
name="email"
value={data.email}
onChange={handleChange}
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="john.doe@company.com"
required
/>
</div>
<div className="mb-6">
<label
htmlFor="password"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
>
Password
</label>
<input
type="password"
id="password"
name="password"
value={data.password}
onChange={handleChange}
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="•••••••••"
required
/>
</div>
<button
type="submit"
className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
>
Submit
</button>
<div className="mt-5">
<p>
Already have an account?{" "}
<Link className="text-blue-700" href="/auth/signin">
Signin
</Link>{" "}
or Go to{" "}
<Link className="text-blue-700" href="/">
Home
</Link>
</p>
</div>
</form>
);
};
export default Signup;
It is a simple sign-up form. To keep it simple I put it right into the route. You can keep your sign-in, and sign-up form inside the components folder.
I’ve also used toast to show the message. Check out the Setup the Session Provider section to see how to configure toast notification.
Sign In Form
Lastly, we need this sign-in form.
Just like the sign-up route create the sign-in route and paste this code into it.
"use client";
import Link from "next/link";
import React, { useState } from "react";
import { signIn } from "next-auth/react";
import toast from "react-hot-toast";
const Signin = () => {
const [data, setData] = useState({
email: "",
password: "",
});
const { email, password } = data;
const handleChange = (e: any) => {
setData({ ...data, [e.target.name]: e.target.value });
};
const handleSubmit = async (e: any) => {
e.preventDefault();
const result = await signIn("credentials", {
redirect: false,
email,
password,
});
if (result?.ok) {
toast.success("Logged in successfully");
setData({
email: "",
password: "",
});
window.location.href = "/";
} else {
toast.error("Invalid credentials");
}
};
return (
<div className="w-[300px] mx-auto flex flex-col justify-center h-screen">
<h1 className="text-3xl font-bold mb-5">Sign in</h1>
<form onSubmit={handleSubmit}>
<div className="mb-6">
<label
htmlFor="email"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
>
Email
</label>
<input
type="email"
id="email"
name="email"
value={email}
onChange={handleChange}
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="john.doe@company.com"
required
/>
</div>
<div className="mb-6">
<label
htmlFor="password"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
>
Password
</label>
<input
type="password"
id="password"
name="password"
value={password}
onChange={handleChange}
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="•••••••••"
required
/>
</div>
<button
type="submit"
className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
>
Submit
</button>
<div className="mt-5">
<p>
Don't have an account?{" "}
<Link className="text-blue-700" href="/auth/signup">
Signup
</Link>{" "}
or Go to{" "}
<Link className="text-blue-700" href="/">
Home
</Link>
</p>
</div>
</form>
</div>
);
};
export default Signin;
So, we have a simple sign-in form. We used state to store the form data.
And handled the form submission slightly differently, we didn’t call our auth/sign-in API instead we used the signIn
function from NextAuth.
The sign function takes two arguments, first the name of the provider and then a callback function to check the response. (Just like our API)
Setup the Session provider
Lastly, you have to wrap the whole application with the SessionProvider.
The SessionProvider will allow you to use useSession
on your application.
Inside the app directory create providers→index.tsx and paste this code into it.
"use client";
import { SessionProvider } from "next-auth/react";
import { Toaster } from "react-hot-toast";
const Proveiders = ({ children }: { children: React.ReactNode }) => {
return (
<>
<div className="z-[99999]">
<Toaster position="top-center" reverseOrder={false} />
</div>
<SessionProvider>{children}</SessionProvider>
</>
);
};
export default Proveiders;
In this provider's file, you can add any of the providers you need to use, it will help you keep layout.tsx
file clean.
At the top of the session provider, I added Toaster for the toast notification.
Don’t forget to install react-hot-toast
to show the toast notification.
Now, import Providers
inside the layout.tsx
and wrap children
inside it.
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import Proveiders from "./providers";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
<Proveiders>{children}</Proveiders>
</body>
</html>
);
}
Getting the User Session in a Client Component
Now you can see the logged-in user’s data from the Session.
Here’s an example of how to get the session with useSession
hook:
"use client";
import Link from "next/link";
import { useSession } from "next-auth/react";
import { signOut } from "next-auth/react";
export default function Home() {
const { data: session } = useSession();
return (
<main className="flex flex-col items-center justify-between p-24">
{session?.user && (
<div>
<p className="text-3xl">Welcome, {session.user.name}</p>
<div className="mb-5">{JSON.stringify(session?.user, null, 2)}</div>
<button
onClick={() => signOut()}
className="bg-black hover:bg-gray-700 text-white font-bold py-2 px-4 rounded"
>
Logout
</button>
</div>
)}
{!session?.user && (
<div className="mt-5 space-x-3">
<Link
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
href="/auth/signin"
>
Signin
</Link>
<Link
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
href="/auth/signup"
>
Signup
</Link>
</div>
)}
</main>
);
}
Retrieve the user session using the useSession
hook and render the user data. If the session is empty, the user is not logged in. Ask the user to sign in or sign up to view the session.
Getting the User Session in a Server Component
Getting a user session on a Server component is a bit different than a Client component because you can’t use useSession
hook on a server component.
Here’s how you can get the logged-in user session on the Server component.
import React from "react";
import { authOptions } from "../libs/auth";
import { getServerSession } from "next-auth";
import Link from "next/link";
const UserPage = async () => {
const session = await getServerSession(authOptions);
return (
<main className="flex flex-col items-center justify-between p-24">
{session?.user && (
<div>
<p className="text-3xl">Welcome, {session.user.name}</p>
<div className="mb-5">{JSON.stringify(session?.user, null, 2)}</div>
<Link href="/">Go back to home</Link>
</div>
)}
{!session?.user && (
<div className="mt-5 space-x-3">
<Link
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
href="/auth/signin"
>
Signin
</Link>
<Link
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
href="/auth/signup"
>
Signup
</Link>
</div>
)}
</main>
);
};
export default UserPage;
First, import authOptions from auth.ts file and import getServerSession
from next-auth
Inside the component call the getServerSession
function with authOptions
.
It will return the current user session.
You have the user session on the Server Component.
Congrats!
You successfully integrated Authentication on your Next.js App.
Resources
Source code: https://github.com/Coderamrin/nextauth-integration-v4/tree/credentials-signin
Documentation: https://authjs.dev/getting-started/authentication/credentials
Conclusion
In this article, you learned how to integrate NextAuth in the Next.js 14 app. We covered the Credentials provider only, In the next part we will cover Social login and Magic link.
Follow me to get notified when the next part is published.
Connect With Me
Happy Coding.
Top comments (2)
Great article Amrin. I enjoyed learning from it
Thanks Lucian ❤️