5

Im trying to use a specific validator in a form.

That form is for an user to redefine his password, he must also enter his current password. For that I use a built in validator from symfony

in my form:

use Symfony\Component\Security\Core\Validator\Constraints\UserPassword;

and the form type looks like that:

 /**
 * @param FormBuilderInterface $builder
 * @param array $options
 */
public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
        ->add('currentpassword', 'password', array('label'=>'Current password',
            'mapped' => false,
            'constraints' => new UserPassword(array('message' => 'you wot m8?')),
            'required' => true
        ))
        ->add('password', 'repeated', array(
            'first_name' => 'new',
            'second_name' => 'confirm',
            'type' => 'password',
            'required' => true
        ))
    ;
}

I know in my controller I could just get the data form, get the currentpassword value, call the security.encoder_factory, etc but that validator looked handy.

my problem is that the form always return an error (here: 'you wot m8?') just like I had entered the wrong current password.

Any idea what I am doing wrong?

3
  • Did you find a solution? Commented Jul 27, 2015 at 5:08
  • @Daniel no, I had to make the validation myself Commented Jul 27, 2015 at 9:40
  • I'm stuck with the same Problem now. Anyone has an Idea? Commented Apr 12, 2016 at 18:09

3 Answers 3

5

I know this answer is coming a few years late, but as I was stumbleing ybout the same Problem I want to present my solution:

The problem is that there was a connection in my case between the $user Entity I used for FormMapping and the User that comes form the security.context.

See following: (PasswordChange - Controller)

    $username = $this->getUser()->getUsername();
    $user = $this->getDoctrine()->getRepository("BlueChordCmsBaseBundle:User")->findOneBy(array("username"=>$username));
    // Equal to $user = $this->getUser();

    $form = $this->createForm(new ChangePasswordType(), $user);
    //ChangePasswordType equals the one 'thesearentthedroids' posted


    $form->handleRequest($request);
    if($request->getMethod() === "POST" && $form->isValid()) {
        $manager = $this->getDoctrine()->getManager();
        $user->setPassword(password_hash($user->getPassword(), PASSWORD_BCRYPT));
        [...]
    }

    return array(...);

The isValid() function is triggering the UserPassword Constraint Validator:

public function validate($password, Constraint $constraint)
{
    if (!$constraint instanceof UserPassword) {
        throw new UnexpectedTypeException($constraint, __NAMESPACE__.'\UserPassword');
    }

    $user = $this->tokenStorage->getToken()->getUser();

    if (!$user instanceof UserInterface) {
        throw new ConstraintDefinitionException('The User object must implement the UserInterface interface.');
    }

    $encoder = $this->encoderFactory->getEncoder($user);

    if (!$encoder->isPasswordValid($user->getPassword(), $password, $user->getSalt())) {
        $this->context->addViolation($constraint->message);
    }
}

The line of interest is: if (!$encoder->isPasswordValid($user->getPassword(), $password, $user->getSalt()))

In my case the $user->getPassword() was giving back the new Password I just entered in the form as my new Password. Thats why the test allways failed! I did not understand why there could be a connection between the User in the tokenStorage and the one that I loaded from my Database. It feels like both Objects (MyDatabase one and the tokenStorage one) share the same processor address and are actually the same...

Weird!

My solution was to also decouple the (new)password field in ChangePasswordType from the EntityMapping: See

        ->add('currentpassword', 'password', array('label'=>'Current password', 'mapped' => false, 'constraints' => new UserPassword()))
        ->add('password', 'repeated', array(
            'mapped'          => false,
            'type'            => 'password',
            'invalid_message' => 'The password fields must match.',
            'required'        => true,
            'first_options'   => array('label' => 'Password'),
            'second_options'  => array('label' => 'Repeat Password'),
            ))
        ->add('Send', 'submit')
        ->add('Reset','reset')

The line of interest is 'mapped' => false,

By this, the new password entered in the form will not be automatically mapped to the given $user Entity. Instead you now need to grab it from the form. See

    $form->handleRequest($request);
    if($request->getMethod() === "POST" && $form->isValid()) {
        $data = $form->getData();
        $manager = $this->getDoctrine()->getManager();
        $user->setPassword(password_hash($data->getPassword(), PASSWORD_BCRYPT));
        $manager->persist($user);
        $manager->flush();
    }

A bit of a workaround for problem what I could not fully understand. If anyone can explain the connection between the Database Object and the security.context Object I'd be glad to hear it!

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you, this helped me too. To remove the connection between the $user Entity I used for FormMapping and the User that comes form the security.context I added a new model for UserPasswordChange model with oldPassword and newPassword properties and used those instead for creating the form in the controller (not the $this->getUser() as I was doing before)
2

I had the same problem and, after lot of research and practical tests, this is the solution I used:

  1. keep User Entity as it is (no changes)
  2. Define a new entity (ChangePassword), with 2 fields, as MODEL in directory 'Model' in 'Form' Folder, as described in symfony docs: [https://symfony.com/doc/current/reference/constraints/UserPassword.html][1])
    • Field oldPassword: add a constraint: @SecurityAssert\UserPassword(message = "old pass wrong.....")
    • Field: newPassword
      (NB: if you dont define this entity as model, then you can not map it, later in the form, unless you create it throw Doctrine, and that is not our purpose)
  3. Define the form 'ChangePasswordType' for these fields (newPasword could be RepeatedType for password confirmation). The mapping as 'true' MUST be maintained for automatic validation of oldPassword, throw SecurityAssert defined above, and for catching these fields later in the controller

  4. In the controller (changeCurrentUserPasswordAction,... or whatever Action), declare a new ChangePassword Entity and associate it with the form to create ('ChangePasswordType)

  5. Now you can execute and see that wrong password could not be passed for oldPassword (since it should be equal to the actual password of authentificated user)
  6. Finally, in the controller, and when the form is submitted, get the value of the new password entered in the form (using $newpass = $form->getData()->getPassword(); for example) and encode it and set it as the new password $user->setPassword($newpass) before the flush.

I hope this can help someone...

Comments

0

I found myself wasting time with the UserPassword as well and wanted to share a simpler solution for those who want a 'change password form'.

The other answers already explain what goes wrong when using the UserPassword, but rather than adding an extra entity or messing around in the controller a more straightforward approach is to add your own check and add a form-error when invalid:

    public function __construct(
        private UserPasswordHasherInterface $passwordHasher,
    ) {
    }

    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('currentPassword', PasswordType::class, [
                'required' => true,
                'mapped' => false,
            ])
            ->add('newPassword', RepeatedType::class, [
                'type' => PasswordType::class,
                'invalid_message' => 'The password fields must match.',
                'required' => true,
                'mapped' => false,
            ])
        ;

        $builder->addEventListener(FormEvents::SUBMIT, function (FormEvent $event): void {
            $user = $event->getData();
            $form = $event->getForm();

            if (!$this->passwordHasher->isPasswordValid($user, $form->get('currentPassword')->getData())) {
                $form->addError(new FormError('Provided current password is invalid.'));
            } else {
                $user->setPassword($this->passwordHasher->hashPassword($user, $form->get('newPassword')->getData()));
            }
        });
    }

This should keep your controller clean of extra checks (the $form->isValid() is all that needs to be called)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.