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

Adding login authentication to secure React apps

11 min read 3091

Adding Login Authentication to Secure React Apps

Editor’s note: This React and Express.js login authentication tutorial was last updated on 30 November 2022 to define and evaluate the benefits of server-side login authentication in React apps. This update also includes sections on logging out and how to connect to a MongoDB database.

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.

Jump ahead:

What is server-side login authentication?

Server-side login authentication is a method of authenticating the identity of a user attempting to log in to a server. This type of authentication typically involves the user providing a username and password, which are then sent to the server for verification. If the credentials are valid, the server allows the user to log in and access the resources on the server.

Benefits of using server-side login authentication

There are several uses and benefits to using server-side login authentication. First and foremost, it helps ensure the security of the server and the resources it contains. By requiring users to provide a username and password, the server can verify that the person attempting to log in is authorized to do so.

Another benefit of server-side login authentication is that it can provide a centralized system for managing user accounts. This means that the server administrator can easily add, remove, or update user accounts, and the changes will be immediately reflected across the entire server. This can make it easier to manage user access and ensure that only authorized users have access to the server and its resources.

In addition, server-side login authentication can provide a way for users to securely access resources from any device — as long as they have the necessary credentials. This can be especially useful for organizations with multiple locations or employees needing to access resources remotely.

Overall, server-side login authentication is a useful and important tool for protecting the security of servers and the resources they contain. It can help prevent unauthorized access and provide a centralized system for managing user accounts, making it an essential component of any secure server environment.

React authentication server-side login 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 run npm start, you essentially 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. This 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 specific 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.js server.

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

"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.

The Nodemon package is very handy for running and listening for changes. You can install it globally and then run the server by simply running nodemon server.js in the main directory of the project folder. As for the React app, we only have to run the npm start inside the client folder.

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. In our case, this 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 with "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.

First, 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, and the response from this endpoint reaches back to the client if and only if the credentials were 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 will 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.

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.

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.


More great articles from LogRocket:


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.

Reading a cookie from a server is quite straightforward, but you should keep in mind that the endpoint for this functionality should not have the auth variable because authentication for this endpoint should not be required.

Below the log out user section, 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’ll send a response.

Log out user

As for the /clear-cookie endpoint, deleting the cookie is done by referring to the name of the cookie, which is name. This, in turn, performs a simple logout functionality, as it clears out the users’ session:

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, in which you show different contents on.

The first thing we need is the authentication request, which sent the credentials to the server. We also 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:

Connecting to MongoDB

To connect a Node.js application to a MongoDB Atlas database, you will need to perform the following steps:

Install the MongoDB driver for Node.js, which will allow you to connect and interact with a MongoDB database from a Node.js application. You can do this by running npm install mongodb in your terminal.

Create a MongoDB Atlas account. This will give you a URL that you can use to connect to your database.

After successfully creating an account, you should see your dashboard like in the image below. Next, to create a database, click the Build a Database button:

Connecting to MongoDB Step One

Select the free Shared option:

Connecting to MongoDB Step Two

Then, create a Cluster by clicking the Create Cluster button at the bottom of the page. You can also decide to edit and make changes to your Cluster on this page:

Connecting to MongoDB Step Three

Next, set a Username and Password for your Cluster. Then, scroll down to the bottom of the page and click Connect to My Local Environment and add your IP address:

Connecting to MongoDB Step Four

If you followed the above steps correctly, you should be able to see the page shown in the image below:

Connecting to MongoDB Step Five

From there, click the Menu button highlighted in red, and then click Load Sample Dataset. MongoDB provides us with a mock dataset that we can we quickly use to test the connection from our application to the DB.

Next, select the Connect button on the far left, as you can see in the image above. You should see a pop-up modal. Select the Connect Your Application option. This will help us connect to our application using one of MongoDB’s Native Drivers, and in our case, it’s the Node Native Driver.

Using the MongoDB Driver

Copy your connection string from the next pop-up modal that appears on the screen. We’ll add this to our server.js file.

In your Node.js application, use the MongoDB Driver to connect to your database using the URL provided by MongoDB Atlas. You can do this using the following code:

const MongoClient = require('mongodb').MongoClient;
const url = 'mongodb+srv://<username>:<password>@cluster0.mongodb.net/test?retryWrites=true&w=majority';

MongoClient.connect(url, { useNewUrlParser: true, useUnifiedTopology: true }, (err, client) => {
  if (err) {
    console.log(err);
  } else {
    console.log('Connected to MongoDB Atlas');
    // do something with your database here
  }
});

In this code, you will need to replace <username> and <password> with your MongoDB Atlas username and password, respectively.

Once your Node.js application is connected to your MongoDB Atlas database, you can use the MongoDB Driver to perform CRUD operations on your database. For example, you can use the insertOne() method to insert a document into a collection or the find() method to retrieve documents from a collection.

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 endpoint 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 contents.

Additionally, the /get-data endpoint demonstrates another use case for cookie-specific responses 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 in virtually any other type of client-side application.

If you’re interested in how to further your web application security in Node.js, check out our guide to password hashing in Node.js with bcrypt.

LogRocket: Full visibility into your production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard 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 combines session replay, product analytics, and error tracking – empowering software teams to create the ideal web and mobile product experience. What does that mean for you?

Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay problems as if they happened in your own browser to quickly understand what went wrong.

No more noisy alerting. Smart error tracking lets you triage and categorize issues, then learns from this. Get notified of impactful user issues, not false positives. Less alerts, way more useful signal.

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.

7 Replies to “Adding login authentication to secure React apps”

  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?

  3. Won’t
    app.use(express.static(path.join(__dirname, ‘/client/build’)))
    show the whole build/source code even if I am authenticated or not? Is there a way to only serve that if I am authenticated?

Leave a Reply