DEV Community

Cover image for How to implement protected routes in Next.js
Caleb O.
Caleb O.

Posted on • Updated on

How to implement protected routes in Next.js

One of the features that is neccessary in a single page application as it pertains to the authentication or its security is the ability to conditionally show some UI to the users based on their authentication state.

In this article, you’re going to learn how to implement this feature in a Next.js application, as you do not want an unauthorized user getting access to private user inerfaces like the dashboard unless they’re currently authenticated.

DISCLAIMER: I recently published a guide on how to perform authentication in Next.js with getServerSideProps and cookies. It also touches how to implement protected routes on the server without the flash of protected resources. If you end up following this guide, you might want to check this one that walks you through the process of persiting authentication state and choosing the right cookie wrappers.

But, before you read this article any further, you should have an idea of the following:

  • Conditional rendering in React
  • localStorage and its basic methods
  • The basics of the React Context API

Setting up a Next.js App

We’ll be focusing on using Next.js in this article. So Let us creating a Next.js app by typing command below into our terminal

npx create-next-app [name-of-your-app]
Enter fullscreen mode Exit fullscreen mode

Let us have a look at the file structure of the app below. We’ll focus on the important files that we need in this app, so it’ll be concise.

     |--pages
     |   |-- api
     |   |-- _app.js
     |   |-- index.js (dashboard page)
     |--src
     |   |-- context
     |   |     |-- auth-context.js
     |   |__
     |__ 
Enter fullscreen mode Exit fullscreen mode

The pages directory is where all the routing of the app takes place. This is an out-of-the-box feature of Nextjs. It saves you the stress of hard-coding your independent routes.

pages/_app.js: is where all our components get attached to the DOM. If you take a look at the component structure, you’ll see that all the components are passed as pageProps to the Component props too.

npm run dev
Enter fullscreen mode Exit fullscreen mode

Setting up the authContext

In the previous section, we saw the basic structure of a Next.js app and the function of the files that
we’ll be interacting with, in this article.

Let’s start by moving into the context folder where we have the auth-context.js file. This file, with the help of React’s Context API, helps us store the authentication state of our application. You can read more about the context API here if it is new to you.

// src/context/auth-context.js
import React from "react";
import { useRouter } from "next/router";

const AuthContext = React.createContext();
const { Provider } = AuthContext;

const AuthProvider = ({ children }) => {
  const [authState, setAuthState] = React.useState({
   token: "",
  });

  const setUserAuthInfo = ({ data }) => {
   const token = localStorage.setItem("token", data.data);

   setAuthState({
    token,
   });
 };

 // checks if the user is authenticated or not
 const isUserAuthenticated = () => {
  if (!authState.token) {
    return false;
  }
 };

 return (
   <Provider
     value={{
      authState,
      setAuthState: (userAuthInfo) => setUserAuthInfo(userAuthInfo),
      isUserAuthenticated,
    }}
   >
    {children}
   </Provider>
 );
};

export { AuthContext, AuthProvider };
Enter fullscreen mode Exit fullscreen mode

The snippet above contains all that we need to have a preserved auth-state in our application. But, let us break it down into smaller chunks and understand what it does.

You’d notice that we’re making use of the useState hook in React to define the initial value of our authentication state authState and the data type that we assigned to it as an object.

const [authState, setAuthState] = React.useState({
 token: "",
});
Enter fullscreen mode Exit fullscreen mode

Why? You’d ask. Well, it is so that we can factor multiple states in our application. Say, for example, we have other states that needs to be preserved, asides the user’s auth-state, all we’d do is add another property to the authState object.

Now, we need a way to properly store the unique JWT (JSON Web Token) that is assigned to any user when they originally signed up on our app. This is where we employ the use of the browser’s localStorage API

const setUserAuthInfo = ({ data }) => {
 const token = localStorage.setItem("token", data.data);

 setAuthState({
  token,
 });
};
Enter fullscreen mode Exit fullscreen mode

What we did in the snippet above was to store the user token in localStorage and also look for a way to make the value of the token to be available in the app state, by using the setAuthState setter function that we declared in the useState hook.

All we have done up until this moment is store the user info (i.e the token). The next step is to check if there’s any JWToken in the browser’s localStorage when the page is first mounted.

const isUserAuthenticated = () => {
 if (!authState.token) {
  return false;
 }
};
Enter fullscreen mode Exit fullscreen mode

