Securing Passwords with bcrypt

Introduction

In modern web applications, user authentication is a critical component. One of the most important aspects of authentication is ensuring that user passwords are stored securely. Storing plain-text passwords in a database is a severe security risk; if the database is ever compromised, all user accounts are immediately vulnerable. To prevent this, developers use password hashing, a process that transforms passwords into a secure, irreversible format before storing them.

One of the most widely used libraries for password hashing in Node.js is bcrypt. bcrypt allows you to hash passwords, store them securely, and verify them during login without ever needing to store the plain-text password. In this post, we will explore how to use bcrypt for password security, including hashing passwords, verifying them, and understanding why hashing is critical for protecting user credentials.


1. What is bcrypt?

bcrypt is a password-hashing function designed to be computationally intensive, making brute-force attacks costly and slow. It incorporates the following features:

  • Salting: bcrypt automatically generates a unique random salt for each password. Salting ensures that identical passwords produce different hashes, preventing attackers from using precomputed hash tables.
  • Work Factor (Cost Factor): bcrypt allows you to configure the computational complexity of the hash function. A higher cost factor increases security by making brute-force attacks slower.
  • One-Way Hashing: Once a password is hashed with bcrypt, it cannot be reversed to obtain the original password. Verification is done by comparing hashes rather than decrypting the password.

bcrypt is widely adopted because it balances security and performance, making it ideal for web applications where password security is crucial.


2. Installing bcrypt in Node.js

To use bcrypt in a Node.js project, you first need to install it.

2.1. Installing bcrypt

Run the following command to install bcrypt:

npm install bcrypt

For development convenience, you can also use bcryptjs, a pure JavaScript implementation:

npm install bcryptjs

For this post, we will focus on bcrypt (the native implementation), which is faster and more secure.


3. Hashing Passwords

Hashing a password involves transforming the plain-text password into a cryptographically secure string before storing it in the database.

3.1. Basic bcrypt Usage

Here’s a simple example of hashing a password using bcrypt:

const bcrypt = require('bcrypt');

const plainPassword = 'mySecretPassword123';
const saltRounds = 10; // Work factor

bcrypt.hash(plainPassword, saltRounds, (err, hash) => {
  if (err) {
console.error('Error hashing password:', err);
} else {
console.log('Hashed password:', hash);
} });
  • plainPassword: The original password entered by the user.
  • saltRounds: Determines the computational cost of hashing. Higher values increase security but require more processing time.
  • hash: The resulting hashed password, which can be safely stored in the database.

3.2. Using Async/Await

bcrypt also supports promises, making it easier to use with async/await syntax:

const bcrypt = require('bcrypt');

const hashPassword = async (password) => {
  try {
const saltRounds = 12;
const hash = await bcrypt.hash(password, saltRounds);
console.log('Hashed password:', hash);
return hash;
} catch (err) {
console.error('Error hashing password:', err);
} }; hashPassword('mySecretPassword123');
  • Using async/await simplifies asynchronous code and makes it more readable.
  • A higher saltRounds increases security, but remember it also increases processing time, so choose a balanced value (usually between 10–14).

4. Storing Hashed Passwords

Once a password is hashed, you should store only the hash in the database, never the plain-text password. Here’s an example using MongoDB and Mongoose:

const mongoose = require('mongoose');
const bcrypt = require('bcrypt');

const userSchema = new mongoose.Schema({
  username: { type: String, required: true, unique: true },
  password: { type: String, required: true }
});

// Pre-save middleware to hash password
userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) return next();
  try {
const saltRounds = 12;
this.password = await bcrypt.hash(this.password, saltRounds);
next();
} catch (err) {
next(err);
} }); const User = mongoose.model('User', userSchema); module.exports = User;
  • The pre('save') middleware hashes the password before saving it to the database.
  • this.isModified('password') ensures that the password is only hashed if it has been changed.

By storing only the hashed password, even if your database is compromised, attackers cannot retrieve the original passwords.


