React.js authentication using Graphql + JWT + WordPress

Yousef Fatouraee
11 min readNov 28, 2021
JWT

In this article, my solution to the following question is going to be shared with you:
How to authenticate Next.js SPA website to a headless WordPress using JWT?
Note: WPGraphql plugin will be used instead of the default WP REST API.

Head over to the examples section.

I have tried three methods so far, first I started with NextAuth.js which is one of two authentication providers recommended by Next.js official website. Although it is possible to use it as the provider, I left it behind for 2 reasons:
1. Iron-session is more lightweight and simple in this case
2. It is little hard to configure
3. Iron-session encrypts cookies by default which is good to store our sensitive tokens

Afterwards I began my own authentication method using react context and saving encrypted user data in local storage which wasn’t a bad idea as i think, but I started to use iron-session as a recommended package by Next.js to experience it as well. It also encrypts cookies as they mentioned in github.
Maybe one day I’ll share my custom authentication as well.

First, allow me to demonstrate how our JWT authentication should work out:

  1. Send user credentials entries (username and password) to WordPress using Graphql Query.
  2. Get the result,
    if such a user exists:
    return JWT containing Auth/Access token+ Refresh token + user data;
    else:
    return error;
  3. Store the received tokens in a secure way on the client side.
  4. Use the fetched Auth/Access token in HTTP headers (Authorization) to send mutations requests (authorization required queries in WP-Graphql) like creating a new post.
  5. Check for Auth/Access token to be valid, since it is a short-living token it must be replaced by a new one. The new token can be gotten by sending a request containing the refresh token (for security’s sake).
  6. Delete stored tokens whenever the user logs out

Now let’s learn more about the frameworks and libraries we use:

Back-end:

Front-end:

  • Next.js, as the client-side React framework.
  • Iron-session, as the session (cookie) manager.
  • SWR, which provides useful react hooks for data fetching.
  • JWT, for decoding operations.

Important Note:

You may find articles which use refresh token as auth/access token in all requests headers, by mistake. An example.
I think WP-Graphql-JWT-Authentication implementation is the reason why, since it allows developers to do so.
AFAIK it does not follow the mentioned standard procedure, on the other hand, this approach makes it possible for the imposters to log in, since the refresh token is being used forever.
Therefore I decided to fork the project and debug it on my own, which you can find and clone my branch here.
Plus, with this implementation, multi user authentication is now possible.
If you like it, please give it a star.

I‘m going to use the debugged version WP plugin which won’t allow clients to use long-lived refresh tokens as auth tokens.
Simply download it and upload it to your WordPress to install the plugin. Visit here for further information.

You can also download the example next.js with iron-session and JWT auth logic, then skip down to part 3, though I recommend you to read part 2 first.

If you want to build your own version from scratch, let’s get started:

Note that my main focus here will be on client-side development.

Open up your console on preinstalled Next.js project root and run these commands to install the new dependencies mentioned earlier:

npm install swr
npm install next-iron-session
npm install jsonwebtoken

Now let’s have a quick look at the project structure we are going to extend.

components — Form.js //sign in form for login page use
components — Layout.js //(optional) layout for the app
pages — auth — login.js //login page
pages — auth — register.js //(optional) register page
pages — auth — profile.js //(optional) user profile page
pages — api — login.js //login withSession API
pages — api — logout.js //logout withSession API
pages — api — user.js //user withSession API
lib — api.js //(optional) Graphql queries can be written here
lib — fetchJson.js //to query our withSession APIs
lib — session.js //cookie configuration
lib — useUser.js //user hook to get user data in other components
lib — checkExpired.js //to check if the access token is expired
lib — refreshToken.js //to refresh the access token
lib — authenticate.js //to query authentication

1. Set up the Iron session

  • compnents/Form.js

Here is an example form including username and password inputs. You might use your own code.

