Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Users Need Passwords (plainPassword)

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

I just found out that giving everyone the same password - iliketurtles - is apparently not a great security system. Let's give each user their own password. Again, in your security setup, you might not be responsible for storing and checking passwords. Skip this if it doesn't apply to you.

In User, add a private $password that will eventually store the encoded password. Give it the @ORM\Column annotation:

... lines 1 - 12
class User implements UserInterface
{
... lines 15 - 26
/**
* The encoded password
*
* @ORM\Column(type="string")
*/
private $password;
... lines 33 - 73
}

Now, remember the three methods from UserInterface that we left blank?

... lines 1 - 12
class User implements UserInterface
{
... lines 15 - 37
public function getPassword()
{
// leaving blank - I don't need/have a password!
}
public function getSalt()
{
// leaving blank - I don't need/have a password!
}
public function eraseCredentials()
{
// leaving blank - I don't need/have a password!
}
... lines 52 - 61
}

It's finally their time to shine. In getPassword(), return $this->password:

... lines 1 - 12
class User implements UserInterface
{
... lines 15 - 44
public function getPassword()
{
return $this->password;
}
... lines 49 - 73
}

But keep getSalt() blank: we're going to use the bcrypt algorithm, which has a built-in mechanism to salt passwords.

Use the "Code"->"Generate" menu to generate the setter for password:

... lines 1 - 12
class User implements UserInterface
{
... lines 15 - 69
public function setPassword($password)
{
$this->password = $password;
}
}

And next, go make that migration:

./bin/console doctrine:migrations:diff

I should check that file, but let's go for it:

./bin/console doctrine:migrations:migrate

Perfect.

Handling the Plain Password

Here's the plan: we'll start with a plain text password, encrypt it through the bcrypt algorithm and store that on the password property.

How? The best way is to set the plain-text password on the User and encode it automatically via a Doctrine listener when it saves.

To do that, add a new property on User called plainPassword. But wait! Don't persist this with Doctrine: we will of course never store plain-text passwords:

... lines 1 - 12
class User implements UserInterface
{
... lines 15 - 33
/**
* A non-persisted field that's used to create the encoded password.
*
* @var string
*/
private $plainPassword;
... lines 40 - 90
}

This is just a temporary-storage place during a single request.

Next, at the bottom, use Command+N or the "Code"->"Generate" menu to generate the getters and setters for plainPassword:

... lines 1 - 12
class User implements UserInterface
{
... lines 15 - 81
public function getPlainPassword()
{
return $this->plainPassword;
}
public function setPlainPassword($plainPassword)
{
$this->plainPassword = $plainPassword;
}
}

Forcing User to look Dirty?

Inside setPlainPassword(), do one more thing: $this->password = null:

... lines 1 - 12
class User implements UserInterface
{
... lines 15 - 86
public function setPlainPassword($plainPassword)
{
$this->plainPassword = $plainPassword;
// forces the object to look "dirty" to Doctrine. Avoids
// Doctrine *not* saving this entity, if only plainPassword changes
$this->password = null;
}
}

What?! Yep, this is important. Soon, we'll use a Doctrine listener to read the plainPassword property, encode it, and update password. That means that password will be set to a value before it actually saves: it won't remain null.

So why add this weird line if it basically does nothing? Because Doctrine listeners are not called if Doctrine thinks that an object has not been updated. If you eventually create a "change password" form, then the only property that will be updated is plainPassword. Since this is not persisted, Doctrine will think the object is "un-changed", or "clean". In that case, the listeners will not be called, and the password will not be changed.

But by adding this line, the object will always look like it has been changed, and life will go on like normal.

Anyways, it's a necessary little evil.

Finally, in eraseCredentials(), add $this->plainPassword = null:

... lines 1 - 12
class User implements UserInterface
{
... lines 15 - 61
public function eraseCredentials()
{
$this->plainPassword = null;
}
... lines 66 - 93
}

Symfony calls this after logging in, and it's just a minor security measure to prevent the plain-text password from being accidentally saved anywhere.

The User object is perfect. Let's add the listener.

Leave a comment!

16
Login or Register to join the conversation
Default user avatar
Default user avatar Blueblazer172 | posted 5 years ago

btw is bcrypt still safe ?
and are there any other possibilities to salt the password ?

Reply

Yo Hannes,

Yes, it's the best encrypt algorithm which is suggested by Symfony: http://symfony.com/doc/curr...

