Symfony 6 and EasyAdmin 4: Hashing password
Tags: php, security, symfony, password
Published: 2022-11-11 13:23:31

<small>\* The cover image is originally by [geralt](https://pixabay.com/users/geralt-9301/) and edited with great appreciation.</small>

---

## Summary

With [EasyAdmin](https://symfony.com/bundles/EasyAdminBundle/4.x/index.html) bundle, you can create admin panel easily:

{% post nabbisen/symfony-6-and-easyadmin-4-admin-panel-for-user-management-system-53p1 %}

Well, as to User entity, given it has password field, you must want to hash it before it stored for security.

This post shows how to implement it with EasyAdmin.

### Environment

- [Alpine Linux](https://www.alpinelinux.org/) [3.16](https://wiki.alpinelinux.org/wiki/Release_Notes_for_Alpine_3.16.0) (on [Docker](https://www.docker.com/))
- [PHP](https://www.php.net/) [8.1](https://www.php.net/releases/8.1/)
- [Symfony](https://symfony.com/) [6.1](https://symfony.com/releases/6.1)
- [MariaDB](https://mariadb.org/) [10.9](https://mariadb.com/kb/en/mariadb-server-109/)

## How to hash password

### The source code

Here is an example.
Modify `src/Controller/Admin/UserCrudController.php` like this:

```php
<?php

namespace App\Controller\Admin;

use App\Entity\User;
use EasyCorp\Bundle\EasyAdminBundle\Config\{Action, Actions, Crud, KeyValueStore};
use EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto;
use EasyCorp\Bundle\EasyAdminBundle\Field\{IdField, EmailField, TextField};
use Symfony\Component\Form\Extension\Core\Type\{PasswordType, RepeatedType};
use Symfony\Component\Form\{FormBuilderInterface, FormEvent, FormEvents};
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;

class UserCrudController extends AbstractCrudController
{
    public function __construct(
        public UserPasswordHasherInterface $userPasswordHasher
    ) {}

    public static function getEntityFqcn(): string
    {
        return User::class;
    }

    public function configureActions(Actions $actions): Actions
    {
        return $actions
            ->add(Crud::PAGE_EDIT, Action::INDEX)
            ->add(Crud::PAGE_INDEX, Action::DETAIL)
            ->add(Crud::PAGE_EDIT, Action::DETAIL)
            ;
    }

    public function configureFields(string $pageName): iterable
    {
        $fields = [
            IdField::new('id')->hideOnForm(),
            EmailField::new('email'),
        ];

        $password = TextField::new('password')
            ->setFormType(RepeatedType::class)
            ->setFormTypeOptions([
                'type' => PasswordType::class,
                'first_options' => ['label' => 'Password'],
                'second_options' => ['label' => '(Repeat)'],
                'mapped' => false,
            ])
            ->setRequired($pageName === Crud::PAGE_NEW)
            ->onlyOnForms()
            ;
        $fields[] = $password;

        return $fields;
    }

    public function createNewFormBuilder(EntityDto $entityDto, KeyValueStore $formOptions, AdminContext $context): FormBuilderInterface
    {
        $formBuilder = parent::createNewFormBuilder($entityDto, $formOptions, $context);
        return $this->addPasswordEventListener($formBuilder);
    }

    public function createEditFormBuilder(EntityDto $entityDto, KeyValueStore $formOptions, AdminContext $context): FormBuilderInterface
    {
        $formBuilder = parent::createEditFormBuilder($entityDto, $formOptions, $context);
        return $this->addPasswordEventListener($formBuilder);
    }

    private function addPasswordEventListener(FormBuilderInterface $formBuilder): FormBuilderInterface
    {
        return $formBuilder->addEventListener(FormEvents::POST_SUBMIT, $this->hashPassword());
    }

    private function hashPassword() {
        return function($event) {
            $form = $event->getForm();
            if (!$form->isValid()) {
                return;
            }
            $password = $form->get('password')->getData();
            if ($password === null) {
                return;
            }

            $hash = $this->userPasswordHasher->hashPassword($this->getUser(), $password);
            $form->getData()->setPassword($hash);
        };
    }
}
```

---

### Description

I will break it down into several parts.

#### [Constructor Property Promotion](https://wiki.php.net/rfc/constructor_promotion)

This style is valid since PHP 8.0.

```php
    public function __construct(
        public UserPasswordHasherInterface $userPasswordHasher
    ) {}
```

When your PHP version is prior to them, write like below instead:

```php
    private $userPasswordHasher;
    
    public function __construct(
        UserPasswordHasherInterface $userPasswordHasher
    ) {
        $this->userPasswordHasher = $userPasswordHasher;
    }
```

#### Add menus

This is optional. Menus are added to index page and edit.

```php
    public function configureActions(Actions $actions): Actions
    {
        return $actions
            ->add(Crud::PAGE_EDIT, Action::INDEX)
            ->add(Crud::PAGE_INDEX, Action::DETAIL)
            ->add(Crud::PAGE_EDIT, Action::DETAIL)
            ;
    }
```

#### Generate password field

`configureFields` is one of EasyAdmin's functions to [configure the fields to display](https://symfony.com/bundles/EasyAdminBundle/4.x/fields.html#configuring-the-fields-to-display).
Here, password field is defined as PasswordType and RepeatedType. Also, it is as an [unmapped field](https://symfony.com/doc/6.1/forms.html#unmapped-fields) to prevent validation exception in the case when null is set in order not to change password.

```php
    public function configureFields(string $pageName): iterable
    {
        $fields = [
            IdField::new('id')->hideOnForm(),
            EmailField::new('email'),
        ];

        $password = TextField::new('password')
            ->setFormType(RepeatedType::class)
            ->setFormTypeOptions([
                'type' => PasswordType::class,
                'first_options' => ['label' => 'Password'],
                'second_options' => ['label' => '(Repeat)'],
                'mapped' => false,
            ])
            ->setRequired($pageName === Crud::PAGE_NEW)
            ->onlyOnForms()
            ;
        $fields[] = $password;

        return $fields;
    }
```

#### Handle events

Here, Symfony form events are used with EasyAdmin event handlers. It's because as of now [EasyAdmin's events](https://symfony.com/bundles/EasyAdminBundle/4.x/events.html) don't support handling form validation.

```php
    public function createNewFormBuilder(EntityDto $entityDto, KeyValueStore $formOptions, AdminContext $context): FormBuilderInterface
    {
        $formBuilder = parent::createNewFormBuilder($entityDto, $formOptions, $context);
        return $this->addPasswordEventListener($formBuilder);
    }

    public function createEditFormBuilder(EntityDto $entityDto, KeyValueStore $formOptions, AdminContext $context): FormBuilderInterface
    {
        $formBuilder = parent::createEditFormBuilder($entityDto, $formOptions, $context);
        return $this->addPasswordEventListener($formBuilder);
    }

    private function addPasswordEventListener(FormBuilderInterface $formBuilder): FormBuilderInterface
    {
        return $formBuilder->addEventListener(FormEvents::POST_SUBMIT, $this->hashPassword());
    }
```

#### Hash password

This is the key part on hashing password with Symfony's [PasswordHasher](https://symfony.com/doc/6.1/security/passwords.html#hashing-the-password).
When password is not entered, skip the field.
When it is, hash it✨ and add the field to the entity data🌟

```php
    private function hashPassword() {
        return function($event) {
            $form = $event->getForm();
            if (!$form->isValid()) {
                return;
            }
            $password = $form->get('password')->getData();
            if ($password === null) {
                return;
            }

            $hash = $this->userPasswordHasher->hashPassword($this->getUser(), $password);
            $form->getData()->setPassword($hash);
        };
    }
```

---

That's it.
I'm happy if the code and the description would help you :)