Adding & Checking the User's Password

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Until now, we've allowed users to login without any password. As much fun as it would be to deploy this to production... I think we should probably fix that. If you look at your User class, our users actually don't have a password field at all:

... lines 1 - 10
class User implements UserInterface
{
... lines 13 - 17
private $id;
... lines 19 - 22
private $email;
... lines 24 - 27
private $roles = [];
... lines 29 - 32
private $firstName;
... lines 34 - 116
}

When you originally use the make:user command, you can tell it to create this field for you. We told it to not do this... just to keep things simpler as we were learning. So, we'll do it now.

Adding the password Field to User

Find your terminal and run:

php bin/console make:entity

Update the User class to add a new field called password. Make it a string with length 255. It doesn't need to be quite that long, but that's fine. Can it be null? Say no: in our system, each user will always have a password.

And... done! It updated the User.php file, but it did not generate the normal getPassword() method because we already had that method before. We'll check that out in a minute.

Before that, run:

php bin/console make:migration

Move over and check out the Migrations directory. Open the new file and... yes! It looks perfect: ALTER TABLE user ADD password:

... lines 1 - 10
final class Version20180831181732 extends AbstractMigration
{
public function up(Schema $schema) : void
{
... lines 15 - 17
$this->addSql('ALTER TABLE user ADD password VARCHAR(255) NOT NULL');
}
public function down(Schema $schema) : void
{
... lines 23 - 25
$this->addSql('ALTER TABLE user DROP password');
}
}

Close that, go back to your terminal, and migrate:

php bin/console doctrine:migrations:migrate

Awesome!

Updating the User Class

Go open the User class. Yep - we now have a password field:

... lines 1 - 10
class User implements UserInterface
{
... lines 13 - 34
/**
* @ORM\Column(type="string", length=255)
*/
private $password;
... lines 39 - 128
}

And all the way at the bottom, a setPassword() method:

... lines 1 - 10
class User implements UserInterface
{
... lines 13 - 122
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
}

Scroll up to find getPassword():

... lines 1 - 10
class User implements UserInterface
{
... lines 13 - 85
/**
* @see UserInterface
*/
public function getPassword()
{
// not needed for apps that do not check user passwords
}
... lines 93 - 128
}

This already existed from back when our user had no password. Now that it does, return $this->password:

... lines 1 - 10
class User implements UserInterface
{
... lines 13 - 85
/**
* @see UserInterface
*/
public function getPassword()
{
return $this->password;
}
... lines 93 - 128
}

Oh, and just to be clear, this password will not be a plain-text password. No, no, no! The string that we store in the database will always be properly salted & encoded. In fact, look at the method below this: getSalt():

... lines 1 - 10
class User implements UserInterface
{
... lines 13 - 93
/**
* @see UserInterface
*/
public function getSalt()
{
// not needed for apps that do not check user passwords
}
... lines 101 - 128
}

In reality, there are two things you need to store in the database: the encoded password and the random salt value that was used to encode the password.

But, great news! Most modern encoders - including the one we will use - store the salt value as part of the encoded password string. In other words, we only need this one field. And, the getSalt() method can stay blank. I'll update the comment to explain why:

... lines 1 - 10
class User implements UserInterface
{
... lines 13 - 93
/**
* @see UserInterface
*/
public function getSalt()
{
// not needed when using bcrypt or argon
}
... lines 101 - 128
}

I love doing no work!

Configuring the Encoder

Symfony will take care of all of this password encoding stuff for us. Nice! We just need to tell it which encoder algorithm to use. Go back to security.yaml. Add one new key: encoders. Below that, put the class name for your User class: App\Entity\User. And below that, set algorithm to bcrypt:

security:
encoders:
App\Entity\User:
algorithm: bcrypt
... lines 5 - 44

Tip

In Symfony 4.3, you should use auto as your algorithm. This will use the best possible algorithm available on your system.

There are at least two good algorithm options here: bcrypt and argon2i. The argon2i encoder is actually a bit more secure. But, it's only available on PHP 7.2 or by installing an extension called Sodium.

If you and your production server have this available, awesome! Use it. If not, use bcrypt. Just know that once you start encoding passwords, changing algorithms in the future is a pain.

Oh, and for both encoders, there is one other option you can configure: cost. A higher cost makes passwords harder to crack... but will take more CPU. If security is really important for your app, check out this setting.

Anyways, thanks to this config, Symfony can now encrypt plaintext passwords and check whether a submitted password is valid.

Encoding Passwords

Open the UserFixture class because first, we need to populate the new password field in the database for our dummy users.

To encode a password - surprise! - Symfony has a service! Find your terminal and run our favorite:

php bin/console debug:autowiring

