Hashing a password upon saving a user record

What do we lack in our current scheme? Obviously, before saving a user record in the database, we need to compute the secure hash of the password provided and store the hashed value instead of the plaintext one. More than that, when updating the user record, we don't want to rehash the already hashed value in case we don't change the password at all.

Yii 2 (as well its predecessor, Yii 1.1.x) defines several methods for the instances of the ActiveRecord class, which we can override to do stuff at several predefined stages of the active record life. While you can read about them all in more detail in the Events of yiidbBaseActiveRecord section of Chapter 10, Events and Behaviors, we will be using one of them right now, the beforeSave() method. We will use it right before the active record is to be saved to the database.

It has the following default definition:

public function beforeSave($insert) { return true; }

The $insert argument passed to this method indicates whether the active record in question is a new one that is to be inserted into the database or if it's an already existing one that is to be updated into the database.

This method must return a Boolean value indicating whether this record is allowed to be saved. If not, it will not be saved, without any additional indication to the user or program. It's really important to remember this fact. If you don't return anything from this method, then a null will be implicitly returned, which is equal to false and you will effectively prohibit the relevant operation completely. We will be overriding this method in the UserRecord class for our purposes.

Additionally, Yii 2 has the special yiiaseSecurity application component that holds, among others, the helper methods for securely computing hashes for passwords. This component is accessible through the Yii::$app->security invocation. This is of great help to us, as we become relieved from the task of manually fiddling with the specifics of PHP's crypt() method. We are particularly interested in the Security::generatePasswordHash() and Security::validatePassword() methods.

Functional tests for password hashing

How do we check that a password is stored in the hashed form in the database? After saving a user record, we will simply check that a call to the Security::validatePassword() method returns true for a given plaintext and hash.

This feature doesn't need to be tested by the end-to-end acceptance test. We will be using the functional testing suite for it, which is already included by the Codeception framework for us since Chapter 2, Making a Custom Application with Yii 2, but is unused until now.

For functional (or, in different words, integration) testing in Codeception, a special module called Db exists, documentation for which can be found at http://codeception.com/docs/modules/Db. Its most important feature for us is the fact that before each test run it reverts the database to the state described in the tests/_data/dump.sql file, which does not exist yet. This SQL file should contain instructions about how to create the database schema. With the default toolset of MySQL 5 it can be generated as follows, given that your database doesn't require credentials and is named crmapp:

$ mysqldump -d crmapp > tests/_data/dump.sql

The -d flag means no data, only schema .

We need to configure the functional test suite in the tests/functional.suite.yml file like this (lines to be inserted are highlighted):

class_name: FunctionalTester
modules:
    enabled: [Db, Filesystem, FunctionalHelper]
    config:
      Db:
        dsn: 'mysql:host=localhost;dbname=crmapp'
        user: 'root'
        password: 'mysqlroot'
        dump: tests/_data/dump.sql

Of course, you need to insert your own hostname, database name, username, and password for a database on the deploy target. You also need to rebuild the Codeception suite using the ./cept build invocation.

So, generate the new functional test, as we will be testing the database:

$ ./cept generate:test functional PasswordHashing

Here's how we encode this test in the generated tests/functional/PasswordHashingTest.php file:

    /** @test */
    public function PasswordIsHashedWhenSavingUser()
    {
        $user = $this->imagineUserRecord();

        $plaintext_password = $user->password; //1

        $user->save();

        // Don't care about mutated model now, just fetch new one.
        $saved_user = UserRecord::findOne($user->id); //2

        $security = new yiiaseSecurity();
        $this->assertInstanceOf(get_class($user), $saved_user);
        $this->assertTrue(
            $security->validatePassword( // 3
                $plaintext_password,
                $saved_user->password
            )
        );
    }

The entire idea is expressed in highlighted lines of the preceding code:

  • At 1, the user is not stored in the database yet, and the password field still holds the plaintext value, which we save for future reference.
  • At 2, after the user is saved to the database it is assigned an ID. To imitate the temporal gap between creating a new user record and logging in, we fetch this user again from the database.
  • At 3, we test whether our initially generated plaintext version of the password can be successfully validated against the now hashed value, which is stored in the password field of the UserRecord class fetched from the database.

We don't really care here how exactly the password is hashed. All we want to know is whether validatePassword() will return true for the given plaintext and hash value.

We are imagining the user record the same way as in our acceptance tests for user management:

    private function imagineUserRecord()
    {
        $faker = FakerFactory::create();

        $user = new UserRecord();
        $user->username = $faker->word;
        $user->password = md5(time());
        return $user;
    }

