DEV Community

loading...

React Authentication - Protecting and Accessing Routes/Endpoints

ebereplenty profile image NJOKU SAMSON EBERE ・11 min read

In the previous tutorial, we were able to successfully login our user. That was awesome.

Jump To

We know that the main reason for logging in or authenticating a user is to give access to certain restricted features of an application. In view of that, this article will now focus on how to protect routes and how to access such routes. Let's begin by creating two routes. Follow my lead...

Follow Me

Creating the Routes

Jump To

Create Two (2) Components

  • Create a new file in the src directory and name it FreeComponent.js
  • The file should have the following content:

import React from "react";

export default function FreeComponent() {
  return (
    <div>
      <h1 className="text-center">Free Component</h1>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode
  • Next, create a another file and name it AuthComponent.js

  • The file should have the following content:


import React from "react";

export default function AuthComponent() {
  return (
    <div>
      <h1 className="text-center">Auth Component</h1>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

Set up the route

  • Install react-router-dom

npm install --save react-router-dom

Enter fullscreen mode Exit fullscreen mode
  • Navigate to index.js file

  • Import import BrowserRouter


import { BrowserRouter } from "react-router-dom";

Enter fullscreen mode Exit fullscreen mode
  • wrap the <App> component with the </BrowserRouter> component. So index.js file now looks like:

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import "bootstrap/dist/css/bootstrap.min.css";
import { BrowserRouter } from "react-router-dom";

ReactDOM.render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>,
  document.getElementById("root")
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

Enter fullscreen mode Exit fullscreen mode
  • Now navigate to the App.js file
  • Import import Switch and Route at the top of the file

import { Switch, Route } from "react-router-dom";

Enter fullscreen mode Exit fullscreen mode
  • Replace the Account Component with the following code

     <Switch>
        <Route exact path="/" component={Account} />
        <Route exact path="/free" component={FreeComponent} />
        <Route exact path="/auth" component={AuthComponent} />
      </Switch>

Enter fullscreen mode Exit fullscreen mode

You will notice that nothing changed. This is so because the Account component is still our default component when routing. However, we now have access to multiple routes

  • Add links for navigation purpose under the React Authentication Tutorial heading like so:

     <Row>
        <Col className="text-center">
          <h1>React Authentication Tutorial</h1>

          <section id="navigation">
            <a href="/">Home</a>
            <a href="/free">Free Component</a>
            <a href="/auth">Auth Component</a>
          </section>
        </Col>
      </Row>

Enter fullscreen mode Exit fullscreen mode
  • Navigate to index.css to add the following styling for aesthetics purpose

#navigation{
  margin-top: 5%;
  margin-bottom: 5%;
}

#navigation a{
  margin-right: 10%;
}

#navigation a:last-child{
  margin-right: 0;
}

Enter fullscreen mode Exit fullscreen mode

Protecting Routes

Jump To:

Having successfully setup routes, we now want to protect one (i.e. the AuthComponent). To do this, we need to create a new component which will help us check if a certain condition has been met before giving allowing a user to access that route.

The condition we will be using in our own case is the token generated during our login. So before we create this ProtectedRoute Component, let's go get the token from the Login component and make it available in all our application.

Get the token

  • Install universal-cookie. This is a cookie package that helps us share a value or variable across our application

npm i universal-cookie -s

Enter fullscreen mode Exit fullscreen mode
  • Navigate to the Login.js file

  • Import universal-cookie at the top and initialize it like so:


import Cookies from "universal-cookie";
const cookies = new Cookies();

Enter fullscreen mode Exit fullscreen mode
  • Next add the following code in the then block of our axios call

       // set the cookie
        cookies.set("TOKEN", result.data.token, {
          path: "/",
        });

Enter fullscreen mode Exit fullscreen mode

In the code above, we are setting cookie with cookie.set(). It takes 3 arguments: Name of the cookie ("TOKEN". it can be any name; just keep it in mind), Value of the cookie (result.data.token) and which page or route we want it to be available (setting the path to "/" makes the cookie available in all the pages). Hopefully, that makes sense

  • Below the cookie.set(), add the following line of code to redirect the user to the authComponent after a successful login

        // redirect user to the auth page
        window.location.href = "/auth";

Enter fullscreen mode Exit fullscreen mode

If you checkout the Login, it should redirect you to the auth page

Create a Component to Protect Routes

Since we have made the token available across the whole application, we now have access to it on all the components or pages already created or yet to be created. Let's continue...

  • Create a file with a the name: ProtectedRoutes.js

  • Enter the following code into the file


import React from "react";
import { Route, Redirect } from "react-router-dom";
import Cookies from "universal-cookie";
const cookies = new Cookies();

// receives component and any other props represented by ...rest
export default function ProtectedRoutes({ component: Component, ...rest }) {
  return (

    // this route takes other route assigned to it from the App.js and return the same route if condition is met
    <Route
      {...rest}
      render={(props) => {
        // get cookie from browser if logged in
        const token = cookies.get("TOKEN");

        // return route if there is a valid token set in the cookie
        if (token) {
          return <Component {...props} />;
        } else {
          // return the user to the landing page if there is no valid token set
          return (
            <Redirect
              to={{
                pathname: "/",
                state: {
                  // sets the location a user was about to assess before being redirected to login
                  from: props.location,
                },
              }}
            />
          );
        }
      }}
    />
  );
}

Enter fullscreen mode Exit fullscreen mode

Hold up! Hold up!! What is actually going on in the ProtectedRoutes component?

First of all, this is more like a template. What actually changes is the condition on which our ProtectedRoutes component is based. In our own case, it is based on the token received from the cookie upon login. So in other application, the condition may be different

Now, this is what is going on here. The ProtectedRoutes component receives a component and then decides if the component should be returned to the user or not. In order to take this decision, it checks if there is a valid token (token is set upon a successful login) coming from the cookie. If the token is undefined, then it redirects to the default path (the landing page in our own case).

The comments in the code will also guide you to understanding what is going on in the component. Follow Patiently...

Using the ProtectedRoutes component

Let's now use the ProtectedRoutes component to guard our Auth Component since we want it to be accessible only to authenticated users.

  • Navigate to the App.js file

  • Import the ProtectedRoutes component


import ProtectedRoutes from "./ProtectedRoutes";

Enter fullscreen mode Exit fullscreen mode
  • Replace <Route exact path="/auth" component={AuthComponent} /> with <ProtectedRoutes path="/auth" component={AuthComponent} />

My App.js at this point looks like this:


import { Switch, Route } from "react-router-dom";
import { Container, Col, Row } from "react-bootstrap";
import Account from "./Account";
import FreeComponent from "./FreeComponent";
import AuthComponent from "./AuthComponent";
import ProtectedRoutes from "./ProtectedRoutes";

function App() {
  return (
    <Container>
      <Row>
        <Col className="text-center">
          <h1>React Authentication Tutorial</h1>

          <section id="navigation">
            <a href="/">Home</a>
            <a href="/free">Free Component</a>
            <a href="/auth">Auth Component</a>
          </section>
        </Col>
      </Row>

      {/* create routes here */}
      <Switch>
        <Route exact path="/" component={Account} />
        <Route exact path="/free" component={FreeComponent} />
        <ProtectedRoutes path="/auth" component={AuthComponent} />
      </Switch>
    </Container>
  );
}

export default App;

Enter fullscreen mode Exit fullscreen mode

Now try to access http://localhost:3000/auth without logging in and notice how it redirects you to the landing page. That is amazing. Right?

See Mine Below

Alt Text

Making API calls using useEffect Hook

Jump To:

We have already seen how to make API calls using Axios when we created our Register and Login. I want to use this section to clearly different how to make API call to different kinds of endpoints.

We will make API call to two endpoints that we created in this tutorial using the useEffect hook. The useEffect hook does for react functional component what componentDidMount() does for react class component.

  1. Free Endpoint: https://nodejs-mongodb-auth-app.herokuapp.com/free-endpoint

  2. Protected Endpoint: https://nodejs-mongodb-auth-app.herokuapp.com/auth-endpoint

API call to a free endpoint

  • Navigate to the FreeComponent.js file
  • Import useEffect and useState by Adjusting your react import line with the following

import React, { useEffect, useState,  } from "react";

Enter fullscreen mode Exit fullscreen mode
  • Next, import axios

import axios from "axios";

Enter fullscreen mode Exit fullscreen mode
  • Set an initial state for message like so:

const [message, setMessage] = useState("");

Enter fullscreen mode Exit fullscreen mode
  • Just above the return statement, declare the useEffect function like so

  useEffect(() => {

  }, [])

Enter fullscreen mode Exit fullscreen mode

The empty array (i.e. []) is very important to avoid continuous execution after the API call have been completed

  • In the function, set the following configurations

  useEffect(() => {
    // set configurations for the API call here
    const configuration = {
      method: "get",
      url: "https://nodejs-mongodb-auth-app.herokuapp.com/free-endpoint",
    };
  }, [])

Enter fullscreen mode Exit fullscreen mode
  • Next make the API call using axios like so:

  // useEffect automatically executes once the page is fully loaded
  useEffect(() => {
    // set configurations for the API call here
    const configuration = {
      method: "get",
      url: "https://nodejs-mongodb-auth-app.herokuapp.com/free-endpoint",
    };

    // make the API call
    axios(configuration)
      .then((result) => {
        // assign the message in our result to the message we initialized above
        setMessage(result.data.message);
      })
      .catch((error) => {
        error = new Error();
      });
  }, [])

Enter fullscreen mode Exit fullscreen mode

setMessage(result.data.message); assigns the message in our result(i.e. result.data.message) to the message we initialized above. Now we can display the message in our component

I have already shown in the last article how to check for the result of our API call in the console. You can do that to trace how we got to result.data.message.

  • To display the message we got on our FreeComponent page, enter the following code below <h1 className="text-center">Free Component</h1> line

<h3 className="text-center text-danger">{message}</h3>

Enter fullscreen mode Exit fullscreen mode

React will read the message as a variable because of the curly bracket. If the message is without the curly bracket, React reads is as a normal text

This is my FreeComponent.js file at this point:


import React, { useEffect, useState } from "react";
import axios from "axios";

export default function FreeComponent() {
  // set an initial state for the message we will receive after the API call
  const [message, setMessage] = useState("");

  // useEffect automatically executes once the page is fully loaded
  useEffect(() => {
    // set configurations for the API call here
    const configuration = {
      method: "get",
      url: "https://nodejs-mongodb-auth-app.herokuapp.com/free-endpoint",
    };

    // make the API call
    axios(configuration)
      .then((result) => {
        // assign the message in our result to the message we initialized above
        setMessage(result.data.message);
      })
      .catch((error) => {
        error = new Error();
      });
  }, []);

  return (
    <div>
      <h1 className="text-center">Free Component</h1>

      {/* displaying our message from our API call */}
      <h3 className="text-center text-danger">{message}</h3>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode
  • My FreeComponent Page right now:

Alt Text

API call to a protected endpoint

  • Navigate to the AuthComponent.js file
  • Import useEffect and useState by Adjusting your react import line with the following

import React, { useEffect, useState,  } from "react";

Enter fullscreen mode Exit fullscreen mode
  • Next, import axios

import axios from "axios";

Enter fullscreen mode Exit fullscreen mode
  • Import and initialize universal-cookie like so:

import Cookies from "universal-cookie";
const cookies = new Cookies();

Enter fullscreen mode Exit fullscreen mode
  • Get the token generated on login like so:

const token = cookies.get("TOKEN");

Enter fullscreen mode Exit fullscreen mode
  • Set an initial state for message like so:

const [message, setMessage] = useState("");

Enter fullscreen mode Exit fullscreen mode
  • Just above the return statement, declare the useEffect function like so

  useEffect(() => {

  }, [])

Enter fullscreen mode Exit fullscreen mode

The empty array (i.e. []) is very important to avoid continuous execution after the API call have been completed

  • In the function, set the following configurations

  useEffect(() => {
    // set configurations for the API call here
    const configuration = {
      method: "get",
      url: "https://nodejs-mongodb-auth-app.herokuapp.com/auth-endpoint",
      headers: {
        Authorization: `Bearer ${token}`,
      },
    };
  }, [])

Enter fullscreen mode Exit fullscreen mode

Notice that this configuration contains a header. That is the main difference from the free-endpoint configuration. This is so because the auth-enpoint is a protected endpoint that is only accessible using an Authorization token as specified in this article. So it is in the header that we specify the Authorization token. Without this header, the API call will return a 403:Forbidden error

  • Next, we make the API call like so

// useEffect automatically executes once the page is fully loaded
  useEffect(() => {
    // set configurations for the API call here
    const configuration = {
      method: "get",
      url: "https://nodejs-mongodb-auth-app.herokuapp.com/auth-endpoint",
      headers: {
        Authorization: `Bearer ${token}`,
      },
    };

    // make the API call
    axios(configuration)
      .then((result) => {
        // assign the message in our result to the message we initialized above
        setMessage(result.data.message);
      })
      .catch((error) => {
        error = new Error();
      });
  }, []);

Enter fullscreen mode Exit fullscreen mode
  • To display the message we got on our AuthComponent page, enter the following code below <h1 className="text-center">Auth Component</h1> line

<h3 className="text-center text-danger">{message}</h3>

Enter fullscreen mode Exit fullscreen mode
  • My AuthComponent Page right now:

Alt Text

Logout Function

Finally, we need to logout when we are done viewing our authComponent Page. You know for security reasons. To do this, add a button in the authComponent page.

  • Import the Button component like so:

import { Button } from "react-bootstrap";

Enter fullscreen mode Exit fullscreen mode
  • Add the following code below the texts

<Button type="submit" variant="danger">Logout</Button>

Enter fullscreen mode Exit fullscreen mode
  • We want a logout function to be triggered when the button is clicked so add onClick={() => logout()} to the button options. So our button will look like this:

{/* logout */}
<Button type="submit" variant="danger" onClick={() => logout()}>
   Logout
</Button>

Enter fullscreen mode Exit fullscreen mode
  • Now lets create the function. Enter the following code just above the return

  // logout
  const logout = () => {

  }

Enter fullscreen mode Exit fullscreen mode
  • Add the following code to the logout function to remove or destroy the token generated during login

// logout
  const logout = () => {
    // destroy the cookie
    cookies.remove("TOKEN", { path: "/" });
  }

Enter fullscreen mode Exit fullscreen mode
  • Redirect the user to the landing page with the following code

// logout
  const logout = () => {
    // destroy the cookie
    cookies.remove("TOKEN", { path: "/" });
    // redirect user to the landing page
    window.location.href = "/";
  }

Enter fullscreen mode Exit fullscreen mode
  • Add className="text-center" to the parent div of the AuthComponent. Just to centralize the whole page. You can now remove it from other places. My AuthComponent.js file now has the following content:

import React, { useEffect, useState } from "react";
import { Button } from "react-bootstrap";
import axios from "axios";
import Cookies from "universal-cookie";
const cookies = new Cookies();

// get token generated on login
const token = cookies.get("TOKEN");

export default function AuthComponent() {
  // set an initial state for the message we will receive after the API call
  const [message, setMessage] = useState("");

  // useEffect automatically executes once the page is fully loaded
  useEffect(() => {
    // set configurations for the API call here
    const configuration = {
      method: "get",
      url: "https://nodejs-mongodb-auth-app.herokuapp.com/auth-endpoint",
      headers: {
        Authorization: `Bearer ${token}`,
      },
    };

    // make the API call
    axios(configuration)
      .then((result) => {
        // assign the message in our result to the message we initialized above
        setMessage(result.data.message);
      })
      .catch((error) => {
        error = new Error();
      });
  }, []);

  // logout
  const logout = () => {
    // destroy the cookie
    cookies.remove("TOKEN", { path: "/" });
    // redirect user to the landing page
    window.location.href = "/";
  }

  return (
    <div className="text-center">
      <h1>Auth Component</h1>

      {/* displaying our message from our API call */}
      <h3 className="text-danger">{message}</h3>

      {/* logout */}
      <Button type="submit" variant="danger" onClick={() => logout()}>
        Logout
      </Button>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

My working Application is demonstrated below

Alt Text

And that is it for React Authentication!!!

Congratulations! You are now a React Authentication Pro!!!

Celebrating Small Wins

Conclusion

We began this series from how to Register a user. We then discussed how to Login such a user in the last article. In this article, we have been able to see how to protect route(s) and access them. We have also been able to see how to make API calls depending on the type of endpoint (free or protected). We then concluded by learning how to logout if we are logged in.

That was a lot. No doubt. However, it helped us cover a lot of topics such as hooks, cookie, axios etc. So YES! it was worth it.

All codes are here

Please share to help someone. Thank you for reading

I promise more content coming your way soon

Discussion (10)

Collapse
Sloan, the sloth mascot
Comment deleted
Collapse
ebereplenty profile image
NJOKU SAMSON EBERE Author

Wow... that's a lot of questions.

For sure you can use local storage but you know that isn't a safe path to thread. Anyone can easily access the information

About the comparison, the protectedRoutes component just check if the token is defined or not. It does not compare it with the given token. However, even if you manage to bypass the protectedRoutes component to the component itself, it is still useless because it is at that point that the token is compared to the original token provided at the point of login. By the way, the token for each component is gotten directly from the cookie not from the parent component.

And YES!!! You can protect as many components as you desire in your project. The protectedRoutes component is like a Higher Order Component (HOC) which super charges other components that is passed through it.

Collapse
namansamra profile image
namansamra

we do not use localStorage for checking token instead of that we take the token from the local storage and make a request to backend to check for the verification of token (like is it associated with some user's id in database) but I have a doubt like if I copy my friend's token and store that in my localStorage then I will be able to access the protected route so this might be security issue. Please correct me if I am saying anything wrong and any other strategy to check that. I am confused that in that case how should we verify the token? Thankyou.

Thread Thread
masharsamue profile image
Samuel Mashar

Yes that would definitely be an issue. How is this solved, i recently saw JWT-decode am still trying to figure out how to retrive an objectid from mongo, to delete, or edit . How do you achieve that, because on postman you copy the id and paste it there.

Thread Thread
masharsamue profile image
Samuel Mashar

I think the flaw issue is now resolved by the refresh token

Thread Thread
ebereplenty profile image
NJOKU SAMSON EBERE Author

Hey Samuel, if you are still looking for how to decode the JWT, my article might be of help. Check thies out: dev.to/ebereplenty/decoding-jasonw...

Collapse
masharsamue profile image
Samuel Mashar

Great explanation. Thanks again. Am i've been stuck for a while trying to protect the routes and everytime i use react router and your tutorial the projects just keeps on loading never ending until i remove all the <switch and Route

Thread Thread
ebereplenty profile image
NJOKU SAMSON EBERE Author

I didn't really get you. Hope you have gotten a fix though?

Thread Thread
masharsamue profile image
Samuel Mashar

yes i did thanks again

Thread Thread
ebereplenty profile image
Forem Open with the Forem app