Authentication For Your React and Express Application w/ JSON Web Tokens

Using React and react-router v4 on the front-end supported on the back-end with a node.js application using Express and MongoDB with Mongoose.
“teal padlock on link fence” by Paulius Dragunas on Unsplash

Many web applications today wouldn’t be complete without authentication; allowing users have an identity on your website.

Authentication allows you to cater content specifically to your user and allow users to personalize their settings and experience on your application.

This guide will demonstrate an approach to implement authentication for a web application built using React and react-router on the frontend and node.js using Express and MongoDB with Mongoose on the backend.

I kept this approach relatively simple so that it’s both easy to understand and won’t require many changes to incorporate into an existing application.

To look at a fully functioning example, you can check out this Github repository

Table of Contents

Boilerplate Application (skip if you already have a working project)

Backend

Frontend


Boilerplate Application

Note: This section can be skipped if you already have a working application

To get the boilerplate application going I used this guide which uses create-react-app with some tweaks to add an express server. Then I followed this guide to get react-router running.

Keep in mind I am using npm for this tutorial but this can be interchanged with yarn.

I also made sure I had Mongoose installed and connected in my server code and MongoDB running on my computer:

const mongoose = require('mongoose');
const mongo_uri = 'mongodb://localhost/react-auth';
mongoose.connect(mongo_uri, function(err) {
if (err) {
throw err;
} else {
console.log(`Successfully connected to ${mongo_uri}`);
}
});

Then, using Express, I created two routes that look like this:

app.get('/api/home', function(req, res) {
res.send('Welcome!');
});
app.get('/api/secret', function(req, res) {
res.send('The password is potato');
});

For the frontend I created two components with their own corresponding routes which will fetch those messages from the back-end:

import React, { Component } from 'react';
import { Link, Route, Switch } from 'react-router-dom';
import Home from './Home';
import Secret from './Secret';

export default class App extends Component {
render() {
return (
<div>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/secret">Secret</Link></li>
</ul>
        <Switch>
<Route path="/" exact component={Home} />
<Route path="/secret" component={Secret} />
</Switch>
</div>
);
}
}

And here is an example of my Home component (the Secret component is almost identical):

export default class Home extends Component {
constructor() {
super();
//Set default message
this.state = {
message: 'Loading...'
}
}
  componentDidMount() {
//GET message from server using fetch api
fetch('/api/home')
.then(res => res.text())
.then(res => this.setState({message: res}));
}
  render() {
return (
<div>
<h1>Home</h1>
<p>{this.state.message}</p>
</div>
);
}
}

Which ends up looking like this:

Now with this working we can get to the fun part!


Backend

User Model

To get started we need to create a MongoDB/Mongoose model for a User object. Here is an example of one that I created:

// User.js
const mongoose = require('mongoose');
const UserSchema = new mongoose.Schema({
email: { type: String, required: true, unique: true },
password: { type: String, required: true }
});
module.exports = mongoose.model('User', UserSchema);

Pretty simple, this allows us to create User objects which have their own unique email and password fields which can then be saved and retrieved from MongoDB to authenticate users.

Secure Passwords

Obviously we can’t just store passwords in plain text, that’s how bad things happen.

To secure our passwords we will use a nice little library called bcrypt. This will allow us to hash our passwords (if you don’t know what that means read this).

npm install --save bcrypt

Once bcrypt is installed, we can add a hook to our User schema to hash passwords before we save them to our database:

// User.js
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const saltRounds = 10;
const UserSchema = new mongoose.Schema({
email: { type: String, required: true, unique: true },
password: { type: String, required: true }
});
UserSchema.pre('save', function(next) {
// Check if document is new or a new password has been set
if (this.isNew || this.isModified('password')) {
// Saving reference to this because of changing scopes
const document = this;
bcrypt.hash(document.password, saltRounds,
function(err, hashedPassword) {
if (err) {
next(err);
}
else {
document.password = hashedPassword;
next();
}
});
} else {
next();
}
});
module.exports = mongoose.model('User', UserSchema);

If you’re curious about what saltRounds is used for, read here.

Now that we have our User object setup we can test it out and create some users. To do so, I’ll create another express route like so:

// Import our User schema
const User = require('./models/User.js');
...
// POST route to register a user
app.post('/api/register', function(req, res) {
const { email, password } = req.body;
const user = new User({ email, password });
user.save(function(err) {
if (err) {
res.status(500)
.send("Error registering new user please try again.");
} else {
res.status(200).send("Welcome to the club!");
}
});
});

And then we can test it using Postman or in this case I’ll just do a simple cURL on the command line:

curl -X POST \
http://localhost:3000/api/register \
-H 'Content-Type: application/json' \
-d '{
"email": "me@example.com",
"password": "mypassword"
}'

With which we should get back a nice message saying Welcome to the club! and if we check our MongoDB records, we should see something like this:

