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.
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.
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.
Setting up the API
Now let's add the NextAuth package by running the following command.
yarn add next-auth@beta
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' }
},
async authorize(credentials, req) {
const payload = {
email: credentials.email,
password: credentials.password,
};
const res = await fetch('https://cloudcoders.azurewebsites.net/api/tokens', {
method: 'POST',
body: JSON.stringify(payload),
headers: {
'Content-Type': 'application/json',
},
});
const user = await res.json();
if (!res.ok) {
throw new Error(user.message);
}
// 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.token,
refreshToken: user.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, signOut
etc.) 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' }
},
async authorize(credentials, req) {
const payload = {
email: credentials.email,
password: credentials.password,
};
const res = await fetch('https://cloudcoders.azurewebsites.net/api/tokens', {
method: 'POST',
body: JSON.stringify(payload),
headers: {
'Content-Type': 'application/json',
},
});
const user = await res.json();
if (!res.ok) {
throw new Error(user.message);
}
// 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.token,
refreshToken: user.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'),
})}
onSubmit={async (values, { setSubmitting }) => {
const res = await signIn('credentials', {
redirect: false,
email: values.email,
password: values.password,
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"
/>
</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"
/>
</label>
<div className="text-red-600 text-sm">
<ErrorMessage name="password" />
</div>
</div>
<div className="flex items-center justify-center">
<button
type="submit"
className="bg-green-400 text-gray-100 p-3 rounded-lg w-full"
>
{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:
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>
</>
}
The source code used in this article is here.
Read about how to create protected routes in NextJs.
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 and if you like the contents then please share this article.