Password Hashing

Hashing, Automatic Rehashing, and Password Rules

The Hash facade wraps password_hash() (Password Hashing): bcrypt at cost 12 by default (BCRYPT_ROUNDS), with argon2i and argon2id selectable as the driver in config/hashing.php (publish it with php artisan config:publish hashing).

hash.php: two algorithms, checks and rehash testsPHP
<?php
require __DIR__.'/vendor/autoload.php';
$app = require __DIR__.'/bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
use Illuminate\Support\Facades\Hash;
$pw = 'correct-horse-9';
echo Hash::make($pw), "\n", Hash::driver('argon2id')->make($pw), "\n";
$hash = Hash::make($pw);
echo json_encode([Hash::check($pw, $hash), Hash::check('Correct-horse-9', $hash),
    Hash::needsRehash($hash), Hash::needsRehash($hash, ['rounds' => 13])]), "\n";
Output
$2y$12$Qjetps7dlf/JL7rrbSkD0u06kMbAbqnlKO4WgpEJokiBLMiywxQ0.
$argon2id$v=19$m=65536,t=4,p=1$RlMvb0lSYlVxZU1NMko5Qg$wzHp/7eKFo2jjIFWuH4dCYaF4nxfIy6x986yPQU2O
  ac
[true,false,false,true]

Each hash carries its algorithm, cost and salt, so no salt column is needed; each took about a quarter of a second here. argon2id is memory-hard (64 MiB per hash), which hurts GPU attackers more; bcrypt reads only the first 72 bytes of a password. Switching drivers is not free: with bcrypt configured, Hash::check() on an argon2id hash threw "This password does not use the Bcrypt algorithm." Raising the cost is easy, because attempt() rehashes at the next login, the only time the plain password is at hand:

Raising the bcrypt cost, then logging in oncePHP
q() { sqlite3 database/database.sqlite 'select substr(password, 1, 7) from users'; }
q; sed -i 's/^BCRYPT_ROUNDS=12/BCRYPT_ROUNDS=13/' .env; sleep 2
rm jar; T=$(tok /login); curl -s $J -o /dev/null -d "_token=$T&$P=correct-horse-9" $B/login; q
Output
$2y$12$
$2y$13$

The sleep lets artisan serve restart, as it does whenever .env changes; rehash_on_login => false disables the upgrade. For strength, Password::min(12)->mixedCase()->numbers()->symbols()->uncompromised() rejected bookshop four times, rejected Password123! only as "appeared in a data leak" (the Have I Been Pwned API sees just five hex digits of its SHA-1), and accepted Tr1cky-0wl-Barn. The kit sets such rules with Password::defaults(), in production only.