import React from 'react'
import PropTypes from 'prop-types'
const Form = ({ errorMessage, onSubmit }) => (
<form onSubmit={onSubmit}>
<label>
<span>Username</span>
<input type="text" name="username" required />
</label>
<label>
<span>Password</span>
<input type="password" name="password" required />
</label>
<button type="submit">Login</button>
{errorMessage && <p className="error">{errorMessage}</p>} <style jsx>{`
form,
label {
display: flex;
flex-flow: column;
}
label > span {
font-weight: 600;
}
input {
padding: 8px;
margin: 0.3rem 0 1rem;
border: 1px solid #ccc;
border-radius: 4px;
}
.error {
color: brown;
margin: 1rem 0 0;
}
`}</style>
</form>
)
export default FormForm.propTypes = {
errorMessage: PropTypes.string,
onSubmit: PropTypes.func,
}

Note that the prop-types library is used in this example, write the following line in the console to install it, if necessary:

npm i prop-types
  • pages/auth/login.js

We need to render the login form and get values by using handleSubmit().
Then we have to query the credentials to the server and get user data. This will be done in login API but for now, we request the API using our fetchJson() to set the returning values to a user cookie.

If the authentication fails, an error will be returned and displayed and the user will not be mutated.

import { useState } from 'react'
import useUser from '../lib/useUser'
import Layout from '../components/Layout'
import Form from '../components/Form'
import fetchJson from '../lib/fetchJson'
const Login = () => {
const { mutateUser } = useUser({
redirectTo: '/profile-sg',
redirectIfFound: true,
})
const [errorMsg, setErrorMsg] = useState('') async function handleSubmit(e) {
e.preventDefault()
const body = {
username: e.currentTarget.username.value,
password: e.currentTarget.password.value,
}
try {
mutateUser(
await fetchJson('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
)
} catch (error) {
console.error('An unexpected error happened:', error)
setErrorMsg(error.data.message)
}
}
return (
<Layout>
<div className="login">
<Form isLogin errorMessage={errorMsg} onSubmit={handleSubmit} />
</div>
<style jsx>{`
.login {
max-width: 21rem;
margin: 0 auto;
padding: 1rem;
border: 1px solid #ccc;
border-radius: 4px;
}
`}</style>
</Layout>
)
}
export default Login

Now let’s write fetchJson in a library, Which is just a promise fetcher function that uses the popular fetch interface.

  • lib/fetchJson.js
export default async function fetcher(...args) {
try {
const response = await fetch(...args)
// if the server replies, there's always some data in json
// if there's a network error, it will throw at the previous line
const data = await response.json()
if (response.ok) {
return data
}
const error = new Error(response.statusText)
error.response = response
error.data = data
throw error
} catch (error) {
if (!error.data) {
error.data = { message: error.message }
}
throw error
}
}

Nothing special as you might have noticed.

  • lib/useUser.js

We need to add our custom user hook as well,
Take a look first:

import { useEffect } from 'react'
import Router from 'next/router'
import useSWR from 'swr'
import fetchJson from './fetchJson'
export default function useUser({
redirectTo = false,
redirectIfFound = false,
} = {}) {
const { data: user, mutate: mutateUser } = useSWR('/api/user',fetchJson)
useEffect(() => {
// if no redirect needed, just return (example: already on /dashboard)
// if user data not yet there (fetch in progress, logged in or not) then don't do anything yet
if (!redirectTo || !user) return
if (
// If redirectTo is set, redirect if the user was not found.
(redirectTo && !redirectIfFound && !user?.isLoggedIn) ||
// If redirectIfFound is also set, redirect if the user was found
(redirectIfFound && user?.isLoggedIn)
) {
Router.push(redirectTo)
}
}, [user, redirectIfFound, redirectTo])
return { user, mutateUser }
}

This function accepts an object to redirect if anyone requests, by the useEffect() hook.
The main responsibility of this function is to set and get users.
Here SWR comes into the picture.

The useSWR hook accepts a key string and a fetcher function. key is a unique identifier of the data (normally the API URL) and will be passed to fetcher

We pass “/api/user” as the key, where the fetcher is our predefined fetchJson().

There are 2 ways to define fetcher for SWR, the first one is to config globally in app.js, and the second one is to define locally inside its hook as we did now.
Learn more here about SWR.

  • lib/session.js

Write the following:

// this file is a wrapper with defaults to be used in both API routes and `getServerSideProps` functions
import { withIronSession } from 'next-iron-session'
export default function withSession(handler) {
return withIronSession(handler, {
password: process.env.SECRET_COOKIE_PASSWORD,
cookieName: 'next.js/examples/with-iron-session-and-JWT-authentication',
cookieOptions: {
// the next line allows to use the session in non-https environments like
// Next.js dev mode (http://localhost:3000)
secure: process.env.NODE_ENV === 'production' ? true : false,
},
})
}

Note that the following should be set as an environment variable:

SECRET_COOKIE_PASSWORD=AStrongStringAsSecret

This password is used to encrypt cookies by Iron Session.

Let’s continue with writing the APIs:

  • api/login.js

This API sets the fetched user data in a cookie.

import withSession from '../../lib/session'
import authenticate from '../../lib/authenticate'
export default withSession(async (req, res) => {
try {
//we check that the user exists on server and store tokens and login data in session
const { username } = await req.body
const { password } = await req.body
const data = await authenticate(username,password)
const user = { isLoggedIn: true, ...data}
req.session.set('user', user)
await req.session.save()
res.json(user)
} catch (error) {
const { response: fetchResponse } = error
res.status(fetchResponse?.status || 500).json(error.data)
}
})

We add authenticate.js from the lib directory which returns a user session if authentication is successful. (Defined in the next part)
Then we store the returned data along with a login flag.

You may need user roles to be defined by the server and saved in this cookie, for your further use.

  • api/user.js

This API provides user data that have been gotten from the user cookie.

import withSession from "../../lib/session";
import checkExpired from "lib/checkExpired";
import refreshAuthToken from '../../lib/refreshToken';
export default withSession(async (req, res) => {
const user = req.session.get("user");
const aTIndex = process.env.ACCESS_TOKEN_INDEX_IN_SERVER_AUTH_JSON_RESPONSE
const rtIndex = process.env.REFRESH_TOKEN_INDEX_IN_SERVER_AUTH_JSON_RESPONSE
if (user) {
// in a real world application you might read the user id from the session and then do a database request
// to get more information on the user if needed
if (checkExpired(user[aTIndex])) {
// Get new access/auth token
const newAccessToken = (await refreshAuthToken(user[rtIndex]))[aTIndex]
// Remove old access/auth token and store in cookie
let oldUser = user
delete oldUser[aTIndex]
const newUser = { ...oldUser, [aTIndex]: newAccessToken }
await req.session.set('user', newUser)
await req.session.save();
// Send back the updated user data
const savedUser = await req.session.get('user')
res.json({
isLoggedIn: true,
...savedUser,
})
} else {
res.json({
isLoggedIn: true,
...user,
})
}
} else {
res.json({
isLoggedIn: false,
})
}
});

We are going to add another responsibility to it in the next part which is verifying the auth/access token.

Don’t forget to set these 2 environment variables we used here:

ACCESS_TOKEN_INDEX_IN_SERVER_AUTH_JSON_RESPONSE=authTokenREFRESH_TOKEN_INDEX_IN_SERVER_AUTH_JSON_RESPONSE=refreshToken

These are the JSON key names used for access/auth and refresh tokens which the server will send you when authenticating (in this case wp-graphql-jwt-authentication).

  • api/logout.js

This API handles logout:

import withSession from '../../lib/session'export default withSession(async (req, res) => {
req.session.destroy()
res.json({ isLoggedIn: false })
})

You can use the following state hook from “lib/useUser”:

mutateUser(await fetchJson('/api/logout', { method: 'POST' }),false)

Whenever a logout order is needed.

So far we have set our iron session essentials. You can download a built next.js example with-iron-session here instead.

2. Add JWT authentication logic

Now it’s time to continue coding our authentication procedure.

Let’s begin with:

  • lib/authenticate.js

This is the function we use to send login data to the server, (in our case “wp-graphql”).

import fetchJson from "./fetchJson"
export default async function authentication(username, password) {
const query = `
mutation LoginUser($input : LoginInput!) {
login(input: $input ) {
authToken,
refreshToken,
user {
id
name
}
}
}
`
const variables = {
input: {
username,
password
}
}
const data = await fetchJson(process.env.API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ query, variables })
});
return data.data.login
}

