DEV Community

Cover image for Next.js 14 and NextAuth v4 : Credentials Authentication A Detailed Step-by-Step Guide
Amrin
Amrin

Posted on • Originally published at coderamrin.com

Next.js 14 and NextAuth v4 : Credentials Authentication A Detailed Step-by-Step Guide

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:

  1. A basic understanding of HTML, CSS, and JavaScript.
  2. Some familiarity with Next.js and TypeScript.
  3. Experience using Tailwind CSS, even if it’s just the basics.
  4. 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
Enter fullscreen mode Exit fullscreen mode

Follow the prompts and go with the default.

nextjs installation options

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

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

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])
}

Enter fullscreen mode Exit fullscreen mode

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:

  1. Neon
  2. Supabase
  3. Xata

After you generate the DATABASE_URL update the .env file

DATABASE_URL="YOUR_DATABASE_URL"
Enter fullscreen mode Exit fullscreen mode

Migration commands

Now, run these commands to create the tables on the database.

npx prisma generate
npx prisma db push
Enter fullscreen mode Exit fullscreen mode

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

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",
};

Enter fullscreen mode Exit fullscreen mode

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

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

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);
}

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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&#39;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;

Enter fullscreen mode Exit fullscreen mode

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

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>
  );
}

Enter fullscreen mode Exit fullscreen mode

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>
  );
}

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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

Twitter/x

Github

LinkedIn

Happy Coding.

Top comments (2)

Collapse
 
devluc profile image
Devluc

Great article Amrin. I enjoyed learning from it

Collapse
 
coderamrin profile image
Amrin

Thanks Lucian ❤️