Truly Protected React Routes

Are you in doubt that your React routes are really protected ? There are lots of tutorials out there that talk about “secured routes”, “protected routes”, “private routes” or “restricted routes”. It’s pretty misleading because even though you have protected routes, it’s possible to get past your login page and get access to all the code in your protected routes.

Perhaps people are telling you that it isn’t worth going for server side rendering (SSR) in order to truly protect the graphic content of your website. Maybe they’re saying that securing the backend should be enough because that will prevent you from displaying any sensitive data to fake-logged-in users. But WHAT IF you still want more security and you want to block all access? After all, you don’t want your competitors to hack into your admin dashboard, to see how you’re progressing or stealing your ideas.

If you’re eager to find out how to avoid this (without SSR), you can skip the first 3 parts and go straight to how to truly protect my React routes

What’s not secure about protecting routes in the client?

In React there is nothing such as truly private routes as it’s a single page application (SPA) which means that all the code is served to the browser.

This is typically how we protect routes in the browser:

{isLoggedIn ? <PrivateRoutes> : <PublicRoutes/>}

With this code here above, you can’t guarantee that the user won’t tweak your javascript, change isLoggedIn to value true and pretend to be an authenticated user.

Let’s see how we can get access to ALL the code of your application. Here’s a sandbox example where I’ve made a typical login system with protected routes. Notice that I lazy loaded two components: “PrivatePage” and the “LoginPage”. Then I used the “classic” way of importing “AnotherPrivatePage” even though that component is not being used (this is on purpose).

import React, { useState, Suspense } from "react"
import AnotherPrivatePage from "./AnotherPrivatePage"
const PrivatePage = React.lazy(() => import("./PrivatePage"))
const LoginPage = React.lazy(() => import("./LoginPage"))

export default function App() {
  const [isAuthenticated, setAuthenticated] = useState(false)
  return (
    <Suspense fallback={<div>Loading .. </div>}>
      {isAuthenticated ? <PrivatePage /> : <LoginPage />}
    </Suspense>
  )
}

You can either follow the article or test yourself by opening up the sandbox example, and opening page in new window by clicking on the two squares in the upper right corner (the icon can vary between browsers):

codesandbox

Go to devtools by right clicking, choose “Inspect” (if you’re in Chrome). Then go to “Sources”.

source_chrome

Here above you can see that we have two components loaded to the browser, “LoginPage” because isAuthenticated = false. We also have “AnotherPrivatePage” because if you don’t lazy load, we can very easily access that component as well. The “hacker” doesn’t even have to hack to look around and read the code and maybe see some static data.

It needs a bit more effort to get hold of the other component “PrivatePage.js” as it’s lazy loaded. There are lots of ways to do that, but here’s one: Install React dev tools if you don’t have it already, go to ⚛️Components:

devtools1

Then click on “App” and change hook’s state to true:

devtools2

And you’ll see how we get access to the “PrivatePage”, the last component we didn’t have loaded in of our application and was supposed to be protected. There are of course lots of other ways to hack React. To increase security you could for example disable access to devtools in production but there’s most often some other way to get around things.

But why do we then protect our routes in the front end?

You can protect your components/graphics on a:

  • component level

  • route level

Either way, the main reason for why we’re protecting those graphics is just to make the user experience nicer. The reason why we do it on a route level is just to make our code more organized by avoiding duplications.

How are protected routes nicer for the user ? Imagine, the user has already visited our page. Next time he visits, he’ll tap the url of your website and his browser autocompletes the website url without adding /login to the end of the URL. He goes straight to http://www.somewebsite.com, but he’s not authenticated anymore (let’s say that he logged out the last time or his authorization token has expired). And because he’s not logged in anymore the user will see the page without any content and no possibility to interact with anything that has to do with server data. It would be nicer for the user to have no direct access to the private pages and instead automatically land on the login page.

But is it so important to have truly protected routes?

In the worst case scenario, the user can hack its way with javscript to your private routes and will see some empty tables, graphs, or messages that tell you that there is no data etc. And without content, your website will look like nothing, might even be ugly or at least it will be unusable. Well that’s not so serious, we could even say that our hacker deserves that! 😈. But you have to make sure that there is no possibility for the hacker to access sensitive data 🔓 You should not leave any sensitive static data in your client and ensure that all your API endpoints are secure and make the server throw 401 if the user is not really authenticated and authorized.

But is that really enough? Like I said above you might have built an admin dashboard for your company. Even without access to sensitive data, your competitor could possibly deduce where your company is heading by reading any static texts in your app, or by trying to make sense of your graphics, even though they’re missing the content. Apart from that, truly securing the private part of your app adds an extra layer of security to your app, which can only be positive.

gandalf

How to make truly secured routes?

There are several ways to achieve this. You could use SSR to solve this problem or you could stay with 100% SPA and serve your application in two parts. I’ve an example of the how to achieve the latter solution. There are lots of ways to do this and here I have an example of this using Express server in Node.js that serves two different SPAs, one containing the login page and the other containing the app itself. You can see this project here on github.

If you clone that project and run it, you should be aware that it takes pretty much time. Instead you can also just follow the article and check out the code.

If you run the project and go to devtools, you’ll see in “sources” that you only have the login page loaded to the browser.

unauthenticated

Here there’s no possibility to access the authenticated part of the application because it won’t be served to the browser unless you provide the correct auth inputs in username and password thanks to this code in server.js

app.get("/protected", (req, res) => {
  if (req.signedCookies.name === "admin") {
    app.use(express.static(path.join(__dirname, `/${privatePage}/build`)))
    res.sendFile(path.join(__dirname, `/${privatePage}/build/index.html`))
  }
})

You can try to log in, username: admin and password: 123

and voilà:

authenticated

Here we’re logged in and now we have the authenticated part of the application loaded in the browser and as a side effect, the login page is no more loaded in the browser.

I hope this article has been useful for boosting the security of some of your websites that might use some extra layer of restriction! If you liked the article, don’t hesitate to tweet it. I’m still reflecting on whether I should implement a comment system or just stay with Twitter. So if you have something to point out, also don’t hesitate to get in touch :)


Get more tips in my almost monthly newsletter about CSS & React!