A cost factor chosen two years ago is the wrong one now, and a table full of unsalted SHA-1 rows from before that is worse. Neither can be fixed by a migration, because the plaintext is not in the database. password_needs_rehash() exists so that the upgrade happens at the only moment the plaintext is legitimately in hand: a successful login.
const BCRYPT_COST = 11;
function authenticate(UserRepository $users, $email, $plain)
{
$user = $users->findByEmail($email);
if (!$user || !password_verify($plain, $user->password_hash)) {
return null;
}
$options = array('cost' => BCRYPT_COST);
if (password_needs_rehash($user->password_hash, PASSWORD_BCRYPT, $options)) {
$users->storeHash($user->id, password_hash($plain, PASSWORD_BCRYPT, $options));
}
return $user;
}
The function reads the algorithm and the parameters back out of the stored string and compares them with what you asked for, so raising the cost next year is a one-line change and the fleet migrates itself as people sign in. Passing PASSWORD_BCRYPT with an explicit cost rather than PASSWORD_DEFAULT is what makes that comparison stable — PASSWORD_DEFAULT is allowed to change between PHP releases, so a server upgrade would otherwise decide that every hash in the table needs rehashing at once. The limit is dormant accounts: someone who has not logged in since the old scheme keeps the old hash indefinitely, so “everyone is on bcrypt now” is a claim that needs a cut-off date and a forced reset rather than a SELECT.