Securing Front-End Web Apps With Tokens, HTTP Cookies, a React JS Manual

The reason for writing this article is to find a solution to the security vulnerabilities in many front-end websites and web applications using HTTP Cookies and Tokens against XSS and CRSF

Cross-site scripting is a type of security vulnerability that can be found in some web applications. XSS attacks enable attackers to inject client-side scripts into web pages viewed by other users. A cross-site scripting vulnerability may be used by attackers to bypass access controls such as the same-origin policy.

Cross-site request forgery, also known as one-click attack or session riding and abbreviated as CSRF or XSRF, is a type of malicious exploit of a website where unauthorized commands are submitted from a user that the web application trusts.


So for the most part, top software engineers have recommended to stay away for storing access tokens in local storage which could present a security risk by attacker using the above evil plots.

Let's look at Tokens as very very very secure passwords given by a back-end system to a front-end system to verify that hey, we are best friends and we really really trust each other. 

But when a token is passed once and stored in local storage, then there's no real-time verification of  the front-end to the back-end to prove their relationship and further more, this token can be stolen using xss. 

Setting Up React JS to Use HTTP Cookies

- Firstly, the back-end sends a jwt token that is used to verify our access, so we'll have our API set a cookie in the user’s browser on the first request. 

Cookies are set in browsers if the response to an HTTP request contains a Set-Cookie header. This header will have a string of cookie names and values, plus any additional settings for the cookies, eg.


How to securely set a cookie in an Express Server 

In our express API, start by installing cookie-parser. It’s an express middleware that allows us to parse cookies on incoming requests. This will help to read the incoming cookie value and grant access to the any route. In a route, you can set a cookie on the response object, with some important properties:


// Set a cookie
response.cookie('nameOfCookie''cookieValue', {
  maxAge: 60 * 60 * 1000// 1 hour
  httpOnly: true,
  secure: true,
  sameSite: true,
})


  • Same Site - prevents the cookie from being sent in cross-site requests
  • HTTP Only - cookies are only accessible from a server
  • Secure - cookie must be transmitted over HTTPS
Now if we inspect the cookies tab, we can see it in there as an HttpOnly cookie.



Validate the JWT from the Cookie

Now that the JWT is in a cookie, it will automatically be sent to the API in any calls we make to it. This is how the browser behaves by default. But again, we need to have our front-end and back-end served over the same origin to make this happen.

Using an HTTP Proxy In React JS

So far, the React app project has been running on let's say port 3000 and the API on 3001. This is fine if you’re sending a JWT in the Authorization header of your API calls, but since we now want to send it in a cookie, we need to run the two apps on the same port. This is because cookies can only go to origins from which they came.

Since we used create-react-app, we can do this pretty easily in development mode. We just need to set the API URL as a proxy value in our package.json file.


{
  ...
  "proxy""http://localhost:3001"


Now in the React app, we can make API calls to a relative path instead of prefixing the calls with our API URL.



Get a cookie in server

The cookie can now be read in subsequent responses.


// Get a cookie
response.cookies.nameOfCookie





Back to our React JS project

-  In our index.js we send a get request to get our first initial token and save it to Redux to still keep it away from evil eyes...


import { ourGeneralApifrom "./config";
import { jwtToken } from "./redux/auth/authSlice";

const JWT = async () => {
  const { data } = await ourGeneralApi.get("/jwt");
  store.dispatch(
    jwtToken({
      token: data,
    })
  );
};

/** here we send our token to Redux and render our app */
if (JWT ) {
  JWT();
  Render();
else {
  Render();
}



-  In our ourGeneralApi we use axios to configure a url that just sends a normal get request to recieve the jwtToken and store it in redux



// Our API creation
const ourGeneralApi = axios.create({
  baseURL: apiUrl,
  headers: {
    "Content-Type": `application/json`,
  },
});




In the Back-End Express Server

The JWT validation middleware supplied by express-jwt looks for a JWT on the Authorization header of requests by default. You can update it to use a custom getToken function which will look for the token on an incoming cookie instead.


// server.js
app.use(cookieParser());
app.use(
  jwt({
    secret: 'secret123',
    getToken: req => req.cookies.token
  })
);


Not much needs to change in this case. Since you're using cookie-parser, youcan just read the token right off of the cookies on the incoming request.


Back to our React JS project

- Then in our App,js on componentDidMount we get the jwtToken stored in our redux and send a post request to validate this cookie, 

I guess here the back-end can confirm if this token being received is valid by checking all it's properties..
  • The Value of the Cookie
  • Same Site
  • HTTP Only 
  • Secure 
Then in App.js we send a validateJwt get request to the back-end to validate the jwtToken in our redux.


    /** WE WRITE A AXIOS REQUEST IN OUR ONBOARDING API FOLDER */ 
    import { ourGeneralApi from "../config";
    import { useSelector } from "react-redux";
    import { jwtToken from "./redux/auth/authSlice";

    // Onboarding.js
    const ourJwtToken = useSelector(jwtToken );

    async SendToken(ourJwtToken) {
      const data = {
        ourJwtToken,
      };
      const stringifiedData = JSON.stringify(data);
      return ourGeneralApi.get("/validateJwtToken"stringifiedData);
    };



// Our App.js
// How we Validate the jwtToken

import React, { useEffect } from "react";
import "./App.css";
import onboarding from "../../../api/onboarding";
import { useHistory } from "react-router-dom";
import Routes from "./routes";
import { NonAuthRoutesAuthRoutes } from "./constants";


function App() {
  const history = useHistory();

  // Our App.js useEffect
  useEffect(() => {
    const ac = new AbortController();

 // Whenever a reload / page refresh happens (NOT ON ROUTING based on react router)
 // We Check if JWT Token is valid and then route user based on back-end response
// Meaning this request is only ran once on user arrival to the app
    onboarding
      .SendToken()
      .then((response=> {
     if (response.status === 200) {
       // here we can also request a new verified jwt token and store to redux
       // say verifiedToken
       // and use it for any internal request inside the app as a privateApi
       history.push(AuthRoutes.dashboard);
      } else {
      history.push(NonAuthRoutes.homepage);
     }
    };

    // eslint-disable-next-line consistent-return
    return function cleanup() {
      ac.abort();
    };
  }, [historyauthenticated]);

  return <Routes />;
}

export default App;



- Finally so we can also make sure every API request sent out from within the app is secured whenever we make a request call using an axios configured url below...



// Our API creation for private requests

import axios from "axios";
import { useSelector } from "react-redux";
import { verifiedToken from "./redux/auth/authSlice";

// Attaching the
verifiedToken
const ourVerifiedToken = useSelector(verifiedToken);
const apiUrl = "http://localhost:3000/api";

const ourPrivateApi = axios.create({
  baseURL: apiUrl,
  headers: {
    Authorization: `Bearer ${ourJwtToken}`,
    "Content-Type": `application/json`,
  },
});



- So finally all our internal request will import ourPrivateApi into src/api/dashboard.js and use for all request happening within the app.

To get step-by-step context on how it all works and on how to create the express back-end part of things then read the below 2 articles.






No comments:

Post a Comment