Authentication in headless WordPress is an involved process when you have to write the code from scratch. The intricacies involved in integrating these technologies can pose significant challenges. From configuring the authentication flow to handling user sessions and implementing secure endpoints, every step demands meticulous attention to detail.
Luckily, the Faust.js framework has built-in authentication to take care of all your authentication needs.
In the first part of this two-part article, we will explain the two strategies Faust.js uses which are redirect and local. In part two, we will take a deeper dive into the codebase and how the functionality works. If you want to follow along in video format, I created one here on our YouTube channel.
Table of Contents
Getting Started
To benefit from this post, you should be familiar with the basics of WordPress development, WPGraphQL, Next.js, and Apollo Client.
Steps for Local Development Set-up
WordPress Setup:
- Set up a WordPress site on local, WP Engine, or any host of your choice
- Install and activate the WPGraphQL plugin
- Install and activate the Faust.js plugin
Faust.js Setup
- Clone down my GitHub repository locally and run
npm install
. Once you do that, in your.env.local
file, add yourNEXT_PUBLIC_WORDPRESS_URL
endpoint and yourFAUST_SECRET_KEY
.
I separated the repository into 2 branches: main
and next-whitelabel
.
Once you follow these steps, you should be able to visit http://localhost:3000/ and see the Faust.js front page template.
Faust.js
The JavaScript framework
specifically for WordPress.
Faust Authentication Strategies Explained
Redirect Based Authentication
Redirect-based authentication is the first strategy we will explore, and it is the default method in Faust.js. This strategy involves the user being redirected to the WP admin login page to authenticate. Once the user has logged into WordPress, they get directed back to the Faust.js app with an authorization code that is used to request refresh and access tokens. The user then sees the gated content and has a Logout
button to log out. This completes the login process as shown below:
Local Based Strategy
Faust.js offers local authentication as an alternative strategy, giving users a white-label login experience in the Next.js application. With the useLogin
hook, users can initiate a login request and receive an authorization code upon successful authentication. This code is used to obtain both the refresh and access tokens, effectively finalizing the login process.
The local authentication strategy proves useful when aiming for a customizable login and registration interface. This approach directs unauthenticated requests to a designated Next.js login page. Furthermore, it eliminates the need for users to interact directly with the WordPress login screen, granting developers the freedom to design and optimize their desired user flow.
A Note on Security
Both the redirect and local strategies in Faust use cookies that have the HttpOnly
attribute, which means it is not accessible at all via client-side JavaScript, and therefore not vulnerable to those types of attacks where rogue client-side JavaScript code would be able to reach into your browser and take the non-HttpOnly cookie to get the auth token, then make authenticated requests on behalf of the user without their permission. This results in more robust security.
Implementing Redirect Authentication
Creating a Gated Page
Let’s dive into the default redirect strategy that we explained earlier to see how it works. We will need one file and some code to use the custom hooks Faust.js core has for authentication. The following example can be found in the main
branch of the repo at src/pages/gated.js
import { useAuth, getApolloAuthClient, useLogout } from "@faustwp/core";
import { gql, useQuery } from "@apollo/client";
function AuthenticatedView() {
const client = getApolloAuthClient();
const { logout } = useLogout();
const { data, loading } = useQuery(
gql`
{
viewer {
posts {
nodes {
id
title
}
}
name
}
}
`,
{ client }
);
if (loading) {
return <>Loading...</>;
}
return (
<>
<p>Welcome {data?.viewer?.name}!</p>
<button onClick={() => logout("/")}>Logout</button>
<p>My posts</p>
<ul>
{data?.viewer?.posts?.nodes.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</>
);
}
export default function Page(props) {
const { isAuthenticated, isReady, loginUrl } = useAuth();
if (!isReady) {
return <>Loading...</>;
}
if (isAuthenticated === true) {
return <AuthenticatedView />;
}
return (
<>
<p>Welcome!</p>
<a href={loginUrl}>Login</a>
</>
);
}
Code language: JavaScript (javascript)
At the very top of the file, we import the necessary dependencies and custom hooks: useAuth
, getApolloAuthClient
, and useLogout
from the @faustwp/core
module, and gql
and useQuery
from the @apollo/client module
.
The getApolloAuthClient
function returns an instance of the Apollo client with the proper access token attached to it. With this client, you can make authenticated requests either by calling client.query
on a page or template component or passing the client
into Apollo’s useQuery
hook. In this example, we are using the useQuery
hook.
The useLogout
function is a React hook that facilitates logout from your Faust app.
This next section defines a component called AuthenticatedView
. Inside the component, we initialize a client
variable by calling the getApolloAuthClient
function. We also destructure the logout function from the useLogout
hook:
function AuthenticatedView() {
const client = getApolloAuthClient();
const { logout } = useLogout();
Code language: JavaScript (javascript)
Next, we use the useQuery
hook from Apollo Client, passing a GraphQL query (gql
) as the first argument and an object with the client
property as the second argument. The query fetches data related to the authenticated WordPress viewer’s posts
, nodes
, and name
. The useQuery
hook returns data
and loading
variables, that we destructure and use to store the result of the query and the loading state.
const { data, loading } = useQuery(
gql`
{
viewer {
posts {
nodes {
id
title
}
}
name
}
}
`,
{ client }
);
Code language: JavaScript (javascript)
If the loading
variable is true
, indicating that the query is still loading, we return a JSX fragment displaying "Loading..."
.
if (loading) {
return <>Loading...</>;
}
Code language: JavaScript (javascript)
Otherwise, if it is false
, meaning our query has resolved with details about the authenticated user, it renders JSX elements including a welcome message, a logout button that triggers the logout function, a heading for “My posts”, and a list of posts retrieved from the data
.
In this section, we define a component named Page
. Inside the component, the useAuth
hook is used to obtain the isAuthenticated
, isReady
, and loginUrl
variables.
export default function Page(props) {
const { isAuthenticated, isReady, loginUrl } = useAuth();
Code language: JavaScript (javascript)
If isReady
is false
, indicating that the authentication status is not yet determined, it returns a JSX fragment displaying "Loading..."
.
If isAuthenticated
is true
, meaning the user is authenticated, it renders the AuthenticatedView
component.
Otherwise, it renders JSX elements including a welcome message and a login link that points to the loginUrl
obtained from the useAuth
hook which directs you to the WP Admin login.
if (!isReady) {
return <>Loading...</>;
}
if (isAuthenticated === true) {
return <AuthenticatedView />;
}
return (
<>
<p>Welcome!</p>
<a href={loginUrl}>Login</a>
</>
);
}
Code language: JavaScript (javascript)
Overall, this code block presents a Faust.js component structure that displays different content based on the authentication state. When authenticated, it fetches and displays user-specific data, and provides a logout button. When not authenticated, it displays a welcome message and a login link. This strategy works out of the box with Faust.js.
Using the useAuth
Hook
The useAuth
hook is a custom hook made for the Faust.js framework that makes authentication easier to handle in your headless WordPress site.
Below are the properties in the object returned by useAuth
. The strategy prop is set to the value redirect
for that strategy. The shouldRedirect
prop is set to the boolean value true. We can change our strategies accordingly here.
const { isAuthenticated, isReady, loginUrl } = useAuth({
strategy: 'redirect',
shouldRedirect: true,
});
Code language: JavaScript (javascript)
isAuthenticated
: It is a boolean value indicating whether the user is authenticated or not. It determines whether the user has successfully logged in or has valid authentication credentials.
isReady
: A boolean to determine if the useAuth
exports are ready to be used.
loginUrl
: It is a string representing the URL where the user should be redirected for the login process. Based on the strategy we use, it is either a Next.js page or the WordPress backend.
Implementing Local Authentication
Switching Branches
We will be using the local strategy in this section. Please switch over to the next-whitelabel
branch in the repository to follow along.
Creating a Login Page
You can navigate in the browser to the /log-in
URL to see this page, and open pages/log-in.js
to view its code. The page contents is a page
component that takes in the Login
component and renders it.
The code for the Login component can be found at components/LoginForm/LoginForm.js
import { useLogin } from "@faustwp/core";
import { useState } from "react";
import Link from "next/link";
import styles from "./LoginForm.module.scss";
import classNames from "classnames/bind";
let cx = classNames.bind(styles);
export default function LoginForm({ className }) {
const [usernameEmail, setUsernameEmail] = useState("");
const [password, setPassword] = useState("");
const { login, loading, data, error } = useLogin();
return (
<form
className={cx(["login-form", className])}
onSubmit={(e) => {
e.preventDefault();
login(usernameEmail, password, "/members");
}}
>
<fieldset className={cx(["login-form fieldset", className])}>
<label
className={cx(["login-form label", className])}
htmlFor="usernameEmail"
>
Login with your Username or Email
</label>
<input
className={cx(["login-form input", className])}
id="usernameEmail"
type="text"
disabled={loading === true}
value={usernameEmail}
onChange={(e) => setUsernameEmail(e.target.value)}
/>
</fieldset>
<fieldset>
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
disabled={loading === true}
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</fieldset>
<div className={cx(["div", className])}>
<Link href="/forgot-password">
<a className={cx(["a", className])}>
🤷🏽♂️ Forgot password? Click Here! 👈🏽
</a>
</Link>
</div>
{data?.generateAuthorizationCode.error && (
<p
dangerouslySetInnerHTML={{
__html: data.generateAuthorizationCode.error,
}}
/>
)}
<fieldset>
<button type="submit">Login</button>
</fieldset>
<p className={cx(["p", className])}>
Don't have an account yet?{" "}
<Link href="/sign-up">
<a>Sign up here! 👈🏽</a>
</Link>
</p>
</form>
);
}
Code language: JavaScript (javascript)
At the top of the file, we import the necessary dependencies and hooks, specifically the useLogin
hook that comes from Faust.
This code initializes state variables usernameEmail
and password
using the useState
hook. The initial values are empty strings. It also calls the useLogin
hook from @faustwp/core
and assigns the returned values to login
, loading
, data
, and error
.
export default function LoginForm({ className }) {
const [usernameEmail, setUsernameEmail] = useState("");
const [password, setPassword] = useState("");
const { login, loading, data, error } = useLogin();
Code language: JavaScript (javascript)
Next, is the JSX markup for the LoginForm
component. It renders a form
element. The onSubmit
event handler is defined to prevent the default form submission behavior and instead call the login
function from the useLogin
hook, passing the usernameEmail
, password
, and the redirect path "/members"
.
return (
<form
className={cx(["login-form", className])}
onSubmit={(e) => {
e.preventDefault();
login(usernameEmail, password, "/members");
}}
Code language: JavaScript (javascript)
The rest of the JSX markup represents the login form UI, including labels and input fields for username/email and password, a link for forgotten passwords, an error message display, a submit button, and a link to the sign-up page.
This is what it looks like on the browser:
Creating a Members-only Page
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. In addition, this page has a navigation menu specifically for logged-in users to access the gated features.
If they are not logged in or authenticated, they will be redirected to a local Faust.js custom page that will allow them to do so.
Let’s break this file down and explore how it is using the Faust core auth functionality.
The location of this file is in components/AuthForms/AuthContent.js
:
import { gql, useQuery } from "@apollo/client";
import { getApolloAuthClient, useAuth, useLogout } from "@faustwp/core";
import { NavAuth } from "../NavAuth";
import PacmanLoader from "react-spinners/PacmanLoader";
import styles from "./AuthContent.module.scss";
import classNames from "classnames/bind";
let cx = classNames.bind(styles);
function AuthenticatedView({ className }) {
const client = getApolloAuthClient();
const { logout } = useLogout();
const { data, loading } = useQuery(
gql`
{
viewer {
posts {
nodes {
id
title
excerpt
}
}
name
}
}
`,
{ client }
);
if (loading) {
return (
<PacmanLoader loading={loading} color="#ffeb3b" speedMultiplier={3} />
);
}
return (
<>
<NavAuth />
<h1 className={cx(["header", className])}>
Members Only Jackets- for authenticated eyes only 🥼🔐{" "}
</h1>
<p className={cx(["header", className])}>Welcome {data?.viewer?.name}!</p>
<p className={cx(["body", className])}>
If you are seeing My posts titles & excerpts, you have been
authenticated! Stoked!
</p>
<ul className={cx(["ul", className])}>
{data?.viewer?.posts?.nodes.map((post) => (
<li className={cx(["li", className])} key={post.id}>
<h2>{post.title}</h2>
<p dangerouslySetInnerHTML={{ __html: post.excerpt }}></p>
</li>
))}
</ul>
<div className={cx(["container", className])}>
<button className={cx(["button", className])} onClick={() => logout()}>
Logout
</button>
</div>
</>
);
}
export default function AuthContent({ className }) {
const { isAuthenticated, isReady, loginUrl } = useAuth({
strategy: "local",
shouldRedirect: false,
loginPageUrl: "/log-in",
});
if (!isReady) {
return (
<PacmanLoader loading={!isReady} color="#ffeb3b" speedMultiplier={3} />
);
}
if (isAuthenticated === true) {
return <AuthenticatedView />;
}
return (
<>
<div className={cx(["container", className])}>
<p className={cx(["p", className])}>
You have either been logged out or need to create an account!
</p>
</div>
<div className={cx(["container", className])}>
<a className={cx(["a", className])} href={loginUrl}>
Login Here or Sign Up to see Authenticated Content🔐
</a>
</div>
</>
);
}
Code language: JavaScript (javascript)
At the top of the file, we begin with importing the necessary dependencies. GraphQL
and useQuery
are imported from the @apollo/client
package, which is used for GraphQL queries. The getApolloAuthClient
, useAuth
, and useLogout
hooks are imported from the @faustwp/core
custom auth library. NavAuth
is imported from a local file named NavAuth
for navbar functionality. The last two imports are for CSS module styles.
The next lines of code define a component called AuthenticatedView
, which accepts a className
prop. Inside the component, it initializes a client
variable using the getApolloAuthClient
function and assigns the logout function from useLogout
to the logout
variable.
function AuthenticatedView({ className }) {
const client = getApolloAuthClient();
const { logout } = useLogout();
Code language: JavaScript (javascript)
Then, the useQuery
hook is called with a GraphQL query defined using the gql
template literal. The query requests data
for the viewer
object, including posts
with their id
, title
, and excerpt
, as well as the viewer’s name
. The useQuery
hook returns data
and loading
variables. The data variable contains the result of the query.
Next, we have a conditional that If the loading variable is true
, indicating that the data is still being fetched, the component returns the JSX fragment Loading....
This is a temporary loading state.
const { data, loading } = useQuery(
gql`
{
viewer {
posts {
nodes {
id
title
excerpt
}
}
name
}
}
`,
{ client }
);
if (loading) {
return (
<PacmanLoader loading={loading} color="#ffeb3b" speedMultiplier={3} />
);
}
Code language: JavaScript (javascript)
If the data is loaded, the component returns JSX elements representing the authenticated view with the gated post data and content that a user sees if they have been authenticated.
Lastly, a button for logging out contains the onClick
event that triggers the logout
function which comes from the Faust.js core auth library.
<div className={cx(["container", className])}>
<button className={cx(["button", className])} onClick={() => logout()}>
Logout
</button>
Code language: HTML, XML (xml)
The following lines of code are where we change our strategy from redirect to local in Faust.
export default function AuthContent({ className }) {
const { isAuthenticated, isReady, loginUrl } = useAuth({
strategy: "local",
shouldRedirect: false,
loginPageUrl: "/log-in",
});
Code language: JavaScript (javascript)
The code defines another component called AuthContent
, which accepts a className
prop for the dynamic css
. Inside the component, the useAuth
hook is called with an options object. It returns an object with isAuthenticated
(a boolean indicating whether the user is authenticated), isReady
(a boolean indicating whether the authentication process is complete), and loginUrl
(the URL for the login page).
Then, we have a condition that checks if the isReady
variable is false. If it is, it means that the authentication process is not yet complete. In this case, the component returns a JSX fragment Loading....
This displays a loading state until the authentication process is finished.
The next condition checks if the isAuthenticated
variable is true. If it is, it means that the user is authenticated. In this case, the component returns the AuthenticatedView
component that contains our gated content.
if (!isReady) {
return (
<PacmanLoader loading={!isReady} color="#ffeb3b" speedMultiplier={3} />
);
}
if (isAuthenticated === true) {
return <AuthenticatedView />;
}
Code language: JavaScript (javascript)
If none of the previous conditions are met, it means that the user is not authenticated. In this case, the component returns a JSX fragment containing a message and a link. The message says "You have been logged out!"
. The link allows the user to navigate to the login URL, which leads to the login page allowing the user to re-authenticate and log in.
This is what the members
page looks like when you are authenticated in the browser:
Wrapping Up & Next Steps
That concludes our section on Faust.js authentication and how to leverage it’s strategies and custom hooks. Next, let’s continue the authentication user experience and see how we update data when authenticated.
Getting Started
We will stay on the next-whitelabel
branch for this part of the article.
In the Managing User Accounts part of this section, we will explore two features that will be outside of the scope of authentication in Faust.js
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.
When you install and activate the WPGraphQL CORS plugin in your WP Admin, from the sidebar, go to GraphQL
> Settings
and click the CORS Settings
tab.
Then check the following 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 should now look like this:
We also will need the Headless WP Email Settings plugin which will allow us to point our email reset links to be sent to the front-end Faust application. Download and install that plugin.
Allowing Users to Update Data
What if we wanted to give authenticated users the ability to not only read data but also update data via mutations? We are going to explore and dive into that functionality in this section.
Update User Profile
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/ProfileForm.js
.
import { gql, useMutation } from "@apollo/client";
import { getApolloAuthClient, useAuth } from "@faustwp/core";
import { useEffect, useState } from "react";
import styles from "./ProfileForm.module.scss";
import classNames from "classnames/bind";
let cx = classNames.bind(styles);
function useViewer() {
const [viewer, setViewer] = useState(null);
const { isAuthenticated } = useAuth({
shouldRedirect: true,
strategy: "local",
loginPageUrl: "/log-in",
});
useEffect(() => {
if (isAuthenticated !== true) {
return;
}
(async () => {
const client = getApolloAuthClient();
const { data } = await client.query({
query: gql`
query GetViewer {
viewer {
id
email
firstName
lastName
}
}
`,
});
setViewer(data?.viewer || null);
})();
}, [isAuthenticated]);
return viewer;
}
export default function ProfileForm({ props, className }) {
const viewer = useViewer();
const authClient = getApolloAuthClient();
const [successMessage, setSuccessMessage] = useState();
const [firstName, setFirstName] = useState(viewer?.firstName || "");
const [lastName, setLastName] = useState(viewer?.lastName || "");
const [email, setEmail] = useState(viewer?.email || "");
useEffect(() => {
setFirstName(viewer?.firstName || "");
setLastName(viewer?.lastName || "");
setEmail(viewer?.email || "");
}, [viewer]);
const [updateProfile] = useMutation(
gql`
mutation UpdateProfile(
$id: ID!
$firstName: String!
$lastName: String!
$email: String!
) {
updateUser(
input: {
id: $id
firstName: $firstName
lastName: $lastName
email: $email
}
) {
user {
id
email
firstName
lastName
}
}
}
`,
{ client: authClient }
);
const handleSubmit = async (e) => {
e.preventDefault();
try {
await updateProfile({
variables: { firstName, lastName, email, id: viewer.id },
});
setSuccessMessage("Profile updated successfully");
setFirstName("");
setLastName("");
setEmail("");
} catch (error) {
// Handle error if the update fails
console.error(error);
setSuccessMessage("Profile update failed");
}
};
return (
<>
{successMessage && (
<p className={cx(["success-message", className])}>{successMessage}</p>
)}
<form
method="post"
className={cx(["profile-form", className])}
onSubmit={handleSubmit}
>
<label className={cx(["profile-label", className])} htmlFor="firstName">
First Name
</label>
<input
className={cx(["profile-input", className])}
type="text"
id="firstName"
name="firstName"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
/>
<label className={cx(["profile-label", className])} htmlFor="lastName">
Last Name
</label>
<input
className={cx(["profile-input", className])}
type="text"
id="lastName"
name="lastName"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
/>
<label className={cx(["profile-label", className])} htmlFor="email">
Email
</label>
<input
className={cx(["profile-input", className])}
type="email"
id="email"
name="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button className={cx(["profile-button", className])} type="submit">
Update Profile
</button>
</form>
</>
);
}
Code language: JavaScript (javascript)
Writing a Custom useViewer
Hook
In order to fetch and manage the viewer’s data from the server we have a custom useViewer
hook. Inside the useViewer
hook, it initializes the viewer
state using the useState
hook, setting it to null
initially. The viewer
state represents the data of the currently authenticated user, such as their ID
, email
, first name
, and last name
.
function useViewer() {
const [viewer, setViewer] = useState(null);
Code language: JavaScript (javascript)
After checking if the user is authenticated with the useAuth
hook from Faust.js, we run an async function with the useEffect
hook if the user is authenticated. This performs a query using the Apollo Client (client.query
) to fetch the viewer’s data from the server. The query is defined using the gql
function and requests the ID
, email
, first name
, and last name
of the viewer
.
Once the query is successful, the setViewer
function is called to update the viewer
state with the received data (data.viewer
). If the query fails or the user is not authenticated, the viewer
state remains null
.
useEffect(() => {
if (isAuthenticated !== true) {
return;
}
(async () => {
const client = getApolloAuthClient();
const { data } = await client.query({
query: gql`
query GetViewer {
viewer {
id
email
firstName
lastName
}
}
`,
});
setViewer(data?.viewer || null);
})();
}, [isAuthenticated]);
return viewer;
Code language: JavaScript (javascript)
Explaining GraphQL Mutations
Mutations in GraphQL are an operation that allows us to modify or change data on the server.
Let’s focus on the useMutation
hook from the Apollo Client and what it is doing.
const [updateProfile] = useMutation(
gql`
mutation UpdateProfile(
$id: ID!
$firstName: String!
$lastName: String!
$email: String!
) {
updateUser(
input: {
id: $id
firstName: $firstName
lastName: $lastName
email: $email
}
) {
user {
id
email
firstName
lastName
}
}
}
`,
{ client: authClient }
);
Code language: PHP (php)
The useMutation
hook is used to define a mutation operation called updateProfile
. It takes two arguments: the mutation query and an options object.
The mutation query is defined using the gql
function. The mutation query specifies the UpdateProfile
mutation and its input variables ($id
, $firstName
, $lastName
, $email
). The mutation updates a user’s profile by calling the updateUser
mutation and passing the input variables.
The options object passed to useMutation
contains a client
property that specifies the Apollo Client to be used for executing the mutation. It uses the authClient
obtained from the getApolloAuthClient
function.
When the user submits the profile 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.
Create Post
We have an additional feature that deals with authorization which is the /create-post
page. 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.
This file is located at components/CreatePostForm/CreatePostForm.js
:
import { gql, useMutation } from "@apollo/client";
import { getApolloAuthClient, useAuth } from "@faustwp/core";
import { useEffect, useState } from "react";
import styles from "./CreatePost.module.scss";
import classNames from "classnames/bind";
import PacmanLoader from "react-spinners/PacmanLoader";
let cx = classNames.bind(styles);
function useViewer() {
const [viewer, setViewer] = useState(null);
const { isAuthenticated } = useAuth({
shouldRedirect: true,
strategy: "local",
loginPageUrl: "/login",
});
useEffect(() => {
if (isAuthenticated !== true) {
return;
}
(async () => {
const client = getApolloAuthClient();
const { data } = await client.query({
query: gql`
query Viewer {
viewer {
capabilities
}
}
`,
});
setViewer(data.viewer);
})();
}, [isAuthenticated]);
return viewer;
}
export default function Page(props, className) {
const viewer = useViewer();
const authClient = getApolloAuthClient();
const [successMessage, setSuccessMessage] = useState("");
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const canCreatePosts = Boolean(
viewer?.capabilities?.includes("publish_posts")
);
const [createPost] = useMutation(
gql`
mutation CreatePost(
$title: String!
$content: String!
$status: PostStatusEnum!
) {
createPost(
input: { title: $title, content: $content, status: $status }
) {
post {
databaseId
}
}
}
`,
{ client: authClient }
);
const handleSubmit = async (e) => {
e.preventDefault();
try {
await createPost({
variables: {
title,
content,
status: canCreatePosts ? "PUBLISH" : "DRAFT",
},
});
setSuccessMessage("Post added successfully");
setTitle("");
setContent("");
} catch (error) {
console.error(error);
setSuccessMessage("Failed to add post");
}
};
// Render loading state while checking viewer capabilities
if (viewer === null) {
return (
<div className={cx(["loading-container", className])}>
<PacmanLoader loading={true} color="#ffeb3b" speedMultiplier={3} />
</div>
);
}
return (
<>
{successMessage && (
<p className={cx(["createsuccess-message", className])}>
{successMessage}
</p>
)}
<h1 className={cx(["header", className])}>Create A Post ✍🏽</h1>
{canCreatePosts ? (
<form
className={cx(["create-form", className])}
method="post"
onSubmit={handleSubmit}
>
<label
className={cx(["create-label", className])}
htmlFor="create-post-title"
>
Title
</label>
<input
className={cx(["create-input", className])}
type="text"
id="create-post-title"
name="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<label
className={cx(["create-label", className])}
htmlFor="create-post-content"
>
Content
</label>
<textarea
className={cx(["create-input", className])}
id="create-post-content"
name="content"
value={content}
onChange={(e) => setContent(e.target.value)}
required
/>
<button className={cx(["create-button", className])} type="submit">
Add Post
</button>
</form>
) : (
<p>You don't have the permissions necessary to create posts.</p>
)}
</>
);
}
Code language: JavaScript (javascript)
The code block is very similar syntax-wise to the Profile Form component that I discussed earlier in the article using the useMutation
function to create a post title and content. The difference is in how we are setting up the functionality to establish authorization.
In this function, I have a custom hook named useViewer
which uses the useState
hook to initialize the viewer state variable with nothing or null as the initial value and the setViewer
function updates that value when there is a new viewer logged in.
Following that, the useAuth
hook from Faust is called to retrieve the isAuthenticated
value from the authentication context. This is where we check if the viewer is authenticated and sets the isAuthenticated
variable. We also set the strategy here in accordance with Faust.js auth to local
since we need to depend on the white-label strategy.
Next, the useEffect
hook is used to run an effect when the isAuthenticated
value changes. If isAuthenticated
is not true
, the effect returns early and does nothing. Otherwise, it calls an asynchronous function.
Authorization Using User Capabilities
Inside this async function, it retrieves the Apollo Client using getApolloAuthClient
and makes a query using client.query
. The query requests the capabilities
field of the viewer
object.
The query result is then destructured to get the data
object and the viewer
data is extracted from it and assigned to the viewer
state variable using setViewer
.
The effect is then invoked when the component mounts and when the isAuthenticated
value changes.
Finally, the viewer
variable is returned from the useViewer
hook.
function useViewer() {
const [viewer, setViewer] = useState(null);
const { isAuthenticated } = useAuth({
shouldRedirect: true,
strategy: "redirect",
loginPageUrl: "/login",
});
useEffect(() => {
if (isAuthenticated !== true) {
return;
}
(async () => {
const client = getApolloAuthClient();
const { data } = await client.query({
query: gql`
query Viewer {
viewer {
capabilities
}
}
`,
});
setViewer(data.viewer);
})();
}, [isAuthenticated]);
return viewer;
}
Code language: JavaScript (javascript)
Next, we have a default component named CreatePostForm
. The 2 lines to focus on are when the useViewer
hook is called to get the viewer object and the canCreatePosts
variable:
export default function CreatePostForm(props, className) {
const viewer = useViewer();
const canCreatePosts = Boolean(
viewer?.capabilities?.includes("publish_posts")
);
Code language: JavaScript (javascript)
The canCreatePosts
variable is assigned a boolean value based on whether the viewer
object has the capability to publish posts. It uses optional chaining (?.
) to safely access nested properties and the includes
method to check if the capabilities
array includes the string "publish_posts"
.
Focusing a few lines down, we have a handleSubmit
function that is asynchronous, being called when the form is submitted. The default form submission behavior is prevented by using e.preventDefault.
Inside a try-catch block, I have the createPost
mutation being called with the appropriate variables based on the form inputs and the canCreatePosts
value. If canCreatePosts
is true
, the post will have a status of "PUBLISH"
; otherwise, it will have a status of "DRAFT"
. Meaning, if it’s not true
it stays in "DRAFT"
, which is a non-authorized capability to publish a post.
const handleSubmit = async (e) => {
e.preventDefault();
try {
await createPost({
variables: {
title,
content,
status: canCreatePosts ? "PUBLISH" : "DRAFT",
},
});
setSuccessMessage("Post added successfully");
setTitle("");
setContent("");
} catch (error) {
console.error(error);
setSuccessMessage("Failed to add post");
}
};
Code language: JavaScript (javascript)
After that, we have the JSX to be rendered. In the JSX, focusing on the conditional rendering we are doing with canCreatePosts
variable. If that is false, the viewer does not have authorization to publish a post. It does render a form to create a post if canCreatePosts
is true and the viewer can publish a 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.
One use-case that this authorization functionality can come in handy is an educational site where students can create an account, take a course, then leave a review.
What you could do is register a Review custom post type in WordPress, and register a Student user role, which is assigned the publish_review
capability. In the decoupled Faust 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.
Please let me know other use cases you may try with this! Stoked!
Managing User Accounts
New User Registration
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 the WordPress 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:
- The 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.
- The user opens that email and clicks the link, which sends them back to the
/set-password
page in the Faust.js app. The link includeskey
andlogin
query string parameters, which are required for setting a user password. - User types their password into the
Password
andConfirm 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 the 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 Login page. It should look like this:
Creating a Registration Form
This is the SignupForm
component that provides that functionality which is located at components/SignupForm/SignupForm.js
:
import { useMutation, gql } from "@apollo/client";
import Link from "next/link";
import styles from "./Signup.module.scss";
import classNames from "classnames/bind";
let cx = classNames.bind(styles);
const REGISTER_USER = gql`
mutation registerUser(
$email: String!
$firstName: String!
$lastName: String!
) {
registerUser(
input: {
username: $email
email: $email
firstName: $firstName
lastName: $lastName
}
) {
user {
databaseId
}
}
}
`;
export default function SignUpForm({ className }) {
const [register, { data, loading, error }] = useMutation(REGISTER_USER);
const wasSignUpSuccessful = Boolean(
data &&
data.registerUser &&
data.registerUser.user &&
data.registerUser.user.databaseId
);
function handleSubmit(event) {
event.preventDefault();
const data = new FormData(event.currentTarget);
const values = Object.fromEntries(data);
register({
variables: values,
}).catch((error) => {
console.error(error);
});
}
if (wasSignUpSuccessful) {
return (
<p className={cx(["signup-message", className])}>
Thanks! Check your email – an account confirmation link has been sent to
you.
</p>
);
}
return (
<form
className={cx(["signup-form", className])}
method="post"
onSubmit={handleSubmit}
>
<fieldset disabled={loading} aria-busy={loading}>
<label
className={cx(["signup-label", className])}
htmlFor="sign-up-first-name"
>
First name
</label>
<input
className={cx(["signup-input", className])}
id="sign-up-first-name"
type="text"
name="firstName"
autoComplete="given-name"
required
/>
<label
className={cx(["signup-label", className])}
htmlFor="sign-up-last-name"
>
Last name
</label>
<input
className={cx(["signup-input", className])}
id="sign-up-first-name"
type="text"
name="lastName"
autoComplete="family-name"
required
/>
<label
className={cx(["signup-label", className])}
htmlFor="sign-up-email"
>
Email
</label>
<input
className={cx(["signup-input", className])}
id="sign-up-email"
type="email"
name="email"
autoComplete="username"
required
/>
{error ? (
error.message.includes("This username is already registered") ? (
<p className="error-message">
You're already signed up! <Link href="/log-in">Log in</Link>
</p>
) : (
<p className="error-message">{error.message}</p>
)
) : null}
<button
className={cx(["signup-button", className])}
type="submit"
disabled={loading}
>
{loading ? "Signing up..." : "Sign up"}
</button>
</fieldset>
<p>
Already have an account? Go To The
<Link href="/log-in">
<a className={cx(["a", className])}>Log in Page</a>
</Link>
</p>
</form>
);
}
Code language: JavaScript (javascript)
At the top of the file, we import the necessary dependencies: useMutation
and gql
from @apollo/client
, Link
from next/link
. As well as the css
imports needed.
Next, we define a GraphQL mutation query using the gql
template literal from @apollo/client
. The REGISTER_USER
mutation registers a user by accepting email
, firstName
, and lastName
as variables and returns the databaseId
of the user.
const REGISTER_USER = gql`
mutation registerUser(
$email: String!
$firstName: String!
$lastName: String!
) {
registerUser(
input: {
username: $email
email: $email
firstName: $firstName
lastName: $lastName
}
) {
user {
databaseId
}
}
}
`;
Code language: PHP (php)
Following that, we have a SignUpForm component as the default export function. Inside this component, the useMutation
hook is called with the REGISTER_USER
mutation query. The hook returns the register
function for executing the mutation, along with the data
, loading
, and error
variables for managing the mutation’s state. The wasSignUpSuccessful
variable is defined to check if the sign-up was successful by checking if the necessary data is present.
export default function SignUpForm({ className }) {
const [register, { data, loading, error }] = useMutation(REGISTER_USER);
const wasSignUpSuccessful = Boolean(
data &&
data.registerUser &&
data.registerUser.user &&
data.registerUser.user.databaseId
);
Code language: JavaScript (javascript)
Following that, we need to define the handleSubmit
function, which is called when the sign-up form is submitted. It prevents the default form submission behavior, extracts the form data using FormData
, converts it into an object using Object.fromEntries
, and then calls the register
function from the useMutation
hook, passing the form values as variables. Any errors during the mutation are caught and logged to the console.
function handleSubmit(event) {
event.preventDefault();
const data = new FormData(event.currentTarget);
const values = Object.fromEntries(data);
register({
variables: values,
}).catch((error) => {
console.error(error);
});
}
Code language: JavaScript (javascript)
A conditional is run to see if the signup was successful. If it is, the component returns a message that notifies success.
if (wasSignUpSuccessful) {
return (
<p className={cx(["signup-message", className])}>
Thanks! Check your email – an account confirmation link has been sent to
you.
</p>
);
}
Code language: JavaScript (javascript)
If the sign-up was not successful, the component returns a sign-up form. The method
attribute is set to "post"
, and the onSubmit
event handler is set to the handleSubmit
function defined earlier.
The rest of the JSX markup represents the form UI, including input fields for first name, last name, and email, an error message display, and a submit button. There is also a link to the login page using next/link
for navigation.
return (
<form
className={cx(["signup-form", className])}
method="post"
onSubmit={handleSubmit}
>
<fieldset disabled={loading} aria-busy={loading}>
<label
className={cx(["signup-label", className])}
htmlFor="sign-up-first-name"
>
First name
</label>
<input
className={cx(["signup-input", className])}
id="sign-up-first-name"
type="text"
name="firstName"
autoComplete="given-name"
required
/>
<label
className={cx(["signup-label", className])}
htmlFor="sign-up-last-name"
>
Last name
</label>
<input
className={cx(["signup-input", className])}
id="sign-up-first-name"
type="text"
name="lastName"
autoComplete="family-name"
required
/>
<label
className={cx(["signup-label", className])}
htmlFor="sign-up-email"
>
Email
</label>
<input
className={cx(["signup-input", className])}
id="sign-up-email"
type="email"
name="email"
autoComplete="username"
required
/>
{error ? (
error.message.includes("This username is already registered") ? (
<p className="error-message">
You're already signed up! <Link href="/log-in">Log in</Link>
</p>
) : (
<p className="error-message">{error.message}</p>
)
) : null}
<button
className={cx(["signup-button", className])}
type="submit"
disabled={loading}
>
{loading ? "Signing up..." : "Sign up"}
</button>
</fieldset>
<p>
Already have an account? Go To The
<Link href="/log-in">
<a className={cx(["a", className])}>Log in Page</a>
</Link>
</p>
</form>
);
}
Code language: JavaScript (javascript)
Generate a 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:
"https://frontend-js-app.com/set-password/?key={$key}&login={$login}"
Code language: JSON / JSON with Comments (json)
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.
Creating a Set Password Form
The SetPasswordForm
component which is located at components/SetPasswordForm/SetPasswordForm.js
that allows the user to set a password looks like this:
import { useState } from "react";
import { useMutation, gql } from "@apollo/client";
import Link from "next/link";
import styles from "./SetPassword.module.scss";
import classNames from "classnames/bind";
let cx = classNames.bind(styles);
const RESET_PASSWORD = gql`
mutation resetUserPassword(
$key: String!
$login: String!
$password: String!
) {
resetUserPassword(
input: { key: $key, login: $login, password: $password }
) {
user {
databaseId
}
}
}
`;
export default function SetPasswordForm({ resetKey, login }) {
console.log(login);
const [password, setPassword] = useState("");
const [passwordConfirm, setPasswordConfirm] = useState("");
const [clientErrorMessage, setClientErrorMessage] = useState("");
const [resetPassword, { data, loading, error }] = useMutation(RESET_PASSWORD);
const wasPasswordReset = Boolean(data?.resetUserPassword?.user?.databaseId);
function handleSubmit(event) {
event.preventDefault();
const isValid = validate();
if (!isValid) return;
resetPassword({
variables: {
key: resetKey,
login: login,
password,
},
}).catch((error) => {
console.error(error);
});
}
function validate() {
setClientErrorMessage("");
const isPasswordLongEnough = password.length >= 5;
if (!isPasswordLongEnough) {
setClientErrorMessage("Password must be at least 5 characters.");
return false;
}
const doPasswordsMatch = password === passwordConfirm;
if (!doPasswordsMatch) {
setClientErrorMessage("Passwords must match.");
return false;
}
return true;
}
if (wasPasswordReset) {
return (
<>
<p className={cx("p")}>Your new password has been set Stoked!🙌🏽</p>
<div className={cx("div-tag")}>
<Link href="/log-in">
<a className={cx("a-tag")}>Log in Here 👈🏽</a>
</Link>
</div>
</>
);
}
return (
<form className={cx("set-form")} method="post" onSubmit={handleSubmit}>
<fieldset
className={cx("set-fieldset")}
disabled={loading}
aria-busy={loading}
>
<label className={cx("set-label")} htmlFor="new-password">
Password
</label>
<input
className={cx("set-input")}
id="new-password"
type="password"
value={password}
autoComplete="new-password"
onChange={(event) => setPassword(event.target.value)}
required
/>
<label className={cx("set-label")} htmlFor="password-confirm">
Confirm Password
</label>
<input
className={cx("set-input")}
id="password-confirm"
type="password"
value={passwordConfirm}
autoComplete="new-password"
onChange={(event) => setPasswordConfirm(event.target.value)}
required
/>
{clientErrorMessage ? (
<p className="error-message">{clientErrorMessage}</p>
) : null}
{error ? <p className="error-message">{error.message}</p> : null}
<button className={cx("set-button")} type="submit" disabled={loading}>
{loading ? "Saving..." : "Save password"}
</button>
</fieldset>
</form>
);
}
Code language: JavaScript (javascript)
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 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.
Sending Password Resets
As a logged-out user, click the “Forgot password?” link below the login 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 includeskey
andlogin
query string parameters, which are required for setting a user password. - User types their password into the
Password
andConfirm 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 the 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 Login 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.
This is how the flow should look like:
The /forgot-password
page component (pages/forgot-password.js
) renders the SendPasswordresetEmail
component (components/SendPaswordresetForm/SendPasswordresetForm.js)
.
import { useMutation, gql } from "@apollo/client";
import { useState } from "react";
import styles from "./SendPasswordReset.module.scss";
import classNames from "classnames/bind";
let cx = classNames.bind(styles);
const SEND_PASSWORD_RESET_EMAIL = gql`
mutation sendPasswordResetEmail($username: String!) {
sendPasswordResetEmail(input: { username: $username }) {
user {
databaseId
}
}
}
`;
export default function SendPasswordResetEmailForm({ className }) {
const [sendPasswordResetEmail, { loading, error, data }] = useMutation(
SEND_PASSWORD_RESET_EMAIL
);
const wasEmailSent = Boolean(
data &&
data.sendPasswordResetEmail &&
data.sendPasswordResetEmail.user &&
data.sendPasswordResetEmail.user.databaseId
);
const [email, setEmail] = useState("");
function handleSubmit(event) {
event.preventDefault();
const data = new FormData(event.currentTarget);
const { email } = Object.fromEntries(data);
sendPasswordResetEmail({
variables: {
username: email,
},
}).catch((error) => {
console.error(error);
});
setEmail("");
alert("Check your email for a password reset link.");
}
if (wasEmailSent) {
return (
<p>
{" "}
Please check your email. A password reset link has been sent to you.
</p>
);
}
return (
<form
className={cx("reset-form", className)}
method="post"
onSubmit={handleSubmit}
>
<p>
Enter the email associated with your account and you'll be sent a
link to reset your password.
</p>
<fieldset
className={cx("reset-fieldset", className)}
disabled={loading}
aria-busy={loading}
>
<label
className={cx("reset-label", className)}
htmlFor="password-reset-email"
>
Email
</label>
<input
className={cx("reset-input", className)}
id="password-reset-email"
type="email"
name="email"
autoComplete="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
{error ? <p className="error-message">{error.message}</p> : null}
<button
className={cx("reset-button", className)}
type="submit"
disabled={loading}
>
{loading ? "Sending..." : "Send password reset email"}
</button>
</fieldset>
</form>
);
}
Code language: JavaScript (javascript)
When the user submits the form, the sendPasswordResetEmail
mutation is executed. If it is successful, the form gets replaced with a 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.
Conclusion 🚀
I hope this article provided a better understanding for you in using Faust.js and its authentication strategies with some authorization sprinkled in!
If you are looking for a place to host your next headless WordPress project, be sure to get a free sandbox account on our Atlas Platform. The platform combines Node.js hosting, WordPress hosting, and a CDN layer into one convenient dashboard to manage your headless sites.
Atlas
The all-in-one headless
platform for radically fast sites.
As always, super stoked to hear your feedback and any questions you might have on headless WordPress. Hit us up in our discord channel!