Working With Passwords In PHP 8

In our previous article, we discussed how we should hash and salt our user’s passwords to make them harder to crack if our database becomes compromised. In this article, we’re going to discuss how to use PHP’s built-in functions to do this.

As someone who has maintained this kind of logic in other systems, I’m so glad it was added to the core.

Before we get too far into this, these functions were added in PHP 5.5 so our examples won’t work with earlier versions. These versions are no longer supported so they should be upgraded ASAP!

password_hash()

The first function we’re going to discuss is the password_hash() function. This function takes just the password and a constant indicating the algorithm to use and returns a hash. We’re going to use the PASSWORD_DEFAULT constant.

echo password_hash('ALongPassword!', PASSWORD_DEFAULT), PHP_EOL;

The string we get back from this is almost gibberish which is a good indication we’re getting a good hash.

$2y$10$nqtoiMsWow3PKz8Fry7RIu/Cac.bg3sAEsRt9aL2qPXnbc4sDVqOW

Because the password_hash() function automatically salts our passwords, every time we run the same password through the function we’ll get different outputs. This means the function won’t be susceptible to a rainbow table exploit.

for ($i = 0; $i < 3; $i++) {
    echo password_hash('ALongPassword!', PASSWORD_DEFAULT), PHP_EOL;
}

Output:

$2y$10$7lmWDeZjkddfWax4oZDCo.1ljtnPz0F2FN59d0HTTM4UzngY2JYD2
$2y$10$AGhT74u7uQIfW0u8R7QMee1irjRXCLgDLunJwYm9Cf1EcJNQZlUfq
$2y$10$YPNbZ1zPVwAzG43Lo8jR6OcBbAWYTbzsgHf3uMq00w8HomRcyu42K

We can also specify the algorithm to be used to generate the hash and some options for the specified algorithm.

The algorithm choices are PASSWORD_DEFAULT, PASSWORD_BCRYPT, PASSWORD_ARGON2I, and PASSWORD_ARGON2ID for bcrypt, bcrypt, Argon2i, and Argon2id respectively. You may have noticed we said “bcrypt” twice and that’s because PASSWORD_DEFAULT is equivalent to PASSWORD_BCRYPT.

Our suggestion is to stick with PASSWORD_DEFAULT as people with more security knowledge than us have decided that it is the best option for most people and if that changes in future releases we’ll most likely upgrade to the new algorithm automatically.

The options array parameter for bcrypt is the algorithmic cost that should be used. The higher the number the longer it will take to generate the hash and the harder it will be for an attacker to brute force the password.

password_hash('ALongPassword!', PASSWORD_DEFAULT, [
    'cost' => 10,
]);

The default cost is 10 but it’s hard to understand how that affects anything so let’s run through how long it takes to generate a hash at each level. Please note these numbers will not match on any other computer so take them with a grain of salt and use your server to determine the best value for you. We recommend something around a tenth of a second as a good compromise between security and performance.

for ($cost = 4; $cost < 18; $cost++) {
    $startTime = microtime(true);
    password_hash('ALongPassword!', PASSWORD_DEFAULT, [
        'cost' => $cost,
    ]);
    $endTime = microtime(true);

    echo round($endTime - $startTime, 3), " for cost = $cost", PHP_EOL;
}

The output:

0.001 for cost = 4
0.002 for cost = 5
0.005 for cost = 6
0.011 for cost = 7
0.021 for cost = 8
0.042 for cost = 9
0.098 for cost = 10
0.161 for cost = 11
0.502 for cost = 12
0.687 for cost = 13
1.495 for cost = 14
2.684 for cost = 15
3.796 for cost = 16
7.737 for cost = 17

password_verify()

The next part of the password management cycle is to verify the password an unauthenticated user has passed us. This is done using the password_verify() function.

Notice how the hashes we’ve generated start with $2y$10$? That is how password_verify() knows how to compare the hash to the input. This makes it so the password_verify() function needs just two parameters, the password sent by the user and the hash we created using password_hash(). I love this because even if we change our algorithm or cost the function call doesn’t need to change.

if (password_verify($password, $user->password)) {
    echo "Success!";
}

password_needs_rehash()

Computing power is only getting more and more powerful so as our servers get faster we must rehash our user’s passwords as we increase our “cost” parameter. PHP provides the password_needs_rehash() function to determine if we need to rehash our user’s passwords based on the current settings. It has almost the same parameters as password_hash() but the first parameter is the hash and not the password.

echo password_needs_rehash($hash, PASSWORD_DEFAULT, [
    'cost' => $cost,
]) ? "Yes" : "No";

As a form of completionism, a sample login process looks something like the following.

if (password_verify($password, $user->password)) {
    if (password_needs_rehash($user->password, PASSWORD_DEFAULT, ['cost' => $cost])) {
        $user->password = password_hash($password, PASSWORD_DEFAULT, ['cost' => $cost]);
        $user->save();
    }

    // do what needs to be done here to set the session up
}

password_get_info()

The very last thing we want to discuss is the password_get_info() function. This function is used to extract the information from the hash. It’s mostly for fun but we wanted to make sure you were aware of it.

var_dump(password_get_info(password_hash('ALongPassword!', PASSWORD_DEFAULT)));
array:3 [
  "algo" => "2y"
  "algoName" => "bcrypt"
  "options" => array:1 [
    "cost" => 10
  ]
]

What You Need To Know to Nail a Job Interview

  • PHP has built-in password functions after the 5.5 release
  • password_hash() to generate the hash before storing it
  • password_verify() to compare the hash against a user-provided password
  • password_needs_rehash() to determine if we need to rehash the password
  • password_get_info() for debugging information

As always thanks for reading our article. Make sure you subscribe to our newsletter and let us know how you’re using PHP password functions in the comments.