Securityintermediate11 min read

Password Security Best Practices for Developers

Learn how to store passwords securely, which hashing algorithms to use, why salting matters, and what modern password policy guidance recommends.

How Password Breaches Happen

The majority of credential breaches follow one of three paths:

  1. Database dumps — an attacker gains read access to the database (via SQL injection, misconfigured cloud storage, or a compromised server) and downloads the user table including password fields
  2. Credential stuffing — reused passwords from one breach are automatically tested against other services; if users reuse passwords, a breach on site A compromises their account on site B
  3. Phishing — users are tricked into entering credentials on a fake site

As a developer, you can't prevent users from reusing passwords or falling for phishing — but you absolutely control how passwords are stored, which determines the damage scope if your database is stolen.

Why You Must Never Store Plaintext Passwords

Storing passwords in plaintext means any database access — by an attacker, a disgruntled employee, or an accidental log — immediately exposes every user's password.

This matters beyond your own site: most users reuse passwords, so exposing one site's database can compromise the user's email, banking, and other accounts.

Legal and compliance obligations reinforce this: - GDPR — passwords are personal data and must be protected with appropriate technical measures - SOC 2 — auditors specifically check for proper password hashing - PCI DSS — requires strong cryptography for authentication data

The minimum requirement is one-way hashing — but not all hash functions are appropriate for passwords.

The Right Algorithms: bcrypt, scrypt, Argon2

General-purpose hash functions (SHA-256, MD5) are designed to be fast — attackers can test billions of guesses per second on a GPU. Password hashing needs to be deliberately slow.

  • bcrypt — designed in 1999 specifically for passwords. Uses a configurable cost factor (work factor) that you increase as hardware improves. Widely supported. Output includes the salt. Cost factor 12 is the current baseline for new systems.
  • scrypt — memory-hard: requires large amounts of RAM, not just CPU cycles. Defeats GPU and ASIC attacks. Used by some cryptocurrencies.
  • Argon2id — winner of the 2015 Password Hashing Competition. Combines time-hardness and memory-hardness. OWASP recommends Argon2id for all new systems.

All three automatically handle salting.

// Node.js — Argon2 (recommended for new systems)
const argon2 = require('argon2');

// Hash
const hash = await argon2.hash(password, {
  type: argon2.argon2id,
  memoryCost: 65536,  // 64 MB
  timeCost: 3,        // 3 iterations
  parallelism: 4,
});

// Verify
const valid = await argon2.verify(hash, password);

// Node.js — bcrypt (battle-tested, widely available)
const bcrypt = require('bcrypt');
const hash = await bcrypt.hash(password, 12);  // cost factor 12
const valid = await bcrypt.compare(password, hash);

Salting: Why Every Hash Must Be Unique

A salt is a random value generated uniquely for each password and combined with it before hashing. This ensures that two users with the same password produce different hashes.

Without salts, attackers can use rainbow tables — pre-computed tables mapping common passwords to their hashes — to reverse millions of hashes instantly.

With salts, the attacker must crack each hash individually, even if they're identical passwords.

All modern password hashing libraries (bcrypt, scrypt, Argon2) generate and embed the salt automatically in the output hash string. You don't need to store the salt separately — it's encoded in the hash. Never implement your own salting.

Password Policy: Entropy Over Complexity Rules

NIST SP 800-63B (the authoritative US government password guidance) updated in 2024 recommends moving away from traditional complexity rules toward length and breach-checking:

  • Require a minimum of 15 characters (was 8 in older guidance)
  • Allow maximum of at least 64 characters — don't truncate
  • Check against breach databases — reject passwords found in known breach lists (HaveIBeenPwned API)
  • Allow paste — blocking paste prevents password manager usage, reducing security
  • Remove periodic forced resets — forcing regular changes causes users to choose predictable patterns (`P@ssword1` → `P@ssword2`)
  • No mandatory complexity rules — length provides more entropy than mandatory symbols

Entropy formula: `log2(charset_size ^ length)` — 16 lowercase letters gives ~75 bits; 8 characters with mixed case+symbols gives ~52 bits.

Generating Test Passwords with DevForge

The DevForge Password Generator creates high-entropy passwords with configurable length and character sets — useful when seeding test user accounts, generating service account credentials, or demonstrating strong password requirements to stakeholders.

The Hash Generator lets you compute SHA-256 and other hashes from a known input, which is useful during integration testing to verify that your application is producing and storing the correct hash format without needing a fully running auth stack.

Try it on DevForge

Free online tools related to this tutorial — no signup required.

Related Tutorials