Menu

PHP password_hash() and password_verify(): Store Passwords

Hash a password with password_hash($password, PASSWORD_DEFAULT), store the 60-character result, and check a login with password_verify($password, $hash). Learn why the hash changes every time, cost, password_needs_rehash, Argon2id, and why md5 and sha1 are wrong for passwords.

This page includes runnable editors - edit, run, and see output instantly.

Store passwords with password_hash($password, PASSWORD_DEFAULT) and check a login with password_verify($password, $hash), which returns true or false. Never store the password itself, and never use md5() or sha1() for it.

Run it twice: the hash is different every time, and both versions verify. The same goes for every hash on this page, so your output will not match anyone else's.

Log in with password_verify

A login checks the submitted password against the hash saved when the account was created. This block "registers" one user and then shows a login form; try the right password (hunter2), then a wrong one.

The error message is the same for an unknown email and a wrong password. Saying which one was wrong tells an attacker which emails have accounts. After a successful login, a real site stores the user id in the session, as shown on the sessions page.

What is inside the hash

A bcrypt hash is one string that holds everything password_verify() needs: the algorithm, the cost and the salt, followed by the hash itself. password_get_info() reads the first parts back.

Because the salt is stored in the hash, you need only one database column, and two users with the same password still get different hashes. Make that column VARCHAR(255): bcrypt is 60 characters, but PASSWORD_DEFAULT is allowed to change to an algorithm with longer output.

Choose the cost

The cost is how slow each hash is on purpose, and each step up doubles the work. PHP 8.3 uses cost 10 for bcrypt (PHP 8.4 raised the default to 12). A login should take roughly 100 to 300 ms of hashing; time it on your own server:

The numbers depend on the machine, but each line should be about twice the one before it. Higher is safer against cracking and slower for every login, so pick the highest cost your login page can afford.

Upgrade old hashes with password_needs_rehash

When you raise the cost or PHP changes PASSWORD_DEFAULT, existing hashes keep working, and password_needs_rehash() tells you which ones are out of date. The only moment you have the plain password is at login, so that is where you upgrade:

The $2y$10$ prefix becomes $2y$12$, and on the next login password_needs_rehash() returns false.

Never use md5 or sha1 for passwords

md5() and sha1() are fast general hashes with no salt. Fast is exactly wrong for passwords: an attacker with a leaked table can try billions of guesses per second on a GPU, and an unsalted hash of a common password is already in public lookup tables. Compare the speed:

In the time password_hash() makes one hash, md5 makes hundreds of thousands, so every guess a cracker makes is that many times cheaper. Search for 5f4dcc3b5aa765d61d8327deb882cf99 and you will find the word "password" immediately.

If you inherit a table of md5 hashes, you cannot convert them directly. Wrap them instead: store password_hash($oldMd5) and verify with password_verify(md5($input), $stored), then replace each one with a normal hash when that user next logs in.

Argon2id and the 72-byte limit

PASSWORD_ARGON2ID uses more memory per hash, which makes GPU cracking much more expensive. It is available when PHP is built with libargon2 (most Linux packages are); check with defined('PASSWORD_ARGON2ID').

The last line prints bool(true): two different passwords match because bcrypt ignores everything after byte 72. Real passwords rarely reach that, but passphrases in Japanese or other multibyte scripts reach 72 bytes at about 24 characters. If you allow very long passwords, use Argon2id, which has no such limit. password_verify() works with either algorithm, so you can switch with password_needs_rehash() as above.

Frequently Asked Questions

Why does password_hash give a different result every time?

Each call generates a new random salt and stores it inside the hash, so the same password gives a different string every time. That is intended: password_verify() reads the salt back out of the stored hash, so it still returns true for the right password.

How long should the password column be for password_hash?

A bcrypt hash from PASSWORD_DEFAULT is 60 characters today, but the default algorithm can change in a future PHP version. Use VARCHAR(255) so a longer hash fits without a migration.

Why is password_verify always returning false?

The usual causes are a truncated hash (a VARCHAR(50) column cuts the 60-character hash), passing a fresh password_hash() of something as the second argument instead of the hash you stored, comparing against a hash made with md5(), or extra whitespace in the stored value. Check that strlen($hash) is 60 and pass the plain password as the first argument.

Can I decrypt a password_hash value in PHP?

No. A hash is one-way: there is no function that turns it back into the password. To check a password you hash the attempt and compare, which is what password_verify() does. For a forgotten password, send a reset link instead of the old password.

Should I use PASSWORD_DEFAULT, PASSWORD_BCRYPT or PASSWORD_ARGON2ID?

PASSWORD_DEFAULT is the usual choice: it is bcrypt today and PHP may move it to a stronger algorithm later, which password_needs_rehash() lets you adopt gradually. PASSWORD_ARGON2ID is stronger against GPU cracking and is fine to use when your PHP build includes it.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED