How to implement NextAuth credentials provider with external API and login page

In this post, we'll learn how to use NextAuth credentials provider with a custom backend and a custom login page.

NextAuth credentials provider with custom backend and login page

When it comes to adding authentication to your next.js project, NextAuth is a wonderful option. It's easy to see why, given its extensive provider support, which includes Apple, GitHub, Azure Active Directory, Azure Active Directory B2C, Google, and more. It can help you set up your authentication in minutes!

However, for different reasons, you may need to implement your custom backend or external API with an email/password login. That's where the credentials provider associated with your API server comes in handy. I was in the same situation and couldn't find a clear description with examples, so I had to pull together the details myself (especially handling errors from the custom backend and handling them on your custom login page). If you're in a similar boat, I hope this helps!

Starting your Project

Open a command prompt or terminal window in the location where you wish to save your project and run the following command.

npx create-next-app -e with-tailwindcss my-project

This creates a new project in the my-project directory. I have also included Tailwind CSS. You should see something like this in the my-project directory.

next-auth-credentials-provider-project-layout
File structure after running the create-next-app command

After that, change the directory to my-projects and type yarn dev from your terminal to start the next development server. You can now go to http://localhost:3000 and see the following.

next-auth-credentials-provider-home-page

Setting up the API

Now let's add the NextAuth package by running the following command.

yarn add [email protected]

In our .env file, we need to create a NEXTAUTH URL. Add the following to your .env file.

NEXTAUTH_URL=http://localhost:3000

Now, create a file called [...nextauth].js in pages/api/auth to include NextAuth.js in your project. This contains the NextAuth.js dynamic route handler, as well as all of your global NextAuth.js configuration.

We need to add the following to the [...nextauth].js, each section will be described later.

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

export default NextAuth({
  providers: [
    CredentialsProvider({
      // The name to display on the sign in form (e.g. 'Sign in with...')
      name: 'my-project',
      // The credentials is used to generate a suitable form on the sign in page.
      // You can specify whatever fields you are expecting to be submitted.
      // e.g. domain, username, password, 2FA token, etc.
      // You can pass any HTML attribute to the <input> tag through the object.
      credentials: {
        email: {
          label: 'email',
          type: 'email',
          placeholder: '[email protected]',
        },
        password: { label: 'Password', type: 'password' },
        tenantKey: {
          label: 'Tenant Key',
          type: 'text',
        },
      },
      async authorize(credentials, req) {
        const payload = {
          email: credentials.email,
          password: credentials.password,
        };

        const res = await fetch('http://localhost:5000/api/tokens', {
          method: 'POST',
          body: JSON.stringify(payload),
          headers: {
            'Content-Type': 'application/json',
            tenant: credentials.tenantKey,
            'Accept-Language': 'en-US',
          },
        });

        const user = await res.json();
        if (!res.ok) {
          throw new Error(user.exception);
        }
        // If no error and we have user data, return it
        if (res.ok && user) {
          return user;
        }

        // Return null if user data could not be retrieved
        return null;
      },
    }),
    // ...add more providers here
  ],
  secret: process.env.JWT_SECRET,
  pages: {
    signIn: '/login',
  },
  callbacks: {
    async jwt({ token, user, account }) {
      if (account && user) {
        return {
          ...token,
          accessToken: user.data.token,
          refreshToken: user.data.refreshToken,
        };
      }

      return token;
    },

    async session({ session, token }) {
      session.user.accessToken = token.accessToken;
      session.user.refreshToken = token.refreshToken;
      session.user.accessTokenExpires = token.accessTokenExpires;

      return session;
    },
  },
  theme: {
    colorScheme: 'auto', // "auto" | "dark" | "light"
    brandColor: '', // Hex color code #33FF5D
    logo: '/logo.png', // Absolute URL to image
  },
  // Enable debug messages in the console if you are having problems
  debug: process.env.NODE_ENV === 'development',
});

