Creating an app to show the password hashing usage of bcrypt

John Au-Yeung
Sep 9 · 5 min read
Photo by CoinView App on Unsplash

bcrypt is a popular library for hashing text. One useful application is for hashing passwords. We simply pass in our text and we get a hash that is salted as many times as you want.

You cannot get the existing text back once you’ve hashed something with bcrypt. It has a compare, so you can compare your raw text with the hashed version to see if they’re the same. This is handy for storing passwords securely in your app.

To show the password hashing usage of bcrypt, we will create an app which allows users to add and save passwords and update them. With this plan in place, 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. @babel/register allows us to use the latest JavaScript features.

express-jwt generates the JWT and verifies it against a secret. bcrypt does 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.

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

npx sequelize-cli model:generate --name User --attributes username:string, password:string, email: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. It 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 the following code to use the latest features in JavaScript:

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

And we need this code to read our config in an .env file:

require('dotenv').config();

This is the entry point. We will create the userController in the controllers folder 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 });
}
});module.exports = router;

The login route searches for the User entry and, if it’s found, it checks for the hashed password with the compare function of bcrypt. If both are successful, a JWT is generated.

The signup route gets the JSON payload of the 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 the number of salt rounds. bcrypt has full support for promises which makes the code a lot cleaner than using callbacks.

hashedPassword cannot be recovered, it can only use a comparison, using the bcrypt.compare function.

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.

We will add the authCheck middleware next.

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 if in between the URL and our main route code in each authenticated route by importing this and referencing it.

We make an .env file of the root of the back-end app folder with this 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 we can use a front-end app, mobile app, or any HTTP client to sign in.

Better Programming

Advice for programmers.

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

Better Programming

Advice for programmers.

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