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

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

Table of Contents

Boilerplate Application

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

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

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

Note: Don’t actually share production data hashed or not

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

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

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.

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

View full code on Github

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

Note: I’ve hardcoded our secret here again which is bad practice and I’m only doing so to keep the example simple

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

View full code on Github

// 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
// server.js...app.get('/checkToken', withAuth, function(req, res) {
res.sendStatus(200);
});

Frontend

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

View full code on Github

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

View full code on Github

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.

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/

--

--

Software Developer at Lyft. Georgia Tech ‘18.Check me out at 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