Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Giving Users Passwords

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

Symfony doesn't really care if the users in your system have passwords or not. If you're building a login system that reads API keys from a header, then there are no passwords. The same is true if you have some sort of SSO system. Your users might have passwords... but they enter them on some other site.

But for us, we do want each user to have a password. When we used the make:user command earlier, it actually asked us if we wanted our users to have passwords. We answered no... so that we could do all of this manually. But in a real project, I would answer "yes" to save time.

PasswordAuthenticatedUserInterface

We know that all User classes must implement UserInterface:

... lines 1 - 7
use Symfony\Component\Security\Core\User\UserInterface;
... lines 9 - 12
class User implements UserInterface
{
... lines 15 - 130
}

Then, if you need to check user passwords in your application, you also need to implement a second interface called PasswordAuthenticatedUserInterface:

... lines 1 - 6
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
... lines 8 - 12
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 15 - 128
}

This requires you to have one new method: getPassword().

If you're using Symfony 6, you won't have this yet, so add it:

... lines 1 - 12
class User implements UserInterface
{
... lines 15 - 90
/**
* This method can be removed in Symfony 6.0 - is not needed for apps that do not check user passwords.
*
* @see PasswordAuthenticatedUserInterface
*/
public function getPassword(): ?string
{
return null;
}
... lines 100 - 130
}

I do have it because I'm using Symfony 5 and the getPassword() method is needed for backwards compatibility: it used to be part of UserInterface.

Now that our users will have a password, and we're implementing PasswordAuthenticatedUserInterface, I'm going to remove this comment above the method:

... lines 1 - 12
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 15 - 90
/**
* @see PasswordAuthenticatedUserInterface
*/
public function getPassword(): ?string
{
return null;
}
... lines 98 - 128
}

Storing a Hashed Password for each User

Ok, let's forget about security for a minute. Instead, focus on the fact that we need to be able to store a unique password for each user in the database. This means that our user entity needs a new field! Find your terminal and run:

symfony console make:entity

Let's update the User entity, to add a new field call password... which is a string, 255 length is overkill but fine... and then say "no" to nullable. Hit enter to finish.

Back over in the User class, it's... mostly not surprising. We have a new $password property... and at the bottom, a new setPassword() method:

... lines 1 - 12
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 15 - 36
/**
* @ORM\Column(type="string", length=255)
*/
private $password;
... lines 41 - 134
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
}

Notice that it did not generate a getPassword() method... because we already had one. But we do need to update this to return $this->password:

... lines 1 - 12
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 15 - 98
public function getPassword(): ?string
{
return $this->password;
}
... lines 103 - 140
}

Very important thing about this $password property: it is not going to store the plaintext password. Never ever store the plaintext password! That's the fastest way to have a security breach... and lose friends.

Instead, we're going to store a hashed version of the password... and we'll see how to generate that hashed password in a minute. But first, let's make the migration for the new property:

symfony console make:migration

Go peek at that file to make sure everything looks good:

... lines 1 - 12
final class Version20211001185505 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE user ADD password VARCHAR(255) NOT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE user DROP password');
}
}

Tip

If you are using PostgreSQL, you should modify your migration. Add DEFAULT '' at the end so that the new column can be added without an error:

$this->addSql('ALTER TABLE product ADD description VARCHAR(255) NOT NULL DEFAULT \'\'');

And... it does! Close it... and run it:

symfony console doctrine:migrations:migrate

The password_hashers Config

Perfect! Now that our users have a new password column in the database, let's populate that in our fixtures. Open up src/Factory/UserFactory.php and find getDefaults().

Again, what we are not going to do is set password to the plain-text password. Nope, that password property needs to store the hashed version of the password.

Open up config/packages/security.yaml. This has a little bit of config on top called password_hashers, which tells Symfony which hashing algorithm it should use for hashing user passwords:

security:
... lines 2 - 6
# https://symfony.com/doc/current/security.html#c-hashing-passwords
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
... lines 10 - 39

