Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 69

Hashing Passwords with `bcrypt` for Secure Registration

Recommended resources

Hello! In our last lesson, we established a strong theoretical foundation for modern authentication by exploring the JSON Web Token (JWT) flow. We covered why stateless, token-based systems are ideal for scalable, modern web applications.

Today, we move from theory to the first crucial implementation step. Before we can issue a JWT, we need to securely register a user. The single most important aspect of this is protecting their password. This lesson is dedicated to achieving that goal: you will learn how to hash and salt user passwords securely during registration using bcrypt.

Storing passwords in plaintext is one of the most severe security mistakes a developer can make. If your database is ever compromised, every user's password would be exposed. We'll learn the industry-standard technique to prevent this from ever happening in your applications.

The Foundation of Password Security: One-Way Hashing

First, let's address a common point of confusion: hashing vs. encryption.

  • Encryption is a two-way process. You can encrypt data into a scrambled format and then, with the correct key, decrypt it back to its original form.
  • Hashing is a one-way process. You run data through a hashing algorithm to produce a unique, fixed-length string called a hash. It is computationally infeasible to reverse this process and get the original data back from the hash.

Think of it like this: hashing a password is like making a smoothie. You can easily blend an apple, a banana, and spinach into a drink. But you can't take the smoothie and un-blend it to get the original, whole ingredients back.

For passwords, we want this one-way street. We never need to know the user's actual password. We only need to verify that the password they provide during login, when put through the same hashing process, produces the same hash we have stored in our database.

To get a deeper understanding of why hashing is the right tool for this job, let's start with a foundational article.

Hashing in Action: Understanding bcrypt

The article 'Hashing in Action: Understanding bcrypt' from Auth0 provides an excellent introduction to why we hash passwords and how algorithms like SHA can fall short, leading to the need for specialized tools like bcrypt.

Please read the introduction, up to the section titled 'Motivation Behind bcrypt'. Focus on the distinction between fast hashing algorithms (like SHA) and the need for a slow, adaptive function for password security.


The Problem with Simple Hashing: Rainbow Tables

So, we hash passwords. But what if two users choose the same password, like password123? A simple hashing algorithm would produce the exact same hash for both.

Attackers exploit this with rainbow tables. These are massive, pre-computed dictionaries mapping common passwords to their corresponding hashes. If an attacker gets a database of hashed passwords, they can simply look up the hashes in their rainbow table to find the original passwords.


The Solution: Salting

To defeat rainbow tables, we use a technique called salting. A "salt" is a random string of data that is unique to each user. We append this salt to the user's password before hashing it.

The flow looks like this:

  1. A user signs up with the password password123.
  2. Your system generates a unique random salt for this user, e.g., E1F53135E559.
  3. You combine the password and the salt: password123E1F53135E559.
  4. You hash this combined string.
  5. You store both the hash and the salt in the user's database record.

Now, if another user also signs up with password123, they will get a different random salt, resulting in a completely different hash. This makes rainbow tables useless.

The good news is that the library we're about to use, bcrypt, handles this entire salting process for us automatically and securely.

To see a clear visual explanation of hashing and salting, let's watch a segment from this excellent video by Web Dev Simplified.

Build Node.js User Authentication - Password Login

This video, 'Build Node.js User Authentication - Password Login', provides a very clear, step-by-step conceptual explanation of why we need salting.

Watch from 04:30 to 07:26. The presenter does a great job of explaining why simple hashing is insecure and how salting solves the problem.


Introducing bcrypt: Slow by Design

We'll use bcrypt, the industry-standard Node.js library for password hashing. Its most important feature is that it's intentionally slow. This is a good thing! A slow algorithm makes it incredibly expensive and time-consuming for an attacker to try and guess passwords (a brute-force attack).

bcrypt's slowness is configurable via a cost factor (also called "work factor" or "salt rounds"). This number determines how many iterations of the hashing algorithm are performed.

  • A higher cost factor = more secure hash = takes longer to compute.
  • A lower cost factor = less secure hash = faster to compute.

The time required grows exponentially with the cost factor. You must find a balance between security and user experience. For a web server, a cost factor of 10 to 12 is a secure and common choice, typically resulting in a hash time of under 100 milliseconds, which is imperceptible to a user during login.

