Web Application Security

Secure Password Hashing: bcrypt, argon2, and What Never to Use

By Domain India Security Team · DomainIndia Security EngineeringPublished 10 min read
Knowledge base article
Contents (12 sections)

Most password leaks become disasters because of weak hashing. The defence is cheap and well understood: store a slow, salted, one-way hash of each password, never the password itself. This guide covers what to use in 2026 (argon2id, bcrypt), what never to use (MD5, plain SHA-256), how to tune the settings, and how to upgrade old hashes without forcing every user to reset.

Key takeaways

Use argon2id, or bcrypt with a cost of at least 12 if argon2 is not available. In PHP that is password_hash($password, PASSWORD_ARGON2ID) and password_verify(); in Node.js, the argon2 package. Never use MD5, SHA-1 or a single round of SHA-256 for passwords. Aim for roughly 250-500 ms per hash on your server, and use password_needs_rehash() to upgrade old hashes when users next log in.

1. The principle

Never store a user's password. Store a slow, one-way derivation of it. At login, hash the submitted password the same way and compare. If your database leaks, the attacker gets hashes, and a slow hash makes guessing each password expensive.

Two properties make a hash suitable for passwords:

  1. Slowness. Each computation takes a noticeable fraction of a second, which makes large-scale guessing slow.
  2. Memory-hardness. Each computation needs a significant amount of RAM, which blunts GPU and custom-hardware attacks.

General-purpose hashes such as MD5, SHA-256 and SHA-3 are built for speed: a single modern GPU computes billions of them per second. That is the opposite of what passwords need. A salt (a random value per password) stops attackers cracking many hashes at once; modern password functions create and store it for you.

2. Which algorithm to use in 2026

AlgorithmVerdictNotes
argon2idUse thisWinner of the Password Hashing Competition; memory-hard; recommended by OWASP
scryptAcceptableMemory-hard and well tested; fewer convenient libraries than argon2
bcryptAcceptableAvailable everywhere; not memory-hard; use cost 12 or more; only the first 72 bytes of input count
PBKDF2-SHA-256Only for complianceAccepted by NIST; use at least 600,000 iterations
SHA-256 or SHA-512 (single round)Never for passwordsNot broken, but far too fast
MD5 or SHA-1NeverBroken and extremely fast
Your own schemeNeverHome-made crypto fails in ways you won't see

Argon2 has three variants: argon2d (strong against GPUs, weaker against side channels), argon2i (side-channel resistant) and argon2id, a hybrid of both. Use argon2id.

If you already use bcrypt at a sensible cost, migrate to argon2id when convenient with the method in section 4.

3. PHP: hash and verify

PHP's built-in password_hash() and password_verify() are maintained by the core team, generate the salt for you and store everything in one string.

php
// Registration or password change
$hash = password_hash($_POST['password'], PASSWORD_ARGON2ID);
// Store $hash in a VARCHAR(255) column

The result is self-contained: algorithm, parameters, salt and hash in one string, for example $argon2id$v=19$m=65536,t=4,p=1$.... There is no separate salt column to manage.

php
// Login
if (password_verify($_POST['password'], $user['password_hash'])) {
    // correct password: log the user in
} else {
    // wrong password: show one generic error message
}

password_verify() compares in constant time, so it leaks nothing about how close a guess was. Never compare hashes with ==.

PASSWORD_ARGON2ID exists from PHP 7.3 when PHP is built with Argon2 support. If defined('PASSWORD_ARGON2ID') returns false on your server, use PASSWORD_BCRYPT with ['cost' => 12].

4. Upgrading old hashes on login

Hardware gets faster, so you will want to raise the cost, or move from bcrypt or MD5 to argon2id. You can do it without a forced reset: rehash when the user next logs in successfully.

php
if (password_verify($password, $storedHash)) {
    if (password_needs_rehash($storedHash, PASSWORD_ARGON2ID)) {
        $newHash = password_hash($password, PASSWORD_ARGON2ID);
        // save $newHash for this user
    }
    // log the user in
}

For legacy MD5 or SHA-1 hashes, don't wait for every user to log in. Wrap them now: store password_hash(md5_value, PASSWORD_ARGON2ID), mark the row as wrapped, and at login check password_verify(md5($password), $wrapped), then rehash the plain password properly. Users who never return can be sent a reset link.

5. Node.js: argon2 or bcrypt

bash
npm install argon2
javascript
import argon2 from 'argon2';

// Registration
const hash = await argon2.hash(password, { type: argon2.argon2id });

// Login
if (await argon2.verify(hash, submittedPassword)) {
  // correct password
  if (argon2.needsRehash(hash, { memoryCost: 2 ** 16, timeCost: 3 })) {
    const newHash = await argon2.hash(submittedPassword, { type: argon2.argon2id, memoryCost: 2 ** 16, timeCost: 3 });
    // save newHash for this user
  }
}

If you need bcrypt, bcrypt (native, faster) and bcryptjs (pure JavaScript, no build step) share the same API:

javascript
import bcrypt from 'bcrypt';
const hash = await bcrypt.hash(password, 12);
const ok = await bcrypt.compare(submittedPassword, hash);

argon2 and bcrypt are native modules. If they fail to install on a host, bcryptjs avoids the build step at some speed cost.

6. Tuning the cost

Slower hashes resist attack better but make logins slower and use more server resources. Target roughly 250-500 ms per hash on your production server, and measure it there:

