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

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

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

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

Issuing Tokens

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)

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

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

Frontend

Login Page

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

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

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.

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