password_needs_rehash

Checks whether a stored hash was made with a different algorithm or options than you now want.

Signature
password_needs_rehash(string $hash, string|int|null $algo, array $options = []): bool
  • Added in PHP 5.5.0

Parameters

ParameterTypeDescription
$hashstringA hash created by password_hash().
$algostring|int|nullThe password algorithm constant you now want hashes to use, such as PASSWORD_DEFAULT or PASSWORD_ARGON2ID.
$optionsoptionalarray= []The options you now want, such as ['cost' => 12]. Any option you omit is compared against the algorithm's current default.

Return value

booltrue if the hash should be recreated to match the given algo and options, false if it already matches.

Errors

Does not throw. An unrecognised algo returns false, and a hash that is not a recognised password hash returns true.

Changelog

  • PHP 8.4.0Because the default bcrypt cost rose from 10 to 12, calling this with PASSWORD_DEFAULT and no options started returning true for every hash made under an earlier PHP.
  • PHP 7.4.0The algo parameter expects a string now, but still accepts ints for backward compatibility.

password_needs_rehash compares what is baked into a stored hash against the algorithm and options you want today, and reports whether the hash is out of date. It does not touch the password and does no hashing work, so it is cheap enough to call on every successful login.

The login-time upgrade pattern

The only moment you hold a user's plain-text password is when they log in. That is the one opportunity to re-hash it. The standard sequence is: verify the password, and if it checks out, ask this function whether the stored hash is stale; if it is, hash the same password again with current settings and update the row. Over a few weeks of normal traffic, your whole user table migrates itself with no forced resets.

Two behaviours catch people out. Any option you leave out of $options is compared against the algorithm's current default, which is why PHP 8.4's bcrypt cost bump from 10 to 12 suddenly made this return true for every legacy hash. And a garbage or legacy hash — an MD5, an empty column — also returns true, since it does not match the algorithm you asked for.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Transparently upgrade every user's hash to a higher bcrypt cost as they log in
  • Migrate a user table from bcrypt to Argon2id without forcing password resets
  • Flag accounts still holding pre-migration hashes in an admin security report
  • Re-hash after raising the cost baseline following a hardware upgrade

Frequently asked questions

When should I call password_needs_rehash?

Immediately after a successful password_verify during login, because that is the only time you have the plain-text password available to re-hash. Calling it anywhere else tells you a hash is stale but leaves you unable to do anything about it.

Why does password_needs_rehash suddenly return true for all my hashes on PHP 8.4?

PHP 8.4 raised the default bcrypt cost from 10 to 12, and any option you omit is compared against the current default. Hashes created under earlier defaults are correctly reported as needing an upgrade.

Does password_needs_rehash check the password itself?

No. It only inspects the algorithm and options recorded inside the hash string, so it never validates or even reads a password. Always pair it with password_verify.

More examples

The full login-time upgrade

The first login prints "(hash upgraded)" and rewrites the row; the second finds the hash already matches the policy and leaves it alone. Note the ordering: verify first, because you must not re-hash a password that failed authentication.

Example · php
<?php
$row = ['hash' => password_hash('s3cret', PASSWORD_BCRYPT, ['cost' => 4])];
$options = ['cost' => 6]; // today's policy

function login(array &$row, string $submitted, array $options): bool
{
    if (!password_verify($submitted, $row['hash'])) {
        return false;
    }
    if (password_needs_rehash($row['hash'], PASSWORD_BCRYPT, $options)) {
        $row['hash'] = password_hash($submitted, PASSWORD_BCRYPT, $options);
        echo "(hash upgraded)\n";
    }
    return true;
}

var_dump(login($row, 's3cret', $options));
var_dump(login($row, 's3cret', $options)); // already current

Legacy and unknown hashes both report true

Only the matching bcrypt hash returns false. The MD5 and the empty string return true because neither was produced by the requested algorithm, which is convenient for migrations but means a true result does not by itself prove the hash is merely outdated rather than corrupt.

Example · php
<?php
$cases = [
    'bcrypt cost 4' => password_hash('x', PASSWORD_BCRYPT, ['cost' => 4]),
    'legacy md5'    => md5('x'),
    'empty column'  => '',
];

foreach ($cases as $label => $hash) {
    $needs = password_needs_rehash($hash, PASSWORD_BCRYPT, ['cost' => 4]);
    printf("%-14s => %s\n", $label, var_export($needs, true));
}

Discussion

  • Be the first to comment on this lesson.