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.
- 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.
- 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.
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.
useEffect(() => {
const fetchMe = async () => {
const result = await fetch(`${process.env.NEXT_PUBLIC_CMS_URL}/api/users/me`, {
// Make sure to include cookies with fetch
credentials: 'include',
})
.then((req) => req.json());
setUser(result.user || null);
};
fetchMe();
}, []);
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
.
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.
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.
const onSubmit = useCallback(async (data: FormData) => {
const response = await fetch(`${process.env.NEXT_PUBLIC_CMS_URL}/api/users`, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
},
})
if (response.ok) {
// Automatically log the user in
await login({
email: data.email,
password: data.password
});
// Set success message for user
setSuccess(true);
// Clear any existing errors
setError('')
} else {
setError('There was a problem creating your account. Please try again later.');
}
}, []);
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.
Top comments (0)