Headless WordPress Authentication with Native Cookies
https://wpengine.com/builders/headless-wordpress-authentication-native-cookies
Last updated
Was this helpful?
https://wpengine.com/builders/headless-wordpress-authentication-native-cookies
Last updated
Was this helpful?
When developing a traditional, monolithic WordPress site, you typically don’t give authentication a second thought. WordPress already provides a native cookie-based authentication system that works out of the box.
What you may not know is that for a headless WordPress project, it’s possible to leverage that same authentication system in your decoupled JavaScript frontend app!
In this post, we’ll explore a Next.js app that uses WordPress’ own native authentication cookies to provide the following functionality:
Log in
Log out
New user sign-ups
Password resets
User profile page
A “Members” page with gated content that only authenticated users can access
A “Create Post” page where users with the publish_posts
capability can create new posts, but other users can’t.
A useAuth()
custom hook that provides the user’s loggedIn
status and user details to the rest of the app via React context.
Helper components to limit certain content to only authenticated/unauthenticated users.
(A Gatsby.js port of the codebase also exists, here)
To accomplish this, we’ll leverage the WPGraphQL CORS plugin, which allows us to tell WordPress to not only accept cookies from the domain where WP is installed, but also from the decoupled frontend app’s domain. That way, the same cookies can be used to authenticate users on both domains.
To benefit from this post, you should be familiar with the basics of local WordPress development, WPGraphQL, React, and Apollo Client.
Here are the steps for getting set up:
Set up a local WordPress site and get it running.
Install and activate the WPGraphQL, WPGraphQL CORS, and Headless WordPress Email Settings WordPress plugins.
From the WordPress admin sidebar, go to GraphQL
> Settings
and click the CORS Settings
tab.
Check the checkboxes next to these options:
– Send site credentials
– Enable login mutation
– Enable logout mutation
In the Extend "Access-Control-Allow-Origin” header
field, enter http://localhost:3000
and click the button to save your changes.
The CORS Settings page should now look like this:
Clone down the Next.js app repo.
Create a .env.local
file inside of the app’s root folder. Open that file in a text editor and paste in NEXT_PUBLIC_WORDPRESS_API_URL=https://headlesswpcookieauth.local/graphql
, replacing headlesswpcookieauth.local
with the domain for your local WordPress site. This is the endpoint that Apollo Client will use when it sends requests to your WordPress backend.
Run npm install
(or yarn
) to install the app’s NPM dependencies.
Run npm run dev
to get the server running locally.
You should now be able to visit http://localhost:3000/ in a web browser and see the app’s homepage.
Alternatively, you can follow the steps for getting set up with the Gatsby.js port of this codebase.
Take a minute to explore the app. Try logging in and logging out. You’ll notice that the top navigation bar re-renders to show different things when you’re logged in vs. logged out, and that some pages are only accessible when you’re logged in/out.
At a high level, our authentication system works like this:
An unauthenticated user navigates to the Log In page and attempts to log in.
Our app fires off a GraphQL request to the WordPress backend with that user’s email address and password.
If the credentials are valid, WordPress sends an HttpOnly cookie back to the client, along with a success response.
Knowing that the user has been logged in successfully, our app is then able to fire off a request to get some basic info about that user and re-render the app to display links and pages meant for authenticated users.
Requests made to the WordPress backend after that point include the auth cookie in the request headers, which is used to authenticate the user.
That “HttpOnly” cookie part is important. Some other authentication strategies involve storing an authentication token in local storage or in a cookie that does not have the HttpOnly
attribute. That means that the site would be vulnerable to a cross-site scripting (XSS) attack where some rogue client-side JavaScript code would be able to “reach” into local storage/the non-HttpOnly cookie to get the auth token, then make authenticated requests on behalf of the user without their permission. More info on that type of vulnerability can be found here.
Since our cookie does have the HttpOnly
attribute, it is not accessible at all via client-side JavaScript, and therefore not vulnerable to the type of cross-site scripting (XSS) attack mentioned above. This results in more robust security.
Apollo Client is initialized in lib/apolloClient.ts
.
The credentials: 'include',
option is what tells Apollo to send the auth cookie along with every request.
The client
from that file is then passed as a prop into the ApolloProvider
component in pages/_app.tsx
, which allows us to make GraphQL network requests from anywhere in our app.
useAuth()
HookTake a look at hooks/useAuth.tsx
.
When our app first boots up, the GET_USER
query defined here is fired off to see if the user is authenticated. If they are, loggedIn
is toggled to true
. It, along with the user
data and loading
and error
states are passed down to the rest of our app via React context. This way, any component or hook in our app can use the useAuth()
hook to access that authentication-related data.
These two components are used to conditionally render parts of our application that should only be shown to authenticated or unauthenticated users. They also redirect users who don’t meet that criterion to the proper page.
AuthContent
Open up components/AuthContent.tsx
to see how this component works.
The logic works like this:
If the app just booted up and we don’t know whether or not the user is logged in yet, display a “Loading…” message.
If we have determined the user is logged in, display the children nested inside of this component and allow the user to stay on this page.
If we have determined the user is NOT logged in, redirect the user to the Log In page. The children nested inside of this component are never rendered.
To see that in action, you can head over to the /profile
page. The component for that page (pages/profile.tsx
) does this:
If you try to navigate directly to that page as a logged-in user, you’ll see a loading message momentarily, followed by the user profile form.
If you try to navigate there directly as a logged-out user though, you’ll see the loading message momentarily, then you will be redirected to the Log In page to log in.
Obviously, you shouldn’t hardcode any sensitive information into your JavaScript code, since then it would be accessible to anyone whether they’re authenticated or not. This component is merely helpful for knowing when it’s safe to proceed with rendering components that depend on the user being logged in to work properly, such as the ProfileForm
component.
UnAuthContent
This component simply does the opposite of the AuthContent
component. If the user is unauthenticated, the children nested inside of this component are rendered and the user is allowed to stay on this page. Authenticated users are redirected to the /members
page.
Now that we understand how authentication is being done and how the useAuth()
hook and AuthContent
& UnAuthContent
components can help us, let’s peruse the app’s features.
This component lives in components/Nav.tsx
. it calls useAuth()
to get the loggedIn
value for the current user. It uses that to do some conditional rendering; logged-out users are shown some navigation links, whereas logged-in users are shown others.
You can navigate to /log-in
to see this page, and open pages/log-in.tsx
to view its code.
The page contents are wrapped in the <UnAuthContent>
component to ensure that only unauthenticated users can view the Log In form.
You can find the code for the LogInForm
component in components/LogInForm.tsx
.
It renders out the form and fires off the logIn
mutation when the user submits it. It also displays any errors that come back in the response.
If the log in is successful, it tells Apollo Client to refetch the GET_USER
query (defined in hooks/useAuth.tsx
). This results in the user being navigated away from this unauthenticated-users-only page and over to the /members
page.
This Log In form asks for the user’s email address, but you could modify that to instead ask for their WordPress login (a.k.a. their “username”) instead, if desired. Either one works with the loginWithCookies
mutation.
If you visit /log-out
, the code in pages/log-out.tsx
runs.
When the useEffect()
callback runs, the logOut
mutation is executed to log the user out.
Different messages are rendered to the page depending on whether the mutation has not yet completed, an error has occurred, or the user was successfully logged out.
This code is implemented so that as soon as the user hits the Log Out page, they are logged out. As an alternative approach, you could have users click a Log Out
button in your app, execute the logOut
mutation, and then once they’re logged out, navigate them to another page and present them with a “You have been logged out” confirmation message. Either way works.
You can visit /sign-up
as an unauthenticated user to test out this feature. It allows new users to register an account on the site.
Note that users will only be able to sign up if the Anyone can register
box is checked on the Settings
> General
page in theWordPress admin. Otherwise, if new user registrations are disabled, users will see a “User registration is currently not allowed” error message when attempting to submit the form.
The user flow goes like this:
User visits /sign-up
, fills out the form and clicks the button to sign up.
They see a message telling them a confirmation email has been sent to them.
User opens that email and clicks the link, which sends them back to the /set-password
page in the Next.js app. The link includes key
and login
query string parameters, which are required for setting a user password.
User types their password into the Password
and Confirm Password
fields and hits the button to set it.
If an error occurred, such as if the link is old and no longer valid, the user will see error text.
Otherwise, if the new user’s password was successfully set, they see a Your new password has been set
confirmation message and a link they can click to go to the Log In page.
This is the SignUpForm
component that provides that functionality:
Set Password Link
The link in this email that users must click to set their password is generated by the Headless WordPress Email Settings plugin. It follows this format:
When clicked, it will send users to the /set-password
page of your frontend JS app where they can set their password. It also includes the key
and login
query string parameters, which WordPress requires to set a new password.
Set Password Form
The SetPasswordForm
component (components/SetPasswordForm.tsx
) that allows the user to set a password looks like this:
When the form is submitted and the field values pass validation, the resetUserPassword
mutation is executed to perform the password reset.
Password Strength Validation
SetPasswordForm
contains a validate()
function that is called before the mutation gets fired off to set the user’s password. Currently, it ensures that the Password
and Confirm Password
values match, and that the password is at least 5 characters long. So just know that with the current implementation, a user would be able to set a very weak password, such as “12345”.
If desired, you can use something like zxcvbn to enforce strong passwords on the client-side, and/or a WordPress plugin that enforces strong passwords on the server-side.
As a logged-out user, click the “Forgot password?” link below the Log In form to be navigated to the /forgot-password
page.
The password reset user flow goes like this:
User enters their email address and clicks the Send password reset email
button.
If an error occurs, such as if there is no user with that email address, error text will be displayed.
If the password reset email was successfully sent, the form is replaced with a success message telling the user to check their email.
User opens that email and clicks the link, which sends them back to the /set-password
page in the Next.js app. The link includes key
and login
query string parameters, which are required for setting a user password.
User types their password into the Password
and Confirm Password
fields and hits the button to set it.
If an error occurred, such as if the link is old and no longer valid, the user will see error text.
Otherwise, if the new user’s password was successfully set, they see a Your new password has been set
confirmation message and a link they can click to go to the Log In page.
You may have noticed that steps 4-7 on this list are identical to steps 3-6 of the New User Sign-up flow. That’s because the /set-password
page and the SetPasswordForm
component are used for both new user signups and for password resets.
The /forgot-password
page component (pages/forgot-password.tsx
) renders the SendPasswordResetEmailForm
component (components/SendPasswordResetEmailForm.tsx
).
You can see that when the user submits the form, the sendPasswordResetEmail
mutation is executed. If it is successful, the form gets replaced with the confirmation message telling the user to check their email. They can then click the link in the email to be sent to the /set-password
page and perform the reset.
As a logged in user, you can head over to the /profile
page to view and edit your user profile information.
The ProfileForm
component that provides this functionality is in components/ProfileForm.tsx
.
When the user submits this form, the updateProfile
mutation is fired off, which updates their information in the WordPress database. A success message is then displayed at the top of the form.
You can try submitting this form, then viewing that user’s profile page in the WordPress admin to see the modified first name, last name, and/or email details reflected there.
This page’s content is only visible to authenticated users. The app treats this page as the “home base” for logged-in users. Users are sent here immediately after logging in, and are also automatically redirected here if they attempt to visit a page that’s only for logged-out users, such as the /log-in
page.
You know how I said this blog post is only about authentication? I lied! ????
The /create-post
page gets into authorization. That is, it checks to see if the user has the publish_posts
capability. If they do, the CreatePostForm
component is rendered. If not, a message is displayed to let them know they don’t have the permissions necessary to create posts.
CreatePostForm
is in components/CreatePostForm.tsx
and looks like this:
When the user submits the form, the createPost
mutation is fired off to create the post.
Go ahead and try to log in as a user with the publish_posts
capability (WP’s built-in Author, Editor or Administrator roles should work fine), and submit the form to create a new post. You can then visit the Posts page in the WordPress admin to see your newly created post on the list.
But…why?
You may be wondering: “Why would I want users who create blog posts to do so from my decoupled JS app? Shouldn’t they do that from the WordPress admin instead?”. You’re right– having content creators write blog posts from the WordPress admin makes much more sense. You may want users to be able to create some custom post types (CPT) posts from your frontend app, however. Here’s an example:
Let’s say I have an educational site where students can create an account, take a course, then leave a review. I register a Review custom post type in WordPress, and register a Student user role, which is assigned the publish_review
capability. In my decoupled JS application, I set it up so that logged-in students who have the publish_review
capability are able to fill out a Course Review form. When they submit the form, a new Review CPT post is created in WordPress to capture that information. The student is then presented with a “Thanks for leaving a review!” message.
You might have noticed that:
When you’re logged into the frontend JS app, you’re also logged into the WordPress admin and vice versa.
When you’re logged out of the frontend JS app, you’re also logged out of the WordPress admin and vice versa.
This happens because the same cookie is being used in both places to authenticate you.
If you want to use native WordPress cookies to authenticate the users of your frontend JS app, but never want them to be able to log into the WordPress admin, you can use this WordPress plugin:
Headless WordPress Admin Access
You can follow the steps in the readme to lock down access to the WordPress admin to only users with a certain role or capability.
We saw how on the /create-post
page, our app allows users to create new posts. If you want to go further than that and build an app where users can view, create, edit, and delete Custom Post Type posts, check out this other video of mine that covers how to do CRUD (create, read, update, delete) operations in headless WordPress:
Post Type CRUD Operations for WPGraphQL
In order to use this authentication method in production, you need to do the following:
Install and activate the same plugins mentioned in the WordPress Backend Setup section, above.
From the WordPress admin sidebar, go to Graphql
> Settings
and click the CORS Settings
tab.
Check the checkboxes next to these options:
– Send site credentials
– Enable login mutation
– Enable logout mutation
In the Extend "Access-Control-Allow-Origin” header
field, enter the URL of your decoupled frontend JS app and click the button to save your changes.
In the UI that your frontend JS app hosting company provides, define an environment variable named NEXT_PUBLIC_WORDPRESS_API_URL
. Set its value to the GraphQL endpoint for your headless WordPress backend. For example: https://api.my-cool-site.com/graphql
I hope this blog post and the code repos provided give you a strong foundation for building headless WordPress projects that use WordPress’ native cookies for authentication.
Please reach out to let us know what cool things you’re able to create!