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

“teal padlock on link fence” by Paulius Dragunas on Unsplash

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

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}`);
}
});
app.get('/api/home', function(req, res) {
res.send('Welcome!');
});
app.get('/api/secret', function(req, res) {
res.send('The password is potato');
});
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>
);
}
}
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>
);
}
}

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);

Secure Passwords

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

npm install --save bcrypt
// 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);
// 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!");
}
});
});
curl -X POST \
http://localhost:3000/api/register \
-H 'Content-Type: application/json' \
-d '{
"email": "me@example.com",
"password": "mypassword"
}'
{
"_id" : ObjectId("5b89a402ec9ad51db3d37c44"),
"email" : "me@example.com",
"password" : "$2b$10$j/e4G.D1HzW1HjlkC9NclOQDDiIsLCm09Euj9QGvTzJNTOLmI9Tpm",
"__v" : 0
}

Authentication

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

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.

npm install --save jsonwebtoken
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);
}
});
}
});
});

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.

npm install --save cookie-parser
// server.js
const cookieParser = require('cookie-parser');
...app.use(cookieParser());
// middleware.jsconst jwt = require('jsonwebtoken');
const secret = 'mysecretsshhh';
const withAuth = function(req, res, next) {
const 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 {
req.email = decoded.email;
next();
}
});
}
}
module.exports = withAuth;
// server.js
const withAuth = require('./middleware');
...app.get('/api/secret', withAuth, function(req, res) {
res.send('The password is potato');
});
Our secret is now off limits

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.

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

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>
);
}
}

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.

// 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');
});
}
import Login from './Login';...<Route path="/login" component={Login} />
Functioning login screen
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
// withAuth.jsximport 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);
throw error;
}
})
.catch(err => {
console.error(err);
this.setState({ loading: false, redirect: true });
});
}
render() {
const { loading, redirect } = this.state;
if (loading) {
return null;
}
if (redirect) {
return <Redirect to="/login" />;
}
return <ComponentToProtect {...this.props} />;
}
}
}
import withAuth from './withAuth';...<Route path="/secret" component={withAuth(Secret)} />
Protected route using react-router and higher-order component

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/

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Faizan Virani

Faizan Virani

Software Developer at Lyft. Georgia Tech ‘18.Check me out at https://faizanv.com/