Reset Password

You may have noticed references to password() and resetPassword() functions in the Authentication tutorial. These are used to allow users to reset their password when they forget it.

Add Password and Reset Functions

When a user forgets their password, we need a method for them to reset it. We can verify them by email, send them an email to that verified address, then send them a link that allows them to reset their password.

At the top of the Users Controller, /src/Controller/UsersController.php, below the namespace add the following imports:

use Exception;
use Cake\Routing\Router;
use Cake\Mailer\Mailer;

Be sure to configure your server's EmailTransport configuration in /config/app_local.php. Then add the following functions to allow for a request of a new password, sending the email, and accepting the token to actually reset the password:

  public function password()
  {
    $this->Authorization->skipAuthorization();
    if ($this->request->is('post')) {
      $email = $this->request->getData('email');
      $query = $this->Users->findByEmail($email);
      $user = $query->first();
      if (is_null($user)) {
        $this->Flash->error('Email address ' . $email . ' does not exist. Please try again');
      } else {
        $passkey = uniqid('reset_', true);
        $url = Router::Url(['controller' => 'users', 'action' => 'reset-password'], true) . '/' . $passkey;
        $timeout = time() + (60 * 60); // 1 hour timeout
        if ($this->Users->updateAll(['passkey' => $passkey, 'timeout' => $timeout], ['id' => $user->id])) {
          $this->sendResetEmail($url, $user);
          $this->redirect(['action' => 'login']);
        } else {
          $this->Flash->error('Error saving reset passkey/timeout');
        }
      }
    }
  }
  
  private function sendResetEmail($url, $user)
  {
    $mailer = new Mailer('default');
    $mailer
      ->setViewVars(['url' => $url, 'username' => $user->username])
      ->setFrom(['[email protected]' => 'Do not reply'])
      ->setTo($user->email, $user->full_name)
      ->setEmailFormat('both')
      ->setSubject('Reset your password')
      ->viewBuilder()->setTemplate('resetpw');
    try {
      $result = $mailer->deliver();
      if ($result) {
        $this->Flash->success(__('Check your email for your reset password link.'));
      } else {
        $this->Flash->error(__('Error sending email.'));
      }
    } catch (Exception $e) {
      $this->Flash->error('Error: ' . $e->getMessage());
    }
  }
  
  public function resetPassword($passkey = null)
  {
    $this->Authorization->skipAuthorization();
    if ($passkey) {
      $query = $this->Users->find()
        ->where([
          'passkey' => $passkey,
          'timeout >' => time()
        ])
        ->all();
      $user = $query->first();
      if ($user) {
        if (!empty($this->request->getData())) {
          $user = $this->Users->patchEntity($user, $this->request->getData());
          if ($this->Users->save($user)) {
            // Clear passkey and timeout
            $this->Users->updateAll(['passkey' => null, 'timeout' => null], ['id' => $user->id]);
            $this->Flash->success(__('Your password has been updated.'));            
            return $this->redirect(['action' => 'login']);
          } else {
            $this->Flash->error(__('The password could not be updated. Please, try again.'));
          } 
        }
      } else {
        $this->Flash->error('Invalid or expired passkey. Please check your email or try again.');
        $this->redirect(['action' => 'password']);
      }
      $this->set(compact('user'));
    } else {
      $this->redirect('/');
    }
  }

Observe that this request, which anyone can make, doesn't actually do anything to the User record except add a passkey token and a timeout.

Add Reset Password and Request Password Views

To utilize these new methods, we need their accompanying views.

/templates/Users/password.php displays a simple form for the user to enter their email when they click "Forgot Password" on the /user/login page.

<?php $this->assign('title', 'Request Password Reset'); ?>
<div class="users form content">
  <?php echo $this->Form->create() ?>
  <fieldset>
    <legend><?php echo __('Request password reset') ?></legend>
    <?php
      echo $this->Form->create();
      echo $this->Form->control('email', ['autofocus' => true, 'label' => 'Email address', 'required' => true]);
    ?>
  </fieldset>
  <?php echo $this->Form->button('Request reset email'); ?>
  <?php echo $this->Html->link(__('Cancel'), ['action' => 'index'], ['class' => 'button']); ?>
  <?php echo $this->Form->end(); ?>
</div>

/templates/Users/reset_password.php displays a simple form for the user to change their password after their passkey and token have been verified.

<?php $this->assign('title', 'Reset Password'); ?>
<div class="users form content">
  <?php echo $this->Form->create($user) ?>
  <fieldset>
    <legend><?php echo __('Reset Password') ?></legend>
      <?php
      echo $this->Form->control('password', ['value' => '', 'required' => true, 'autofocus' => true]);
      ?>
      <p class="helper">Passwords must be at least 8 characters and contain at least 1 number, 1 uppercase, 1 lowercase and 1 special character</p>
      <?php 
      echo $this->Form->control('confirm_password', ['type' => 'password', 'required' => true]);
      ?>
    </fieldset>
  <?php echo $this->Form->button(__('Submit')); ?>
  <?php echo $this->Form->end(); ?>
</div>

Create Email Templates

For the sendResetEmail() function we need to create the email templates that display the two variables we are sending, and since we're sending both plain text and html formats, we need to create a template for each.

/templates/email/html/resetpw.php

<p>Your username is <?php echo $username; ?></p>
<p>Click on the link below to Reset Your Password.</p>
<p><a href="<?php echo $url; ?>">Click here to Reset Your Password</a></p>
<pre>or Visit this Link</pre><br/>
<p><a href="<?php echo $url; ?>"><?php echo $url; ?></a></p>

/templates/email/text/resetpw.php

Your username is <?php echo $username; ?>
Click on the link below or copy and paste it into your web browser to reset your password:
<?php echo $url; ?>