cult3

Setting up a Password Reminder Service

Mar 02, 2015

Table of contents:

  1. How do Password Reminders work?
  2. Setting up the structure
  3. Checking for valid emails
  4. Requesting a Password Reminder
  5. Checking for valid reminder codes
  6. Resetting the user’s password
  7. Conclusion

In last week’s tutorial we looked at building out the User Registration Service.

The Application Service provides a public API to the functionality and deals with basic validation of requests and dispatching Domain Events.

The Application Service passes the request to the Domain Service to do the heavy lifting of enforcing the Domain Rules and finally registering the new user.

A while ago we wrote the Domain Service for requesting a password reminder. This will enable a user to reset their password should they forget it.

This is a similar situation to how the user registration process works and so we can write a similar type of Application Service to deal with it.

How do Password Reminders work?

Before we jump into the code, first we will recap how the password reminder process works.

I’m sure resetting a password is not a new concept to you, but I think it’s important to understand the mechanics of how the process works.

There are basically three steps when it comes to resetting a password.

Firstly, when the user can’t remember their password, they click on the “forgotten password link”. This will take the user to a form so they can submit their email email address. First we check to make sure the email address is valid, and if it is, we can email the user a URL with a unique token that will allow them to reset their password.

Next, the user will receive the email and click the link with the unique token in the URL. This token is checked against the database to ensure it is valid and it belongs to the correct user. If the token is valid, the user can enter a new password.

Finally the application validates the token once again and then resets the user’s password. The reset tokens are deleted from the database and the user is redirected to the application.

If you would like to see how the Domain Service was implemented, take a look at Building a Password Reminder Domain Service.

Setting up the structure

So the first thing we will do will be to set up the structure. This is another Application Service and so it will sit within the Application namespace.

Resetting a password is also an important part of the user’s identity and so we will place this code under the Identity namespace.

Next I will create a new file called PasswordReminder.php and I will define the class:

<?php namespace Cribbb\Application\Identity;

class PasswordReminder
{
}

Inside the __construct() method we can inject an instance of the Domain Service as well as instantiate a new instance of MessageBag to hold any errors that might arise:

/**
 * @var ReminderService
 */
private $service;

/**
 * @var MessageBag
 */
private $errors;

/**
 * @param ReminderService $service
 * @return void
 */
public function __construct(ReminderService $service)
{
    $this->service = $service;
    $this->errors = new MessageBag;
}

I will also provide an errors() method for accessing the MessageBag instance:

/**
 * Return the errors
 *
 * @return MessageBag
 */
public function errors()
{
    return $this->errors;
}

Finally we can also create a new PasswordReminderTest.php file to hold the tests:

<?php namespace Cribbb\Tests\Application\Identity;

use Mockery as m;
use Illuminate\Hashing\BcryptHasher;
use Cribbb\Application\Identity\PasswordReminder;
use Cribbb\Domain\Services\Identity\ReminderService;
use Cribbb\Infrastructure\Services\Identity\BcryptHashingService;

class PasswordReminderTest extends \TestCase
{
    /** @var PasswordReminder */
    private $service;

    /** @var UserRepository */
    private $users;

    /** @var ReminderRepository */
    private $reminders;

    public function setUp()
    {
        parent::setUp();

        $this->users = m::mock("Cribbb\Domain\Model\Identity\UserRepository");
        $this->reminders = m::mock(
            "Cribbb\Domain\Model\Identity\ReminderRepository"
        );

        $this->service = new PasswordReminder(
            new ReminderService(
                $this->reminders,
                $this->users,
                new BcryptHashingService(new BcryptHasher())
            )
        );
    }
}

I will create a new instance of the PasswordReminder service inside of the setUp() method so that it is available for each test. I will also mock the UserRepository and the ReminderRepository.

Checking for valid emails

The first thing I’m going to do with this class is to provide a helper method to check for valid email address. If the request does not contain a valid email address we can just abort straight away rather than letting the error get caught later in the process.

To do this I will create a new validate() method:

/**
 * Check that the email is valid
 *
 * @param string $email
 * @return bool
 */
private function validate($email)
{
    $validator = Validator::make(compact('email'), ['email' => 'email']);

    if ($validator->passes()) return true;

    $this->errors = $validator->messages();

    return false;
}

In this method I’m creating a new instance of Laravel’s Validate class and then checking to make sure the email address is valid.

Due to the fact that this method is private it means we can’t test it directly.

Requesting a Password Reminder

Next we can write the method that will accept the user’s request for a password reminder.

/**
 * Request a new password reminder
 *
 * @param string $email
 * @return Reminder
 */
public function request($email)
{
    if ($this->validate($email)) {
        try {
            $reminder = $this->service->request($email);

            /* Dispatch Domain Events */

            return $reminder;
        } catch (ValueNotFoundException $e) {
            $this->errors()->add('email', $e->getMessage());
        }
    }
}

First we can check to make sure the email address is valid. If the email address is not valid, we can just exit here.

Next we pass the $email to the request() method of the Domain Service.

If everything goes smoothly we will be returned a new instance of Reminder that is loaded with Domain Events. At this point we can dispatch those Domain Events and return the Reminder object from the Application Service.

If the email address is not found a new ValueNotFoundException will be throw. This can be caught and then pushed into the MessageBag. Alternatively you might want to catch the Exception at the next level up.

The first test I will write will be to ensure that nothing is returned from the method when an invalid email address is used:

/** @test */
public function should_check_valid_email_address()
{
    $this->assertEquals(null, $this->service->request('not_a_valid_email'));
    $this->assertEquals(1, $this->service->errors()->count());
}

I’m also asserting that the MessageBag instance should contain one error.

