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 by ForgotPasswordController
  • the reset link arrives to the relevant mail address