Next.js 14 and NextAuth v4 : Credentials Authentication A Detailed Step-by-Step Guide
Tags: webdev, authjs, nextjs, beginners
Published: 2024-06-30 16:20:05

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.

```bash
npx create-next-app@latest
```

Follow the prompts and go with the default. 

![nextjs installation options](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hhnbgfij0greyd1ybf61.png)


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:

```bash
npm run dev
```

This will open your project on [localhost:3000](http://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.

```bash
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. 

```jsx
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:

1. Neon 
2. Supabase
3. Xata

After you generate the DATABASE_URL update the .env file

```jsx
DATABASE_URL="YOUR_DATABASE_URL"
```

**Migration commands**

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

```jsx
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. 

```jsx
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.

```jsx
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

```jsx
import CredentialsProvider from "next-auth/providers/credentials";
```

Now, add this code snippet inside the providers array:

```jsx
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. 

```jsx
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.

```jsx
"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. 

```jsx
"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;

```

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. 

```jsx
"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.

```jsx
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:

```jsx
"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.

```jsx
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**

[Twitter/x](https://x.com/CoderAmrin)

[Github](https://github.com/coderamrin/)

[LinkedIn](https://www.linkedin.com/in/coderamrin/)

Happy Coding.