Bcrypt Password Hashing Algorithm Flow
Caption: This flowchart illustrates the internal workings of the bcrypt algorithm. It highlights the iterative nature of the process and the role of the 'Cost' factor, which is key to making bcrypt deliberately slow and resistant to brute-force attacks.

Implementing bcrypt in Our Registration Route

Now for the practical part. Let's integrate bcrypt into the user registration endpoint we built previously.

Step 1: Install bcrypt

Open your terminal in your project's root directory and run the following command to install the package:

npm install bcrypt

Step 2: Hash the Password in the Registration Endpoint

We need to modify our user creation route (e.g., in users/controllers.js or wherever you defined the logic for POST /api/users). The goal is to take the plaintext password from the request body, hash it, and then store the hashed password in the database.

The bcrypt library makes this straightforward. The function we'll use is bcrypt.hash(), which is asynchronous and thus works perfectly with async/await.

Here's how the core logic in your registration function will change.

Before (insecure):

// ... inside your registration route handler
const { email, password } = req.body;
// Storing the plaintext password - BAD!
const newUser = await pool.query(
  "INSERT INTO users (email, password) VALUES ($1, $2) RETURNING *",
  [email, password] 
);
// ...

After (secure with bcrypt):

// At the top of your file
import bcrypt from 'bcrypt';
// Or if using CommonJS: const bcrypt = require('bcrypt');

// ... inside your registration route handler
const { email, password } = req.body;

// 1. Define the salt rounds (cost factor)
const saltRounds = 10;

// 2. Hash the password
const hashedPassword = await bcrypt.hash(password, saltRounds);

// 3. Store the HASHED password in the database
const newUser = await pool.query(
  "INSERT INTO users (email, user_email, user_password) VALUES ($1, $2) RETURNING *",
  [email, hashedPassword] // Use hashedPassword here!
);
// ...

Note: Make sure your database table schema for the users table is updated. The column for the password should be a VARCHAR or TEXT type, long enough to hold the generated hash (e.g., VARCHAR(255)).

The bcrypt.hash function automatically generates a random salt and embeds it within the resulting hash string. You don't need to store the salt in a separate database column. When you later need to verify the password, bcrypt knows how to extract the salt from the hash itself.

To see this implementation in a live coding context, the following video segment is invaluable.

JWT Authentication using Node, Express & Postgres

The 'JWT Authentication using Node, Express & Postgres' video shows exactly how to integrate bcrypt.hash into a PostgreSQL-backed registration route. This aligns perfectly with our tech stack.

Watch from 41:29 to 48:33. The presenter first explains the concept and then implements the exact code we just discussed, showing how to hash the password from the request body before inserting it into the database.

Test your understanding!

You are reviewing a colleague's code for a new project and see this line:
const hashedPassword = crypto.createHash('sha256').update(password).digest('hex');

While this does produce a hash, what two major security features of bcrypt are missing from this simple SHA-256 hash, and why are they critically important for password security?

Show answer
  1. Salting: The code performs a direct hash of the password. It does not add a unique, random salt for each user. This makes it vulnerable to rainbow table attacks, as every user with the same password will have the same hash.
  2. Adaptive Cost Factor: SHA-256 is designed to be a very fast hashing algorithm. It's not designed to be slow and cannot be tuned to become slower as computer hardware improves. bcrypt's configurable cost factor makes it resistant to brute-force attacks by making the hashing process computationally expensive.

Conclusion

Congratulations! You've just implemented the most fundamental security measure in any application that has user accounts. By correctly hashing and salting passwords, you've ensured that even in a worst-case scenario of a database breach, your users' actual passwords remain protected.

Key Takeaways:

  • Always hash passwords, never store them in plaintext.
  • Hashing is a one-way function, making it impossible to reverse and retrieve the original password.
  • Salting (adding unique random data before hashing) is essential to defeat rainbow table attacks.
  • bcrypt is the standard library in Node.js that handles both hashing and salting securely.
  • bcrypt is intentionally slow, and its speed can be tuned with a cost factor to stay ahead of advances in computing power and protect against brute-force attacks.

Preview of the Next Lesson:
We can now securely store a password. The next logical step is to verify that password when a user tries to log in. In the next lesson, we will implement user registration and login endpoints that issue a JWT upon success. We'll use bcrypt.compare() to check the incoming password against the stored hash, and if they match, we'll generate and send back the JWT we learned about previously. This is where all the pieces start coming together