Editor’s note: This guide to password hashing in Node.js with bcrypt was last updated by Shalitha Suranga on 1 October 2024 to include a practical bcrypt password hashing demo and best practices for security with bcrypt.
Password hashing is a way to transform a plaintext password into a string sequence using a hash function that performs one-way string obfuscation. Unlike encryption, hashing cannot be reversed. This makes it particularly useful in data breaches, where data is unintelligible and irreversible to hackers even if they know the hashing algorithm.
Here’s an example of hashing a plaintext password:
hash('HeypasswordIsSafe@') = '1b21hb2hb1u2gu3g2fxy1v2ux1v2y3vu12g4u3ggvgu43598sa89da98sd79adshuavusdva9sdguasd'
Some popular algorithms for secure password hashing include:
Among the various password-hashing algorithms, bcrypt is one of the most widely used and is generally considered the most secure and reliable. In this article, we’ll demonstrate how to perform secure passwording hashing using bcrypt in Node.js.
bcrypt is a password hashing algorithm designed by Niels Provos and David Mazières based on the Blowfish cipher. The name “bcrypt” is made of two parts: b and crypt, where “b” stands for Blowfish and “crypt” is the name of the hashing function used by the Unix password system.
bcrypt was created as a result of the failure of Crypt to adapt to technology and hardware advancement. bcrypt is designed to be a slow algorithm, which makes it ideal for password hashing. Its design offers native protection from brute-force attacks.
On the surface, bcrypt takes a user-submitted plain password and converts it into a hash. The hash is what is stored in the database. This prevents attackers from accessing users’ plain passwords in the event of a data breach. Unlike some other password-hashing algorithms that just hash the plain password, bcrypt uses the concept of salt.
This salt value, a unique and randomly generated string, provides an additional level of security for a generated hash. Before the plain password is hashed, a salt is generated. Then, it is appended to the plain password, and everything is hashed (the plain password and salt). This helps protect against rainbow table attacks because attackers can’t guess known passwords using a pre-computed hash-password lookup table.
bcrypt also uses a cost factor (or work factor) to determine how long it takes to generate a hash. This cost factor can be increased to make it slower as hardware power increases. The higher the cost factor, the more secure the hash and the slower the process. Therefore, you need to find the right balance between security and performance.
The generated hash will include the salt and other things, like the hash algorithm identifier prefix, the cost factor, and the hash. The hashing process is irreversible; the hash cannot be converted back to the original plain password. Therefore, to determine whether a user is providing the correct password, the password is hashed and compared against the hash stored in the database.
The bcrypt package offers a simple API to generate and compare hashes so Node.js developers can easily install it via any Node.js package manager and use it without wasting more time reading the documentation.
Let’s create a Node.js project and use bcrypt to hash passwords.
The bcrypt library uses a C++-based implementation of the bcrypt algorithm from prebuilt, platform-specific Node binary addons. This allows you to install the library without installing C++ compilers or any Node add-on compilation dependencies.
Create a new Node.js project using the npm/Yarn init
command and install bcrypt as follows:
npm install bcrypt # --- or --- yarn add bcrypt
Now, you are ready to work with bcrypt:
const bcrypt = require('bcrypt'); # --- or --- import bcrypt from 'bcrypt';
Note: The pure JavaScript bcryptjs package offers the same API structure as the bcrypt package. However, bcryptjs doesn’t use a C++-based algorithm implementation — it is written in JavaScript — so the bcryptjs
hash comparison is slower than bcrypt.
In this section, we’ll create a simple CLI program to hash and verify passwords using a simple JSON file-based storage. In this tutorial, we don’t use a popular database system or RESTful web API implementation because we’re focusing on bcrypt password hashing, but you can use this bcrypt password hashing method in any web, mobile, or desktop app project with Node.js.
Our CLI program implements two sub-commands to hash and verify passwords:
store
: Asks the username and password and stores a hashed password using an auto-generated salt valueverify
: Asks the username and password and verifies the credentials based on the stored password hashCreate the basic functionality of the project without password hashing by adding the following source code to the index.js
file:
const fs = require('fs/promises'); const prompt = require('prompt-sync')(); const pprompt = require('password-prompt'); const cmd = process.argv[2]; switch(cmd) { case 'store': store(); break; case 'verify': verify(); break; } async function saveCredentials(username, password) { await fs.writeFile('db.json', JSON.stringify([username, password]) + '\n'); } async function readCredentials() { const content = await fs.readFile('db.json', { encoding: 'utf8' }); return JSON.parse(content); } async function store() { const username = prompt('Username: '); const password = await pprompt('Password: '); await saveCredentials(username, password); console.log('OK: Credentials saved.'); } async function verify() { const username = prompt('Username: '); const password = await pprompt('Password: '); const [storedUsername, storedPassword] = await readCredentials(); const usernameMatched = username === storedUsername; const passwordMatched = password === storedPassword; if(usernameMatched && passwordMatched) console.log('OK: Verification is successful.'); else console.error('ERR: Verification failed.'); }
Note that you should also install prompt-sync
and password-prompt
packages because the above source file uses them to capture usernames as passwords from the terminal.
Run the above source file using the store
sub-command. The program asks for the username and password and stores them in db.json
:
Run the source file using the verify
sub-command to verify the previously stored credentials. The program verifies the stored username as a password:
The functionality of this program is perfectly fine, but what if someone finds the contents of the db.json
file? Then your private password becomes a public one:
So, let’s hash the password using bcrypt. That way, unauthorized users can’t reveal the actual password by reading the db.json
file contents.
bcrypt offers functions to generate a secure random salt and the hash, so we can update the store()
function as follows:
const bcrypt = require('bcrypt'); // ---- async function store() { const username = prompt('Username: '); const password = await pprompt('Password: '); const salt = await bcrypt.genSalt(); const hashedPassword = await bcrypt.hash(password, salt); await saveCredentials(username, hashedPassword); console.log('OK: Credentials saved.'); }
Here, we used the genSalt()
async function to generate a salt value. By default, this function uses 10 salting rounds, but you can change it by passing a preferred numerical value parameter to the genSalt()
function.
The hash()
function generates a hash value using the plain password and the generated salt value. Inspect the db.json
contents to view the hashed password. Unauthorized users can’t access the plain password because bcrypt is an irreversible algorithm:
One last thing — do you think I will get the same hash if I re-run the store
command by entering the same password I entered before?
Obviously, not! The bcrypt.hash()
function will generate a unique hash based on a special salt every time. That’s how it prevents rainbow table attacks.
You can also use the following technique to generate both the salt and the final hash with only one function call:
const hashedPassword = await bcrypt.hash(password, 10); // 10 is the salt rounds parameter
bcrypt.compare()
function to hash passwords in Node.jsNow, how will we validate that hash? This will be necessary to perform user logins. To handle this, bcrypt provides the bcrypt.compare()
function, which compares the plaintext password with the stored hash. The current version of the verify()
function won’t work anymore because we are now storing hashed passwords.
Compare the plain password and the stored hashed password using the bcrypt.compare()
function as follows:
async function verify() { const username = prompt('Username: '); const password = await pprompt('Password: '); const [storedUsername, storedPassword] = await readCredentials(); const usernameMatched = username === storedUsername; const passwordMatched = await bcrypt.compare(password, storedPassword); if(usernameMatched && passwordMatched) { console.log('OK: Verification is successful.'); } else { console.error('ERR: Verification failed.'); } }
Now the verify
sub-command will work as usual even if we store the hashed password in db.json
:
Using the same code modifications, you can also hash passwords in your web projects. Check out this comprehensive article to learn how to use the Node.js bcrypt password hashing library with a Node MVC web app.
As you see at the end, you’ll get a 60 characters long bcrypt hash that uses the following format:
$<algorithm>$<cost>$<salt><hashed-data>
Here’s an example of the above hash format:
$2b$10$KYVbZ5JFVfqu0oV98LnF5eTk4QTe2e4PQG7QNYfhumEpGdi/867AO
This is how the bifurcation of a hash works:
Algorithm
: Two characters for hash algorithm identifier ($2a$
or$2b$
means bcrypt)Cost
: Cost factor that represents the exponent used to determine how many salting iterations (2^rounds
)Salt
: (16-byte (128-bit)), base64 encoded to 22 charactersHashed-data
: (24-byte (192-bit)), base64 encoded to 31 charactersThe bcrypt hashing process performs a series of salting rounds before creating the final hash, resulting in a secure hash that is unpredictable to any system or user.
Hashing options data costs generally refer to the time one hash round takes, which depends on the system’s hardware. On a 2GHz core processor, you can roughly expect the following result:
rounds=8 : ~40 hashes/sec rounds=9 : ~20 hashes/sec rounds=10: ~10 hashes/sec rounds=11: ~5 hashes/sec rounds=12: 2-3 hashes/sec rounds=13: ~1 sec/hash rounds=14: ~1.5 sec/hash rounds=15: ~3 sec/hash rounds=25: ~1 hour/hash rounds=31: 2-3 days/hash
bcrypt has significant advantages over general-purpose hashing methods like MD5, SHA1, SHA2, and SHA3. While these methods can hash large amounts of data quickly, they are vulnerable when it comes to password security.
bcrypt was built on the Blowfish encryption algorithm and uses a “work factor,” which decides how expensive the hash function will be. The work factor ensures that bcrypt hashing becomes slower as more requests are made, making it more difficult for attackers to make multiple requests in a single time frame.
Moreover, bycrypt incorporates “salting,” which protects against attacks like rainbow table attacks by adding a unique value to each password before hashing. This makes it incredibly challenging to crack a well-crafted password, even those containing only eight characters.
Storing passwords with bcrypt hashes improves password security but doesn’t remove every vulnerability within a software system. It is important to implement best practices to maximize security, such as:
Apart from these bcrypt hashing practices, always strive to eliminate vulnerabilities and strengthen security by implementing two-factor authentication (2FA), data encryption (i.e., using HTTPS), secure coding, and conducting security-focused audits.
It is crucial to secure data to avoid significant damage. An attacker may find a way to access your data storage, but well-hashed passwords are a waste of time and effort for an attacker. They won’t get any benefits from our encrypted data because they would have to run a computer for thousands or millions of years to find a plaintext password that generates a matching hash.
Node.js makes it easy to use bcrypt, which is essential for securely hashing passwords and protecting sensitive data. By using bcrypt, you can build a robust system and avoid the risk of exposing users’ sensitive information.
Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third-party services are successful, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowConsider using a React form library to mitigate the challenges of building and managing forms and surveys.
In this article, you’ll learn how to set up Hoppscotch and which APIs to test it with. Then we’ll discuss alternatives: OpenAPI DevTools and Postman.
Learn to migrate from react-native-camera to VisionCamera, manage permissions, optimize performance, and implement advanced features.
SOLID principles help us keep code flexible. In this article, we’ll examine all of those principles and their implementation using JavaScript.
One Reply to "Password hashing in Node.js with bcrypt"
is that bcrypt.compare implemented correctly? I had to compare plain password with a hash stored in DB, you hashed plain password and compared to itself