Password Hashing

⭐ Interview Importance: LOW
⏱️ Revision Time: 13 min

Password Hashing is a one-way cryptographic process that converts a user’s plain-text password into an unreadable string. It ensures that even if an attacker steals your database, they cannot easily discover the users’ real passwords.

Overview

Never, ever store plain-text passwords in a database.

If you use standard encryption (which is two-way), an attacker who steals the database might also steal the decryption key, giving them access to all passwords.

Hashing is a one-way mathematical function. You can hash “password123” into xy$z98..., but you cannot reverse xy$z98... back into “password123”. When a user logs in, you hash the password they just typed and compare it to the hash stored in the database.

Key Concepts

  • Salting: Attackers use “Rainbow Tables” (massive pre-computed lists of common passwords and their hashes) to crack databases. A “Salt” is a random string added to the password before hashing, which completely neutralizes Rainbow Tables.
  • Work Factor (Cost): Fast hashing algorithms (like MD5 or SHA-256) are terrible for passwords because an attacker can guess billions of passwords per second on modern GPUs. Password hashing algorithms (like bcrypt or Argon2) are intentionally slow. You can configure a “Work Factor” to ensure it takes ~250ms to hash a password, making brute-forcing millions of passwords mathematically impossible.

Code Examples

1. Using bcrypt in NestJS

bcrypt is the industry standard for password hashing. It automatically handles salt generation and incorporates the salt directly into the resulting hash string.

npm install bcrypt
npm install @types/bcrypt -D
import { Injectable } from '@nestjs/common';
import * as bcrypt from 'bcrypt';

@Injectable()
export class PasswordService {
  // 10 is the standard cost factor. 
  // It determines how many rounds of hashing to perform.
  // Higher = slower and more secure, but uses more CPU on your server.
  private readonly saltOrRounds = 10; 

  async hashPassword(plainTextPassword: string): Promise<string> {
    // This automatically generates a salt and hashes the password
    const hash = await bcrypt.hash(plainTextPassword, this.saltOrRounds);
    return hash;
  }

  async comparePassword(plainText: string, storedHash: string): Promise<boolean> {
    // Compares the typed password against the stored hash safely
    const isMatch = await bcrypt.compare(plainText, storedHash);
    return isMatch;
  }
}

2. Integrating Hashing into TypeORM

You should hash the password immediately before saving it to the database. A great place to do this is inside a TypeORM Entity Listener.

import { Entity, Column, PrimaryGeneratedColumn, BeforeInsert, BeforeUpdate } from 'typeorm';
import * as bcrypt from 'bcrypt';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  email: string;

  // IMPORTANT: Never return the password in standard JSON responses!
  // Setting select: false prevents TypeORM from fetching it unless explicitly requested.
  @Column({ select: false }) 
  password_hash: string;

  @BeforeInsert()
  @BeforeUpdate()
  async hashPassword() {
    // Only hash if the password was modified (to avoid double-hashing on updates)
    if (this.password_hash && !this.password_hash.startsWith('$2b$')) {
      const salt = await bcrypt.genSalt(10);
      this.password_hash = await bcrypt.hash(this.password_hash, salt);
    }
  }
}

Best Practices

  • Never use MD5 or SHA-256 for passwords: These are designed to be extremely fast. Use bcrypt, scrypt, or Argon2.
  • Argon2 is the modern standard: While bcrypt is still secure and widely used, Argon2 won the Password Hashing Competition. It is designed to be highly resistant to GPU cracking and ASIC hardware attacks by requiring a massive amount of RAM to compute the hash. If starting a new project today, prefer the argon2 npm package.
  • Implement Rate Limiting: Hashing is intentionally CPU-intensive. If an attacker spams your /login endpoint with 10,000 requests a second, your NestJS server will consume 100% CPU trying to hash all those passwords, resulting in a Denial of Service (DoS). You must strictly rate-limit authentication endpoints.