Next.js starter your AI actually understands. Ship internal tools in days not weeks. Pre-order $199 $499 → [Get it now]

How to hash passwords in JavaScript

Storing passwords in plain text is one of the most dangerous security mistakes in modern web development, yet it remains surprisingly common. With over 25 years of experience in software development and as the creator of CoreUI, I’ve implemented secure password handling in countless production applications. The most reliable approach is to use bcrypt for server-side hashing or the Web Crypto API for client-side operations. Both methods ensure passwords are cryptographically hashed with salt and proper iterations, making them virtually impossible to reverse.

Use bcrypt with at least 10 salt rounds to hash passwords securely on the server.

const bcrypt = require('bcrypt')

const hashPassword = async (password) => {
  const saltRounds = 12
  const hash = await bcrypt.hash(password, saltRounds)
  return hash
}

const verifyPassword = async (password, hash) => {
  const match = await bcrypt.compare(password, hash)
  return match
}

// Usage
const password = 'userPassword123'
const hash = await hashPassword(password)
console.log('Hash:', hash)

const isValid = await verifyPassword('userPassword123', hash)
console.log('Valid:', isValid)

This code uses bcrypt to hash a password with 12 salt rounds, which determines the computational cost of hashing. The hash() function automatically generates a salt and combines it with the password hash. The compare() function verifies a password against the stored hash without needing to extract the salt manually. Higher salt rounds mean more security but slower performance, so 12 rounds is a good balance for modern applications.

Understanding Hashing vs Encryption

Hashing is fundamentally different from encryption. Encryption is reversible with the correct key, but hashing is a one-way function that cannot be reversed. This makes hashing ideal for passwords because even if your database is compromised, attackers cannot retrieve the original passwords.

// This is WRONG - encryption is reversible
const crypto = require('crypto')

const encryptPassword = (password, key) => {
  const cipher = crypto.createCipher('aes-256-cbc', key)
  let encrypted = cipher.update(password, 'utf8', 'hex')
  encrypted += cipher.final('hex')
  return encrypted
}

// Never store passwords like this
// An attacker with the key can decrypt all passwords

Encryption allows decryption with the proper key, meaning passwords can be recovered. Hashing produces a fixed-length output that cannot be reversed, so passwords remain secure even if the hash is exposed. Always use hashing for passwords, never encryption.

Using Salt and Iterations

Salt is random data added to each password before hashing to ensure identical passwords produce different hashes. Iterations (also called work factor or cost) determine how many times the hashing algorithm runs, making brute-force attacks exponentially slower.

const bcrypt = require('bcrypt')

const hashWithSalt = async (password) => {
  // bcrypt automatically generates and includes salt
  const saltRounds = 12 // 2^12 iterations
  const hash = await bcrypt.hash(password, saltRounds)

  // The hash contains: algorithm, cost, salt, and hash
  // Format: $2b$12$saltvalueHashvalue
  return hash
}

// Even identical passwords get different hashes
const hash1 = await hashWithSalt('password123')
const hash2 = await hashWithSalt('password123')

console.log(hash1)
console.log(hash2)
console.log('Are they different?', hash1 !== hash2) // true

Bcrypt embeds the salt directly into the hash string, so you don’t need to store it separately. Each hash contains the algorithm version, cost factor, salt, and the actual hash value. This design ensures that every password hash is unique, even for identical passwords, preventing rainbow table attacks.

Using Web Crypto API for Client-Side

While password hashing should primarily happen on the server, the Web Crypto API provides browser-based hashing for specific use cases like client-side validation or generating derived keys.

const hashPasswordBrowser = async (password) => {
  const encoder = new TextEncoder()
  const data = encoder.encode(password)

  const hashBuffer = await crypto.subtle.digest('SHA-256', data)

  const hashArray = Array.from(new Uint8Array(hashBuffer))
  const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('')

  return hashHex
}

// Usage
const clientHash = await hashPasswordBrowser('myPassword')
console.log('SHA-256 hash:', clientHash)

This code encodes the password as UTF-8 bytes, hashes it with SHA-256, and converts the result to a hexadecimal string. However, this approach lacks salt and iterations, making it unsuitable for storing passwords. Use it only for non-critical operations like generating unique identifiers or client-side cache keys.

Implementing PBKDF2 for Custom Hashing

PBKDF2 (Password-Based Key Derivation Function 2) is a standard algorithm that applies a hash function multiple times with salt, making it more secure than simple hashing.

const crypto = require('crypto')

const hashWithPBKDF2 = (password, salt, iterations = 100000) => {
  return new Promise((resolve, reject) => {
    crypto.pbkdf2(password, salt, iterations, 64, 'sha512', (err, derivedKey) => {
      if (err) reject(err)
      else resolve(derivedKey.toString('hex'))
    })
  })
}

const generateSalt = () => {
  return crypto.randomBytes(16).toString('hex')
}