As you can see our query sends usernames and passwords as the Graphql query variables and then fetches authToken, refreshToken, and user information.

As it is a promise function, never forget to write the async keyword behind the function declaration.

Also, don’t forget to add your Graqhql API URL to the environment variables:

API_URL=https://URLToGraphqlAPI
  • lib/checkExpired.js

This function is responsible for checking whether the access token is valid or expired by taking out the expiry time from access JWT:

import jwt from "jsonwebtoken"
export default function checkExpired(accessToken) {
const expIndex = process.env.EXPIRES_AT_INDEX_IN_TOKEN
const decodedToken = jwt.decode(accessToken)
/*
Expiry time is in seconds with our example data,
we need milliseconds (might be different in other implementations) so we do *1000
*/
const expiresAt = new Date((decodedToken[expIndex]) * 1000)
const now = new Date()
if (now < expiresAt) {
// Not expired
return false;
} else {
// Expired
return true;
}
}

I also remind you to set the below line in the environment variables list, which is “exp” in wp-graphql returned JWT:

EXPIRES_AT_INDEX_IN_TOKEN=exp

Make sure your exp time value is in milliseconds or matches with the format of “now” with which it is compared.

  • lib/refreshToken.js

This is where the new access token will be requested, whenever checkExpired returns true.

import fetchJson from "./fetchJson"
export default async function refreshAuthToken(refreshToken) {
const query = `
mutation refreshJwtAuthToken($input: RefreshJwtAuthTokenInput!) {
refreshJwtAuthToken(input: $input) {
authToken
}
}
`
const variables = {
input: {
"jwtRefreshToken": refreshToken
}
}
const data = await fetchJson(process.env.API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ query, variables })
});
return data?.data.refreshJwtAuthToken
}

