Doğacan Bilgili A software developer who is also into 3D-modeling and animation.

How to secure a React app with basic server-side login authentication

8 min read 2253

Editor’s note: This React and Express.js authentication tutorial was last updated on 28 May 2021 but may still contain information that is out of date.

It’s well-known that the client side is unsafe due to its exposed nature. In your web application, you can conditionally render views to show different content to different users. But if that information is already stored on the client side, it’s no longer secure.

To ensure that only users with a secure login can see the limited content, you should ship the content data from your server upon authentication.

In this tutorial, we’ll show you how to secure your React app by implementing basic server-side authentication for an Express.js server. Although the client side will be a React app, you can apply it to virtually any other type of client-side application. We’ll also discuss some React authentication best practices for implementing secure login functionality on the server side.

We’ll cover the following in detail:

React authentication example: Server-side setup

The easiest way to bootstrap a React project is to use the Create React App package. When you create a project with this package and then run npm start, you basically start a webpack server. This works fine on your local machine, but when you want to deploy it to a remote server, you need your own server to serve your React application, which is basically a package of HTML, JavaScript, and CSS.

We’ll refer to the following folder structure for this React authentication example project:

--- Project Folder
 |__ client (React App)
 |__ server.js
 |__ package.json

There is a Project Folder and, inside it, a client folder containing the React App. The folder also contains a server.js and package.json files, which you can create by using the following commands on the terminal in the project directory:

npm init -y
touch server.js

Serving the React app from an Express.js server

Proxy the React app

Your deployed React application will be built and the build folder will be served from an Express.js server. However, when developing your app locally, you shouldn’t be building for production on every single change. To avoid this, you can proxy your React app to a certain port. That way, you’ll be using the built-in webpack server for running the React app locally and will still be able to communicate with your Express server.

Add the following line to the package.json file of your React app, assuming that Express server will be serving on port 5000:

We made a custom demo for .
No really. Click here to check it out.

“proxy”: “http://localhost:5000/"

Serve the build folder

The Express.js server should serve the build folder, which will be created during the deployment to a remote server.

The following snippet is a basic Express.js server. We’ll add authentication and other things on top of it.

const express = require('express');
const path = require('path');
const app = express();

const PORT = process.env.PORT || 5000;

app
  .use(express.static(path.join(__dirname, '/client/build')))
  .listen(PORT, () => console.log(`Listening on ${PORT}`));

app.get('/', (req, res) => {
  res.sendFile(path.join(__dirname, '/client/build/index.html'));
});

Run the Express.js server locally

As mentioned earlier, the React app will still be using the webpack server as it will proxy to port 5000. However, we still have to run the Express server separately.

Nodemonpackage is very handy for running and listening for changes, so you can install it globally and then run the server by simply running the following command in the main directory of the project folder.

nodemon server.js

As for the React app we only have to run the following command inside the client folder.

npm start

How to run on a remote server

Although this is an optional step, it’s important to mention. Let’s assume we want to deploy our application to a Heroku dyno.

Heroku detects a Node.js application, installs dependencies, and runs it automatically. But you still have to tell it to go into the specific folder, install dependencies, and build the React app for production — which, in our case, is going into /client running npm install and then npm run build, respectively.

For this purpose, Heroku has a post-build command:

"heroku-postbuild": "cd client && npm install && npm run build"

Add this under the "scripts" key inside the package.json of the server.

Also, make sure your entry point for the Node.js application is server.js in the package.json file. This is likely to be index.js if you initialized your npm package with -y flag as npm init -y.

"main": "server.js"

Basic authentication in React and Express.js

As the name suggests express-basic-auth is a very convenient and easy-to-use package for basic authentication purposes.

Install the package and then require it at the top of your server.js. We’ll define the secure login credentials by using the instance of the package.

const basicAuth = require('express-basic-auth');

const auth = basicAuth({
  users: {
    admin: '123',
    user: '456',
  },
});

Now when the auth variable is used as a parameter of an endpoint, the response from this endpoint reaches back to the client if and only if the credentials sent along with the request match.

In the code below, you’ll see both the /authenticate endpoint on the server side and the GET request sent from the client along with the auth object, which contains the credentials.

// End-point on Server

app.get('/authenticate', auth, (req, res) => {
  if (req.auth.user === 'admin') {
    res.send('admin');
  } else if (req.auth.user === 'user') {
    res.send('user');
  }
});

// Request on Client

const auth = async () => {
  try {
    const res = await axios.get('/authenticate', { auth: { username: 'admin', password: '123' } });
    console.log(res.data);
  } catch (e) {
    console.log(e);
  }
};

In the example above, passing the correct credentials sends back either admin or user as a string response depending on the username used. Wrong login credentials simply return a response of 401 (Unauthorized).

React authentication using HTTP cookies

Now that we’ve figured out how to send data from server to client if the credentials are correct, the next step is to persist that authentication through a cookie session.

Instead of sending a response from the authenticate endpoint, we can set a cookie on the client from the server. By deploying another endpoint, we can check for the cookie and actually send the data to populate the view.

Once the user is authenticated, this information should be stored somewhere on the client side so that the user does not need to authenticate every time. The common practice is to use cookies to store this session information. Cookies are safe as long as the correct flags are set.

The React httpOnly cookie

When using a cookie session to persist authentication in React, the httpOnly flag ensures that no client-side script can access the cookie other than the server.

secure

The secure flag ensures that cookie information is sent to the server with an encrypted request over the HTTPS protocol.

When using secure flag, you also need a key to sign the cookie. For this purpose, we use cookie-parser middleware for the Express.js server.

A cookie simply has a name and a value. Even with the aforementioned flags, never disclose any vulnerable information within cookie parameters.

How to create a user session using cookies in React