// Usage
const salt = generateSalt()
const hash = await hashWithPBKDF2('userPassword', salt)

console.log('Salt:', salt)
console.log('Hash:', hash)

PBKDF2 requires you to store the salt separately along with the hash. The iterations parameter controls how many times the hash function runs, with 100,000 being a recommended minimum for modern security standards. The algorithm produces a 64-byte derived key, which is then converted to hexadecimal for storage.

Security Best Practices

Never implement your own hashing algorithm. Always use established, peer-reviewed algorithms like bcrypt, Argon2, or PBKDF2. Never send plain-text passwords over HTTP—always use HTTPS. Never log passwords, even in development. And never trust client-side hashing alone; always re-hash passwords on the server.

const bcrypt = require('bcrypt')

const securePasswordFlow = async (password) => {
  // 1. Validate password strength first
  if (password.length < 8) {
    throw new Error('Password must be at least 8 characters')
  }

  // 2. Hash on the server, never trust client hashing
  const hash = await bcrypt.hash(password, 12)

  // 3. Store only the hash, never the password
  // await db.users.insert({ passwordHash: hash })

  return hash
}

const securePasswordVerification = async (inputPassword, storedHash) => {
  // Use constant-time comparison
  const isValid = await bcrypt.compare(inputPassword, storedHash)

  // Never reveal why authentication failed
  if (!isValid) {
    throw new Error('Invalid credentials')
  }

  return true
}

This flow validates password strength, hashes only on the server, and uses constant-time comparison to prevent timing attacks. Bcrypt’s compare() function is designed to take the same amount of time whether the password matches or not, preventing attackers from using response times to guess passwords.

Handling Password Updates

When users change passwords, always verify their current password before allowing them to set a new one. This prevents unauthorized password changes if someone gains temporary access to an authenticated session.

const updatePassword = async (userId, oldPassword, newPassword) => {
  // 1. Retrieve current hash from database
  const user = await db.users.findById(userId)

  // 2. Verify old password
  const isValid = await bcrypt.compare(oldPassword, user.passwordHash)
  if (!isValid) {
    throw new Error('Current password is incorrect')
  }

  // 3. Hash new password
  const newHash = await bcrypt.hash(newPassword, 12)

  // 4. Update database
  await db.users.update(userId, { passwordHash: newHash })

  return true
}

// Usage
await updatePassword('user123', 'oldPass', 'newSecurePass')

This function ensures the user knows their current password before changing it. The new password is hashed with fresh salt rounds, and only the hash is stored. Never allow password changes without verifying the old password, as this would allow session hijacking to lead to permanent account compromise.

Migrating from Weak Hashing

If you inherit a system using weak hashing like MD5 or SHA-1, you need a migration strategy that doesn’t require users to reset passwords immediately.

const migrateHash = async (userId, plainPassword, oldHash) => {
  // 1. Verify against old weak hash
  const crypto = require('crypto')
  const oldHashCheck = crypto.createHash('md5').update(plainPassword).digest('hex')

  if (oldHashCheck !== oldHash) {
    throw new Error('Invalid password')
  }

  // 2. Upgrade to bcrypt on successful login
  const newHash = await bcrypt.hash(plainPassword, 12)
  await db.users.update(userId, { passwordHash: newHash, hashType: 'bcrypt' })

  return true
}

// Gradually migrate users to strong hashing
const authenticateAndMigrate = async (userId, password) => {
  const user = await db.users.findById(userId)

  if (user.hashType === 'md5') {
    return await migrateHash(userId, password, user.passwordHash)
  } else {
    return await bcrypt.compare(password, user.passwordHash)
  }
}

This approach allows users to continue logging in with their existing passwords while transparently upgrading their hashes to bcrypt. Once authenticated with the weak hash, the system immediately re-hashes the password with bcrypt and updates the database. Over time, all active users will be migrated to strong hashing without forced password resets.

Installing bcrypt in Node.js

Before using bcrypt, you need to install it via npm. Bcrypt is a native module, so it requires compilation during installation.

npm install bcrypt

For environments where native modules are problematic, consider using bcryptjs, a pure JavaScript implementation that works everywhere but is slower.

npm install bcryptjs
const bcrypt = require('bcryptjs')

// Usage is identical to native bcrypt
const hash = await bcrypt.hash('password', 12)
const isValid = await bcrypt.compare('password', hash)

The API is identical between bcrypt and bcryptjs, so you can switch between them without changing your code. Native bcrypt offers better performance, while bcryptjs provides better compatibility with serverless environments and platforms with limited native module support.

Best Practice Note

This is the same approach we use in CoreUI-based authentication systems to ensure enterprise-grade security. For additional security layers, consider implementing rate limiting to prevent brute-force attacks and generating secure random numbers for salt values. Never implement custom hashing algorithms—stick to battle-tested libraries like bcrypt, Argon2, or scrypt. When building authentication flows with CoreUI components, always perform password hashing on the server and use HTTPS to protect credentials in transit.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author