Skip to content

Commit cf073c4

Browse files
committed
feature #46224 [Form] Add hash_property_path option to PasswordType (Seb33300)
This PR was merged into the 6.2 branch. Discussion ---------- [Form] Add `hash_property_path` option to `PasswordType` | Q | A | ------------- | --- | Branch? | 6.2 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | Fix #29066 | License | MIT | Doc PR | symfony/symfony-docs#15872 Same as #42883 but using a Form Extension and rebased to 6.1 & tests. This PR adds a new `hash_mapping` option to `PasswordType`. The `hash_mapping` option can be set with a property path where we want to set the hashed password. The `hash_mapping` option can only be used on unmapped fields to minimize plain password leak. Commits ------- 7065dfe [Form] Add hash_mapping option to PasswordType
2 parents 9effc28 + 7065dfe commit cf073c4

File tree

10 files changed

+420
-0
lines changed

10 files changed

+420
-0
lines changed

src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php

+7
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
use Symfony\Component\EventDispatcher\EventDispatcher;
3434
use Symfony\Component\ExpressionLanguage\Expression;
3535
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
36+
use Symfony\Component\Form\Extension\PasswordHasher\PasswordHasherExtension;
3637
use Symfony\Component\HttpFoundation\ChainRequestMatcher;
3738
use Symfony\Component\HttpFoundation\RequestMatcher\AttributesRequestMatcher;
3839
use Symfony\Component\HttpFoundation\RequestMatcher\HostRequestMatcher;
@@ -124,6 +125,12 @@ public function load(array $configs, ContainerBuilder $container)
124125
$container->removeDefinition('security.is_granted_attribute_expression_language');
125126
}
126127

128+
if (!class_exists(PasswordHasherExtension::class)) {
129+
$container->removeDefinition('form.listener.password_hasher');
130+
$container->removeDefinition('form.type_extension.form.password_hasher');
131+
$container->removeDefinition('form.type_extension.password.password_hasher');
132+
}
133+
127134
// set some global scalars
128135
$container->setParameter('security.access.denied_url', $config['access_denied_url']);
129136
$container->setParameter('security.authentication.manager.erase_credentials', $config['erase_credentials']);

src/Symfony/Bundle/SecurityBundle/Resources/config/password_hasher.php

+23
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@
1111

1212
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
1313

14+
use Symfony\Component\Form\Extension\Core\Type\FormType;
15+
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
16+
use Symfony\Component\Form\Extension\PasswordHasher\EventListener\PasswordHasherListener;
17+
use Symfony\Component\Form\Extension\PasswordHasher\Type\FormTypePasswordHasherExtension;
18+
use Symfony\Component\Form\Extension\PasswordHasher\Type\PasswordTypePasswordHasherExtension;
1419
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory;
1520
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
1621
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasher;
@@ -26,5 +31,23 @@
2631
->args([service('security.password_hasher_factory')])
2732
->alias('security.password_hasher', 'security.user_password_hasher')
2833
->alias(UserPasswordHasherInterface::class, 'security.password_hasher')
34+
35+
->set('form.listener.password_hasher', PasswordHasherListener::class)
36+
->args([
37+
service('security.password_hasher'),
38+
service('property_accessor')->nullOnInvalid(),
39+
])
40+
41+
->set('form.type_extension.form.password_hasher', FormTypePasswordHasherExtension::class)
42+
->args([
43+
service('form.listener.password_hasher'),
44+
])
45+
->tag('form.type_extension', ['extended-type' => FormType::class])
46+
47+
->set('form.type_extension.password.password_hasher', PasswordTypePasswordHasherExtension::class)
48+
->args([
49+
service('form.listener.password_hasher'),
50+
])
51+
->tag('form.type_extension', ['extended-type' => PasswordType::class])
2952
;
3053
};

src/Symfony/Component/Form/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ CHANGELOG
99
* Deprecate calling `Button/Form::setParent()`, `ButtonBuilder/FormConfigBuilder::setDataMapper()`, `TransformationFailedException::setInvalidMessage()` without arguments
1010
* Change the signature of `FormConfigBuilderInterface::setDataMapper()` to `setDataMapper(?DataMapperInterface)`
1111
* Change the signature of `FormInterface::setParent()` to `setParent(?self)`
12+
* Add `PasswordHasherExtension` with support for `hash_property_path` option in `PasswordType`
1213

