Buy
Buy

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

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!

  • 2019-03-12 Victor Bocharsky

    Hey Mike,

    If you're on the newest PHP 7.3 - yes, use PASSWORD_ARGON2ID. See related RFC: https://wiki.php.net/rfc/ar... - it looks like Argon2 Password Hash Enhancements.

    Cheers!

  • 2019-03-12 Mike

    You said that Argon2i is recommended for PHP 7.2=<


    security:
    encoders:
    App\Entity\User:
    algorithm: argon2i


    Since Argon2id is implemented into PHP 7.3, is Argon2id now the recommended algorithm?

  • 2019-02-26 Diego Aguiar

    Hey Christina Vandendyck

    That's the normal behavior after a successful login in. Symfony automatically redirects you to the target path

    Cheers!

  • 2019-02-25 Christina Vandendyck

    Strange... Just after the post, I have a 302...

  • 2019-02-11 weaverryan

    Hey Rob!

    Woh! That is super strange! And... ya know... probably shouldn't happen ;). So let's see what we can figure out:

    A) First, do you see a stack trace when you run our of memory? I'd like to see *where* the error is happening. If you don't see a stack trace (you only see the error), installing XDebug might help.

    B) When you say "My browser locks up" - what do you mean exactly? Does it really freeze or lock up? Or just a 500 error?

    My first instinct is that there is possibly some accidental recursion (e.g. you're calling the same method that you're inside of) - but that's just a wild guess. I would try commenting out some code until you can get the memory error to go away - so that we can figure out where it's coming from!

    Let me know what you find out!

    Cheers!

  • 2019-02-09 Rob

    Hi SymfonyCasts Crew!

    I was rolling along perfect with Symfony Security: Beautiful Authentication, Powerful Authorization until Chapter 12 - Checking the User’s Password.

    When I set up the password field in the User entity and try the Login functions, I’m getting the PHP Fatal error in PhpStorm: Allowed memory size of 268435456 bytes exhausted (tried to allocators 262144 bytes …. Blah blah fun stuff! My browser locks up with HTTP 500 error…!

    I know has nothing with the tutorial code but rather a problem with my AMP setup, but I’m wondering if you might have some tips on how to correct for this error. I’m on iMac with Mohave running PHP version 7.1.19. My php.ini has memory_limit set to 256mb and using PhpStorm with built in server.

    Not sure if anyone has run across this type of memory error but any ideas for a fix would be welcome! Thanks in advance! Rob

  • 2019-01-08 Diego Aguiar

    Hmm, interesting. Which command are you running? Try re-creating your whole DB

  • 2019-01-08 mike

    Actually my table is empty anyhow.

  • 2019-01-07 Diego Aguiar

    Hey @mike

    The thing is that you already have some users loaded in your DB, so you can't add a new "not nullable" field to that table, you have to truncate it first (If it were a production table, then, you would have to do a three steps migration: add column, update existing records with a default value, alter column to not nullable)

    Cheers!

  • 2019-01-07 mike

    Hi,
    the faker isn't filling the database and also this error is being shown
    An exception occurred while executing 'ALTER TABLE user ADD COLUMN password VARCHAR(255) NOT NULL':

    SQLSTATE[HY000]: General error: 1 Cannot add a NOT NULL column with default value NULL
    while following exactly your steps

  • 2018-12-31 Diego Aguiar

    Hey Nassim Bennouna

    We're glad to know that you are liking our tutorials :)

    Back to your question, the logout functionality is handled by Symfony by default but you have to activate it, you only have to specify the logout option to `true` (the quick way)


    // config/packages/securityl.yaml
    security:
    firewalls:
    main:
    logout: true

    You can find more info at the docs: https://symfony.com/doc/cur...

    Cheers & Happy new year!!

  • 2018-12-29 Nassim Bennouna

    Hello !
    First things first, THANK YOU for all your work, it is just awesome !

    Now the question ;-)
    Is it me missing something or we actually do not logout, when reaching login, nor on failed login attempt ?
    Especially if there is already a session for the failed email ?
    I tried to invalidate session but it appears to be a abd idea as it makes fail the csrf check

    So , what do you think is the best way to handle that ?

    Thank you in advance for your advice !

  • 2018-11-16 Peter Tsiampas

    Well now I feel a little silly! My schedule was cleared so I started to debug this problem.

    It would probably help if was actually returning the value from getPassword() inside the user controller. Mind you with your help, it was pretty easy to debug when I started to drill down.

    Thanks Ryan

  • 2018-11-09 christianstrang

    It worked for me as well and my assumption is, that Symfony will check the type of hashing algorithm used by the password-hash stored in the database ($2y$13 for bcrypt) and then use this encoder instead and will use the argon2i only for newly created user passwords. It also worked in the opposite direction (changing the encoders back from argon2i to bcrypt).

    This is quite nice, so you can use bcrypt now and if you update your encoders later to argon2i, your bcrypt-passwords will still work. However, you should still upgrade the bcrypt passwords to the new algorithm after a successful login (at this point you have the cleartext password and can upgrade the hash using argon2i without the user even noticing).

  • 2018-11-08 weaverryan

    Hey Peter Tsiampas!

    > I was having a "WHaaaaaT" moment.

    LOL. That makes *perfect* sense :D. So, you're kinda right - there IS some extra crazy stuff happening inside the encode function: the "salt" is being auto-generated FOR you. Each time you say "encode the password 1234", argon2i (and also bcrypt) generates a new, random salt, uses THAT to hash the password, and then the salt is stored inside the encoded string itself. So yes, every time you encode, you will get a different result. When the password validity is checked, internally, the salt is "read" from the encoded password, and the 1234 is encoded using that *exact* salt. It's hard to notice this because, from the outside, you never see anything related to the salt. But, under the hood, encode always generates a new salt and the "is this password valid" function is smart enough to encode the submitted password using the *existing* salt.

    That doesn't necessarily solve your problem... but hopefully you will have less WHaaaaaT moments ;).

    And thanks for the SUPER nice note! I'm SO happy you like the tone. Learning this stuff is a lot of work, but it's also fun to unlock everything!

    Cheers!

  • 2018-11-07 Diego Aguiar

    Awesome! Let's keep learning :D

  • 2018-11-07 Victor Le Fourn

    I finally figured out what the problem was !
    I just forgot to override the getPassword method.
    Now it works perfectly :)

  • 2018-11-07 Victor Le Fourn

    Hi Diego !
    I re-run the command doctrine:fixtures:load and checked that each user had a password and a username but when I try to dump($user->getPassword()) in the checkCredentials function, I get null as a response.

  • 2018-11-06 Peter Tsiampas

    What I was doing is a dump of the encoded password (which is simple like 1234) then I was seeing what was being stored in the database and they were both different.

    I know with hashes that when you use the same salt+password it will always give you back the same hash, so feeding the encode function the same values, I expected it to always be the same, but it kept changing! Which was doing my head in and was having a "WHaaaaaT" moment.

    The only other thing I can think of is the encode function is including something extra like say "salt+password+userid" or something crazy like that, good for security, bad for my brain if I can't see it.

    I will do some more research using the tools you mentioned above and see what I have done wrong.

    Thanks for the reply Ryan

    Also on a personal note! I love your screen casts, I don't know how you do it keeping such an awesome happy delivery of content every screen cast, but that's what keeps me subscribed to your stuff

  • 2018-11-06 Diego Aguiar

    Hey Victor Le Fourn

    Did you run the fixtures? Could you double check that the user credentials you are submitting exist on the Database?

    Cheers!

  • 2018-11-06 weaverryan

    Hey Peter Tsiampas!

    Ah, interesting! Debugging these issues can be a pain - because... all the passwords are encrypted, so it's tough to see what's going on!

    > the password keeps changing (meaning the encoded password for the match)

    Where are you seeing this exactly? Are you looking in this function? https://github.com/symfony/... I'm asking because, as you can see, WE never see the plaintext password get encoded and then compared directly to the one in the database (this is on purpose, it can cause timing attacks). We just see the , for example, \Sodium\crypto_pwhash_str_verify($encoded, $raw) method called, which handles all of this behind the scenes.

    Anyways, when I hit these situations, I sometimes use an online tool - like https://argon2-generator.com/ - to help me figure out what's going on. You could use the "Decrypt" to see if the hash in the database matches the expected plain password. Oh, and by the way, if you use the "Encrypt" functionality to encrypt a plain text password, it WILL be different every time you do this. That's because Argon creates a unique "salt" each time, and this is stored inside the hash itself. When we check to see if a plain password matches this, the hashed password's salt is read, and this salt is used to encrypt the plain password. THEN the values will match.

    Let me know what you find out!

    Cheers!

  • 2018-11-06 Victor Le Fourn

    I have an authenticator problem.
    When I try different passwords in my login page, none of them passes the authenticator, even with "engage".

    I tried to copy and paste the code for LoginFormAuthenticator from the folder "finish" but it still doesn't work.
    Can you help me ?

  • 2018-11-06 Peter Tsiampas

    I am having an odd problem with the argon2i algorithm, clearly I must of missed something but I checked all the code pieces and they seem correct.

    $this->passwordEncoder->isPasswordValid($user, $credentials['password']);

    Keeps reporting false, I did some digging to see what was going on and the password keeps changing (meaning the encoded password for the match) so it never matches.

    Has anyone else run into this?

  • 2018-10-19 Victor Bocharsky

    Hey Serge,

    Ah, really, "final class" thing, thanks sharing this information with others!

    Cheers!

  • 2018-10-19 Serge Boyko

    Good guess, but unfortunately all of them have this line...

    After spending some time digging this issue I found the defining difference!
    Files with that "white thing" have "final class" keyword in them.

  • 2018-10-19 Victor Bocharsky

    Hey Serge,

    Can't you find differences between migrations with that white thing and ones without it? :) I suppose in those last 2 migrations you have "declare(strict_types=1);", but not sure 100%.

    Cheers!

  • 2018-10-19 Serge Boyko

    Question about PhpStorm.
    Do you know what this white thing in the top left corner of two latest migration files mean?
    https://www.dropbox.com/s/c...

  • 2018-10-08 weaverryan

    Hey Ahmad Mayahi!

    Hmmm, yes they should stop working :). Here is the "flow" and why it should not work:

    1) You configure bcrypt to be used
    2) You then create a few users (e.g. via the fixtures, but it doesn't matter). These password field on these users is now a bcrypt-encoded salt+string
    3) You change to argon2i
    4) When you try to login, the password encode (which will now be using argon2i) will fail to decode your password.

    Are you sure you didn't reload the fixtures after changing the algorithm? I could totally be wrong about the above "flow", but I'm *pretty* sure that there's not any magic where argon2i can understand the the bcrypt algorithm. Let me know!

    Cheers!

  • 2018-10-07 Ahmad Mayahi

    I changed the algorithm from bcrypt to argon2i, but the existing logins still work, shouldn't they stop?

  • 2018-09-26 Peter Kosak

    Thanks I have missed it. I will shift my logic there.

  • 2018-09-26 Victor Bocharsky

    Hey Peter,

    As you understand by yourself, checkCredentials() is far from an ideal place to do so :) So yes, I'd better do it in onAuthenticationSuccess(). However, you're not correct about the fact you can't get logged in user inside it, take a look at $token variable that implements TokenInterface. If you look at it, you will find getUser() method, so you can just call: "$token->getUser()" in onAuthenticationSuccess() to get current logged in user, and then you can set lastLoginDate on it and flush(), i.e. you don't need to pass the user with setLoggedInUser() method between methods.

    Cheers!

  • 2018-09-26 Peter Kosak

    Hello, it's me
    I was wondering if after all these years you'd like to meet
    To go over everything.

    Sorry, I couldnt help myself :-D but reminds me hello song from adele anyway.

    I just want to double check if this is a proper approach and if its ok to have more logic inside of checkCredentials method.

    I would like to update last login date for user; only if credentials are correct so I am thinking to have something like this:


    public function checkCredentials($credentials, UserInterface $user)
    {
    if($this->passwordEncoder->isPasswordValid($user, $credentials['password'])){
    $user = $this->manager->getRepository(User::class)->findOneBy([
    'id' => $user->getId()
    ]);
    $user->setLastLoginDate(new \DateTime());
    $this->manager->persist($user);
    $this->manager->flush();
    $this->setLoggedInUser($user);
    return true;
    }
    return false;
    }


    this works but then I realise there is


    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
    dd($this->getLoggedInUser());
    return new RedirectResponse($this->router->generate('welcome'));
    }


    this method that would be actually better place to have it because I dont have to check if user pw is valid but also I could query user object in order to redirect user to correct route based on his preferences.

    It is strange that user has been authenticated but there is no easy way to access his properties easily from onAuthenticationSuccess method so I have to setLoggedInUser($user) in checkCredential() and then getLoggedInUser() in authenticationSuccess but there is getUser method that will return user but then we need to pass credential so it starting to be complicated and left me confused.

    So the question: is my approach correct to update last login date for authenticated users and the way of getting user object into onAuthenticationSuccess method?

  • 2018-09-24 Victor Bocharsky

    Hey Stephane!

    Ah, good catch! I fixed it in https://github.com/knpunive...

    Thank you for reporting this misprint!

    Cheers!

  • 2018-09-22 Stéphane

    Hello,

    You forget the n in $this->passwordEcoder->encodePassword()