{
"_id" : ObjectId("5b89a402ec9ad51db3d37c44"),
"email" : "me@example.com",
"password" : "$2b$10$j/e4G.D1HzW1HjlkC9NclOQDDiIsLCm09Euj9QGvTzJNTOLmI9Tpm",
"__v" : 0
}
Note: Don’t actually share production data hashed or not

Authentication

Now that we have some users saved to the database, we need a way to authenticate them against our database.

To do this we will add a method to our User schema that will take in a password and use bcrypt tell us if it's the correct password for that User

UserSchema.methods.isCorrectPassword = function(password, callback){
bcrypt.compare(password, this.password, function(err, same) {
if (err) {
callback(err);
} else {
callback(err, same);
}
});
}

Issuing Tokens

Now that we have all the tools in place, we can start issuing tokens to clients.

Note: Here is a quick summary of token-based authentication

First step is we need a secret string to use when signing the tokens. For the sake of this example I will simply define it at the top of my server file

const secret = 'mysecretsshhh';

For any real application, you should keep your secret an actual secret using environment variables or some other method and make sure you DO NOT commit it to version control if you happen to be using git.

Next we need to install the jsonwebtoken library which will allow us to issue and verify JSON web tokens:

npm install --save jsonwebtoken

Finally we can create a new express route which, given an email and password, will find a User with the given email and verify that the given password is correct. If the password is correct, we will issue a signed token to the requester:

const jwt = require('jsonwebtoken');
app.post('/api/authenticate', function(req, res) {
const { email, password } = req.body;
User.findOne({ email }, function(err, user) {
if (err) {
console.error(err);
res.status(500)
.json({
error: 'Internal error please try again'
});
} else if (!user) {
res.status(401)
.json({
error: 'Incorrect email or password'
});
} else {
user.isCorrectPassword(password, function(err, same) {
if (err) {
res.status(500)
.json({
error: 'Internal error please try again'
});
} else if (!same) {
res.status(401)
.json({
error: 'Incorrect email or password'
});
} else {
// Issue token
const payload = { email };
const token = jwt.sign(payload, secret, {
expiresIn: '1h'
});
res.cookie('token', token, { httpOnly: true })
.sendStatus(200);
}
});
}
});
});

This looks like a lot going but basically we are checking if we have a user registered with the given email and if we do, then we check if the given password is correct and issue a token to the client if it is.

One particular things to note here is that when we issued the token, we set it as a cookie and set the httpOnly flag to true . This method of issuing tokens is ideal for a browser environment because its sets an httpOnly cookie which helps secure the client from certain vulnerabilities such as XSS.

There are definitely other ways to issue tokens depending on the client but this particular method works well for browser authentication where we aren’t explicitly using any data stored within the token itself on our client and using it purely as a means of authentication.

Protecting Routes (express)

Now that we have established a way to issue signed token to authorized users, we need to define what routes in our application are off limits to non-authenticated users. In this case, we want our '/secret' route to only be accessible if the requesting client has a valid token.

First, let’s make sure that we have cookie-parser installed so that can express can parse cookies passed by our browser:

npm install --save cookie-parser

and let’s add the middleware to our express setup:

// server.js
const cookieParser = require('cookie-parser');
...
app.use(cookieParser());

Next, we will create our own custom express middleware which will sit in between a request and a protected route and verify if the request is authorized.

This middleware function will look for the token in the request body, query string, headers, or cookies in that order and then validate it.

Note: I’ve hardcoded our secret here again which is bad practice and I’m only doing so to keep the example simple
// middleware.js
const jwt = require('jsonwebtoken');
const secret = 'mysecretsshhh';
const withAuth = function(req, res, next) {
const token =
req.body.token ||
req.query.token ||
req.headers['x-access-token'] ||
req.cookies.token;
  if (!token) {
res.status(401).send('Unauthorized: No token provided');
} else {
jwt.verify(token, secret, function(err, decoded) {
if (err) {
res.status(401).send('Unauthorized: Invalid token');
} else {
res.email = decoded.email;
next();
}
});
}
}
module.exports = withAuth;

Finally we can use this middleware whenever we want to have a protected route by simply editing its route configuration:

// server.js
const withAuth = require('./middleware');
...
app.get('/api/secret', withAuth, function(req, res) {
res.send('The password is potato');
});

We can test this change with some more cURL commands or we can just take a look at our React application which should now look like this:

Our secret is now off limits

As you can see, our secret is now actually a secret that will require a valid signed JSON web token to view.

Verifying Tokens

It will be a lot more apparent later why this is useful, but sometimes we need a way to simply ask our server if we have a valid token saved to our browser cookies.

For this, we are going to just create a simple route will return a 200 HTTP status if our requester has a valid token:

