John Au-Yeung
Aug 31 · 7 min read

With single-page front-end apps and mobile apps being more popular than ever, the front end is decoupled from the back end.

Since almost all web apps need authentication, there needs to be a way for front-end or mobile apps to store user identity data in a secure fashion.

Since a lot of users will forget their password eventually, most apps would be password reset functionality.

Fortunately, this is an easy feature to add. To make password reset functionality secure, we generate a password reset token on the fly when the user requests a user password reset, then we send a email message that has a link to reset the user’s password to the email address that the user registered with, then the user can open that email, click on the link, and if the password reset token is valid, then they can reset their password.

For the authentication feature, we issue and verify JSON Web Tokens so that client side web apps, mobile apps, and other back end apps can use our API with the token.

Overview

For the back end, we’ll use the Express framework, which runs on Node.js, and for the front end, we’ll use the Angular framework. Both have their own JWT add-ons. On the back end, we have the jsonwebtoken package for generating and verify the token.

On the front end, we have the @auth0/angular-jwt module for Angular. In our app, when the user enters user name and password and they are in our database, then a JWT will be generated from our secret key, returned to the user, and stored on the front-end app in local storage. Whenever the user needs to access authenticated routes on the back end, they’ll need the token.

There will be a function in the back-end app called middleware to check for a valid token. A valid token is one that is not expired and verifies as valid against our secret key. There will also be a sign-up and user credential settings pages, in addition to a login page.

Building the App

With this plan, we can begin.

First, we create the front- and back-end app folders. Make one for each.

Then we start writing the back-end app. First, we install some packages and generate our Express skeleton code. We run npx express-generator to generate the code. Then we have to install some packages. We do that by running npm i @babel/register express-jwt sequelize bcrypt sequelize-cli dotenv jsonwebtoken body-parser cors crypto @sendgrid/mail. @babel/register allows us to use the latest JavaScript features.

express-jwt generates the JWT and verifies it against a secret key.bcryptdoes the hashing and salting of our passwords. sequelize is our ORM for doing CRUD. cors allows our Angular app to communicate with our back end by allowing cross-domain communication. dotenv allows us to store environment variables in an .env file. body-parser is needed for Express to parse JSON requests. We use the crypto library to generate our password reset token. We use @sendgrid/mail to send emails with SendGrid. The service makes sending emails very easy. You don’t have to make your own email server.

Then we make our database migrations. First, we run npx sequelize-cli init to generate skeleton code for our database-to-object mapping. Then we run:

npx sequelize-cli model:generate --name User --attributes username:string, password:string, email:string, passwordResetToken:string

We make another migration and put:

