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.
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:
- Slowness. Each computation takes a noticeable fraction of a second, which makes large-scale guessing slow.
- 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
| Algorithm | Verdict | Notes |
|---|---|---|
| argon2id | Use this | Winner of the Password Hashing Competition; memory-hard; recommended by OWASP |
| scrypt | Acceptable | Memory-hard and well tested; fewer convenient libraries than argon2 |
| bcrypt | Acceptable | Available everywhere; not memory-hard; use cost 12 or more; only the first 72 bytes of input count |
| PBKDF2-SHA-256 | Only for compliance | Accepted by NIST; use at least 600,000 iterations |
| SHA-256 or SHA-512 (single round) | Never for passwords | Not broken, but far too fast |
| MD5 or SHA-1 | Never | Broken and extremely fast |
| Your own scheme | Never | Home-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.
// Registration or password change
$hash = password_hash($_POST['password'], PASSWORD_ARGON2ID);
// Store $hash in a VARCHAR(255) columnThe 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.
// 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.
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
npm install argon2import 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:
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:
$start = microtime(true);
password_hash('test', PASSWORD_ARGON2ID, ['memory_cost' => 65536, 'time_cost' => 4, 'threads' => 1]);
echo round((microtime(true) - $start) * 1000) . " ms\n";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.
- Generate a random token.Use at least 32 random bytes from a secure source, such as
bin2hex(random_bytes(32))in PHP. - 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.
- Email the link.Put the plain token in a link to your reset page.
- Expire it quickly.15 to 60 minutes, and single use.
- 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.
- Log out other sessionsafter 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
10. After a breach
- End all sessions and rotate session or JWT signing secrets.
- Force a password reset for affected accounts at their next login.
- Find and fix the cause, such as SQL injection, an exposed backup file or a phished admin account.
- 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.
- 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_ARGON2IDworks 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_ARGON2IDworks 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.
cPanel hosting offers PHP 8.1 to 8.5, and argon2id password hashing works on every version.
See cPanel hosting