Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using Symfony2 UserPassword validator in form type

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?

like image 786
thesearentthedroids Avatar asked Feb 26 '14 13:02

thesearentthedroids


2 Answers

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!

like image 54
Martin Avatar answered Oct 31 '22 03:10

Martin


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...

like image 44
Hamid ER-REMLI Avatar answered Oct 31 '22 03:10

Hamid ER-REMLI