5. Verifying Passwords

When a user attempts to log in, you need to verify that the entered password matches the stored hash. bcrypt provides a compare method for this purpose.

5.1. Using bcrypt.compare

const bcrypt = require('bcrypt');

const storedHash = '$2b$12$eXampleHashStringFromDB...';
const enteredPassword = 'mySecretPassword123';

bcrypt.compare(enteredPassword, storedHash, (err, result) => {
  if (err) throw err;
  if (result) {
console.log('Password is correct');
} else {
console.log('Invalid password');
} });
  • bcrypt.compare compares the entered plain-text password with the stored hash.
  • Returns true if the password matches, otherwise false.

5.2. Using Async/Await for Verification

const verifyPassword = async (enteredPassword, storedHash) => {
  try {
const match = await bcrypt.compare(enteredPassword, storedHash);
if (match) {
  console.log('Password is correct');
} else {
  console.log('Invalid password');
}
return match;
} catch (err) {
console.error('Error verifying password:', err);
} }; verifyPassword('mySecretPassword123', storedHash);

6. Implementing Password Security in Express.js

Let’s put it all together in an Express.js application.

6.1. User Registration with Hashed Password

const express = require('express');
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const User = require('./models/User'); // User model from previous example

const app = express();
app.use(express.json());

// Connect to MongoDB
mongoose.connect('mongodb://localhost:27017/bcryptExample', {
  useNewUrlParser: true,
  useUnifiedTopology: true
});

// Registration route
app.post('/register', async (req, res) => {
  const { username, password } = req.body;

  try {
const hashedPassword = await bcrypt.hash(password, 12);
const newUser = new User({ username, password: hashedPassword });
await newUser.save();
res.status(201).json({ message: 'User registered successfully' });
} catch (err) {
res.status(400).json({ error: err.message });
} }); app.listen(3000, () => { console.log('Server running on http://localhost:3000'); });
  • Password is hashed before storing it in the database.
  • Users never see or store the plain-text password.

6.2. User Login with Password Verification

app.post('/login', async (req, res) => {
  const { username, password } = req.body;

  try {
const user = await User.findOne({ username });
if (!user) return res.status(404).json({ message: 'User not found' });
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) return res.status(400).json({ message: 'Invalid password' });
res.status(200).json({ message: 'Login successful' });
} catch (err) {
res.status(500).json({ error: err.message });
} });
  • User-provided password is compared with the stored hash.
  • Login succeeds only if the comparison returns true.

7. Best Practices for Password Security

  1. Always Hash Passwords: Never store plain-text passwords. Use bcrypt or similar hashing libraries.
  2. Use Sufficient Salt Rounds: A value between 10–14 provides a good balance between security and performance.
  3. Avoid Rehashing: Only hash the password if it has been changed or is new.
  4. Implement Rate Limiting: Protect login routes to prevent brute-force attacks.
  5. Use HTTPS: Encrypt data in transit to prevent eavesdropping.
  6. Password Policies: Encourage strong passwords with a combination of letters, numbers, and symbols.
  7. Two-Factor Authentication: Add an extra layer of security beyond passwords.

8. Why Hashing is Critical

  • Database Breaches: If attackers gain access to your database, hashed passwords prevent them from obtaining actual passwords.
  • Reusing Passwords: Users often reuse passwords across websites. Hashing prevents compromising accounts on other platforms.
  • Irreversibility: bcrypt hashes cannot be reversed, unlike encryption methods that can be decrypted.
  • Salt Protection: Salting ensures that identical passwords do not produce identical hashes, making precomputed attacks infeasible.

9. Advanced Features of bcrypt

  • Adjustable Work Factor: Allows developers to increase hashing complexity as computing power improves.
  • Automatic Salting: Each hash gets a unique salt, protecting against rainbow table attacks.
  • Cross-Platform Support: bcrypt works consistently across Node.js, Python, PHP, and other platforms.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *