In PHP, why should a login flow call password_needs_rehash(), and what does the PHP 8.4 bcrypt cost change mean for existing hashes?
answer
- only the plaintext at login can be rehashed
- password_needs_rehash checks algo and cost
- default cost rose to 12 in 8.4
- verify first, then rehash and store
- PASSWORD_DEFAULT can change between versions
basics
~20 sYou can only upgrade a hash when you hold the plaintext, which is at login. After a successful password_verify, call password_needs_rehash($hash, PASSWORD_DEFAULT); if it returns true, rehash the plaintext and store it. PHP 8.4 raised bcrypt's default cost from 10 to 12, so pre-8.4 hashes need a rehash.
solid answer
~50 sA stored password hash is one-way, so you can never re-derive a stronger hash from it — you can only recompute one **while you hold the plaintext**, which happens exactly once per login. The pattern: after `password_verify($input, $hash)` succeeds, call **`password_needs_rehash($hash, PASSWORD_DEFAULT, $options)`**. It returns `true` when the stored hash's algorithm or parameters differ from what you would produce now; then you run `password_hash($input, PASSWORD_DEFAULT, $options)` and write the new string back. This keeps hashes current without ever forcing a password reset. **PHP 8.4 raised bcrypt's default `cost` from 10 to 12**, so a hash created on PHP 8.3 with the default cost will make `password_needs_rehash(..., PASSWORD_DEFAULT)` return `true` on 8.5 — the upgrade path is automatic if you have the rehash step. The same mechanism handles a future change of `PASSWORD_DEFAULT` to a new algorithm. Without the rehash-on-login step, old weak hashes linger forever.
go deeper
Know that hashes are upgraded on login: verify, then rehash and store when needed.
Explain that password_needs_rehash compares algorithm and cost, and that PHP 8.4 raised bcrypt's default cost from 10 to 12.
Design the verify-then-rehash-then-persist flow, handle dormant accounts, and decide whether to pin cost or track PASSWORD_DEFAULT.
Own the policy for work-factor increases across a fleet and weigh login latency against attacker cost as hardware improves.
## Why rehashing can only happen at login Password hashes are deliberately irreversible, so there is no batch job that can "upgrade" them in the database — you do not have the plaintext to feed a stronger hash. The one moment the plaintext exists on your server is when the user submits it to log in. That is why algorithm and cost upgrades are done **opportunistically, on successful login**: 1. Look up the stored hash for the account. 2. `password_verify($submittedPassword, $storedHash)` — if `false`, reject. 3. `password_needs_rehash($storedHash, PASSWORD_DEFAULT, $options)` — if `true`, 4. `$new = password_hash($submittedPassword, PASSWORD_DEFAULT, $options)` and persist `$new`. Over time, active accounts migrate to the current parameters; dormant accounts keep their old hash until they next log in, which is acceptable because the old hash is still a proper salted bcrypt/Argon2 hash, just at a lower work factor. ## What `password_needs_rehash` actually compares `password_needs_rehash(string $hash, string|int|null $algo, array $options = []): bool` returns `true` when the hash was **not** produced with the given algorithm and options. It inspects the self-describing hash string and checks: - the **algorithm** (e.g. the stored hash is bcrypt but you now ask for `PASSWORD_ARGON2ID`), and - the **parameters** for that algorithm — for bcrypt, the `cost`; for Argon2, `memory_cost`, `time_cost`, `threads`. It does **not** verify the password and does **not** look at password length. It is purely "does this stored hash match today's policy?" ## The PHP 8.4 cost change | PHP version | bcrypt default `cost` | |---|---| | ≤ 8.3 | 10 | | 8.4 and 8.5 | **12** | Cost is a base-2 work factor, so cost 12 is **four times** the work of cost 10. After upgrading the runtime to 8.4+, any hash created earlier at the default cost 10 will satisfy `password_needs_rehash($hash, PASSWORD_DEFAULT)` (because the stored cost `10` ≠ the current default `12`), and your login flow will transparently re-hash it at 12 on next sign-in. If you **pin** a cost explicitly (`['cost' => 12]`), you control this yourself and the version bump does not change behaviour. ## Common mistakes - **Skipping the rehash step**, so `PASSWORD_DEFAULT`'s evolution and the cost bump never reach existing users. - **Rehashing before verifying**, or rehashing the *hash* instead of the *plaintext* — you must feed the plaintext the user just typed. - **Comparing versions by hand** (`if (str_starts_with($hash, '$2y$10$'))`) instead of `password_needs_rehash`; the function already parses this correctly and covers Argon2 parameters too. - **Forgetting the write-back**, so `password_needs_rehash` returns `true` every login but nothing is ever stored. The choice of *what* cost or algorithm your policy should target is a security-design question owned by the password-storage concept home; the PHP mechanics are `password_needs_rehash`, the self-describing hash, and the 8.4 default-cost change.
- Does `password_needs_rehash` re-check whether the password is correct?No. It only compares the stored hash's algorithm and parameters against the ones you pass; it never sees the plaintext. You always call `password_verify` first to authenticate, and only then use `password_needs_rehash` to decide whether to upgrade the stored hash.
- What happens to a dormant account's hash that never logs in after the 8.4 upgrade?It keeps its old cost-10 bcrypt hash indefinitely, because you have no plaintext to rehash it with. That is acceptable: it is still a salted bcrypt hash, just at a lower work factor, and it upgrades automatically the next time that user logs in through the rehash step.
- How do you avoid the cost changing under you on a runtime upgrade?Pin the parameters explicitly, e.g. `password_hash($pw, PASSWORD_BCRYPT, ['cost' => 12])`, and pass the same options to `password_needs_rehash`. Then your policy, not PHP's default, decides the cost, and a version bump does not silently change it.
saying these in an interview costs you the question
- you can rehash stored hashes in a background batch job
- password_needs_rehash also verifies the password
- bcrypt's default cost is 10 on PHP 8.5
- raising cost from 10 to 12 doubles the work
- PASSWORD_DEFAULT can never change, so rehashing is pointless
- rehash the stored hash string rather than the plaintext