php
$start = microtime(true);
password_hash('test', PASSWORD_ARGON2ID, ['memory_cost' => 65536, 'time_cost' => 4, 'threads' => 1]);
echo round((microtime(true) - $start) * 1000) . " ms\n";
argon2id minimum (OWASP)
At least 19 MiB of memory (memory_cost 19456), time cost 2, parallelism 1. PHP's defaults (64 MiB, time 4, 1 thread) are stronger and a good start.
bcrypt cost
Each step doubles the work. 10 is the old baseline, 12 is a sensible minimum today, 14 suits high-value accounts if your server can afford it.

Memory cost is paid for every login attempt, so many logins at once need that memory many times over. Rate-limit your login form so an attacker can't use it to exhaust the server.

7. Pepper: an optional extra layer

A pepper is a secret stored outside the database, for example in an environment variable (see Environment variables and secrets management), and applied before hashing, ideally as an HMAC of the password. If only the database leaks, the attacker can't test guesses without it. The catch: a lost or changed pepper makes every existing hash unusable, so many applications skip it and rely on a strong hash plus other defences.

8. Password reset done right

Forgot-password flows are where many systems fail.

  1. Generate a random token.
    Use at least 32 random bytes from a secure source, such as bin2hex(random_bytes(32)) in PHP.
  2. Store only its hash.
    Save a SHA-256 hash of the token with an expiry time, never the token itself. A fast hash is fine here because the token is long and random.
  3. Email the link.
    Put the plain token in a link to your reset page.
  4. Expire it quickly.
    15 to 60 minutes, and single use.
  5. Verify and replace.
    Hash the token from the link, compare it with the stored hash, then let the user set a new password and delete the token.
  6. Log out other sessions
    after a successful reset.

Respond the same way whether or not the email exists, so the form can't be used to discover accounts, and rate-limit reset requests by IP and by email. Set Referrer-Policy: no-referrer on the reset page and avoid third-party scripts there, so the token in the URL does not leak.

9. Common mistakes

Fast hashes for passwords
MD5, SHA-1 or one round of SHA-256. Migrate with the wrap-and-rehash method in section 4.
Comparing with ==
Use password_verify, argon2.verify or bcrypt.compare, which compare safely.
Passwords in logs
Debug dumps, stack traces and request logs can capture passwords. Scrub them.
Low cost settings
bcrypt below 12 or argon2 below OWASP's minimums. Measure and raise.
Unlimited login attempts
Rate-limit by IP and account, and add two-factor login for admin accounts.

10. After a breach

  1. End all sessions and rotate session or JWT signing secrets.
  2. Force a password reset for affected accounts at their next login.
  3. Find and fix the cause, such as SQL injection, an exposed backup file or a phished admin account.
  4. Notify. In India, report cyber security incidents to CERT-In within its deadline (six hours under its 2022 directions), and follow the Digital Personal Data Protection Act, 2023 and its Rules on informing the Data Protection Board of India and affected people. This is general information, not legal advice; check current rules for your sector.
  5. Watch for credential stuffing, as attackers try leaked passwords on other sites and on yours.

11. Running this on Domain India

  • cPanel shared hosting: PASSWORD_ARGON2ID works on every PHP version we offer there, from 8.1 to 8.5 (measured 24 September 2026). The default PHP version is 8.3.
  • DirectAdmin shared hosting: PASSWORD_ARGON2ID works on PHP 7.4 and later. The older PHP 5.6, 7.2 and 7.3 builds on the DirectAdmin server lack it; move your site to a current PHP version rather than falling back to bcrypt.
  • Node.js apps: the App Platform auto-detects Node.js apps and runs them for you; see getting started with the App Platform. A VPS gives you full control of the runtime.

For the wider picture, see the security checklist if your website has been hacked.

Frequently asked questions

What is the best password hashing algorithm in 2026?

argon2id. It is memory-hard, tunable and recommended by OWASP. If it is not available, bcrypt with a cost of at least 12 is an acceptable choice.

Is SHA-256 safe for storing passwords?

No. SHA-256 is not broken, but it is designed to be fast, so attackers can test billions of guesses per second. Use a password hashing function such as argon2id or bcrypt instead.

Do I need to store a salt separately?

No. PHP's password_hash and the Node.js argon2 and bcrypt libraries generate a random salt and store it inside the hash string, together with the algorithm and parameters.

How do I move users from MD5 or bcrypt to argon2id?

Rehash on login: after password_verify succeeds, call password_needs_rehash and save a new argon2id hash. For MD5, wrap the existing hashes in argon2id straight away so no plain MD5 hash remains in the database.

Does Domain India hosting support argon2id in PHP?

Yes, on current PHP versions. On cPanel hosting, PASSWORD_ARGON2ID works on every PHP version offered, 8.1 to 8.5. On DirectAdmin hosting it works on PHP 7.4 and later; the older 5.6, 7.2 and 7.3 versions lack it.

Should I hash passwords in the browser as well?

Generally no. HTTPS already protects the password in transit, and a client-side hash simply becomes the password. Use HTTPS and hash on the server.

How long should passwords be?

Allow long passwords and passphrases, and don't force composition rules. NIST's current guidance asks for at least 15 characters when a password is the only login factor, and at least 8 when two-factor login is also used.

Ready to build securely? Host PHP apps on cPanel hosting, run Node.js on the App Platform, or open a support ticket if a PHP function you need seems unavailable on your plan.

Host your app on current PHP

cPanel hosting offers PHP 8.1 to 8.5, and argon2id password hashing works on every version.

See cPanel hosting

Was this article helpful?

Your answer helps us decide what to improve next.

Still need help? Open a support ticket and our team will reply.

Prefer an app? Add this site to your home screen.Get the app
Password Hashing in 2026: argon2id vs bcrypt | Domain India