Author
Dan Ribbens headshot
Dan Ribbens
Published On

Handling authentication in Next.js with Payload

Author
Dan Ribbens headshot
Dan Ribbens
Published On
Handling authentication in NextJS on Payload CMS
Handling authentication in NextJS on Payload CMS

Understanding and building authentication can be a burden at best, and a security risk at worst. Let's get through it together!

Payload gives the most complete and flexible way to manage user collections that support all the different authentication needs of your sites. It does so using secure HTTP-only cookies and includes support of boilerplate workflows for password recovery and lockout. You can even use Payload to manage multiple auth collections or build in robust role based access controls.

By the end of this article you will have a full understanding of how to implement authentication of users on your frontend NextJS website that can be managed from the admin UI of Payload CMS.

Overview

There are two code repositories created for this article that will be used as reference.

  1. payloadcms/next-auth-frontend—the React app built in NextJS. It is a starter website that includes login and logout routes and forms for login, forgot password and reset password. It also demonstrates how the frontend will call the backend for the authenticated user.
  2. payloadcms/next-auth-cms—the Payload CMS app setup to handle the database, storing user credentials, and the APIs for handling the REST or GraphQL requests that our auth example is built on.

You can follow the getting started sections of each repo provided. Setup should only take a few minutes for each. The seeding functions are called from the onInit function found in the payload.config.ts of our next-auth-cms app. By starting the server you should already have both an admin user and a customer, as defined by each role and also a home page.

User Auth

Since the user has already been created in the onInit function, we should be able to start both frontend and backend services, open the browser to the locally running site to see the landing page.

We haven't logged in yet and the UI reflects that.

Localhost Screenshot

If we were to look at the browser inspection tools showing network traffic and refresh the page, we'd see a call being made to backend that identifies the user state. The route is a GET request to /api/users/me on the backend which at this point simply returned user: null.

That call has been defined as a useEffect built into the Auth Provider component of auth-next-frontend under /components/Auth/index.ts. It is helpful to understand that the user in this auth context at any given time may be one of three states: undefined, before the query has finished, null as a guest, or the successful user object being returned.

1
useEffect(() => {
2
const fetchMe = async () => {
3
const result = await fetch(`${process.env.NEXT_PUBLIC_CMS_URL}/api/users/me`, {
4
// Make sure to include cookies with fetch
5
credentials: 'include',
6
})
7
.then((req) => req.json());
8
setUser(result.user || null);
9
};
10
fetchMe();
11
}, []);

Getting the correct user back from the API request is possible because of the http cookie included in the fetch request which Payload APIs are automatically built and ready to handle for us.

Login

Now that we have enjoyed lurking as a guest, let's go over login.

From the screenshot we have a basic form that does the work of sending the entered credentials to the auth provider by calling the login from useAuth.

Payload Login Screen Screenshot

The login form can be found in the /pages/login/index.ts of auth-next-frontend and handles basic error messages but is otherwise left as simple as possible.

Account management and access control

Assuming we were successful in entering the user credentials we should be at the /account page. This form is used to send updates on the user collection of the logged in user. There isn't any API work to be done since Payload already takes care of that for us and can be done with GraphQL or REST as we've done here.

Now we are able to make further calls to the API endpoints from our authenticated frontend. The API will read the user in a stateless way and execute the access control for the user needed for that request.

Payload Account Screenshot

Checking into the backend code now, the user collection in next-auth-cms has access defined so that Bob or an Admin role are the only ones able to read or update the user.

Logout

The logout functionality is very similar to login except that we set the user in the frontend state to null on completion. The backend API will no longer accept requests from the authentication cookie that was originally returned from logging in.

Create Account

Now that we are no longer authenticated as bob. We can create a new account. Here is the code that handles submitting the create-account form.

1
const onSubmit = useCallback(async (data: FormData) => {
2
const response = await fetch(`${process.env.NEXT_PUBLIC_CMS_URL}/api/users`, {
3
method: 'POST',
4
body: JSON.stringify(data),
5
headers: {
6
'Content-Type': 'application/json',
7
},
8
})
9
10
if (response.ok) {
11
// Automatically log the user in
12
await login({
13
email: data.email,
14
password: data.password
15
});
16
17
// Set success message for user
18
setSuccess(true);
19
20
// Clear any existing errors
21
setError('')
22
} else {
23
setError('There was a problem creating your account. Please try again later.');
24
}
25
}, []);

At a high level, we are sending form data to the POST endpoint /api/users followed by a login with the same credentials.

This is possible because of our access control on users. You may have noticed that the access for create is set to return true. That allows a guest user to create a new customer account. Had that not been set, Payload would use the default access control, which allows requests for any logged in user. That wouldn't work for anonymous users of course.

You might be asking, What is stopping a user from creating an account with the role of admin?

Within the roles field you can see an additional access included so that only an admin is able to assign this particular field.

Another way you could handle this is by creating a separate collection for admins or customers outside of roles which would further separate security concerns, that is up to you.

Password recovery

The frontend also has pages built for users to recover a lost password. These forms are built to call the API routes built-in to Payload for the workflow needed to enter the email address, receive the email with a secure link, and complete the reset password.

Payload makes these easily configurable along with account lockouts and other security features. You can read the Authentication documentation for details.

Security

By separating our backend and frontend we also need to have Cross Origin Resource Sharing (CORS) properly configured. Without this, we would be prevented from making frontend calls to the API from a different web address. Payload has a simple configuration for both CORS and you can see it is already configured in the payload.config.ts file in next-auth-cms using environment variables defined in .env. You don't need to configure CORS if your requests are made from the same domain.

Using http-only cookies is a principal detail in how modern REST architecture works. It is an essential tool for building secure and scalable applications. Hopefully this guide helped you understand and implement it for your next big idea.

Wrapping up

That's it! We have fully locked down our app and given access to only the right people.

The two template repos built for this blog post are meant to be starting points that anyone can build on or reference. To use them in production be sure to remove the onInit seeding functions.

Find us on twitter @payloadcms if you liked this article or join our growing Discord community.