← All posts
secure password hashingpassword storagebcryptArgon2security best practices

Secure Password Hashing: How to Store Passwords Right

O

OverMCP Team

TL;DR: To securely store passwords, never use plain text or outdated hashes like MD5/SHA-1. Use a modern, slow, salted hashing algorithm such as Argon2id (preferred) or bcrypt with a cost factor of at least 10. Always hash on the server, use a unique random salt per password, and never roll your own crypto.

Why Secure Password Hashing Matters

Password breaches are the most common cause of account takeovers. If an attacker gains access to your database, they will immediately try to extract passwords. Without proper secure password hashing, they can easily reverse or crack the hashes, compromising every user account. Even if you use other security measures like encryption at rest, the database itself can be exfiltrated via SQL injection, backup leaks, or insider threats. Hashing is your last line of defense.

What Makes a Hashing Algorithm "Secure"?

A secure password hashing algorithm must be:

  • Slow: It should take significant time to compute (milliseconds per hash) to slow down brute-force attacks.
  • Salted: Each password gets a unique random salt, so identical passwords produce different hashes, and precomputed rainbow tables are useless.
  • Memory-hard: It should require a lot of memory to prevent specialized hardware (GPUs/ASICs) from accelerating attacks.
  • Resistant to side-channel attacks: The implementation should not leak timing information.
  • The following algorithms meet these criteria:

    | Algorithm | Strength | Memory-hard | Recommended Use |

    |-----------|----------|-------------|-----------------|

    | Argon2id (v1.3+) | Very strong | Yes | Best choice for new applications |

    | bcrypt | Strong | No (but CPU-intensive) | Good, widely supported |

    | scrypt | Strong | Yes | Good, but less common than bcrypt |

    | PBKDF2 | Weak | No (unless using HMAC-SHA256 with high iterations) | Legacy only, avoid if possible |

    Never use: MD5, SHA-1, SHA-256 (fast hashes), or any custom algorithm.

    How to Hash Passwords: Step-by-Step

    Using Argon2id (Recommended)

    Argon2id is the winner of the Password Hashing Competition and the most secure option. Here's how to use it in Python with the argon2-cffi library:

    from argon2 import PasswordHasher
    
    ph = PasswordHasher(
        time_cost=3,          # number of iterations (default 3)
        memory_cost=65536,    # 64 MB memory usage
        parallelism=4,        # number of threads (default 4)
        hash_len=32,          # length of the hash (32 bytes = 256 bits)
        salt_len=16           # random salt length (16 bytes = 128 bits)
    )
    
    # Hashing
    hash = ph.hash("my_secret_password")
    print(hash)  # $argon2id$v=19$m=65536,t=3,p=4$...
    
    # Verification
    try:
        ph.verify(hash, "my_secret_password")
        print("Password matches")
    except:
        print("Password incorrect")

    Important: Argon2id automatically generates a unique salt per password and embeds it in the output string. The verify function extracts the salt and parameters from the stored hash.

    Using bcrypt (Fallback)

    If Argon2id is not available (e.g., older environments), bcrypt is a solid alternative. Use a cost factor of 10 (2^10 = 1024 iterations) or higher:

    import bcrypt
    
    # Generate salt (automatically includes cost factor)
    salt = bcrypt.gensalt(rounds=12)  # cost factor 12
    hashed = bcrypt.hashpw(b"my_secret_password", salt)
    
    # Verification
    if bcrypt.checkpw(b"my_secret_password", hashed):
        print("Password matches")

    Cost factor: Start with 10 and increase until hash computation takes ~0.5-1 second on your server. This balances security and user experience.

    Using Node.js (bcrypt)

    const bcrypt = require('bcrypt');
    const saltRounds = 12;
    
    // Hashing
    const hash = await bcrypt.hash('my_secret_password', saltRounds);
    
    // Verification
    const match = await bcrypt.compare('my_secret_password', hash);
    if (match) {
      console.log('Password matches');
    }

    Common Mistakes to Avoid

  • Using fast hashes like SHA-256: A single SHA-256 hash takes microseconds to compute, making it trivial for attackers to try billions of passwords per second.
  • Omitting salt: Without a unique salt, identical passwords produce identical hashes, and attackers can use precomputed rainbow tables.
  • Rolling your own algorithm: Custom cryptographic code is almost certainly flawed. Use well-vetted libraries.
  • Using encryption instead of hashing: Encryption is reversible if the key is compromised. Hashing is one-way.
  • Storing plaintext or reversible transforms: Never store passwords in plaintext, base64, or XOR-encoded forms.
  • How OverMCP Helps

    When you're building apps quickly with AI coding tools, it's easy to inadvertently use weak hashing or expose passwords. OverMCP automatically scans your code for insecure password storage patterns, such as missing salts, low bcrypt rounds, or even plaintext passwords. It integrates into your CI/CD pipeline and alerts you before deployment. Run a free scan at overmcp.com to catch these issues early.

    Future-Proofing Your Password Storage

  • Use passwordless options: Consider WebAuthn, magic links, or OAuth to eliminate password storage entirely.
  • Monitor for breaches: Use services like Have I Been Pwned to check if your users' passwords appear in known leaks.
  • Regularly update cost factors: As hardware improves, increase the time/memory parameters accordingly. Store the algorithm version in your database to support rolling upgrades.
  • FAQ

    What is the best algorithm for secure password hashing?

    Argon2id is the current gold standard. It is memory-hard, resistant to GPU/ASIC attacks, and has no known vulnerabilities. Use it whenever possible.

    How do I store the salt and hash in the database?

    Modern libraries like argon2-cffi and bcrypt encode the salt, algorithm parameters, and hash into a single string (e.g., $argon2id$v=19$m=65536,t=3,p=4$<salt>$<hash>). Store this string directly in a VARCHAR(255) column. No separate salt column is needed.

    Should I use a pepper (secret salt) in addition to the salt?

    A pepper (a secret stored outside the database, e.g., in an environment variable) adds an extra layer of security if the database is compromised but the pepper is not. However, it is not a replacement for proper hashing. Use it only if you have a secure key management system; otherwise, it may give a false sense of security.

    Is your app secure?

    Free scan in 30 seconds. No signup needed.

    Scan My App Free