All requests to /api/auth/* (signIn, callback, signOutetc.) will automatically be handled by NextAuth.js.

The first section is the providers section:

providers: [
    CredentialsProvider({
      id: 'credentials',
      name: 'my-project',
      credentials: {
        email: {
          label: 'email',
          type: 'email',
          placeholder: '[email protected]',
        },
        password: { label: 'Password', type: 'password' },
        tenantKey: {
          label: 'Tenant Key',
          type: 'text',
        },
      },
      async authorize(credentials, req) {
        const payload = {
          email: credentials.email,
          password: credentials.password,
        };

        const res = await fetch('http://localhost:5000/api/tokens', {
          method: 'POST',
          body: JSON.stringify(payload),
          headers: {
            'Content-Type': 'application/json',
            tenant: credentials.tenantKey,
            'Accept-Language': 'en-US',
          },
        });

        const user = await res.json();
        if (!res.ok) {
          throw new Error(user.exception);
        }
        // If no error and we have user data, return it
        if (res.ok && user) {
          return user;
        }

        // Return null if user data could not be retrieved
        return null;
      },
    }),
    // ...add more providers here
  ],

This returns an array of providers; we only have one in our example, which is the credentials provider, but you may add others like GitHub, Google, or Apple.

When we execute the signin method from the frontend, the id is utilized to trigger the provider that we require, in this case, the credentials.

Next, we have an async authorize method. This method takes a parameter called credentials. Credentials will hold the user, password, and any other payloads required by the custom backend API that you will send from the frontend with the sign-in information.

The next section is secret and custom pages.

  secret: process.env.JWT_SECRET,
  pages: {
    signIn: '/login',
  },

A secret to using for a key generation - you should set this explicitly. This is used to generate the actual signing key and produces a warning message if not defined explicitly.

If you wish to create custom sign-in, sign-out, or error pages, you can utilize pages to provide URLs. I have put the custom login page at  pages/login.js.

Next up we have a series of callbacks. Note that the callbacks will accept a variety of parameters, but not all of them are required for the provider we're using, and some information may vary depending on the provider - the information below is only applicable for the credentials provider.

callbacks: {
    async jwt({ token, user, account }) {
      if (account && user) {
        return {
          ...token,
          accessToken: user.data.token,
          refreshToken: user.data.refreshToken,
        };
      }

      return token;
    },

    async session({ session, token }) {
      session.user.accessToken = token.accessToken;
        
      return session;
    },
  },

The JWT callback is called first, which sets up the data for the session.

The signing method returns the user parameter which contains the user model, but only on the first call, i.e. when the user first signs in, so here, add the user key to the token and set it to be the value of the user model, otherwise it just returns the token.

The session callback sets this in the session with the user key.

Using the NextAuth API in the frontend

The custom login.js page should look like this. I am also using Formik and Yup for client-side validation.

import { useState } from 'react';
import { signIn, getCsrfToken } from 'next-auth/react';
import { Formik, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';
import { useRouter } from 'next/router';

export default function SignIn({ csrfToken }) {
  const router = useRouter();
  const [error, setError] = useState(null);

  return (
    <>
      <Formik
        initialValues={{ email: '', password: '', tenantKey: '' }}
        validationSchema={Yup.object({
          email: Yup.string()
            .max(30, 'Must be 30 characters or less')
            .email('Invalid email address')
            .required('Please enter your email'),
          password: Yup.string().required('Please enter your password'),
          tenantKey: Yup.string()
            .max(20, 'Must be 20 characters or less')
            .required('Please enter your organization name'),
        })}
        onSubmit={async (values, { setSubmitting }) => {
          const res = await signIn('credentials', {
            redirect: false,
            email: values.email,
            password: values.password,
            tenantKey: values.tenantKey,
            callbackUrl: `${window.location.origin}`,
          });
          if (res?.error) {
            setError(res.error);
          } else {
            setError(null);
          }
          if (res.url) router.push(res.url);
          setSubmitting(false);
        }}
      >
        {(formik) => (
          <form onSubmit={formik.handleSubmit}>
            <div className="bg-red-400 flex flex-col items-center justify-center min-h-screen py-2 shadow-lg">
              <div className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
                <input
                  name="csrfToken"
                  type="hidden"
                  defaultValue={csrfToken}
                />

                <div className="text-red-400 text-md text-center rounded p-2">
                  {error}
                </div>
                <div className="mb-4">
                  <label
                    htmlFor="email"
                    className="uppercase text-sm text-gray-600 font-bold"
                  >
                    Email
                    <Field
                      name="email"
                      aria-label="enter your email"
                      aria-required="true"
                      type="text"
                      className="w-full bg-gray-300 text-gray-900 mt-2 p-3 rounded-lg focus:outline-none focus:shadow-outline"
                    />
                  </label>

                  <div className="text-red-600 text-sm">
                    <ErrorMessage name="email" />
                  </div>
                </div>
                <div className="mb-6">
                  <label
                    htmlFor="password"
                    className="uppercase text-sm text-gray-600 font-bold"
                  >
                    password
                    <Field
                      name="password"
                      aria-label="enter your password"
                      aria-required="true"
                      type="password"
                      className="w-full bg-gray-300 text-gray-900 mt-2 p-3 rounded-lg focus:outline-none focus:shadow-outline"
                    />
                  </label>

                  <div className="text-red-600 text-sm">
                    <ErrorMessage name="password" />
                  </div>
                </div>
                <div className="mb-6">
                  <label
                    htmlFor="tenantKey"
                    className="uppercase text-sm text-gray-600 font-bold"
                  >
                    Tenant
                    <Field
                      name="tenantKey"
                      aria-label="enter your Tenant"
                      aria-required="true"
                      type="text"
                      className="w-full bg-gray-300 text-gray-900 mt-2 p-3 rounded-lg focus:outline-none focus:shadow-outline"
                    />
                  </label>

                  <div className="text-red-600 text-sm">
                    <ErrorMessage name="tenantKey" />
                  </div>
                </div>
                <div className="flex items-center justify-center">
                  <button
                    type="submit"
                    className="uppercase text-sm font-bold tracking-wide bg-green-400 text-gray-100 p-3 rounded-lg w-full focus:outline-none focus:shadow-outline hover:shadow-xl active:scale-90 transition duration-150"
                  >
                    {formik.isSubmitting ? 'Please wait...' : 'Sign In'}
                  </button>
                </div>
              </div>
            </div>
          </form>
        )}
      </Formik>
    </>
  );
}

// This is the recommended way for Next.js 9.3 or newer
export async function getServerSideProps(context) {
  return {
    props: {
      csrfToken: await getCsrfToken(context),
    },
  };
}

Line #26 - 31: This uses the credentials provider to call the sign-in function and passes the email and password we need to authenticate against. You can also set the callback Url where you want to redirect after a successful login.

You should be able to log in at this stage if you've entered the correct credentials and your API endpoint is working as expected.

We can also display the error message on our custom login page in any way we want with this configuration. For example, I'm displaying the following server error message:

next-auth-credentials-provider-custom-login-page

The easiest way to check if someone is logged in is to use useSession() React Hook. Make sure that <SessionProvider> is added to pages/_app.js.

import { useSession, signIn, signOut } from "next-auth/react"

export default function Component() {
  const { data: session } = useSession()
  if(session) {
    return <>
      Signed in as {session.user.email} <br/>
      <button onClick={() => signOut()}>Sign out</button>
    </>
  }
  return <>
    Not signed in <br/>
    <button onClick={() => signIn()}>Sign in</button>
  </>
}

Read about how to create protected routes in NextJs Create Protected Routes In NextJS and NextAuth

I hope this post helps, I've only recently started looking into NextJS and NEXTAuth so if you spot any issues or areas where it can be improved then let me know in the comments.