This config says that any User classes that implement PasswordAuthenticatedUserInterface - which our class, of course, does - will use the auto algorithm where Symfony chooses the latest and greatest algorithm automatically.

The Password Hasher Service

Thanks to this config, we have access to a "hasher" service that's able to convert a plaintext password into a hashed version using this auto algorithm. Back inside UserFactory, we can use that to set the password property:

... lines 1 - 28
final class UserFactory extends ModelFactory
{
... lines 31 - 37
protected function getDefaults(): array
{
return [
... lines 41 - 42
'plainPassword' => 'tada',
];
}
... lines 46 - 58
}

In the constructor, add a new argument: UserPasswordHasherInterface $passwordHasher. I'll hit Alt+Enter and go to "Initialize properties" to create that property and set it:

... lines 1 - 6
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
... lines 8 - 29
final class UserFactory extends ModelFactory
{
private UserPasswordHasherInterface $passwordHasher;
public function __construct(UserPasswordHasherInterface $passwordHasher)
{
parent::__construct();
$this->passwordHasher = $passwordHasher;
}
... lines 40 - 67
}

Below, we can set password to $this->passwordHasher->hashPassword() and then pass it some plain-text string.

Well... to be honest... while I hope this makes sense on a high level... this won't quite work because the first argument to hashPassword() is the User object... which we don't have yet inside getDefaults().

That's ok because I like to create a plainPassword property on User to help make all of this easier anyways. Let's add that next, finish the fixtures and update our authenticator to validate the password. Oh, but don't worry: that new plainPassword property won't be stored in the database.

Leave a comment!

13
Login or Register to join the conversation
Rufnex Avatar

Hello,

the auth system requires a password field with the name password for the query in the corresponding table. How do I proceed if the password field should be called e.g. usr_password.

thx!

Reply

HI @Rufnex,

Eeasy-peasy, you can configure everything in security.yaml here the docs https://symfony.com/doc/current/reference/configuration/security.html#reference-security-firewall-form-login

Cheers!

Reply
Rufnex Avatar

Hi sadikoff,

thank you .. thats clear. What I meant I, in the database table the password field is called usr_password and not password. Is it possible to configure that too?

Thank you again.

Reply

Of course, you have total control over your Entity. You can use name property on #[ORM\Column()] attribute, and many overs BTW :-)

Cheers

1 Reply
Rufnex Avatar

OMG . thats fu*** easy! Thank you ;o)

Reply
triemli Avatar
triemli Avatar triemli | posted 1 year ago

After changing nullable field to "not null", probably needs to add DEFAULT value in the migrations
$this->addSql('ALTER TABLE users ADD password VARCHAR(255) NOT NULL DEFAULT \'\'');

Reply

Hey triemli,

And probably not, did you get any issues with that? If yes, can you please share you DB version so we can find why that's not work for you?
If you create a not null field without default data mysql should just add this field with empty data, but probably that is something depending on configuration or server version.

Cheers

Reply
triemli Avatar
triemli Avatar triemli | sadikoff | posted 1 year ago | edited

Maybe because I used Postgres.
First migration:
` $this->addSql('CREATE SEQUENCE users_id_seq INCREMENT BY 1 MINVALUE 1 START 1');

    $this->addSql('CREATE TABLE users (id INT NOT NULL, email VARCHAR(180) NOT NULL, roles JSON NOT NULL, first_name VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))');
    $this->addSql('CREATE UNIQUE INDEX UNIQ_1483A5E9E7927C74 ON users (email)');`

Second
$this->addSql("ALTER TABLE users ADD password VARCHAR(255) NOT NULL DEFAULT ''");

Reply

Yeah that's the key, we are using mysql for tutorial, sounds like we should write a note about it, thanks for tip!

Cheers!

1 Reply
gazzatav Avatar
gazzatav Avatar gazzatav | sadikoff | posted 1 year ago | edited