Next I can check to make sure an error is returned when the user’s email is not registered. To simulate that the user’s email is not found I can instruct the UserRepository to return null when the userOfEmail() method is called:

/** @test */
public function should_fail_request_with_unregistered_email()
{
    $this->users->shouldReceive('userOfEmail')->once()->andReturn(null);

    $this->assertEquals(null, $this->service->request('name@domain.com'));
    $this->assertEquals(1, $this->service->errors()->count());
    $this->assertEquals(
        'name@domain.com is not a registered email address',
            $this->service->errors()->first());
}

I can then assert that the count of the MessageBag is 1 and the correct error message has been set.

Finally I can assert that a new instance of Reminder is returned when everything goes smoothly:

/** @test */
public function should_create_new_reminder_on_request()
{
    $this->users->shouldReceive('userOfEmail')->once()->andReturn(true);
    $this->reminders->shouldReceive('deleteExistingRemindersForEmail')->once();
    $this->reminders->shouldReceive('nextIdentity')->once()->andReturn(ReminderId::generate());
    $this->reminders->shouldReceive('add')->once();

    $reminder = $this->service->request('name@domain.com');

    $this->assertInstanceOf('Cribbb\Domain\Model\Identity\Reminder', $reminder);
}

In this test I’m telling the mocked repositories that they should be expecting certain method calls. I can then assert that I’m successfully returned a new instance of Reminder.

Checking for valid reminder codes

When the user clicks on the link in their email address we need to check to make sure the email address and code are valid.

We can do this by passing the email and code to the Domain Service. It’s up to the Domain Service to decide what’s valid:

/**
 * Check to see if the email and token combination are valid
 *
 * @param string $email
 * @param string $code
 * @return bool
 */
public function check($email, $code)
{
    if ($this->validate($email)) {
        return $this->service->check($email, $code);
    }
}

Once again, first we check to make sure the email address is of a valid format.

Next we pass the $email and $code to the check() method of the Domain Service. We do not need to be concerned with how the Domain Service decides whether the email and code are valid.

To test this method we can make the following assertions.

First we can ensure that the method will return false if the email and code combination is not found:

/** @test */
public function should_check_for_invalid_email_or_reminder_code()
{
    $this->reminders->shouldReceive('findReminderByEmailAndCode')->andReturn(null);

    $this->assertFalse($this->service->check('name@domain.com', 'abc123'));
}

Next we can assert that the method returns true when a Reminder is found and is valid:

/** @test */
public function should_check_for_valid_email_and_reminder_code()
{
    $reminder = m::mock('Cribbb\Domain\Model\Identity\Reminder');
    $reminder->shouldReceive('isValid')->andReturn(true);
    $this->reminders->shouldReceive('findReminderByEmailAndCode')->andReturn($reminder);

    $this->assertTrue($this->service->check('name@domain.com', 'abc123'));
}

Resetting the user’s password

Finally, we can actually reset the user’s password:

/**
 * Reset a user's password
 *
 * @param string $email
 * @param string $password
 * @param string $code
 * @return User;
 */
public function reset($email, $password, $code)
{
    if ($this->validate($email)) {
        try {
            $user = $this->service->reset($email, $password, $code);

            /* Dispatch Domain Events */

            return $user;
        } catch (InvalidValueException $e) {
            $this->errors()->add('code', $e->getMessage());
        }
    }
}

Once again we can first ensure that the $email is of a valid format using the validate() method from earlier.

Next we can pass the $email, $password and $code to the reset() method of the Domain Service.

If everything goes smoothly we will be returned an instance of User loaded with Domain Events that can be dispatched. We can return the User instance from the Application Service.

If the email or code is not valid a new InvalidValueException will be thrown.

This might be a sign that someone is trying some funny business with your application, so you might want to just let this Exception bubble up to the surface. In this case I’m simply catching the Exception and pushing the error code into the MessageBag.

The first test I will write will ensure that an error is returned if the token is invalid:

/** @test */
public function should_return_error_on_invalid_token_during_reset()
{
    $this->reminders->shouldReceive('findReminderByEmailAndCode')->andReturn(null);

    $this->assertEquals(null, $this->service->reset('name@domain.com', 'password', 'abc123'));
    $this->assertEquals(1, $this->service->errors()->count());
    $this->assertEquals(
    'abc123 is not a valid reminder code', $this->service->errors()->first());
}

Next I will ensure that an instance of User is returned when everything goes smoothly:

/** @test */
public function should_reset_password_and_return_user()
{
    $reminder = m::mock('Cribbb\Domain\Model\Identity\Reminder');
    $reminder->shouldReceive('isValid')->andReturn(true);
    $this->reminders->shouldReceive('findReminderByEmailAndCode')->andReturn($reminder);

    $user = m::mock('Cribbb\Domain\Model\Identity\User');
    $this->users->shouldReceive('userOfEmail')->once()->andReturn($user);
    $user->shouldReceive('resetPassword')->once();

    $this->users->shouldReceive('update')->once();

    $this->reminders->shouldReceive('deleteReminderByCode')->once();

    $user = $this->service->reset('name@domain.com', 'password', 'abc123');

    $this->assertInstanceOf('Cribbb\Domain\Model\Identity\User', $user);
}

Conclusion

The functionality to reset a password is really important for an application because it will annoy your users and increase your customer support requests.

Resetting a password is a fairly simple process, but there are a couple of important things to think about.

To reset a password, we must follow the three step process that I’ve outlined above.

In this case, I’ve created a Domain Service that deals with the heavy lifting and determining what is valid and what violates the business rules.

And today we have created an Application Service that acts as a public API to accept requests and return responses.

This is a series of posts on building an entire Open Source application called Cribbb. All of the tutorials will be free to web, and all of the code is available on GitHub.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.