Search for "password". There it is! UserPasswordEncoderInterface. This service can encode and check passwords. Back in UserFixture, add a constructor with one argument: UserPasswordEncoderInterface. I'll re-type the "e" and hit tab to autocomplete and get the use statement I need on top. Call it $passwordEncoder:

... lines 1 - 6
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
class UserFixture extends BaseFixture
{
... lines 11 - 12
public function __construct(UserPasswordEncoderInterface $passwordEncoder)
{
... line 15
}
... lines 17 - 34
}

Press Alt+Enter and select initialize fields to create that property and set it:

... lines 1 - 8
class UserFixture extends BaseFixture
{
private $passwordEncoder;
public function __construct(UserPasswordEncoderInterface $passwordEncoder)
{
$this->passwordEncoder = $passwordEncoder;
}
... lines 17 - 34
}

Now... the fun part: $user->setPassword(). But, instead of setting the plain password here - which would be super uncool... - say $this->passwordEncoder->encodePassword():

... lines 1 - 8
class UserFixture extends BaseFixture
{
... lines 11 - 17
protected function loadData(ObjectManager $manager)
{
$this->createMany(10, 'main_users', function($i) {
... lines 21 - 24
$user->setPassword($this->passwordEncoder->encodePassword(
... lines 26 - 27
));
... lines 29 - 30
});
... lines 32 - 33
}
}

This needs two arguments: the $user object and the plain-text password we want to use. To make life easier for my brain, we'll use the same for everyone: engage:

... lines 1 - 8
class UserFixture extends BaseFixture
{
... lines 11 - 17
protected function loadData(ObjectManager $manager)
{
$this->createMany(10, 'main_users', function($i) {
... lines 21 - 24
$user->setPassword($this->passwordEncoder->encodePassword(
$user,
'engage'
));
... lines 29 - 30
});
... lines 32 - 33
}
}

That's it! The reason we need to pass the User object as the first argument is so that the password encoder knows which encoder algorithm to use. Let's try it: find your terminal and reload the fixtures:

php bin/console doctrine:fixtures:load

You might notice that this is a bit slower now. By design, password encoding is CPU-intensive. Ok, check out the database!

php bin/console doctrine:query:sql 'SELECT * FROM user'

Awesome! Beautiful, encoded passwords. The bcrypt algorithm generated a unique salt for each user, which lives right inside this string.

Checking the Password

Ok, just one more step - and it's an easy one! We need to check the submitted password in LoginFormAuthenticator. This is the job of checkCredentials():

... lines 1 - 17
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 20 - 63
public function checkCredentials($credentials, UserInterface $user)
{
// only needed if we need to check a password - we'll do that later!
return true;
}
... lines 69 - 78
}

We already know which service can do this. Add one more argument to your constructor: UserPasswordEncoderInterface $passwordEncoder. Hit Alt+Enter to initialize that field:

... lines 1 - 9
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
... lines 11 - 18
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 21 - 23
private $passwordEncoder;
public function __construct(UserRepository $userRepository, RouterInterface $router, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder)
{
... lines 28 - 30
$this->passwordEncoder = $passwordEncoder;
}
... lines 33 - 80
}

Then down in checkCredentials(), return $this->passwordEncoder->isPasswordValid() and pass this the User object and the raw, submitted password... which we're storing inside the password key of $credentials:

... lines 1 - 18
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 21 - 66
public function checkCredentials($credentials, UserInterface $user)
{
return $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
}
... lines 71 - 80
}

And.. we're done! Time to celebrate by trying it! Move over, but this time put "foo" as a password. Login fails! Try engage. Yes!

Next: it's finally time to start talking about how we deny access to certain parts of our app. We'll start off that topic with a fun feature called access_control.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
        "knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
        "knplabs/knp-time-bundle": "^1.8", // 1.8.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.2.0
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.1.4
        "symfony/console": "^4.0", // v4.1.4
        "symfony/flex": "^1.0", // v1.8.4
        "symfony/framework-bundle": "^4.0", // v4.1.4
        "symfony/lts": "^4@dev", // dev-master
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.1.4
        "symfony/serializer-pack": "^1.0", // v1.0.1
        "symfony/twig-bundle": "^4.0", // v4.1.4
        "symfony/web-server-bundle": "^4.0", // v4.1.4
        "symfony/yaml": "^4.0", // v4.1.4
        "twig/extensions": "^1.5" // v1.5.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.7
        "fzaninotto/faker": "^1.7", // v1.8.0
        "symfony/debug-bundle": "^3.3|^4.0", // v4.1.4
        "symfony/dotenv": "^4.0", // v4.1.4
        "symfony/maker-bundle": "^1.0", // v1.7.0
        "symfony/monolog-bundle": "^3.0", // v3.3.0
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.1.4
        "symfony/profiler-pack": "^1.0", // v1.0.3
        "symfony/var-dumper": "^3.3|^4.0" // v4.1.4
    }
}