Skip to content

[Form] Add hash_property_path option to PasswordType #46224

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\Form\Extension\PasswordHasher\PasswordHasherExtension;
use Symfony\Component\HttpFoundation\ChainRequestMatcher;
use Symfony\Component\HttpFoundation\RequestMatcher\AttributesRequestMatcher;
use Symfony\Component\HttpFoundation\RequestMatcher\HostRequestMatcher;
Expand Down Expand Up @@ -124,6 +125,12 @@ public function load(array $configs, ContainerBuilder $container)
$container->removeDefinition('security.is_granted_attribute_expression_language');
}

if (!class_exists(PasswordHasherExtension::class)) {
$container->removeDefinition('form.listener.password_hasher');
$container->removeDefinition('form.type_extension.form.password_hasher');
$container->removeDefinition('form.type_extension.password.password_hasher');
}

// set some global scalars
$container->setParameter('security.access.denied_url', $config['access_denied_url']);
$container->setParameter('security.authentication.manager.erase_credentials', $config['erase_credentials']);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\PasswordHasher\EventListener\PasswordHasherListener;
use Symfony\Component\Form\Extension\PasswordHasher\Type\FormTypePasswordHasherExtension;
use Symfony\Component\Form\Extension\PasswordHasher\Type\PasswordTypePasswordHasherExtension;
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory;
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasher;
Expand All @@ -26,5 +31,23 @@
->args([service('security.password_hasher_factory')])
->alias('security.password_hasher', 'security.user_password_hasher')
->alias(UserPasswordHasherInterface::class, 'security.password_hasher')

->set('form.listener.password_hasher', PasswordHasherListener::class)
->args([
service('security.password_hasher'),
service('property_accessor')->nullOnInvalid(),
])

->set('form.type_extension.form.password_hasher', FormTypePasswordHasherExtension::class)
->args([
service('form.listener.password_hasher'),
])
->tag('form.type_extension', ['extended-type' => FormType::class])

->set('form.type_extension.password.password_hasher', PasswordTypePasswordHasherExtension::class)
->args([
service('form.listener.password_hasher'),
])
->tag('form.type_extension', ['extended-type' => PasswordType::class])
;
};
1 change: 1 addition & 0 deletions src/Symfony/Component/Form/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ CHANGELOG
* Deprecate calling `Button/Form::setParent()`, `ButtonBuilder/FormConfigBuilder::setDataMapper()`, `TransformationFailedException::setInvalidMessage()` without arguments
* Change the signature of `FormConfigBuilderInterface::setDataMapper()` to `setDataMapper(?DataMapperInterface)`
* Change the signature of `FormInterface::setParent()` to `setParent(?self)`
* Add `PasswordHasherExtension` with support for `hash_property_path` option in `PasswordType`

6.1
---
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Form\Extension\PasswordHasher\EventListener;

use Symfony\Component\Form\Exception\InvalidConfigurationException;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;

/**
* @author Sébastien Alfaiate <s.alfaiate@webarea.fr>
*/
class PasswordHasherListener
{
private array $passwords = [];

public function __construct(
private UserPasswordHasherInterface $passwordHasher,
private ?PropertyAccessorInterface $propertyAccessor = null,
) {
$this->propertyAccessor ??= PropertyAccess::createPropertyAccessor();
}

public function registerPassword(FormEvent $event)
{
$form = $event->getForm();
$parentForm = $form->getParent();
$mapped = $form->getConfig()->getMapped();

if ($parentForm && $parentForm->getConfig()->getType()->getInnerType() instanceof RepeatedType) {
$mapped = $parentForm->getConfig()->getMapped();
$parentForm = $parentForm->getParent();
}

if ($mapped) {
throw new InvalidConfigurationException('The "hash_property_path" option cannot be used on mapped field.');
}

if (!($user = $parentForm?->getData()) || !$user instanceof PasswordAuthenticatedUserInterface) {
throw new InvalidConfigurationException(sprintf('The "hash_property_path" option only supports "%s" objects, "%s" given.', PasswordAuthenticatedUserInterface::class, get_debug_type($user)));
}

$this->passwords[] = [
'user' => $user,
'property_path' => $form->getConfig()->getOption('hash_property_path'),
'password' => $event->getData(),
];
}

public function hashPasswords(FormEvent $event)
{
$form = $event->getForm();

if (!$form->isRoot()) {
return;
}

if ($form->isValid()) {
foreach ($this->passwords as $password) {
$this->propertyAccessor->setValue(
$password['user'],
$password['property_path'],
$this->passwordHasher->hashPassword($password['user'], $password['password'])
);
}
}

$this->passwords = [];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Form\Extension\PasswordHasher;

use Symfony\Component\Form\AbstractExtension;
use Symfony\Component\Form\Extension\PasswordHasher\EventListener\PasswordHasherListener;

/**
* Integrates the PasswordHasher component with the Form library.
*
* @author Sébastien Alfaiate <s.alfaiate@webarea.fr>
*/
class PasswordHasherExtension extends AbstractExtension
{
public function __construct(
private PasswordHasherListener $passwordHasherListener,
) {
}

protected function loadTypeExtensions(): array
{
return [
new Type\FormTypePasswordHasherExtension($this->passwordHasherListener),
new Type\PasswordTypePasswordHasherExtension($this->passwordHasherListener),
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Form\Extension\PasswordHasher\Type;

use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\PasswordHasher\EventListener\PasswordHasherListener;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvents;

/**
* @author Sébastien Alfaiate <s.alfaiate@webarea.fr>
*/
class FormTypePasswordHasherExtension extends AbstractTypeExtension
{
public function __construct(
private PasswordHasherListener $passwordHasherListener,
) {
}

public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addEventListener(FormEvents::POST_SUBMIT, [$this->passwordHasherListener, 'hashPasswords']);
}

public static function getExtendedTypes(): iterable
{
return [FormType::class];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Form\Extension\PasswordHasher\Type;

use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\PasswordHasher\EventListener\PasswordHasherListener;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\PropertyAccess\PropertyPath;

/**
* @author Sébastien Alfaiate <s.alfaiate@webarea.fr>
*/
class PasswordTypePasswordHasherExtension extends AbstractTypeExtension
{
public function __construct(
private PasswordHasherListener $passwordHasherListener,
) {
}

public function buildForm(FormBuilderInterface $builder, array $options)
{
if ($options['hash_property_path']) {
$builder->addEventListener(FormEvents::POST_SUBMIT, [$this->passwordHasherListener, 'registerPassword']);
}
}

public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'hash_property_path' => null,
]);

$resolver->setAllowedTypes('hash_property_path', ['null', 'string', PropertyPath::class]);

$resolver->setInfo('hash_property_path', 'A valid PropertyAccess syntax where the hashed password will be set.');
}

public static function getExtendedTypes(): iterable
{
return [PasswordType::class];
}
}
Loading