The snippet above doesn't seem appropriate, because the isUserAuthenticated function will not return true as I am already negating the condition in the if block.

Casey Choiniere suggested the changes below — and in the useEffect hook that redirects the user back to the home page if they're not authenticated.

const isUserAuthenticated = () => !!authState.token;
Enter fullscreen mode Exit fullscreen mode

The snippet above checks for the token. If the token is not in localStorage, it returns false. If it is, it returns true.

These functions are, in turn, passed as values to the value prop in the Provider

<Provider
 value={{
  authState,
  setAuthState: (userAuthInfo) => 
  setUserAuthInfo(userAuthInfo),
  isUserAuthenticated,
 }}
/>
Enter fullscreen mode Exit fullscreen mode

Using authContext in the dashboard page

The authContext from the previous section can now be imported into the dashboard page, and we can make use of the isUserAuthenticated prop in the authContext Provider to check if the user is already authenticated.

// pages/dashboard
export default function Dashboard() {
  const router = useRouter();
  const authContext = React.useContext(AuthContext);

React.useEffect(() => {
  // checks if the user is authenticated
  authContext.isUserAuthenticated()
  ? router.push("/dashboard")
  : router.push("/");
}, []);

  return (
   <React.Fragment>
    <Head>
     <title>Dashboard</title>
    </Head>
    <div>
     <h2>Dashboard</h2>
    </div>
   </React.Fragment>
 );
}
Enter fullscreen mode Exit fullscreen mode

For this to work, the conditional statement has to be in the useEffect hook of React.js. Since the hook runs on every new render of the component (which is our dashboard page).

So anytime a user manually goes to the dashboard route, without logging in first, they get sent back to the home page or the login page.

React.useEffect(() => {
  // checks if the user is authenticated
  authContext.isUserAuthenticated()
  ? router.push("/")
  : router.push("/dashboard");
}, []);
Enter fullscreen mode Exit fullscreen mode

In the snippet above you’ll see that we made use of Next.js’ useRouter module to get access to the app’s route. Remember how the isUserAuthenticated function will always return false in the authContext.

So now, in this scenario, if the token isn’t in localStorage the user will always get sent to the login route or at least the homepage.

Conclusion

If you have a lot of routes in your application that you don't want to be accessible to users that are not authenticated, all you have to do is repeat the process in these individual routes.

Thank you for reading this article, I hope it has helped you understand how to implement protected routes in Next.js.

Oldest comments (32)

Collapse
 
caseychoiniere profile image
Casey Choiniere

Just a little feedback on some things that you might want to correct in your article so that people are not misled.

The isUserAuthenticated function will never return true. It will return "false" or "undefined". Instead of what you have:

const isUserAuthenticated = () => {
if (!authState.token) {
return false;
}
};

You might want to consider simply:

const isUserAuthenticated = () => !!authState.token;

Which will always return a boolean value.

Also, where you're checking if the user is authenticated in your useEffect you have your logic backwards. If the user isn't authenticated you're sending them to the dashboard, not the login page.

You have this:
React.useEffect(() => {
// checks if the user is authenticated
authContext.isUserAuthenticated()
? router.push("/")
: router.push("/dashboard");
}, []);

Which should be:
React.useEffect(() => {
// checks if the user is authenticated
authContext.isUserAuthenticated()
? router.push("/dashboard")
: router.push("/");
}, []);

Collapse
 
seven profile image
Caleb O.

Dang!!!

Thank you so much for pointing this out. I'll make the necessary change. Thank you once again for your feedback man!

Collapse
 
kthehatter profile image
Ahmed Khalil YOUSFI

you still haven't updated your article

Thread Thread
 
seven profile image
Caleb O.

All done!

Thank you for reminding me

Thread Thread
 
ajitpatil92002 profile image
Ajitpatil92002

Yghhh

Collapse
 
amandeep2603 profile image
Aman Deep

isUserAuthenticated won't work becasue next js have by default ssr. Here's simple solution that put jwt in cookie. it will work otherwise , it will show undefined.

Collapse
 
mahanjs profile image
mahan-js

Ok

Collapse
 
mahanjs profile image
mahan-js

Ok

Thread Thread
 
mahanjs profile image
mahan-js

Ok

Thread Thread
 
mahanjs profile image
mahan-js

Ok

Thread Thread
 
mahanjs profile image
mahan-js

Ok

Collapse
 
alex067 profile image
Alexandros Panayi

I feel like useEffect is the wrong hook to use. You're rendering all your components, then checking if the user is authenticated.

