skip to content

In Laravel 13, what breaks when you switch HASH_DRIVER from bcrypt to argon2id on an app with existing users, and how do you migrate safely?

level: seniorimportance: should knowfreq 22%

answer

  1. verification checks the algorithm first
  2. HASH_VERIFY defaults to true
  3. This password does not use the Argon2id algorithm
  4. argon is Argon2i, argon2id is Argon2id
  5. rehash at login, then turn verify back on

basics

~10 s

With HASH_VERIFY at its default true, Hash::check() under argon2id throws a RuntimeException for every stored bcrypt hash, so logins fail. Set HASH_VERIFY=false during migration, let rehash_on_login convert users, then restore verification.

solid answer

~50 s

Each Laravel hasher has a `verify` option, set from `HASH_VERIFY` and defaulting to `true` in Laravel 13's `config/hashing.php`. With it on, `Hash::check()` first confirms the stored hash uses the configured algorithm. After switching `HASH_DRIVER` to `argon2id`, every existing `$2y$` bcrypt hash triggers `RuntimeException` "This password does not use the Argon2id algorithm.", so logins turn into server errors instead of upgrades. To migrate, set `HASH_VERIFY=false` with the new driver: `check()` falls back to PHP's `password_verify()`, which understands both formats, and `Hash::needsRehash()` reports every bcrypt hash, so `rehash_on_login` rewrites each one as Argon2id when users sign in. The `hashed` cast also rejects a bcrypt hash assigned under the new driver. Note that the `argon` driver is Argon2i and `argon2id` is Argon2id, and both need PHP built with Argon2 support. Once old hashes are gone, turn verification back on.

code

ini · 6 lines
ini
# migration window
HASH_DRIVER=argon2id
HASH_VERIFY=false

# after the bcrypt hashes are gone
# HASH_VERIFY=true

go deeper

for a junior

Know that bcrypt is Laravel's default driver and that changing HASH_DRIVER affects how new passwords are hashed.

for a middle

Explain algorithm verification, the difference between the argon and argon2id drivers, and why a naive switch makes Hash::check() throw.

for a senior

Run the migration: verify PHP support, relax HASH_VERIFY, convert users at login, track the $2y$ tail, then restore verification.

for a principal

Weigh whether an algorithm change justifies a relaxed-verification window and a forced-reset tail, versus raising bcrypt cost instead.

## The three drivers `config/hashing.php` (merged from the framework, publishable with `php artisan config:publish hashing`) names the default driver with `HASH_DRIVER`. Laravel 13 supports three: | Driver | Hasher class | PHP algorithm | Cost settings | |---|---|---|---| | `bcrypt` (default) | `BcryptHasher` | `PASSWORD_BCRYPT` | `rounds` (`BCRYPT_ROUNDS`, 12) | | `argon` | `ArgonHasher` | `PASSWORD_ARGON2I` | `memory` 65536, `time` 4, `threads` 1 | | `argon2id` | `Argon2IdHasher` | `PASSWORD_ARGON2ID` | the same `argon` block | Two details surprise people: `argon` means **Argon2i**, not Argon2id, and both Argon drivers read one shared `argon` block. The Argon drivers also need PHP compiled with Argon2 support; without it `Hash::make()` throws "Argon2 hashing not supported." ## Algorithm verification Every driver block carries `'verify' => env('HASH_VERIFY', true)`. With verification on, `Hash::check()` starts by reading the stored hash's algorithm with `password_get_info()`: - if it matches the configured driver, it verifies normally; - if not, it throws a `RuntimeException`, for example "This password does not use the Argon2id algorithm." The docs describe this as protection against **hash algorithm manipulation**: in an app that only ever writes one algorithm, a hash of another kind in the users table is suspicious, so the framework refuses to treat it as a normal credential. ## What breaks on a naive switch Flip `HASH_DRIVER=argon2id` on an app full of bcrypt hashes, and: 1. every existing user's login calls `Hash::check()` against a `$2y$...` hash; 2. the Argon2id hasher sees a bcrypt hash and throws; 3. the exception surfaces as a server error, and `rehash_on_login` never runs because it only fires after a successful check. The `hashed` cast fails in the same way: assigning an existing bcrypt hash through the model fails its configuration check and throws. ## A safe migration 1. **Confirm Argon2 support** on every PHP build that serves requests or runs workers. 2. **Deploy the new driver with verification off**: `HASH_DRIVER=argon2id` and `HASH_VERIFY=false`. Now `check()` delegates to `password_verify()`, which recognises bcrypt and Argon2 hashes alike. 3. **Let logins convert users.** `Hash::needsRehash()` returns `true` for any hash whose algorithm differs from the configured one, so `rehash_on_login` saves a new Argon2id hash at each successful `Auth::attempt()`. 4. **Measure progress** by counting hashes that still start with `$2y$`. 5. **Decide on the tail.** Accounts that never return keep bcrypt hashes. Either force password resets for them, or accept leaving verification off. 6. **Turn verification back on** with `HASH_VERIFY=true` once no legitimate bcrypt hashes remain. ## Checking the rollout A migration like this is easy to leave half-finished, so make its state visible: - log or count how many logins per day still hit a bcrypt hash; - alert if the count stops falling, which usually means a login path that bypasses `Auth::attempt()` and so never rehashes; - keep the argon settings (`ARGON_MEMORY`, `ARGON_TIME`, `ARGON_THREADS`) fixed during the window, because changing them mid-way makes `needsRehash()` flag the fresh Argon2id hashes too and doubles the write load; - test the login path under both formats in CI before the deploy, with `HASH_VERIFY=false` set in the test environment. ## Is the switch worth it? Choosing between bcrypt and Argon2id is a password-storage decision in its own right. The Laravel-specific point is that the switch is not a config flip: it is a data migration gated by algorithm verification, rehash-on-login and the hashed cast, and it needs a planned window where verification is relaxed.

  • Why does Laravel check the hash algorithm before verifying a password at all?
    An app normally writes a single algorithm, so a stored hash of another kind suggests the data was tampered with or imported without review. With `HASH_VERIFY` on, the hasher refuses it with a `RuntimeException` rather than silently verifying against a format the app never chose.
  • How do you know when it is safe to set HASH_VERIFY back to true?
    Query the users table for hashes still carrying the bcrypt `$2y$` prefix. When none remain, or the remaining accounts have been forced to reset, every stored hash matches the Argon2id driver and verification can be re-enabled without breaking logins.

saying these in an interview costs you the question

  • Changing HASH_DRIVER converts stored hashes automatically.
  • The argon driver produces Argon2id hashes.
  • Hash::check() quietly returns false for a bcrypt hash under argon2id.
  • HASH_VERIFY=false disables password checking entirely.
  • rehash_on_login converts users even while their checks throw.