On Postgres I had to add some extra escaped quotes round user to get the migration to complete:
$this->addSql("ALTER TABLE \"user\" ADD password VARCHAR(255) NOT NULL DEFAULT''");

Reply

Hey gazzatav

I bet that is because you are using "user" as table name, and this word is reserved. but tutorial uses "users" and that can be ok

Cheers

Reply
gazzatav Avatar

Hi @Vladimir, you are right that 'user' is a reserved word in postgresql and several other sql standards but I have used the class User exactly like the tutorials (there is no use of 'users' in the class). Doctrine is choosing table names not me! It may be that Doctrine chose 'users' for your dbms and version. Maybe Doctrine's knowledge of the dbms is so good it knows how closely it can keep to entity names. Out of interest, the postgresql documentation does say that it's possible to use a reserved word as a bare table name without creating an 'as' alias but anyway Doctrine always seems to create aliases. If I use psql to check my database, when I use the '\d user' command to describe the table, it describes the one Doctrine made. However, if I run 'select * from user;' the result comes from the admin table which postgres made! If I change the query to use 'public.user' I get the results from the fixtures.

Reply

Whoops, I missed somehow that tutorial uses 'user'. You can always force doctrine to use any table name you need with @ORM\Table() annotation =)

Cheers

Reply
Cat in space

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

This tutorial also works great for Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.3", // v3.3.0
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "doctrine/annotations": "^1.0", // 1.13.2
        "doctrine/doctrine-bundle": "^2.1", // 2.6.3
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.1.1
        "doctrine/orm": "^2.7", // 2.10.1
        "knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
        "knplabs/knp-time-bundle": "^1.11", // v1.16.1
        "pagerfanta/doctrine-orm-adapter": "^3.3", // v3.3.0
        "pagerfanta/twig": "^3.3", // v3.3.0
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "scheb/2fa-bundle": "^5.12", // v5.12.1
        "scheb/2fa-qr-code": "^5.12", // v5.12.1
        "scheb/2fa-totp": "^5.12", // v5.12.1
        "sensio/framework-extra-bundle": "^6.0", // v6.2.0
        "stof/doctrine-extensions-bundle": "^1.4", // v1.6.0
        "symfony/asset": "5.3.*", // v5.3.4
        "symfony/console": "5.3.*", // v5.3.7
        "symfony/dotenv": "5.3.*", // v5.3.8
        "symfony/flex": "^1.3.1", // v1.17.5
        "symfony/form": "5.3.*", // v5.3.8
        "symfony/framework-bundle": "5.3.*", // v5.3.8
        "symfony/monolog-bundle": "^3.0", // v3.7.0
        "symfony/property-access": "5.3.*", // v5.3.8
        "symfony/property-info": "5.3.*", // v5.3.8
        "symfony/rate-limiter": "5.3.*", // v5.3.4
        "symfony/runtime": "5.3.*", // v5.3.4
        "symfony/security-bundle": "5.3.*", // v5.3.8
        "symfony/serializer": "5.3.*", // v5.3.8
        "symfony/stopwatch": "5.3.*", // v5.3.4
        "symfony/twig-bundle": "5.3.*", // v5.3.4
        "symfony/ux-chartjs": "^1.3", // v1.3.0
        "symfony/validator": "5.3.*", // v5.3.8
        "symfony/webpack-encore-bundle": "^1.7", // v1.12.0
        "symfony/yaml": "5.3.*", // v5.3.6
        "symfonycasts/verify-email-bundle": "^1.5", // v1.5.0
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.3
        "twig/string-extra": "^3.3", // v3.3.3
        "twig/twig": "^2.12|^3.0" // v3.3.3
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
        "symfony/debug-bundle": "5.3.*", // v5.3.4
        "symfony/maker-bundle": "^1.15", // v1.34.0
        "symfony/var-dumper": "5.3.*", // v5.3.8
        "symfony/web-profiler-bundle": "5.3.*", // v5.3.8
        "zenstruck/foundry": "^1.1" // v1.13.3
    }
}
userVoice