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 callsIlluminateFoundationAuthAuthenticatesUsers::login
via the controller class. - The
login
method callsIlluminateFoundationAuthAuthenticatesUsers::attemptLogin
. - The
attemptLogin
method calls theattempt
method of the controller’s guard object. IlluminateAuthSessionGuard::attempt
callsIlluminateAuthSessionGuard::hasValidCredentials
.IlluminateAuthSessionGuard::hasValidCredentials
calls thevalidateCredentials
method on the guard’s provider object.IlluminateAuthEloquentUserProvider::validateCredentials
calls thecheck
method on its hasher object.IlluminateHashingHashManager::check
calls thecheck
method on its driver.IlluminateHashingBcryptHasher::check
callsIlluminateHashingAbstractHasher::check
.IlluminateHashingAbstractHasher::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?
2
Answers
I have a call to
Hash::make
in the observer for the user class. I discovered that it was running even though it wasn't supposed to, resulting in a duplicate hash.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().