This is obviously a case of code duplication here, and it should be refactored too.

This test will not run yet, though. If you run it right now, it will say that it cannot find the UserRecord model. This is because you don't have the appropriate autoloaders prepared in the Codeception harness. Fortunately, this harness includes special bootstrap scripts, which are executed before all of the tests in the given suite. So, we will utilize the tests/functional/_bootstrap.php file and put the require calls to our autoloaders in there:

require_once(__DIR__ . '/../../vendor/autoload.php'),
require_once(__DIR__ . '/../../vendor/yiisoft/yii2/Yii.php'),

new yiiwebApplication(
    require(__DIR__ . '/../../config/web.php')
);

Unfortunately, we need to create the full application for Yii autoloader to be initialized. We need it to get to our UserRecord class.

After the bootstrap file is in place, configuration is appropriately changed and the Codeception harness is rebuilt, the password hashing test should finally fail with the message Hash is invalid. It's time to actually implement password hashing.

Password hashing implementation inside the active record

To really implement this feature, you need the now-obvious method inside the UserRecord class:

    public function beforeSave($insert)
    {
        $return = parent::beforeSave($insert);

        $this->password = Yii::$app->security->generatePasswordHash($this->password);

        return $return;
    }

Well, turns out, it's not so obvious really. The highlighted part in the preceding code is the line we need. Lines before and after this line are framework-related code to preserve the original behavior of the beforeSave() method on the all the ActiveRecord instances. To be short, parent implementation of beforeSave() uses the concept of Yii 2 called Event. It triggers the particular event telling to whoever is listening in the ActiveRecord component that this record is being saved now. The point is that listeners can deny the event. You can read more about events in Chapter 10, Events and Behaviors, but here we don't really have any use of this system at all.

Note

Note that in our functional test, we generate the Security class instance manually. But in production code, we reuse the application component. In tests, it was done so because it's generally not acceptable that tests are dependent on some global state, which Yii singleton clearly is. In production code, it was done so because it's simply irrational to not use what is already here. However, if for some reason you have some custom-crafted component in your application named security, you will end up changing tests.

We now have a problem that was already mentioned before: when we save the UserRecord instance again, the password will be hashed again. Now, we have no way to know how this user will login, because he now needs to enter the hash of his original password in the password field in login form!

The problem is expressed in the following test:

    /** @test */
    public function PasswordIsNotRehashedAfterUpdatingWithoutChangingPassword()
    {
        $user = $this->imagineUserRecord();
        $user->save();

        /** @var UserRecord $saved_user */
        $saved_user = UserRecord::findOne($user->id);
        $expected_hash = $saved_user->password;

        $saved_user->username = md5(time());
        $saved_user->save();

        /** @var UserRecord $updated_user */
        $updated_user = UserRecord::findOne($saved_user->id);

        $this->assertEquals($expected_hash, $saved_user->password);
        $this->assertEquals($expected_hash, $updated_user->password);
    }

The difference between $saved_user->password and $updated_user->password in the assertions (at the end of the test) is that $saved_user at that point in time is the UserRecord instance modified in the memory, and $updated_user at that point in time is the UserRecord instance newly-fetched from the updated record in the database. We need to be sure we don't have mangled data in both cases.

Tip

Stateful programming is hard, and now we see it ourselves.

Fortunately for us, Yii 2 has a functionality integrated into its active records, which allows us to check whether any field was changed. You can choose among the following options:

Method of ActiveRecord

Usage

getOldAttributes()

This method will get the array of original values of attributes of the current active record since the last save() or find() call.

getDirtyAttributes($names = null)

This method will get the array of all the values of the attributes that were changed since the last save() or find() call. Basically, it returns those results from getOldAttributes() that are different from those returned by getAttributes(). You can specify the $names parameter mentioning which particular attributes you are interested in.

isAttributeChanged($name)

This method will check an attribute whether it was changed since the last save() or find() call.

markAttributeDirty($name)

This method will tell Yii that this particular attribute should be treated as changed irrelevant of whether it really is. This way, you can force the resaving of this attribute to the database.

The idea is that only values of "dirty" (changed) attributes are saved into the database when you call the save() method. Obviously, when you create a new record, all attributes are "dirty," so all of them will be saved to the database.

The method named isAttributeChanged() is exactly what we need. Just adding one line to the beforeSave() handler as follows makes our test pass:

    public function beforeSave($insert)
    {
        ...
        if ($this->isAttributeChanged('password'))
            $this->password = Security::generatePasswordHash($this->password);
        ...
    }

So, we don't unnecessarily rehash the password now. The backend of user management is now complete.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.176.166