And yes, you can use salt to make your encrypted password more robust - just generate salt in User::__construct() and tweak getSalt() method to return it.

Cheers!

Reply
Default user avatar
Default user avatar Blueblazer172 | Victor | posted 5 years ago | edited

victor Thanks for that :) btw awasome tuts Ryan. Totaly worth every penny :) Really like your way of teaching :-) Best instructor for learning Symfony the *funny* and safe way

Reply
Default user avatar
Default user avatar phphozpter | posted 5 years ago

Hi,

The password was changing every time I update the entity. Even though the password is not included in the form.

Reply

Hi Jayson,

Are you coding from scratch? Or are you in the "finish/" directory of downloaded course code? Please, search for "setPlainPassword" and "setPassword" calls in your code base and ensure you call these methods in right places. Probably you did some extra calls of these methods for debugging and forget to remove it later.

Cheers!

Reply
Default user avatar
Default user avatar phphozpter | Victor | posted 5 years ago

Hi Victor,

Thanks for reply, It works now.

Reply

Thank you for this amazing tuto <3
I've noticed that you didn't use the salt mécanisme, I don't know why? all what I know is the primary function of salts is to defend against dictionary attacks or against its hashed equivalent, a pre-computed rainbow table attack.
So we just use salts to encode our password for more sécurity.

I'm asking how we can use salts ? maybe, some explanation about the mécanisme ...

Reply

Hi ahmedbhs!

Ah, very good question! I'm not using the salt for one very important reason: in security.yml, we configure out encoder to us the bcrypt algorithm. This algorithm does salting automatically: it includes the a salt automatically in the encoded password. So, we're returning null from getSalt() only because this encode does not require us to pass it a salt, it computes the salt automatically (which I believe is more secure anyways).

So, it's good news! You are using a salt... but you didn't need to do any work for it ;).

Cheers!

1 Reply
Default user avatar
Default user avatar Ángel Manuel Marqués Ruiz | posted 5 years ago

I've noticed that setting password to null in setPlainPassword method may lead to unwanted behaviour:
https://symfony.com/doc/cur...

It makes the constraint UserPassword to always return incorrect password in my change password form:
https://justpaste.it/19pfj

Is there any work around?

Reply

Hey Ángel Manuel Marqués Ruiz

Great question, and you are right, it may lead to weird behaviour, so you could add a "updatedAt" field to your entity, and change that field

Cheers!

Reply
Default user avatar
Default user avatar Moises Cano | posted 5 years ago

After we define $plainPassword, then we can never migrate this class without Doctrine adding a plainpassword column to our User table?

Reply

Hey Moises,

Note that we do not add @ORM\Column() annotation for plainPassword field, so the column won't be added to the User table.

Cheers!

Reply
Default user avatar
Default user avatar Moises Cano | Victor | posted 5 years ago | edited

Oh yea. I see that now. Is there another video showing how to edit a user information? I tried copying the addAction and added this....


public function editAction(Request $request, User $user)
    {
        $form = $this->createForm(UserRegistrationForm::class, $user);

        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $user = $form->getData();

            $em = $this->getDoctrine()->getManager();
            $em->persist($user);
            $em->flush();

            $this->addFlash('success', $user->getUsername(). ' Updated');

            return $this->redirectToRoute('homepage');
        }

        return $this->render('user/edit.html.twig', [
            'form' => $form->createView()
        ]);
    }

I get an error Unable to guess how to get a Doctrine instance from the request information for parameter "user".

Reply

Hey Moises Cano

Can you show me how you defined your Route ? Look's like you are missing the User's wildcard

Cheers!

Reply
Default user avatar

/**
* @Route("/edit/{usrid}", name="user_edit")
*/

Reply

Cool, you are not missing it, but, in order to work, it must be the name of an unique property (e.g. id, slug). So just change "usrid" to "id" or to whatever you named your id property on User class

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9",
        "symfony/symfony": "3.1.*", // v3.1.4
        "doctrine/orm": "^2.5", // v2.7.2
        "doctrine/doctrine-bundle": "^1.6", // 1.6.4
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.3.11
        "symfony/monolog-bundle": "^2.8", // 2.11.1
        "symfony/polyfill-apcu": "^1.0", // v1.2.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.4", // 1.4.2
        "doctrine/doctrine-migrations-bundle": "^1.1" // 1.1.1
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.0.7
        "symfony/phpunit-bridge": "^3.0", // v3.1.3
        "nelmio/alice": "^2.1", // 2.1.4
        "doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
    }
}
userVoice