I've been spending a few days troubleshooting a failure of certain passwords to validate in Laravel 9. The password testperson
resolves to the hash $2y$10$5xc/wAmNCKV.YhpWOfyNoetCj/r3Fs5TyAskgZuIF/LEItWfm7rPW
. A direct query on the corresponding database table confirms that this is the correct hash. Yet Laravel's authentication infrastructure rejects this password and denies authentication.
This is not universal. I have multiple passwords that are resolving correctly. For example, the password eo
resolves to $2y$10$uNWYvMVmagIwQ2eXnVKLCOAK1QFQdcRtxbvlghf.Xpg0U1w.N./N2
, and Laravel authenticates that password. The same mechanism creates both of these user records, though they have different permissions (indicated by boolean values on the record).
I tracked down the bug to the function password_verify
, which was identified as returning false negatives in this Stack Overflow question and this Treehouse thread.
Specifically, here is the stack in Laravel that gets down to this failure point:
- The
login
route calls\Illuminate\Foundation\Auth\AuthenticatesUsers::login
via the controller class. - The
login
method calls\Illuminate\Foundation\Auth\AuthenticatesUsers::attemptLogin
. - The
attemptLogin
method calls theattempt
method of the controller's guard object. \Illuminate\Auth\SessionGuard::attempt
calls\Illuminate\Auth\SessionGuard::hasValidCredentials
.\Illuminate\Auth\SessionGuard::hasValidCredentials
calls thevalidateCredentials
method on the guard's provider object.Illuminate\Auth\EloquentUserProvider::validateCredentials
calls thecheck
method on its hasher object.Illuminate\Hashing\HashManager::check
calls thecheck
method on its driver.Illuminate\Hashing\BcryptHasher::check
callsIlluminate\Hashing\AbstractHasher::check
.Illuminate\Hashing\AbstractHasher::check
callspassword_verify
.
After unwinding this entire stack, I ran the following code in the login
method of the login controller:
$provider = $this->guard()->getProvider();
$credentials = $this->credentials($request);
$user = $provider->retrieveByCredentials($credentials);
$password_unhashed = $request['password'];
$password_hashed = $user->getAuthPassword();
$password_verify = password_verify($password_unhashed, $password_hashed);
logger('attemping login', compact('password_verify','password_unhashed','password_hashed'));
That dumps this context:
{
"password_verify": false,
"password_unhashed": "testperson",
"password_hashed": "$2y$10$5xc/wAmNCKV.YhpWOfyNoetCj/r3Fs5TyAskgZuIF/LEItWfm7rPW"
}
And if I put that password into a SELECT users WHERE password=
query, I get the user that I'm expecting.
What's going on here? And how do I get around this?
CodePudding user response:
It seems that Laravel's authentication infrastructure is not accepting the password hash for "testperson" because of a false negative from the PHP function password_verify. This function is used in the authentication process to compare the user-entered password with the stored password hash.
You can try to work around this issue by checking for common causes of false negatives in password_verify, such as incorrect hash algorithm settings or incorrect use of salt. Make sure the settings and parameters being used by Laravel match the ones used when the password hash was created.
If the issue persists, you could also try using a different hashing algorithm or library to store and compare the passwords.
CodePudding user response:
I think your assertion that the hash you provided is a hash of 'testperson' is in fact false. Since hashing is one-way, I can't tell you what the hash you showed is derived from. NOTE: This runs on PHP 7.4, but I don't think it will work on PHP 8 and beyond because of the deprecation of the salt option in password_hash().
<?php
//$testhash = '$2y$10$5xc/wAmNCKV.YhpWOfyNoetCj/r3Fs5TyAskgZuIF/LEItWfm7rPW';
$testhash = '$2y$10$uNWYvMVmagIwQ2eXnVKLCOAK1QFQdcRtxbvlghf.Xpg0U1w.N./N2';
//$password = "testperson";
$password = "eo";
$options = array("cost" => 10, "salt" => substr($testhash, 7, 22));
$pwhash = password_hash($password, PASSWORD_BCRYPT, $options);
echo $pwhash."\n";
$salt = substr($pwhash, 0, 29);
echo $salt."\n";
$cryptpw = crypt($password, $salt);
echo $cryptpw."\n";
if (password_verify($password, $cryptpw)) {
echo("Verified.\n");
} else {
echo("NOT Verified.\n");
}
if (password_needs_rehash($cryptpw, PASSWORD_BCRYPT, $options)) {
echo("Needs rehash.\n");
} else {
echo("Doesn't need rehash.\n");
}
/*
testperson results...
$2y$10$5xc/wAmNCKV.YhpWOfyNoeVNPMEcYrxepQeFAssFoAaIYs4WLmgZO
$2y$10$5xc/wAmNCKV.YhpWOfyNoe
$2y$10$5xc/wAmNCKV.YhpWOfyNoeVNPMEcYrxepQeFAssFoAaIYs4WLmgZO
Verified.
Doesn't need rehash.
eo results...
$2y$10$uNWYvMVmagIwQ2eXnVKLCOAK1QFQdcRtxbvlghf.Xpg0U1w.N./N2
$2y$10$uNWYvMVmagIwQ2eXnVKLCO
$2y$10$uNWYvMVmagIwQ2eXnVKLCOAK1QFQdcRtxbvlghf.Xpg0U1w.N./N2
Verified.
Doesn't need rehash.
*/
?>