codelynx.dev
Posts 🇫🇷Posts 🇬🇧

Back 27/08/2024

How to Setup NextAuth Password with Database (Next App Directory)

Written by Melvyn Malherbe on 27/08/2024


If you go to the NextAuth Credentials section, you will see that the functionality is "discouraged" to limit its usage.

Also, you'll need to set up the JWT strategy to make it work.

But... how do we use it with our database? What if we don't want to change to JWT and want flexibility?

I'll help you set it up with your database in this post.

Ce tutoriel vous aide à configurer NextAuth avec un mot de passe et la structure du répertoire NextJS App. Si vous recherchez la version du répertoire de pages, consultez mon post précédent.

Setup

To set up the NextAuth Credentials, we will follow the basic steps! In your NextAuth config, add the Credentials Provider:

TS
import CredentialsProvider from 'next-auth/providers/credentials';

export const { handlers, auth: baseAuth } = NextAuth((request) => ({
  adapter: PrismaAdapter(prisma),
  providers: [
    CredentialsProvider({
      name: 'Credentials',
      credentials: {
        email: { label: 'Email', type: 'text', placeholder: 'your email' },
        password: { label: 'Password', type: 'password' },
      },
      async authorize(credentials, req) {
        // your logic with your database here
      }
    });
  ],
  session: {
    strategy: "database",
  },
  secret: env.NEXTAUTH_SECRET,
}));

Then you can use the handlers method in the app/api/auth/[...nextauth]/route.ts :

TS
import { handlers } from "@/lib/auth/auth";

export const { GET, POST } = handlers;

Authorize function

To create our password strategy, we will need to add a event function to our config.

You will need to add the signIn function like this:

TS
export const { handlers, auth: baseAuth } = NextAuth((request) => ({
  events: {
    async signIn({ user }) {
      // your logic here
    },
  },
});

In this function, we will start by knowing if the signIn method was called with the credentials options, because we won't handle any other case.

TS
export const { handlers, auth: baseAuth } = NextAuth((request) => ({
  events: {
    async signIn({ user }) {
      if (!request) {
        return;
      }

      if (request.method !== "POST") {
        return;
      }

      const currentUrl = request.url;

      // We check if the user login with credentials
      if (!currentUrl.includes("credentials")) {
        return;
      }

      if (!currentUrl.includes("callback")) {
        return;
      }
    },
  },
});

We check if there is no callback OR if there is no credentials in the query OR if the method is not POST, we return true to let NextAuth handle the authentication.

And now, we will need to do exactly like NextAuth does with other methods to handle authentication with a password.

First, at the start of my file, I had this:

TS
const tokenName =
  env.NODE_ENV === 'development'
    ? 'next-auth.session-token'
    : '__Secure-next-auth.session-token';

The tokenName in the Cookie has a different name if we are in development or production. So we will need to do the same thing here.

TS
export const { handlers, auth: baseAuth } = NextAuth((request) => ({
  callbacks: {
    async signIn({ user }) {
      if (!request) {
        return;
      }

      if (request.method !== "POST") {
        return;
      }

      const currentUrl = request.url;

      // We check if the user login with credentials
      if (!currentUrl.includes("credentials")) {
        return;
      }

      if (!currentUrl.includes("callback")) {
        return;
      }

       // We create a session
      const uuid = nanoid();
      const expireAt = addDays(new Date(), 14);
      // I use prisma for the example
      await prisma.session.create({
        data: {
          sessionToken: uuid,
          userId: user.id ?? "",
          expires: expireAt,
        },
      });

      const cookieList = cookies();

      const AUTH_COOKIE_NAME =
        env.NODE_ENV === "development"
          ? "authjs.session-token"
          : "__Secure-authjs.session-token";

      // Create and set a cookie with the exact same value
      cookieList.set(AUTH_COOKIE_NAME, uuid, {
        expires: expireAt,
        path: "/",
        sameSite: "lax",
        httpOnly: true,
        secure: env.NODE_ENV === "production",
      });

      return;
    },
  },
});

In this code, I generate a nanoid that will act as a token. I use the library nanoid to do that.

Then I generate an expireAt date that represents 7 days after today, and I create a session in

prisma using the sessionToken as uuid, the userId, and the expireAt date.

Then I create a cookie with the Cookie library, and I set the tokenName as the uuid, and I set the expireAt date.

Next, I need to add a Cookie to the res object, so I use the library Cookie to do that. Don't forget to add the secure option to true if you are in production.

I finally set the cookie with the tokenName as the uuid, the expireAt date, the secure option, the sameSite option, and the path option.

Override jwt

If you do everything above, it will not work. NextAuth will delete the token because he doesn't reconnaît it.

To fix this, we will disable the jwt method :

TS
export const { handlers, auth: baseAuth } = NextAuth((request) => ({
  // ...
  jwt: {
    encode() {
      return "";
    },
    async decode() {
      return null;
    },
  };
}));

With this code, NextAuth will correctly log in the user with the cookie created together.

Finish

I hope this article will help you make your authentication work with NextJS 14.