1314
6.1
1415
---
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Form\Extension\PasswordHasher\EventListener;
13+
14+
use Symfony\Component\Form\Exception\InvalidConfigurationException;
15+
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
16+
use Symfony\Component\Form\FormEvent;
17+
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
18+
use Symfony\Component\PropertyAccess\PropertyAccess;
19+
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
20+
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
21+
22+
/**
23+
* @author Sébastien Alfaiate <s.alfaiate@webarea.fr>
24+
*/
25+
class PasswordHasherListener
26+
{
27+
private array $passwords = [];
28+
29+
public function __construct(
30+
private UserPasswordHasherInterface $passwordHasher,
31+
private ?PropertyAccessorInterface $propertyAccessor = null,
32+
) {
33+
$this->propertyAccessor ??= PropertyAccess::createPropertyAccessor();
34+
}
35+
36+
public function registerPassword(FormEvent $event)
37+
{
38+
$form = $event->getForm();
39+
$parentForm = $form->getParent();
40+
$mapped = $form->getConfig()->getMapped();
41+
42+
if ($parentForm && $parentForm->getConfig()->getType()->getInnerType() instanceof RepeatedType) {
43+
$mapped = $parentForm->getConfig()->getMapped();
44+
$parentForm = $parentForm->getParent();
45+
}
46+
47+
if ($mapped) {
48+
throw new InvalidConfigurationException('The "hash_property_path" option cannot be used on mapped field.');
49+
}
50+
51+
if (!($user = $parentForm?->getData()) || !$user instanceof PasswordAuthenticatedUserInterface) {
52+
throw new InvalidConfigurationException(sprintf('The "hash_property_path" option only supports "%s" objects, "%s" given.', PasswordAuthenticatedUserInterface::class, get_debug_type($user)));
53+
}
54+
55+
$this->passwords[] = [
56+
'user' => $user,
57+
'property_path' => $form->getConfig()->getOption('hash_property_path'),
58+
'password' => $event->getData(),
59+
];
60+
}
61+
62+
public function hashPasswords(FormEvent $event)
63+
{
64+
$form = $event->getForm();
65+
66+
if (!$form->isRoot()) {
67+
return;
68+
}
69+
70+
if ($form->isValid()) {
71+
foreach ($this->passwords as $password) {
72+
$this->propertyAccessor->setValue(
73+
$password['user'],
74+
$password['property_path'],
75+
$this->passwordHasher->hashPassword($password['user'], $password['password'])
76+
);
77+
}
78+
}
79+
80+
$this->passwords = [];
81+
}
82+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Form\Extension\PasswordHasher;
13+
14+
use Symfony\Component\Form\AbstractExtension;
15+
use Symfony\Component\Form\Extension\PasswordHasher\EventListener\PasswordHasherListener;
16+
17+
/**
18+
* Integrates the PasswordHasher component with the Form library.
19+
*
20+
* @author Sébastien Alfaiate <s.alfaiate@webarea.fr>
21+
*/
22+
class PasswordHasherExtension extends AbstractExtension
23+
{
24+
public function __construct(
25+
private PasswordHasherListener $passwordHasherListener,
26+
) {
27+
}
28+
29+
protected function loadTypeExtensions(): array
30+
{
31+
return [
32+
new Type\FormTypePasswordHasherExtension($this->passwordHasherListener),
33+
new Type\PasswordTypePasswordHasherExtension($this->passwordHasherListener),
34+
];
35+
}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Form\Extension\PasswordHasher\Type;
13+
14+
use Symfony\Component\Form\AbstractTypeExtension;
15+
use Symfony\Component\Form\Extension\Core\Type\FormType;
16+
use Symfony\Component\Form\Extension\PasswordHasher\EventListener\PasswordHasherListener;
17+
use Symfony\Component\Form\FormBuilderInterface;
18+
use Symfony\Component\Form\FormEvents;
19+
20+
/**
21+
* @author Sébastien Alfaiate <s.alfaiate@webarea.fr>
22+
*/
23+
class FormTypePasswordHasherExtension extends AbstractTypeExtension
24+
{
25+
public function __construct(
26+
private PasswordHasherListener $passwordHasherListener,
27+
) {
28+
}
29+
30+
public function buildForm(FormBuilderInterface $builder, array $options)
31+
{
32+
$builder->addEventListener(FormEvents::POST_SUBMIT, [$this->passwordHasherListener, 'hashPasswords']);
33+
}
34+
35+
public static function getExtendedTypes(): iterable
36+
{
37+
return [FormType::class];
38+
}
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Form\Extension\PasswordHasher\Type;
13+
14+
use Symfony\Component\Form\AbstractTypeExtension;
15+
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
16+
use Symfony\Component\Form\Extension\PasswordHasher\EventListener\PasswordHasherListener;
17+
use Symfony\Component\Form\FormBuilderInterface;
18+
use Symfony\Component\Form\FormEvents;
19+
use Symfony\Component\OptionsResolver\OptionsResolver;
20+
use Symfony\Component\PropertyAccess\PropertyPath;
21+
22+
/**
23+
* @author Sébastien Alfaiate <s.alfaiate@webarea.fr>
24+
*/
25+
class PasswordTypePasswordHasherExtension extends AbstractTypeExtension
26+
{
27+
public function __construct(
28+
private PasswordHasherListener $passwordHasherListener,
29+
) {
30+
}
31+
32+
public function buildForm(FormBuilderInterface $builder, array $options)
33+
{
34+
if ($options['hash_property_path']) {
35+
$builder->addEventListener(FormEvents::POST_SUBMIT, [$this->passwordHasherListener, 'registerPassword']);
36+
}
37+
}
38+
39+
public function configureOptions(OptionsResolver $resolver)
40+
{
41+
$resolver->setDefaults([
42+
'hash_property_path' => null,
43+
]);
44+
45+
$resolver->setAllowedTypes('hash_property_path', ['null', 'string', PropertyPath::class]);
46+
47+
$resolver->setInfo('hash_property_path', 'A valid PropertyAccess syntax where the hashed password will be set.');
48+
}
49+
50+
public static function getExtendedTypes(): iterable
51+
{
52+
return [PasswordType::class];
53+
}
54+
}

0 commit comments

Comments
 (0)