'use strict';module.exports = {
up: (queryInterface, Sequelize) => {
return Promise.all([
queryInterface.addConstraint(
"Users",
["email"],
{
type: "unique",
name: 'emailUnique'
}),queryInterface.addConstraint(
"Users",
["userName"],
{
type: "unique",
name: 'userNameUnique'
}),
},
down: (queryInterface, Sequelize) => {
return Promise.all([
queryInterface.removeConstraint(
"Users",
'emailUnique'
),queryInterface.removeConstraint(
"Users",
'userNameUnique'
),
])
}
};

This makes sure we don’t have two entries with the same username or email.

This creates the User model and will create the Users table once we run npx sequelize-cli db:migrate .

Then we write some code. First, we put the following in app.js :

require("@babel/register");
require("babel-polyfill");
require('dotenv').config();
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const user = require('./controllers/userController');
const app = express();app.use(cors())
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use((req, res, next) => {
res.locals.session = req.session;
next();
});
app.use('/user', user);app.get('*', (req, res) => {
res.redirect('/home');
});
app.listen((process.env.PORT || 8080), () => {
console.log('App running on port 8080!');
});

We need:

require("@babel/register");
require("babel-polyfill");

to use the latest features in JavaScript.

And we need:

require('dotenv').config();

to read our config in an .env file.

This is the entry point. We will create userController in the controllersfolder shortly.

app.use(‘/user’, user); routes any URL beginning with user to the userController file.

Next, we add the userController.js file:

const express = require('express');
const bcrypt = require('bcrypt');
const router = express.Router();
const models = require('../models');
const jwt = require('jsonwebtoken');
import { saltRounds } from '../exports';
import { authCheck } from '../middlewares/authCheck';
router.post('/login', async (req, res) => {
const secret = process.env.JWT_SECRET;
const userName = req.body.userName;
const password = req.body.password;
if (!userName || !password) {
return res.send({
error: 'User name and password required'
})
}
const users = await models.User.findAll({
where: {
userName
}
})
const user = users[0];
if (!user) {
res.status(401);
return res.send({
error: 'Invalid username or password'
});
} try {
const compareRes = await bcrypt.compare(password, user.hashedPassword);
if (compareRes) {
const token = jwt.sign(
{
data: {
userName,
userId: user.id
}
},
secret,
{ expiresIn: 60 * 60 }
);
return res.send({ token });
}
else {
res.status(401);
return res.send({
error: 'Invalid username or password'
});
}
}
catch (ex) {
logger.error(ex);
res.status(401);
return res.send({
error: 'Invalid username or password'
});
}});
router.post('/signup', async (req, res) => {
const userName = req.body.userName;
const email = req.body.email;
const password = req.body.password;
try {
const hashedPassword = await bcrypt.hash(password, saltRounds)
await models.User.create({
userName,
email,
hashedPassword
})
return res.send({ message: 'User created' });
}
catch (ex) {
logger.error(ex);
res.status(400);
return res.send({ error: ex });
}
});
router.put('/updateUser', authCheck, async (req, res) => {
const userName = req.body.userName;
const email = req.body.email;
const token = req.headers.authorization;
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const userId = decoded.data.userId;
try {
await models.User.update({
userName,
email
}, {
where: {
id: userId
}
})
return res.send({ message: 'User created' });
}
catch (ex) {
logger.error(ex);
res.status(400);
return res.send({ error: ex });
}});
router.put('/updatePassword', authCheck, async (req, res) => {
const token = req.headers.authorization;
const password = req.body.password;
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const userId = decoded.data.userId;
try {
const hashedPassword = await bcrypt.hash(password, saltRounds)
await models.User.update({
hashedPassword
}, {
where: {
id: userId
}
})
return res.send({ message: 'User created' });
}
catch (ex) {
logger.error(ex);
res.status(400);
return res.send({ error: ex });
}});
router.post('/passwordResetRequest', async (req, res) => {
const email = req.body.email;
const buffer = await crypto.randomBytes(32);
const passwordResetToken = buffer.toString("hex");
try {
await models.User.update(
{
passwordResetToken
}, {
where: {
email
}
}
)
const passwordResetUrl = `${process.env.FRONTEND_URL}/passwordReset?passwordResetToken=${passwordResetToken}`;
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
const msg = {
to: email,
from: process.env.FROM_EMAIL,
subject: 'Password Reset Request',
text: `
Dear user,
You can reset your password by going to ${passwordResetUrl}
`,
html: `
<p>Dear user,</p>
<p>
You can reset your password by going to
<a href="${passwordResetUrl}">this link</a>
</p>
`,
};
sgMail.send(msg);
res.send({ message: 'Successfully sent email' });
}
catch (ex) {
logger.error(ex);
res.send(ex, 500);
}
});
router.post('/passwordReset', async (req, res) => {
const password = req.body.password;
const passwordResetToken = req.body.passwordResetToken;
const hashedPassword = await bcrypt.hash(password, saltRounds);
const buffer = await crypto.randomBytes(32);
const newPasswordResetToken = buffer.toString("hex");
try {
await models.User.update(
{
hashedPassword,
passwordResetToken: newPasswordResetToken
}, {
where: {
passwordResetToken
}
}
)
res.send({ message: 'Successfully reset password' });
}
catch (ex) {
logger.error(ex);
res.send(ex, 500);
}
});module.exports = router;

The login route searches for the User entry. If it’s found, it then checks for the hashed password with the compare function of bcrypt. If both are successful, then a JWT is generated. The signup route gets the JSON payload of username and password and saves it.

Note that there is hashing and salting on the password before saving. Passwords should not be stored as plain text.

This first is the plain text password, and the second is a number of salt rounds.

updatePassword route is an authenticated route. It checks for the token, and if it’s valid, it will continue to save the user’s password by searching for the User with the user ID from the decoded token.

The passwordResetRequest route generates the password reset token when the route is called and save it in to the passwordResetToken column of our Users table. Then an email is sent with the password reset token in the URL. Since the email is sent to the user’s registered email, only the user can see the email and use the link in the email to reset their password.

Then when the passwordReset is called, the user’s password gets reset after checking against their password reset token with this block:

where: {
passwordResetToken
}

in the await models.User.update function call. If the User with the passwordResetToken can’t be found, the password won’t be reset. A new password reset token is also generated as the user resets their password so that the old one cannot be used again.

We will add the authCheck middleware next. We create a middlewares folder and create authCheck.js inside it.

const jwt = require('jsonwebtoken');
const secret = process.env.JWT_SECRET;export const authCheck = (req, res, next) => {
if (req.headers.authorization) {
const token = req.headers.authorization;
jwt.verify(token, secret, (err, decoded) => {
if (err) {
res.send(401);
}
else {
next();
}
});
}
else {
res.send(401);
}
}

You should use the same process.env.JWT_SECRET for generating and verifying the token. Otherwise, verification will fail. The secret shouldn’t be shared anywhere and shouldn’t be checked in to version control.

This allows us to check for authentication in authenticated routes without repeating code. We place it in between the URL and our main route code in each authenticated route by importing and referencing it.

We make an .env file of the root of the back-end app folder, with the following content. (This shouldn’t be checked in to version control.)

DB_HOST='localhost'
DB_NAME='login-app'
DB_USERNAME='db-username'
DB_PASSWORD='db-password'
JWT_SECRET='secret'

The back-end app is now complete. Now can we can use a front-end app, mobile app, or any HTTP client to sign in. They can also reset their password if needed securely.

Subscribe to my email list now at http://jauyeung.net/subscribe/ to get more tutorials.

Follow me on Twitter at https://twitter.com/AuMayeung

JavaScript in Plain English

Learn the web's most important programming language.

John Au-Yeung

Written by

Subscribe to my email list now at http://jauyeung.net/subscribe/ . Follow me on Twitter at https://twitter.com/AuMayeung

JavaScript in Plain English

Learn the web's most important programming language.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade