A free and open-source book on ZF3 for beginners


16.6. Adding UserManager Service

The UserController works with the UserManager service, which contains all the business logic related to user management. The service allows an admin to create and update users, change a user's password and reset a user's password. We will describe some parts of it in more detail, omitting other obvious parts (you still can see the complete code in User Demo sample).

16.6.1. Creating a New User & Storing Password Encrypted

The addUser() method of the UserManager allows us to add a new user. It looks as follows:

/**
 * This method adds a new user.
 */
public function addUser($data) 
{
    // Do not allow several users with the same email address.
    if($this->checkUserExists($data['email'])) {
        throw new \Exception("User with email address " . 
                    $data['$email'] . " already exists");
    }
    
    // Create new User entity.
    $user = new User();
    $user->setEmail($data['email']);
    $user->setFullName($data['full_name']);        

    // Encrypt password and store the password in encrypted state.
    $bcrypt = new Bcrypt();
    $passwordHash = $bcrypt->create($data['password']);        
    $user->setPassword($passwordHash);
    
    $user->setStatus($data['status']);
    
    $currentDate = date('Y-m-d H:i:s');
    $user->setDateCreated($currentDate);        
            
    // Add the entity to the entity manager.
    $this->entityManager->persist($user);
    
    // Apply changes to database.
    $this->entityManager->flush();
    
    return $user;
}

You can see that in this method we first check if another user with the same E-mail address already exists (line 7), and if so we forbid creating the user by throwing an exception.

If the user with such E-mail address doesn't exist, we create a new User entity (line 13) and set its properties accordingly.

What is interesting here is how we save the user's password to the database. For security reasons, we do not save the password as is, but calculate a hash of it with the Bcrypt class provided by Zend\Crypt component of Zend Framework (lines 18-19) .

You can install Zend\Crypt with the following command:

php composer.phar require zendframework/zend-crypt

The Zend\Crypt component also requires that you have mcrypt PHP extension installed.

The Bcrypt algorithm is a hashing algorithm that is widely used and recommended by the security community for storing user's password. Encrypting password with Bcrypt is considered secure nowadays. Some developers still encrypt passwords with MD5 or SHA1 with salt, but this is not considered secure anymore (MD5 and SHA1 hashes can be hacked).

16.6.2. Validating Encrypted Password

When a user logs in, you'll need to check if the password hash stored in the database is the same as the hash calculated by the password entered by the visitor. You do that with the help of the verify() method provided by the Bcrypt class, as follows:

/**
 * Checks that the given password is correct.
 */
public function validatePassword($user, $password) 
{
    $bcrypt = new Bcrypt();
    $passwordHash = $user->getPassword();
    
    if ($bcrypt->verify($password, $passwordHash)) {
        return true;
    }
    
    return false;
}

16.6.3. Creating Admin User

The next important thing to note in UserManager is how we create the Admin user.

The Admin user is an initial user that is created automatically when there are not existing users in the database and allows you to login for the first time.

/**
 * This method checks if at least one user presents, and if not, creates 
 * 'Admin' user with email 'admin@example.com' and password 'Secur1ty'. 
 */
public function createAdminUserIfNotExists()
{
    $user = $this->entityManager->getRepository(User::class)->findOneBy([]);
    if ($user==null) {
        $user = new User();
        $user->setEmail('admin@example.com');
        $user->setFullName('Admin');
        $bcrypt = new Bcrypt();
        $passwordHash = $bcrypt->create('Secur1ty');        
        $user->setPassword($passwordHash);
        $user->setStatus(User::STATUS_ACTIVE);
        $user->setDateCreated(date('Y-m-d H:i:s'));
        
        $this->entityManager->persist($user);
        $this->entityManager->flush();
    }
}

We set the Admin user's email to admin@example.com and password to Secur1ty, so you can login for the first time with these credentials.

16.6.4. Resetting User Password

Sometimes users forget their password. If that happens, you'll need to let the user reset the password - to securely change the password. Password resetting works as follows:

You typically do not store raw password reset tokens in database. Instead, you store a hash of the token. This is done for security reasons. Even if some malicious hacker steals the DB, they won't be able to reset passwords of the users.

The password reset token generation algorithm is implemented inside the generatePasswordResetToken() method of UserManager. To generate a random string, we use the Rand class provided by Zend\Math component.

/**
 * Generates a password reset token for the user. This token is then stored in database and 
 * sent to the user's E-mail address. When the user clicks the link in E-mail message, he is 
 * directed to the Set Password page.
 */
public function generatePasswordResetToken($user)
{
    if ($user->getStatus() != User::STATUS_ACTIVE) {
        throw new \Exception('Cannot generate password reset token for inactive user ' . $user->getEmail());
    }
    
    // Generate a token.
    $token = Rand::getString(32, '0123456789abcdefghijklmnopqrstuvwxyz', true);
    
    // Encrypt the token before storing it in DB.
    $bcrypt = new Bcrypt();
    $tokenHash = $bcrypt->create($token);  
    
    // Save token to DB
    $user->setPasswordResetToken($tokenHash);
    
    // Save token creation date to DB.
    $currentDate = date('Y-m-d H:i:s');
    $user->setPasswordResetTokenCreationDate($currentDate);  
    
    // Apply changes to DB.
    $this->entityManager->flush();
    
    // Send an email to user.
    $subject = 'Password Reset';
        
    $httpHost = isset($_SERVER['HTTP_HOST'])?$_SERVER['HTTP_HOST']:'localhost';
    $passwordResetUrl = 'http://' . $httpHost . '/set-password?token=' . $token . "&email=" . $user->getEmail();
    
    // Produce HTML of password reset email
    $bodyHtml = $this->viewRenderer->render(
            'user/email/reset-password-email',
            [
                'passwordResetUrl' => $passwordResetUrl,
            ]);
    
    $html = new MimePart($bodyHtml);
    $html->type = "text/html";
    
    $body = new MimeMessage();
    $body->addPart($html);
    
    $mail = new Mail\Message();
    $mail->setEncoding('UTF-8');
    $mail->setBody($body);
    $mail->setFrom('no-reply@example.com', 'User Demo');
    $mail->addTo($user->getEmail(), $user->getFullName());
    $mail->setSubject($subject);
    
    // Setup SMTP transport
    $transport = new SmtpTransport();
    $options   = new SmtpOptions($this->config['smtp']);
    $transport->setOptions($options);

    $transport->send($mail);
}

Configuring mail system for your web server typically requires acquiring subscription of some mail service (like SendGrid or Amazon SES).

Password reset token validation is implemented inside the validatePasswordResetToken() method. We check that the token's hash is the same as we saved in database and that the token has not expired (it expires in 1 day since creation).

/**
 * Checks whether the given password reset token is a valid one.
 */
public function validatePasswordResetToken($email, $passwordResetToken)
{
    // Find user by email.
    $user = $this->entityManager->getRepository(User::class)
            ->findOneByEmail($email);
    
    if($user==null || $user->getStatus() != User::STATUS_ACTIVE) {
        return false;
    }
    
    // Check that token hash matches the token hash in our DB.
    $bcrypt = new Bcrypt();
    $tokenHash = $user->getPasswordResetToken();
    
    if (!$bcrypt->verify($passwordResetToken, $tokenHash)) {
        return false; // mismatch
    }
    
    // Check that token was created not too long ago.
    $tokenCreationDate = $user->getPasswordResetTokenCreationDate();
    $tokenCreationDate = strtotime($tokenCreationDate);
    
    $currentDate = strtotime('now');
    
    if ($currentDate - $tokenCreationDate > 24*60*60) {
        return false; // expired
    }
    
    return true;
}

And finally, the setPasswordByToken() allows to set new password for the user.

/**
 * This method sets new password by password reset token.
 */
public function setNewPasswordByToken($email, $passwordResetToken, $newPassword)
{
    if (!$this->validatePasswordResetToken($email, $passwordResetToken)) {
       return false; 
    }
    
    // Find user with the given email.
    $user = $this->entityManager->getRepository(User::class)
            ->findOneByEmail($email);
    
    if ($user==null || $user->getStatus() != User::STATUS_ACTIVE) {
        return false;
    }
            
    // Set new password for user        
    $bcrypt = new Bcrypt();
    $passwordHash = $bcrypt->create($newPassword);        
    $user->setPassword($passwordHash);
            
    // Remove password reset token
    $user->setPasswordResetToken(null);
    $user->setPasswordResetTokenCreationDate(null);
    
    $this->entityManager->flush();
    
    return true;
}

Top