DEV Community

Cover image for Symfony 6 and EasyAdmin 4: Hashing password
nabbisen
nabbisen

Posted on • Updated on • Originally published at scqr.net

Symfony 6 and EasyAdmin 4: Hashing password

* The cover image is originally by geralt and edited with great appreciation.


Summary

With EasyAdmin bundle, you can create admin panel easily:

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

How to hash password

The source code

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

<?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);
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

Description

I will break it down into several parts.

Constructor Property Promotion

This style is valid since PHP 8.0.

    public function __construct(
        public UserPasswordHasherInterface $userPasswordHasher
    ) {}
Enter fullscreen mode Exit fullscreen mode

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

    private $userPasswordHasher;

    public function __construct(
        UserPasswordHasherInterface $userPasswordHasher
    ) {
        $this->userPasswordHasher = $userPasswordHasher;
    }
Enter fullscreen mode Exit fullscreen mode

Add menus

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

    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)
            ;
    }
Enter fullscreen mode Exit fullscreen mode

Generate password field

configureFields is one of EasyAdmin's functions to configure the fields to display.
Here, password field is defined as PasswordType and RepeatedType. Also, it is as an unmapped field to prevent validation exception in the case when null is set in order not to change password.

    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;
    }
Enter fullscreen mode Exit fullscreen mode

Handle events

Here, Symfony form events are used with EasyAdmin event handlers. It's because as of now EasyAdmin's events don't support handling form validation.

    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());
    }
Enter fullscreen mode Exit fullscreen mode

Hash password

This is the key part on hashing password with Symfony's PasswordHasher.
When password is not entered, skip the field.
When it is, hash it✨ and add the field to the entity data🌟

    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);
        };
    }
Enter fullscreen mode Exit fullscreen mode

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

Top comments (9)

Collapse
 
yoannh profile image
YoannH

Hey!

Since Symfony 6.2, you can use the field option 'hash_property_path'.

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

Collapse
 
nabbisen profile image
nabbisen

Hi ! Thank you for your useful information.
It's really great !!!

symfony.com/doc/current/reference/...
symfony.com/doc/current/security/p...

Collapse
 
cc0adolf profile image
Adolfo Muñoz Aguilar

This is usefull for my why with Symfony 7 - EasyAdmin 4 - Php 8.2 can't set event and also the your code it's very simple. Thank you !!!

Collapse
 
anegve profile image
Eugenii Kulik

This is very helpful. Thank you

Collapse
 
nabbisen profile image
nabbisen

Eugenii, I'm really happy to hear it ☺️ Thank you for your comments.

Collapse
 
xedelweiss profile image
Michael

Great article, thank you very much!

I was working with EasyAdmin anonymously and faced with the issue that $this->getUser() was null. I had to replace it with $form->getData().

But this led me to a question: is it okay that we hash the password for an authorized user instead of doing it for a new user?

Collapse
 
nabbisen profile image
nabbisen • Edited

Hello, Michael. Thank you for your comments.

Well, it's up to how your system registers users:

  1. When it follows the current Symfony Secutiry with login form and you will implement the same way in which passwords are hashed, it has already stored password hashed correctly. You can apply hash to existing users.

  2. However, when the registration system didn't use hash or used in another way, it's not the case. When you want to implement hash against it, recovery might be possible if you would reset password of all existing users.

Collapse
 
jalexmelendez profile image
Alex Meléndez

Thanks! It helped me a lot, i was going nuts because i used the Events System as a Container Service to hash the password (bad idea)

Collapse
 
nabbisen profile image
nabbisen

Hello, Alex. Thank you for your comments. It is good news to me 😄
Well, you mean you used custom EventSubscriber (or EventListener) instead of FormEvents::POST_SUBMIT ? Interested.