// server.js
...
app.get('/checkToken', withAuth, function(req, res) {
res.sendStatus(200);
}

Frontend

Now that we’ve got a backend that can register and authenticate users, we can work on securing our React web application.

Note: To keep things simple, we won’t create a registration page and instead use our previous cURL command if we want to create new users

Login Page

To get this started we are going to create a simple React component with a form that will be used to authenticate the user:

// Login.jsx
import React, { Component } from 'react';
export default class Login extends Component {
constructor(props) {
super(props)
this.state = {
email : '',
password: ''
};
}
  handleInputChange = (event) => {
const { value, name } = event.target;
this.setState({
[name]: value
});
}
  onSubmit = (event) => {
event.preventDefault();
alert('Authentication coming soon!');
}
  render() {
return (
<form onSubmit={this.onSubmit}>
<h1>Login Below!</h1>
<input
type="email"
name="email"
placeholder="Enter email"
value={this.state.email}
onChange={this.handleInputChange}
required
/>
<input
type="password"
name="password"
placeholder="Enter password"
value={this.state.password}
onChange={this.handleInputChange}
required
/>
<input type="submit" value="Submit"/>
</form>
);
}
}

This is just a simple form, similar to the example on the react website.

Saving Token

As you probably noticed, the onSubmit method of the Login component is incomplete. We want this method to make a request to authenticate with our backend and save the resulting token to a browser cookie.

Our express application is setting an httpOnly cookie with a signed JSON Web Token for us so all we have to do is redirect the user properly if they receive a 200 HTTP response when calling '/api/authenticate’ from our React application.

So we are going to update the onSubmit method of our Login component to look like this:

// Login.jsx
...
onSubmit = (event) => {
event.preventDefault();
fetch('/api/authenticate', {
method: 'POST',
body: JSON.stringify(this.state),
headers: {
'Content-Type': 'application/json'
}
})
.then(res => {
if (res.status === 200) {
this.props.history.push('/');
} else {
const error = new Error(res.error);
throw error;
}
})
.catch(err => {
console.error(err);
alert('Error logging in please try again');
});
}

All we’ve done here is use fetch to authenticate against our backend and retrieve a JSON Web Token which is saved to our browser cookies.

Now we can simply add our Login component to our route configuration:

import Login from './Login';
...
<Route path="/login" component={Login} />

And now we’ve got ourselves a functioning login screen:

Functioning login screen

Now if you login successfully, the secret should no longer be a secret!

Accessing the secret using our signed JSON web token

Protecting Routes (react-router)

So we have a working login process that will fetch us a signed token from our backend, save it to our cookies, and subsequently use that token to access protected routes on the server:

Users shouldn’t see this and instead be redirected to login

To add the final touches, we need a way to specify routes to protect on our front end so that a user doesn’t have to see this screen when they aren’t logged in.

Instead we need to redirect them to the login page like a good web application should.

To accomplish this, we are going to be using a concept called higher-order components to wrap react-router routes which we want protected.

A higher-order component is nothing more than a function which takes in a component and returns a component. So we want to create a higher-order component, withAuth, that will take in a component we want to protect, like <Secret /> , and slightly modify it so that users can’t access it unless they are logged in:

See here if you are unfamiliar with React.Fragment which was introduced recently.
// withAuth.jsx
import React, { Component } from 'react';
import { Redirect } from 'react-router-dom';
export default function withAuth(ComponentToProtect) {
return class extends Component {
constructor() {
super();
this.state = {
loading: true,
redirect: false,
};
}
    componentDidMount() {
fetch('/checkToken')
.then(res => {
if (res.status === 200) {
this.setState({ loading: false });
} else {
const error = new Error(res.error);
}
})
.catch(err => {
console.error(err);
this.setState({ loading: false, redirect: true });
});
}
    render() {
const { loading, redirect } = this.state;
let view = <h1>Loading...</h1>
if (!loading) {
if (redirect) {
view = <Redirect to="/login" />
} else {
view = <ComponentToProtect {...this.props} />
}
}
return (
<React.Fragment>
{ view }
</React.Fragment>
);
}
}
}

As you can see our '/checkToken' route from earlier came in handy like I said it would. So hopefully it’s clear from the example that withAuth is a high-order component which takes in a component to protect, ComponentToProtect , and rendering it to our view if we have a valid token. Otherwise, we are redirecting the user, using react-router <Redirect />, to '/login' .

We can then incorporate this back into our route configuration like so:

import withAuth from './withAuth';
...
<Route path="/secret" component={withAuth(Secret)} />

And now we should have a protected route!

Protected route using react-router and higher-order component

And that’s it!

We know have a working implementation of a React web app with authentication using JSON web tokens!

There’s definitely much more we can do to polish this up before making it production ready (password validation, cookie expiration checks and refreshes, more user-friendly authentication flow) but my hope is that serves as a good starting point towards a nice and secure application.

You can find a fully functioning example of the above at this Github repository.

Thanks!


Feel free to contact me if you have any questions, suggestions, or anything else! You can comment below, find me on Twitter @OfficialFaizanv, and you can learn more about me at my website https://faizanv.com/