Your Laravel stock-trading app must raise its password hashing cost; how do BCRYPT_ROUNDS, Hash::needsRehash() and rehash_on_login upgrade existing hashes without forcing resets?
answer
- new cost applies to new hashes
- BCRYPT_ROUNDS, default 12
- needsRehash compares stored cost with config
- rehash_on_login true in hashing config
- only attempt(), attemptWhen() and once() rehash
basics
~20 sRaise BCRYPT_ROUNDS; new hashes use the higher cost and old ones still verify. With rehash_on_login true, a successful Auth::attempt() sees Hash::needsRehash() return true and saves a fresh hash, so users upgrade as they log in.
solid answer
~40 sSet `BCRYPT_ROUNDS` (default 12) to the new cost, after measuring login latency on production hardware, because each extra round doubles the work. `Hash::make()` then uses the new cost, and `Hash::check()` still verifies old hashes because it reads the cost stored inside each one. In Laravel 13 the hashing config sets `rehash_on_login` to `true`, so when `Auth::attempt()`, `attemptWhen()` or `once()` succeeds, the guard asks the user provider to call `Hash::needsRehash()`. That returns true when the stored parameters differ from the configured ones, and the provider saves `Hash::make()` of the submitted password. Accounts that never log in keep the old cost, and logins that bypass those guard methods (`Auth::login($user)`, a custom check) never rehash unless you call `needsRehash()` yourself. Re-cache the config and restart workers so every process uses the new cost.
code
ini · 2 lines# .env on the trading platform after benchmarking
BCRYPT_ROUNDS=13go deeper
Know that BCRYPT_ROUNDS sets the cost for new hashes and that old hashes keep working after it changes.
Explain why Hash::check() verifies old hashes, what Hash::needsRehash() compares, and that rehashing only happens when the plain password is present.
Plan the rollout: benchmark the cost, redeploy config and workers, cover login paths that bypass attempt(), and handle dormant accounts.
Treat hashing cost as a capacity budget: set a policy for how cost tracks hardware and who owns the login-latency trade-off.
## The knobs involved Laravel's hashing configuration lives in the framework's `config/hashing.php`, merged into every app. You only publish it with `php artisan config:publish hashing` if you need to change a key that has no environment variable. The keys that matter for a cost change: | Key | Env variable | Laravel 13 default | Role | |---|---|---|---| | `hashing.driver` | `HASH_DRIVER` | `bcrypt` | which hasher `Hash` uses | | `hashing.bcrypt.rounds` | `BCRYPT_ROUNDS` | 12 | bcrypt cost for new hashes | | `hashing.argon.memory` / `time` / `threads` | `ARGON_MEMORY` / `ARGON_TIME` / `ARGON_THREADS` | 65536 / 4 / 1 | Argon cost for new hashes | | `hashing.rehash_on_login` | none | `true` | upgrade hashes at login | The skeleton's `.env.example` sets `BCRYPT_ROUNDS=12`, and its `phpunit.xml` sets `BCRYPT_ROUNDS=4` so tests stay fast. ## What changes when you raise the cost Bcrypt's cost is a base-2 exponent, so moving from 12 to 13 doubles the time each hash takes. After the change: - **new hashes** from `Hash::make()`, including those made by the `hashed` cast, use the new cost; - **old hashes** still verify, because `Hash::check()` reads the cost embedded in each stored hash rather than using the configured one; - **`Hash::needsRehash($hash)`** now returns `true` for every old hash, because PHP's `password_needs_rehash()` reports any difference between the stored and the configured parameters. ## How rehash_on_login upgrades users When `rehash_on_login` is true, the session guard runs an extra step after a successful credential check in three methods: `attempt()`, `attemptWhen()` and `once()`. It calls the user provider's `rehashPasswordIfRequired()`, which for the Eloquent provider: 1. calls `Hash::needsRehash()` on the stored password; 2. if true, computes `Hash::make()` of the password the user just submitted; 3. writes it with `forceFill()` and `save()`. That works only at login because it is the one moment the plain password is available. The whole user base is not converted at once; each account upgrades the next time its owner signs in with the password. ## What the automatic path misses - **Dormant accounts** keep the old cost until their owners return. If that matters, you can force a reset for accounts untouched for a long time; you cannot rehash them without the password. - **Logins that bypass those guard methods.** `Auth::login($user)` after your own `Hash::check()`, a custom API endpoint, or a token flow never reaches the rehash step. Call `Hash::needsRehash()` and save a new hash there yourself. - **Disabled rehashing.** Publishing the config and setting `rehash_on_login` to `false` turns the upgrade off entirely. ## Rolling it out on a trading platform A trading app sees a login spike at market open, so the cost change is a capacity decision as much as a security one: 1. **Measure** `Hash::make()` at the candidate cost on production-class CPUs; aim for a hash time the login path can afford under peak concurrency. 2. **Deploy** the new `BCRYPT_ROUNDS`, rebuild the config cache (`php artisan config:cache`) and restart long-running workers so no process keeps the old cost. 3. **Expect a one-off write** per user at their first login after the change, and watch login latency and CPU through the next market open. 4. **Keep login rate limiting in place**, because slower hashes make each attempt more expensive for you as well as for an attacker. 5. **Track progress** by counting stored hashes that still carry the old cost prefix (for bcrypt, `$2y$12$`).
- Why can Laravel not rehash every stored password in a migration right after BCRYPT_ROUNDS changes?Rehashing needs the plain password, and a hash is one-way. The only time the application holds the plain password is when the user submits it, which is why the upgrade runs after a successful login. Dormant accounts keep the old cost until their owners sign in or reset.
- Does lowering BCRYPT_ROUNDS also trigger rehashing at login?Yes. `Hash::needsRehash()` returns true whenever the stored cost differs from the configured one, in either direction, so users are rehashed down to the lower cost at their next login through `attempt()`. That is why a temporary drop, such as for a load test, should never be deployed to production.
saying these in an interview costs you the question
- Raising BCRYPT_ROUNDS makes existing passwords fail Hash::check().
- rehash_on_login rewrites every user's hash when the config changes.
- Auth::login($user) also rehashes the password when needed.
- Hash::needsRehash() only returns true when the cost went up.
- A migration can rehash all stored passwords at the new cost.