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>
)
}
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>
}
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 };
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;
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;
};
- 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 theloginUser
from theAuthContext
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>
)
}
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>
)
}
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)
}
...
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)
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.