Uses Bcrypt Algorithm to Securely store Passwords in the Database
Bcrypt algorithm - Edition #26
Hey, I'm Marco and welcome to my newsletter!
As a software engineer, I created this newsletter to share my first-hand knowledge of the development world. Each topic we will explore will provide valuable insights, with the goal of inspiring and helping all of you on your journey.
In this episode, I will introduce the Bcrypt algorithm, compare it to SHA-256, and a possible implementation in Node.js.
You can download all the code shown directly from my Github repository: https://github.com/marcomoauro/bcrypt-user-password
⚠️ Why it is important to avoid storing passwords in plain?
Storing passwords in plain text is a serious security risk.
If a database is hacked, all user passwords are exposed, potentially resulting in unauthorized access to accounts and sensitive information.
![](https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b4a5286-f86c-4a80-91cb-4d810aa44d5a_936x600.png)
Attackers can easily exploit passwords in the clear, causing identity theft, financial losses and damaging an organization's reputation.
To safeguard user data, you can use secure hashing algorithms. These algorithms encrypt passwords, making them extremely difficult for attackers to crack.
Bcrypt algorithm
Bcrypt is a widely used cryptographic hash function.
It employs a technique called adaptive hashing to protect passwords from brute force and rainbow table attacks.
Bcrypt uses a salted hashing approach, adding a random string to each password before hashing, which increases security by preventing the use of pre-calculated hash tables.
🔒 How does Bcrypt work?
The information the algorithm needs includes:
password
cost factor: a number indicating how many iterations the algorithm must perform before producing the result. Higher values require more time.
From this information, the algorithm produces a 22-character salt that is added as a prefix to the resulting hashed password. The salt makes hash dictionary and brute force attacks virtually impossible.
The output of the algorithm will always be 60 characters, structured as follows:
![](https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6370da4b-2eb8-46a8-8060-c39308819250_1166x326.png)
The cost factor is what makes the algorithm secure; it determines the number of iterations that must be performed, influencing the time and resources required to produce the final hash.
Password verification
Let's imagine we generated this hash: $2y$10$n0UIs5kJ7naTuTFkBy1veuKOkSxUFXfua0KdOKf9xYTOKKIGSJwFa from the password implementing.
The algorithm extracts the salt (n0UIs5kJ7naTuTFkBy1veu), the cost factor (10), and applies the hashing procedure again to the given password.
The computation will require 2^cost_factor (10) iterations before producing the final hash.
If the final hash matches the initial hash, the password is correct, otherwise, it is incorrect.
If we add a pepper?
A classic approach to enhancing password security is to use a pepper, an additional secret string added to the password, similar to a salt, before hashing.
The pepper is typically stored as an environment variable or in a secrets manager.
However, applying a pepper with the Bcrypt algorithm is not necessarily a good idea for several reasons:
Working against design: Bcrypt is designed to work with a salt. Adding a pepper is not part of its original design and could introduce security vulnerabilities exploitable by attackers.
Complexity is the enemy of security: making the password hashing process more complex does not necessarily make it more secure. Increased complexity can introduce new risks.
It's not maintainable: if the pepper is ever compromised or needs to be changed, you would need to update all stored passwords, as the verification process would no longer work with the old pepper.
You will find an interesting insight here.
Bcrypt vs SHA256
Bcrypt is specifically designed for hashing passwords, intentionally slowing down the process to make brute force attacks more difficult. It is meant to be slow.
SHA-256, on the other hand, is a general-purpose hashing algorithm designed for performance. Although SHA-256 is faster and widely used for various cryptographic purposes, it lacks the security features that Bcrypt has for storing passwords.
Bcrypt is preferred for hashing passwords due to its specialized design and greater resistance to brute-force attacks.
👨💻 Let's implement together
You can download all the code shown directly from my Github repository: https://github.com/marcomoauro/bcrypt-user-password
I show you step by step how to create a backend that allows user registration and verification of login data.
Modelling the database
We need a user table where the information we are interested in is:
ID of the record
email (unique)
hashed password (using Bcrypt algorithm)
timestamps for debugging (created_at / updated_at)
we can create it in MySQL with:
create table users
(
id bigint unsigned auto_increment primary key,
email varchar(100) not null,
hashed_password varchar(100) not null,
created_at datetime not null default CURRENT_TIMESTAMP,
updated_at datetime not null default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP
) engine = InnoDB;
alter table users add unique index (email);
API
We will create two APIs:
/sign-up for user registration using an email and password.
/sign-in for logging in. This API will verify the provided email and password, and then return the user's information.
We start as usual with the backend template for Node.js that I have shown you here:
we start by creating the User.js model, this class allows us to communicate with the database:
import log from '../log.js'
import {query} from '../database.js'
import bcrypt from "bcrypt";
import {APIError401, APIError404} from "../errors.js";
export default class User {
id
email
constructor(properties) {
Object.keys(this)
.filter((k) => typeof this[k] !== 'function')
.map((k) => (this[k] = properties[k]))
}
static fromDBRow = (row) => {
const user = new User({
id: row.id,
email: row.email,
})
return user
}
static isEmailTaken = async (email) => {
log.info('Model::User::isEmailTaken', {email})
const params = [email]
const query_sql = `
select *
from users
where email = ?
`;
const [row] = await query(query_sql, params);
return !!row
}
static create = async ({email, password}) => {
log.info('Model::User::create', {email})
const hashed_password = await bcrypt.hash(password, 12);
const params = [email, hashed_password]
const query_sql = `
insert into users (email, hashed_password)
values (?, ?)
`;
const {insertId} = await query(query_sql, params);
const user = await User.getById(insertId)
return user
}
static getById = async (id) => {
log.info('Model::User::getById', {id})
const params = [id]
const query_sql = `
select *
from users
where id = ?
`;
const [row] = await query(query_sql, params);
if (!row) {
throw new APIError404('User not found')
}
const user = User.fromDBRow(row)
return user
}
static signIn = async ({email, password}) => {
log.info('Model::User::signIn', {email})
const params = [email]
const query_sql = `
select *
from users
where email = ?
`;
const [row] = await query(query_sql, params);
if (!row) {
throw new APIError401()
}
const is_password_valid = await bcrypt.compare(password, row.hashed_password);
if (!is_password_valid) {
throw new APIError401()
}
const user = User.fromDBRow(row)
return user
}
}
exposes the following public methods:
isEmailTaken: checks whether the email has already been used by another user, is used during the registration flow.
create: persists the user in the database by saving the email and the hashed password, calculated using the Bcrypt algorithm from the user password using a cost factor of 12.
getById: retrieves a user from the database by ID.
signIn: retrieves the user by e-mail and password. The user is retrieved from the database via the email, the password provided is checked together with the hashed password stored in the record, if they match then the password is correct.
For password hashing, I used the NPM library bcrypt, you can install it using the following command:
yarn add bcrypt
Let us now create the controller user.js:
import log from "../log.js";
import {APIError400} from "../errors.js";
import User from "../models/User.js";
export const signUp = async ({email, password}) => {
log.info('Controller::user::signUp', {email})
if (!email || !password) {
throw new APIError400('email and password are required');
}
const is_email_taken = await User.isEmailTaken(email)
if (is_email_taken) {
throw new APIError400('email is already taken');
}
const user = await User.create({email, password})
return user
}
export const signIn = async ({email, password}) => {
log.info('Controller::user::signIn', {email})
if (!email || !password) {
throw new APIError400('email and password are required');
}
const user = await User.signIn({email, password})
return user
}
signUp: validates the email and password fields, checks that the email has not been used before, invokes the create method of the User model and then returns the response to the client.
signIn: validates the email and password fields, invokes the signIn method of the User model and returns the response to the client.
We finally create the router router.js that will contain the endpoint definitions:
import Router from '@koa/router';
import {routeToFunction} from "./middlewares.js";
import {signUp, signIn} from "./controllers/user.js";
const router = new Router();
router.post('/sign-up', routeToFunction(signUp));
router.post('/sign-in', routeToFunction(signIn));
export default router;
Try them!
I have deployed the server on Heroku, allowing you to test the functionality of the two endpoints using the cURL command.
I published a post on how to use Heroku to deploy your own applications. You can find it here:
/sign-up:
you can change e-mail and password to create a new user:
curl --location 'https://bcrypt-user-password-0f6f0188e0da.herokuapp.com/sign-up' \
--header 'Content-Type: application/json' \
--data-raw '{
"email": "marcomoauro@hotmail.it",
"password": "implementing"
}'
/sign-in:
I created an account with my email, which you can use to verify the endpoint. Alternatively, you can create a new account using the sign-up endpoint and then use that data to sign in.
curl --location 'https://bcrypt-user-password-0f6f0188e0da.herokuapp.com/sign-in' \
--header 'Content-Type: application/json' \
--data-raw '{
"email": "marcomoauro@hotmail.it",
"password": "implementing"
}'
🌟 Top lectures of the week
8 steps to escaping the prison of overthinking As A Programmer
Deadlines are not so bad, I’ll tell you why
How YouTube Was Able to Support 2.49 Billion Users With MySQL
And that’s it for today! If you are finding this newsletter valuable, consider doing any of these:
🍻 Read with your friends — Implementing lives thanks to word of mouth. Share the article with someone who would like it.
📣 Provide your feedback — We welcome your thoughts! Please share your opinions or suggestions for improving the newsletter, your input helps us adapt the content to your tastes.
💬 Chat with me — If you have any doubts or curiosity, please write to me, I will be happy to answer you!
I wish you a great day! ☀️
Marco
While bcrypt is a capable algorithm, OWASP doesn't recommend it anymore for hashing password, instead use argon2id if you can.
I am not aware of nodejs implementation, but you can do multiple salt rounds with bcrypt. Wondering, what is optimal number for that, considering security and performance? Many rounds - slower execution.