Wouldn't it be better just to:

!authContext.isUserAuthenticated() && router.push("/")

Outside of the useEffect?

Collapse
 
tejaswan profile image
Tejaswan Kalluri

can we use useLayoutEffect() hook. which runs before the rendering part.

youtu.be/sRDUOd1IkS8

Collapse
 
rabbyhossain profile image
Rabby Hossian

If you have hundreds of pages, there will be a lot of code duplication with this approach

Collapse
 
seven profile image
Caleb O.

What would the best approach look like?

Collapse
 
liv_it profile image
liv • Edited

You can create a new function inside your auth-context.js like this.

export const ProtectRoute = ({ children }: any) => {
    const router = useRouter();
    const authContext = React.useContext(AuthContext);

    const isLoggedIn = authContext.isUserAuthenticated();

    if (isLoggedIn && window.location.pathname === "/") {
        router.push("/dashboard");
    } else if (!isLoggedIn && window.location.pathname !== "/") {
        router.push("/");
    }

    return children;
};
Enter fullscreen mode Exit fullscreen mode

And then use it as a wrapper to your pages.

Example in MyApp component, like this:

const MyApp = ({ Component, pageProps }) => {
  return (
   <AuthProvider>
     <ProtectRoute>
        <Component {...pageProps} />
      </ProtectRoute>
    </AuthProvider>
  )
}
Enter fullscreen mode Exit fullscreen mode

Alternatively, you can also add this to any individual page.

export default const ProtectedPage = () => {
  return (
    <ProtectRoute>
      <!-- contents of the page -->
    </ProtectRoute>
  )
}
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
seven profile image
Caleb O.

Thank you for sharing this @liv_it

Collapse
 
mdfaizanahmed786 profile image
Mohammed Faizan Ahmed

thank you for this article.

Collapse
 
seven profile image
Caleb O.

I'm glad you found it helpful

Collapse
 
ajitpatil92002 profile image
Ajitpatil92002

Hhh

Collapse
 
devmuhnnad profile image
Muhnnad Habib

Thank you for this article, but I just got confused a little bit, you didn't use localstorage.getItem at all!, this means whenever the user land on the page he will be not authorized even though the token exists in the localstorage? am I wrong?

Collapse
 
seven profile image
Caleb O.

Hi Muhnnad,

I'm glad you found it helpful. I'm not using the getItem method here because all I'm doing here is to check if the user's auth token is in localStorage.

If it is, authenticate the user. If it isn't, re-route them to the login page.

Hence, the need for this snippet, and the one below, in the use Effect hook.

const isUserAuthenticated = () => !!authState.token;
Enter fullscreen mode Exit fullscreen mode
Collapse
 
matejvykoukal profile image
Matej Vykoukal

Maybe I am wrong, but is it safe to store token in localStorage? AFAIK localStorage can be accesed by 3rd party scripts :/

Collapse
 
seven profile image
Caleb O.

You're absolutely correct.

Storing JWT in localStorage isn't the perfect solution. You can try keeping the token in a cookie.

Whatever works best for everyone's use-case.

Collapse
 
hidaytrahman profile image
Hidayt Rahman

Nice Article, But why we are not using session ?

Collapse
 
seven profile image
Caleb O.

Thanks Hidayt!

Funny enough. This was when I first laid my hands on auth in Next.js. Now that I've been accustomed to it. I'm using cookies and session with getServerSideProps

Hopefully, I'll have an article about it and share it Herr, soon.

Collapse
 
adeleke5140 profile image
Kehinde Adeleke

Hi Caleb, do you have any open source repo demonstrating your use of cookies and session with getServerSideProps?

Thread Thread
 
seven profile image
Caleb O.

Hi Kehinde,

At the moment, No. But, I'm experimenting with cookies and getServerSideProps currently.

I'm using nookies (a Next.js cookie helper package) to parse cookies on the client and server.

Perhaps and when it turns out well. I'll share what I learned.

Collapse
 
hidaytrahman profile image
Hidayt Rahman

Have covered session here.
Handle Protected Route using Session

Collapse
 
seven profile image
Caleb O.

Great! I see that you're using next-auth here too.

My own case would be for an external backend. Thank you for sharing though.

Collapse
 
hidaytrahman profile image
Hidayt Rahman

It works with both cases internal and external backend by using next-auth

Thread Thread
 
seven profile image
Caleb O.

Oh! Great!