DEV Community

Cover image for Build Custom Middleware to Protect Routes in a React/Next.js App with Context API
Stephen Gbolagade
Stephen Gbolagade

Posted on

Build Custom Middleware to Protect Routes in a React/Next.js App with Context API

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, 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:


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>
)
}
Enter fullscreen mode Exit fullscreen mode

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:


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

Enter fullscreen mode Exit fullscreen mode

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:

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 };

Enter fullscreen mode Exit fullscreen mode

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:

// useAuth.jsx

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


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

export default useAuth;

Enter fullscreen mode Exit fullscreen mode

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:


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;
};
Enter fullscreen mode Exit fullscreen mode
  • 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:

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

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

Enter fullscreen mode Exit fullscreen mode

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:

// 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>
)
}
Enter fullscreen mode Exit fullscreen mode

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:

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)

}

...

Enter fullscreen mode Exit fullscreen mode

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.

Top comments (1)

Collapse
 
rodelta profile image
Ro De la Rivera

Anyone who's reading this, be aware that this approach will put the cookie handling client-side, which can be a security vulnerability. In next, try to handle sensitive operations server side, specially if your approach to cookies/auth requires you to expose secrets.