In the code example below, server.js sets a unique cookie upon authentication.

As you can see, after setting the cookie, the response is also sending an object with the screen:admin or screen:user key/value pair. This response will later be utilized in the React application on the client side.

const cookieParser = require('cookie-parser');

// A random key for signing the cookie
app.use(cookieParser('82e4e438a0705fabf61f9854e3b575af'));

app.get('/authenticate', auth, (req, res) => {
  const options = {
    httpOnly: true,
    signed: true,
  };

  if (req.auth.user === 'admin') {
    res.cookie('name', 'admin', options).send({ screen: 'admin' });
  } else if (req.auth.user === 'user') {
    res.cookie('name', 'user', options).send({ screen: 'user' });
  }
});

Since the cookie has an httpOnly flag, we can neither read nor delete it on the client side. Therefore, we need two more endpoints to read and delete the cookie and send back a response accordingly.

How to read/delete a cookie from the server

Reading and deleting a cookie from a server is quite straightforward, but you should keep in mind that the endpoints for these functionalities should not have the auth variable since authentication for these endpoints should not be required.

Below, we have two endpoints: /read-cookie and /clear-cookie.

The signedCookies object with the res contains the name:value pair that we set for the cookie.

res.cookie(‘name’, ‘admin’, options)

Depending on the value of the cookie name, we send a response.

As for the /clear-cookie endpoint, deleting the cookie is simply done by referring to the name of the cookie, which is name.

app.get('/read-cookie', (req, res) => {
  if (req.signedCookies.name === 'admin') {
    res.send({ screen: 'admin' });
  } else if (req.signedCookies.name === 'user') {
    res.send({ screen: 'user' });
  } else {
    res.send({ screen: 'auth' });
  }
});

app.get('/clear-cookie', (req, res) => {
  res.clearCookie('name').end();
});

By following this logic, you can create several endpoints to send different types of data depending on the nature of your application. All you need to do is check the cookie and send the response accordingly.

Below you can find the complete server.js file, which serves the client-side React application that we’ll cover in the next section.

const express = require('express');
const basicAuth = require('express-basic-auth');
const cookieParser = require('cookie-parser');
const path = require('path');

const app = express();

const auth = basicAuth({
  users: {
    admin: '123',
    user: '456',
  },
});

const PORT = process.env.PORT || 5000;

app.use(cookieParser('82e4e438a0705fabf61f9854e3b575af'));

app
  .use(express.static(path.join(__dirname, '/client/build')))
  .listen(PORT, () => console.log(`Listening on ${PORT}`));

app.get('/', (req, res) => {
  res.sendFile(path.join(__dirname, '/client/build/index.html'));
});

app.get('/authenticate', auth, (req, res) => {
  const options = {
    httpOnly: true,
    signed: true,
  };

  console.log(req.auth.user);

  if (req.auth.user === 'admin') {
    res.cookie('name', 'admin', options).send({ screen: 'admin' });
  } else if (req.auth.user === 'user') {
    res.cookie('name', 'user', options).send({ screen: 'user' });
  }
});

app.get('/read-cookie', (req, res) => {
  console.log(req.signedCookies);
  if (req.signedCookies.name === 'admin') {
    res.send({ screen: 'admin' });
  } else if (req.signedCookies.name === 'user') {
    res.send({ screen: 'user' });
  } else {
    res.send({ screen: 'auth' });
  }
});

app.get('/clear-cookie', (req, res) => {
  res.clearCookie('name').end();
});

app.get('/get-data', (req, res) => {
  if (req.signedCookies.name === 'admin') {
    res.send('This is admin panel');
  } else if (req.signedCookies.name === 'user') {
    res.send('This is user data');
  } else {
    res.end();
  }
});

A practical React authentication example

Assume you have an admin screen and a regular user screen, which you show different contents on.

  • The first thing we need is the authentication request, which we sent the credentials to the server.
  • We need another request that we send from the componentDidMount lifecycle hook to check whether there is already a cookie so that we can log in automatically.
  • Then we might need some other requests for getting extra data.
  • Eventually, we need to be able to send a request to clear the cookie so that the session does not persist anymore.

You can find the complete client-side code on CodeSandbox. However, to get it working, obviously, you should run it alongside the server.

Basic server-side authentication with React and Express.js: Key takeaways

Let’s review the most important steps of securing a React app with login authentication.

We have three different state variables: screen, username, password. username and password are for storing the input field data and sending it to the server over the /authenticate endpoint through the auth function. Therefore, the onClick event of the login button calls the auth function. This is only required if the user is authenticating initially.

To check whether the user has already logged in, we use the /read-cookie endpoint in the readCookie function. This function is called only once on component mount. The response from this en-point sets the screen state to change the view to the admin or user screen.

In this React authentication example, both admin and user screens are the same component. But since the response from the server changes depending on the authentication, the same component renders different content.

Additionally, the /get-data endpoint demonstrates another use case for cookie-specific response from the server.

Lastly, /clear-cookie is used with the onClick event of the logout button to clear the cookie and set the screen state variable back to its initial state.

Conclusion

The aim of this tutorial is to give you a foundational understanding of basic server-side React authentication on an Express.js server with the express-basic-auth npm package. The potential use cases for such a simple authentication system range from small personal projects to a secured page for an interface with a fixed number of users. The need to protect data behind a secure login mechanism is nearly universal. If you know how to implement secure login authentication in React and Express.js, you can achieve the same virtually any other type of client-side application.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Doğacan Bilgili A software developer who is also into 3D-modeling and animation.

6 Replies to “How to secure a React app with basic server-side…”

  1. Under section “How to proxy the React app”

    it’s package.json, NOT project.json.

  2. What if the express server and react application are hosted on separate dynos on Heroku? Is this approach not going to work?

Leave a Reply