Build Custom Middleware to Protect Routes in a React/Next.js App with Context API
Tags: webdev, javascript, react, frontend
Published: 2023-04-18 17:16:40

If you're building a web app that requires authentication or authorization, Middleware is one of the most important logics you must handle.

The essence of middleware is simple but important - to safeguard our web apps from unauthorized users having access to a particular page on the web app, let's say the dashboard or any protected route such as an ecommerce checkout page.

There are a couple of different approaches to handle middleware in Reactjs or Nextjs applications such as using Redux, the built-in [middleware function in Nextjs](https://nextjs.org/docs/advanced-features/middleware), and many other tricks frontend developers use.

But in this tutorial, I'm going to use **Context API** and I believe this method is simple.

The only thing you need to know is how custom hooks work and that's all, but if you don't, your knowledge of the React ecosystem is okay.

Now let's get started.

## Step 1: Setup your project

If you have any project you're working on, you can simply integrate the middleware to it, and if don't have one, bootstrap a new React project with Nextjs by running this command:

`yarn create next-app app-name`

Clean it up and let's go to the next step.

## Step 2: Create your components

For the purpose of this tutorial, we will work with simple pages.
 
Navigate to `src` folder to create a new folder called component and create three `folders` inside the component's folder namely as:

- Login
- Dashboard
- Utils

### The Login

In the Login folder, create a new file called `Login.jsx` and put your form there, here is mine:

```javascript

import {useRouter} from "next/router"

export const Login = () => {

const router = useRouter()

const handleSubmit = () => {
  // handle
router.push("/dashboard")
}

return (
<div className=" ">
   <form>
    <input type="text" placeholder="Enter your email address" />

 <input type="password" placeholder="Enter your password" />

<button onClick={handleSubmit}>Login</button>

   </form>
</div>
)
}
```

So this is my login file which I'm still going to add something to, but for now, let's move on.

> PS: I'm not going to write any CSS.

Now, let's go the next folder, Dashboard.

### The Dashboard

Create a new file called `Dashboard.jsx` and put a random thing there like:

```javascript

export const Dashboard = () => {
 return <div>This is a protected page</div>
}

```

So, this dashboard will be our protected page that only an authorised user can see.

> Create page for this component in the page folder.

Now let's go to the next folder.

## Step 3: The Logic in Utils folder.

This is where we are going to handle the Middleware logic.

Create three files inside the Utils folder, and name them as follows:

- AuthContext.jsx
- useAuth.jsx
- ProtectedRoutes.jsx

The `AuthContext.jsx` will hold the Context initialization and the provider, while the `useAuth.jsx` will return the context so that we can easily call the hook anywhere in our app. The `ProtectedRoutes.jsx` will hope the declaration for the routes we want to protect from unauthorized visitors.

In the `AuthContext.jsx` file, we will check using `js-cookie` if the user has an access token, we will also have two methods that will help us set and remove the token in case we need to sign in or sign out users.

Finally, we will return a context provider that will wrap children. The context provider will have `isAuthenticated`, `loginUser`, and `logoutUser` as the value.

Here is the code:

```javascript
import React, { createContext, useState, useEffect } from "react";
import Cookie from "js-cookie";

const AuthContext = createContext(); // create context here

const AuthProvider = ({ children }) => {
  const [isAuthenticated, setIsAuthenticated] = useState(false);

  useEffect(() => {
    const hasAccess = Cookie.get("user"); // the name used to store the user’s token in localstorage

    if (hasAccess) {
      setIsAuthenticated(true);
    }
  }, []);

  const LoginUser = (token) => {
    Cookie.set("user", token, { expires: 14, secure: true, sameSite: 'strict' }); // to secure the token
    setIsAuthenticated(true);
  };

  const logoutUser = () => {
    Cookie.remove("user");
    setIsAuthenticated(false);
  };

  return (
    <AuthContext.Provider value={{ isAuthenticated, loginUser, logoutUser }}>
      {children}
    </AuthContext.Provider>
  );
};

export { AuthContext, AuthProvider };

```
Now, let’s export these values  (isAuthenticated, loginUser, and logoutUser) and returned them in a reusable hook.

In the `useAuth.jsx`, use the code below:

```javascript
// useAuth.jsx

import { useContext } from "react";
import { AuthContext } from "./AuthContext";


const useAuth = () => {
  const auth = useContext(AuthContext);
  return auth;
};

export default useAuth;

```

Nothing much is basically happening here, instead of calling `useContext` hook anytime we want to use any of the values stored in the AuthContent, we are doing it here once and for all, so that we only need to call this hook and we are done.

### Now, the protected routes file.

Right now, our logic is ready but if we wrap the app component with the AuthContext, the middleware will run on every route and of course, we don’t want it to behave like that, we only want to protect some of the routes.

Assuming my protected routes are like this: `domain.com/dashboard`, `domain.com/checkout` etc.

Here is the code for this to handle it:

```javascript

export const ProtectedRoutes = ({ children }) => {
  const router = useRouter();

  const { isAuthenticated } = useAuth(); // remember where we got this


 if (
    !isAuthenticated &&
    (router.pathname.startsWith("/dashboard") ||
      router.pathname.startsWith("/checkout/"))
  ) {
    return (
   <p className=””>You are not allowed here</p>
)
}

  return children;
};
```

- We check if the user doesn’t have an access token and
The route the user is trying to access starts with `/checkout` or `/dashboard`, 
Display a component that the user is not allowed.

But if the user is authenticated, we just allow the user to access the requested route.

> TODO:
> Instead of just saying the user is not allowed, you can return a component that allow the user to login, and when login successfully, store the access token using the `loginUser` from the `AuthContext` and finally, redirect the user back to the page he initially requested for. (This is another topic for next time).

## Step 4: Wrap the `_app` component

Now that we are done with the logic, let’s register it in our project by wrapping the main project component (in `_app.jsx`) with the `AuthContext` and `ProtectedRoutes`.

Here is mine:

```javascript
const MyApp = ({ Component, pageProps }: AppProps) => {
import { AppProps } from 'next/app'
//Remember to import the AuthProvider and ProtectedRoutes

return (
 <AuthProvider>
  <ProtectedRoutes>
        <Component {...pageProps} />
        </ProtectedRoutes>
     </AuthProvider>
)
}

```

## Step 5: Use the hooks to handle authentication

Do you remember we have `loginUser` and `logoutUser` methods in the AuthContext, you need to use these methods to set and retrieve access token.

For example, if you want to logout user from the app, the general idea is to remove the access token stored in the user's localStorage or cookie. We have done this, all your need to do whenever you want to logout user is to call `logoutUser` and pass the token as parameter.

Example:

```javascript
// logout user
import Cookie from 'js-cookie'

import useAuth... // from the path
const OurApp = () => {
...

const {logoutUser} = useAuth();

const userToken = Cookie.get("user")// the name used to store your token

return (
 ....

<button onClick={() => logoutUser(userToken)}>Logout</button>
)
}
```

Whenever a user clicks on the button, his access token will be removed and can no longer access the protected routes.

The same logic is applicable if you want to register a user.

After handling the register logic, backend will send responses and one of the responses JWT (the access token), store the token in the user browser by calling `loginUser` so that you will know the person is registered.

Example, modify the register page:


```javascript
const {loginUser} = useAuth()
...
const handleSubmit = () => {
 const data = axios.post(url) // just ann example, you get?
  const token = data.response.access_token // this is token returned when a user is registered

// now store the token in user's cookie by calling the `loginUser`and pass the received token as parameter to it.

 loginUser(token)
 
}

...

```

You're done!

Now test your project, everything should be working fine. If you visit any other page not registered in the `ProtectedRoutes.jsx`, the middleware will ignore but if you visit any route registered in the file, you’ll be redirected to the ‘not allowed’ or ‘login’ screen.

> **NOTE**: I’m not saying this is the best approach, if you’re using the latest Next.js, you can simply use the middleware features. But if you’re working with React or any older version of Nextjs, you can easily handle middleware with this easy-to-understand logic.

If you have any questions, contributions, or criticism, feel free to ask as I’m very open to learning as well.