Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Dar contraseñas a los usuarios

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

A Symfony no le importa realmente si los usuarios de tu sistema tienen contraseñas o no. Si estás construyendo un sistema de inicio de sesión que lee las claves de la API desde una cabecera, entonces no hay contraseñas. Lo mismo ocurre si tienes algún tipo de sistema SSO. Tus usuarios pueden tener contraseñas... pero las introducen en algún otro sitio.

Pero para nosotros, sí queremos que cada usuario tenga una contraseña. Cuando usamos antes el comando make:user, en realidad nos preguntó si queríamos que nuestros usuarios tuvieran contraseñas. Respondimos que no... para poder hacer todo esto manualmente. Pero en un proyecto real, yo respondería que sí para ahorrar tiempo.

PasswordAuthenticatedUserInterface

Sabemos que todas las clases de usuario deben implementar UserInterface:

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

Entonces, si necesitas comprobar las contraseñas de los usuarios en tu aplicación, también tienes que implementar una segunda interfaz llamada PasswordAuthenticatedUserInterface:

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

Esto requiere que tengas un nuevo método: getPassword().

Si estás usando Symfony 6, no tendrás esto todavía, así que añádelo:

... 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
}

Yo lo tengo porque estoy usando Symfony 5 y el método getPassword() es necesario por compatibilidad con el pasado: antes formaba parte de UserInterface.

Ahora que nuestros usuarios tendrán una contraseña, y que estamos implementandoPasswordAuthenticatedUserInterface, voy a eliminar este comentario sobre el método:

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

Almacenamiento de una contraseña codificada para cada usuario

Bien, vamos a olvidarnos de la seguridad por un momento. En su lugar, céntrate en que necesitamos poder almacenar una contraseña única para cada usuario en la base de datos. Esto significa que nuestra entidad de usuario necesita un nuevo campo! Busca tu terminal y ejecuta:

symfony console make:entity

Actualicemos la entidad User, para añadir un nuevo campo llámalo password... que es una cadena, 255 de longitud es exagerado pero está bien... y luego di "no" a anulable. Pulsa enter para terminar.

De vuelta a la clase User, es... mayormente no sorprendente. Tenemos una nueva propiedad $password... y al final, un nuevo método setPassword():

... 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;
}
}

Fíjate que no ha generado un método getPassword()... porque ya teníamos uno. Pero tenemos que actualizarlo para que devuelva $this->password:

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

Algo muy importante sobre esta propiedad $password: no va a almacenar la contraseña en texto plano. ¡Nunca almacenes la contraseña en texto plano! Esa es la forma más rápida de tener una brecha de seguridad... y de perder amigos.

En su lugar, vamos a almacenar una versión cifrada de la contraseña... y veremos cómo generar esa contraseña cifrada en un minuto. Pero antes, vamos a hacer la migración para la nueva propiedad:

symfony console make:migration

Ve a echar un vistazo a ese archivo para asegurarte de que todo está bien:

... 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

Si utilizas PostgreSQL, deberás modificar tu migración. Añade DEFAULT '' al final para que la nueva columna pueda añadirse sin que se produzca un error`terminal $this->addSql('ALTER TABLE product ADD description VARCHAR(255) NOT NULL DEFAULT \'\'');

Y... ¡lo hace! Ciérralo... y ejecútalo:

symfony console doctrine:migrations:migrate

La configuración de password_hashers

¡Perfecto! Ahora que nuestros usuarios tienen una nueva columna de contraseña en la base de datos, vamos a rellenarla en nuestros accesorios. Abre src/Factory/UserFactory.php y busca getDefaults().

De nuevo, lo que no vamos a hacer es poner en password la contraseña en texto plano. No, esa propiedad password tiene que almacenar la versión hash de la contraseña.

Abre config/packages/security.yaml. Este tiene un poco de configuración en la parte superior llamada password_hashers, que le dice a Symfony qué algoritmo de hash debe utilizar para el hash de las contraseñas de los usuarios:

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

Esta configuración dice que cualquier clase de User que implementePasswordAuthenticatedUserInterface - lo que nuestra clase, por supuesto, hace - utilizará el algoritmo auto donde Symfony elige el último y mejor algoritmo automáticamente.

El servicio de aseado de contraseñas

Gracias a esta configuración, tenemos acceso a un servicio "hasher" que es capaz de convertir una contraseña de texto plano en una versión hash utilizando este algoritmo auto. De vuelta aUserFactory, podemos utilizarlo para establecer la propiedad password:

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

En el constructor, añade un nuevo argumento: UserPasswordHasherInterface $passwordHasher. Yo le doy a Alt+Enter y voy a "Inicializar propiedades" para crear esa propiedad y establecerla:

... 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
}

A continuación, podemos establecer password a $this->passwordHasher->hashPassword() y luego pasarle alguna cadena de texto plano.

Bueno... para ser sincero... aunque espero que esto tenga sentido a alto nivel... esto no funcionará del todo porque el primer argumento de hashPassword() es el objeto User... que aún no tenemos dentro de getDefaults().

No pasa nada porque, de todas formas, me gusta crear una propiedad plainPassword en User para facilitar todo esto. Añadamos eso a continuación, terminemos las fijaciones y actualicemos nuestro autentificador para validar la contraseña. Ah, pero no te preocupes: esa nueva propiedadplainPassword no se almacenará en la base de datos.

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 hace 1 año

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

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

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!

¡Este tutorial también funciona muy bien para Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.4.1 || ^8.0.0",
        "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
    }
}