Diving into Laravel Password Reset
Laravel provides out-of-the-box users management, handling authentication, password recovery, authorization and much more. But to work it expects some preconditions, and if you violate those preconditions you have to manually re-wire many things.
The precondition I've violated today is that users have not a single associated email address, but many contacts of different types are listed in a different table of the database. This implies that password recovery mechanism has no longer a mail address to which send the reset link, and everything breaks badly. Anyway, hacking around I've been able to attach my own username to user to email address logic to the flow.
Preamble: my reset password form asks for a username.
First: I've created my own UserProvider. It extend the usual EloquentUserProvider
and its purpose is to overwrite the retrieveByCredentials()
method, able to retrieve a user from the given email. This is saved in app/Extensions/BypassUserProvider.php
.
<?php
namespace App\Extensions;
use Illuminate\Auth\EloquentUserProvider;
class BypassUserProvider extends EloquentUserProvider
{
public function retrieveByCredentials(array $credentials)
{
foreach($credentials as $key => $value) {
if ($key == 'email') {
/*
Here the code to retrieve the user from the given email
*/
return $user;
}
}
return parent::retrieveByCredentials($credentials);
}
}
Then, I've registered the new UserProvider for later instantiation, adding the following code in AuthServiceProvider::boot()
.
Auth::provider('bypass', function ($app, array $config) {
return new BypassUserProvider($app['hash'], $config['model']);
});
It is also required to map the new UserProvider in the config/auth.php
file. In the relevant arrays, I've added:
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\User::class,
],
/*
This is newly added
*/
'bypass' => [
'driver' => 'bypass',
'model' => App\User::class,
],
],
'passwords' => [
'users' => [
'provider' => 'users',
'table' => 'password_resets',
'expire' => 60,
],
/*
This is newly added
*/
'bypass' => [
'provider' => 'bypass',
'table' => 'password_resets',
'expire' => 60,
],
],
Almost in the end, I've hacked ForgotPasswordController
both to use BypassUserController
as PasswordBroker and to map the input username to the proper mail.
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use App\User;
class ForgotPasswordController extends Controller
{
use SendsPasswordResetEmails {
sendResetLinkEmail as realSendResetLinkEmail;
}
public function __construct()
{
$this->middleware('guest');
}
public function sendResetLinkEmail(Request $request)
{
$username = $request->input('username');
/*
Here, the code to retrieve the mail address from the given username
*/
$request->merge(['email' => $email]);
$this->realSendResetLinkEmail($request);
}
protected function broker()
{
return Password::broker('bypass');
}
}
Final touch: the internals still expect an email
attribute for the User
, to get the mail address to which send the reset password. Let dynamically bind it in the User
model, using an Eloquent mutator.
public function getEmailAttribute()
{
/*
Here the code to retrieve an email for $this User
*/
return $email;
}
Inverse logic path:
- user inputs his username
ForgotPasswordController
inject into the request a mail address for the user- some magic happens
BypassUserProvider
returns the user associated to the mail address (previously injected byForgotPasswordController
- the reset link arrives to the relevant mail address