password_hash() is the function for hashing passwords. It creates a salt, runs a slow algorithm and returns one string that contains everything needed to verify the password later.
<?php
$hash = password_hash("my secret password", PASSWORD_DEFAULT);
echo strlen($hash) . "\n";
echo substr($hash, 0, 7);
60
$2y$10$
The hash is 60 characters here, and it starts with $2y$10$:
$2y$means the bcrypt algorithm.10is the cost (how much work it takes).- The rest is the salt and the hash.
Every call gives a different result
<?php
$a = password_hash("secret", PASSWORD_DEFAULT);
$b = password_hash("secret", PASSWORD_DEFAULT);
var_dump($a === $b);
var_dump(password_verify("secret", $a) && password_verify("secret", $b));
bool(false)
bool(true)
The salt is random, so the strings differ, but each one verifies the same password. You cannot compare hashes with ===. Use password_verify().
The algorithms
| Constant | Algorithm | Notes |
|---|---|---|
PASSWORD_DEFAULT | The best one PHP recommends | Currently bcrypt. PHP may change it in a future version |
PASSWORD_BCRYPT | bcrypt | Widely used, available everywhere |
PASSWORD_ARGON2I, PASSWORD_ARGON2ID | Argon2 | Modern and memory-hard. Needs PHP built with Argon2 support |
Use PASSWORD_DEFAULT unless you have a reason. It is the safe choice, and PHP can upgrade it for you over time.
The cost option
A higher cost means a slower hash, which is safer but uses more of your server’s time.
<?php
$hash = password_hash("secret", PASSWORD_BCRYPT, ["cost" => 12]);
echo substr($hash, 0, 7);
$2y$12$
Choose a cost that takes roughly 50 to 250 milliseconds on your server. A common method is to test increasing values and pick the largest one that stays fast enough for a login.
<?php
foreach ([10, 11, 12] as $cost) {
$start = microtime(true);
password_hash("test", PASSWORD_BCRYPT, ["cost" => $cost]);
$time = round((microtime(true) - $start) * 1000);
echo "cost $cost takes about " . ($time > 0 ? "a measurable time" : "no time") . "\n";
}
cost 10 takes about a measurable time
cost 11 takes about a measurable time
cost 12 takes about a measurable time
(The real numbers depend on your hardware. Print $time yourself to see them. Each cost step roughly doubles the time.)
Inspect a hash
<?php
$info = password_get_info(password_hash("x", PASSWORD_BCRYPT, ["cost" => 11]));
echo $info["algoName"] . ", cost " . $info["options"]["cost"];
bcrypt, cost 11
The 72-byte limit of bcrypt
bcrypt only uses the first 72 bytes of the password. Anything longer is ignored.
<?php
$long = str_repeat("a", 72);
$hash = password_hash($long, PASSWORD_BCRYPT);
var_dump(password_verify($long . "EXTRA", $hash));
bool(true)
The extra characters made no difference. This is rarely a problem for real passwords, but it is why very long inputs should be handled with care. Some developers pre-hash long passwords. Argon2 has no such short limit.
Where to keep the hash
The database column needs room. Use VARCHAR(255), as bcrypt hashes are 60 characters today, but other algorithms and future defaults are longer.
Do not trim or change the password
Hash exactly what the user typed. A password may legitimately start or end with a space.
Never log or print the plain password
#[\SensitiveParameter], so it is hidden in stack traces.