Authentication in Faust.js and Headless WP

Francis Agulto Avatar

·

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.

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:

  1. Set up a WordPress site on local, WP Engine, or any host of your choice
  2. Install and activate the WPGraphQL plugin
  3. Install and activate the Faust.js plugin

Faust.js Setup

  1. Clone down my GitHub repository locally and run npm install. Once you do that, in your .env.local file, add your NEXT_PUBLIC_WORDPRESS_URL endpoint and your FAUST_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 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 getApolloAuthClientfunction 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 useLogouthook:

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&#39;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:

  1. The user visits /sign-up, fills out the form, and clicks the button to sign up.
  2. They see a message telling them a confirmation email has been sent to them.
  3. 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 includes key and login query string parameters, which are required for setting a user password.
  4. User types their password into the Password and Confirm Password fields and hits the button to set it.
  5. If an error occurred, such as if the link is old and no longer valid, the user will see the error text.
  6. 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:

  1. User enters their email address and clicks the Send password reset email button.
  2. If an error occurs, such as if there is no user with that email address, error text will be displayed.
  3. If the password reset email was successfully sent, the form is replaced with a success message telling the user to check their email.
  4. 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.
  5. User types their password into the Password and Confirm Password fields and hits the button to set it.
  6. If an error occurred, such as if the link is old and no longer valid, the user will see the error text.
  7. 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&#39;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.

As always, super stoked to hear your feedback and any questions you might have on headless WordPress. Hit us up in our discord channel!