How to re-purpose Laravel's forgot password functionality

Laravel logo

Introduction

I was introduced to the Laravel framework when it was in version 5.0 and the more I've used it the more I see myself seeking out similar web frameworks for other languages. As the framework has matured I have been impressed at the many official plug-ins and features it has available to it. I truly enjoy how fast and simple it is to get your project set-up with a basic user authentication scaffold as illustrated by the following three commands:

  $ composer create-project --prefer-dist laravel/laravel <project-name>
$ cd <project-name>
$ php artisan migrate
$ php artisan make:auth

and that's all it takes to get you up and running with a registration page, login page, password reset page with token expiration functionality so that the password reset request sent via e-mail expires after a certain amount of time. Of course you still need to set-up a database and email server but Laravel provides temporary alternatives so that the developers focus on writing code. Again I marvel at all of this functionality you get out of the box.

Problem

But sometimes however a one size fits all solution just doesn't cut it as experience tells us but thankfully since most of the heavy lifting was done by Laravel, adjusting the user provisioning becomes easy and straight forward. So without any more fanfare I present to you this exercise: Assuming an admin is provisioning the user on their behalf, how can we use what Laravel gives us to create a password-less user provisioning system that follows best practice security? How do we go about this if the user only provides us an email address and their first and last name?

Disclaimer: This is just one way of implementing this functionality. The approach taken here was to not re-create the wheel and use what was already provided to us by Laravel itself!

Solution

As mentioned before, we will keep with the KISS principle and not re-invent the wheel so we will go with a Laravel-out-of-the-box set-up. We want to build functionality for a third party (admin) to create users given just three things: First name, last name and email. We also do not want to email out a temporary password because that's not all that secure. Having in mind these constraints, we will treat the user creation flow as if he's resetting their password.

This leaves us with the following application flow:

Retrieve the user's first_name, last_name, and email. Generate and save a temporary password for the user as well as generate an expiration token pair and save one key pair in the password_resets table. Send out an email to the user with the other key pair as part of the password (re)setting link. Profit. Once we have that down we can proceed to investigate how Laravel does its password reset functionality. Fortunately, I have already done a deep dive into the inner workings of Laravel's password reset code and I will spare you the trouble and simply point you to the appropriate methods required to complete our goal.

The first modification comes to the default users table migration. In every vanilla Laravel installation the users table migration comes with a name, email, password, remember_me and timestamp fields. We'll go ahead and modify the users migration to accommodate for the first name, last name and remove the remember_me token.

  Schema::create('users', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('first_name');
$table->string('last_name');
$table->string('email')->unique();
$table->string('password');
$table->timestamps();
});

With that out of the way we will now focus on building out the main core of the functionality. If you are curious all of the methods extracted can be found in the DatabaseTokenRepository.php class found under the vendor/laravel/framework/src/Illuminate/Auth/Passwords directory. The methods we will go over are the creation of the hashed key that we utilize to create a password reset token that is saved to the database. We will email out the hashed key and the users email as parameters of the password reset URL.

  /**
* Create a hashed key for the user.
*
* @return string
*/

protected function createHashedKey()
{
$key = config('app.key');

if (Str::startsWith($key, 'base64:')) {
$key = base64_decode(substr($key, 7));
}

$key = hash_hmac('sha256', Str::random(40), $key);
return $key;
}

The hashed key generation is verbatim from the inner workings of Laravel. Here we can see that Laravel uses the application's key as a salt applied to the random string that is generated. With the hashed key generated we can save it for future use as well as utilize it to generate a token as we see below.

  /**
* Create a new token for the user.
*
* @return string
*/

protected function createNewToken($hashedKey)
{
return Hash::make($hashedKey);
}

The token generator is very basic. Here the method accepts the hashed key we generated earlier and make a new hashed value from it using PHP's bcrypt encryption library. With these two pieces of information we can utilize them to issue password resets when the user wants to, as well as provision the user with a temporary password and prompt the user to set their real password via email.

Now that we have a way to generate tokens we also need a method to compare the tokens for validity.

  /**
* @param $token the emailed token
* @param $hashedKey the saved key on the password_resets
* @return boolean
*/

private function compareTokens($token, $hashedKey)
{
return Hash::check($token, $hashedKey);
}

To round out the functionality we need to delete the saved key(s) from the password_resets table on an adjustable time interval as well as the ability to delete a token record using a user's email.

  /**
* Delete expired tokens.
*
* @return void
*/

private function deleteExpiredTokens()
{
$expiredAt = Carbon::now()->subSeconds(config('app.token_expiration_date'));

PasswordReset::where('created_at', '<', $expiredAt)->delete();
}

/**
* Delete a token record by user email.
*
* @param $email
* @return void
*/

private function delete($email)
{
PasswordReset::whereEmail($email)->delete();
}

We now have everything set-up for us to start implementing the feature request. First we need to generate the hashed key in order to generate a token with it.

  {
//...
$hashedKey = $this->createHashedKey();
$token = $this->createNewToken($hashedKey);
//...

We then save the generated token to our password_resets table as well as the user account with their temporary password:

  //..
// $input comes from a validated Illuminate\Http\Request object.
// A database transaction saves us from having incomplete data being stored.
try {
DB::transaction(function () use ($token, $input) {
PasswordReset::create([
'email' => $input['email'],
'token' => $token,
'created_at' => Carbon::now()->toDateTimeString()
]);
User::create([
'first_name' => $input['first_name'],
'last_name' => $input['last_name'],
'email' => $input['email'],
'password' => Str::random()
]);
});
} catch (\PDOException $e) {
// catch exception
}
//..

Finally we send the user an email containing the hashed token:

  //..
// email user
Mail::to($input['email'])->send(new AccountRegistration($input, $hashedKey));
//...

Once a user receives their email with the password reset/account activation link. We will call the deleteExpiredTokens() look up the user by their email and compare the emailed hashedKey with the token we saved on the database. Once we do that we delete the token from the database.

  //...
$this->deleteExpiredTokens();
$user = PasswordReset::whereEmail($email)->first();
if (!is_null($user)) {
$result = $this->compareTokens($token, $user->token);
if ($result) {
$this->delete($email);
// allow the user to reset their password
// by displaying a front end view with the password reset fields.
}
}
//...
}

In closing

If you've followed along with this guide then you should have the necessary back-end functionality to provision user accounts on behalf of others. The front-end work omitted here can serve as a nice little implementation challenge.