3. How to Query

Now that everything is set, we can start querying mutations from Graphql which need authorization.

— Queries

Before we get into it, for encapsulation's sake, I recommend you to add all your queries along with their variables inside a file like:

  • lib/api.js
export const login = (username, password) => {
const query = `
mutation LoginUser($input : LoginInput!) {
login(input: $input ) {
authToken,
refreshToken,
user {
id
name
}
}
}
`
const variables = {
input: {
username,
password
}
}
return JSON.stringify({ query, variables })
}

Then you can change the authentication query like:

  • lib/authenticate.js
import fetchJson from "./fetchJson"
import { login } from "./api";
export default async function authentication(username, password) {
const data = await fetchJson(process.env.API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: login(username,password)
});
return data?.data.login
}

As you can see it’s now cleaner than before. Do it for the refresh token as well if you like.

— Mutations

All of the queries we sent so far, didn’t need the Authorization to be set in the header.

Now let's see how we can add a new post to WordPress as a Graphql mutation example:

Add below to

  • lib/api.js
export const addPost = (title,content) => {
const query =
`
mutation createPost($input :CreatePostInput!){
createPost(input:$input){
post{
content
}
}
}
`
const variables = {
input: {
title,
content
}
}
return JSON.stringify({ query, variables });
}

Then in a react hook where you want to send a new post, you can write like:

  • pages/test.js
import { addPost } from "lib/api";
import fetchJson from "lib/fetchJson";
import useUser from "lib/useUser";
export default function test() {
const { user } = useUser();
async function post() {
const data = await fetchJson(process.env.API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${user[process.env.ACCESS_TOKEN_INDEX_IN_SERVER_AUTH_JSON_RESPONSE]}`
},
body: addPost("test title!","some content!")
});
return data?.createPost;
}
return (
<button onClick={(e) => { e.preventDefault(); post() }}>
Add a test Post!
</button>
)
}

Note how headers are set.
The authorization header must be set like “Bearer [accessToken]”.

Feel free to break it into pieces of React functions and components.

Now you can check out your WordPress to see the newly submitted post.

Conclusion

When it comes to Next.js authentication you may find several different methods which may be confusing. JWT is a simple and light approach and is also supported by Graphql and WP-Graphql.

Although on the next.js official website, a “JWT” keyword is written for next-auth, now in this article you have been guided to build a web application using iron-session, JWT, and Graphql to implement authentication.

--

--