Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Symfony 4 change password by username - email can not be null

Introduction

I have been trying to figure out how to create a reset password form that's governed by username value.

The Error

Path        Message                           Invalid value     Violation
data.email  This value should not be blank.   null 



ConstraintViolation {#945 ▼
  -message: "This value should not be blank."
  -messageTemplate: "This value should not be blank."
  -parameters: [▶]
  -plural: null
  -root: Form {#620 ▶}
  -propertyPath: "data.email"
  -invalidValue: null
  -constraint: NotBlank {#477 …}
  -code: "c1051bb4-d103-4f74-8988-acbcafc7fdc3"
  -cause: null
}

What's expected

Update my User Object with the new password.

My Code

ForgotController.php

I know this probably isn't the correct way to get the password, but searching Symfony 4 forgotten password form brings up symfony2.4 posts which aren't relevant to my version

    <?php
        namespace App\Controller\User;

        use App\Entity\User;
        use App\Form\User\ChangePasswordType;
        use App\Repository\UserRepository;
        use Symfony\Bundle\FrameworkBundle\Controller\Controller;
        use Symfony\Component\HttpFoundation\Request;
        use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;

        class ForgotController extends Controller
        {
            public function forgot(Request $request, UserPasswordEncoderInterface $encoder)
            {
                $entityManager = $this->getDoctrine()->getManager();

                $changePassword = $request->request->get('change_password');

                $username = $changePassword['username'];
                $password = $changePassword['plainPassword']['first'];

                $user       = $entityManager->getRepository(User::class)->findBy(['username' => $username]);
                $userEntity = new User();

                if (!$user) {
                    $this->addFlash('danger', 'User not found for '. $username);
                }

                $form = $this->createForm(ChangePasswordType::class, $userEntity);
                $form->handleRequest($request);

                if ($form->isSubmitted() && $form->isValid()) {
                    try {
                        $pass = $encoder->encodePassword($userEntity, $password);

                        $userEntity->setPassword($pass);
                        $entityManager->flush();

                        $this->addFlash('success', 'Password Changed!');
                    } catch (Exception $e) {
                        $this->addFlash('danger', 'Something went skew-if. Please try again.');
                    }

                    return $this->redirectToRoute('login');
                }

                return $this->render('user/forgot.html.twig', array('form' => $form->createView()));
            }
        }

ChangePasswordType.php

<?php
    namespace App\Form\User;

    use App\Entity\User;
    use Symfony\Component\Form\AbstractType;
    use Symfony\Component\Form\Extension\Core\Type\PasswordType;
    use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
    use Symfony\Component\Form\Extension\Core\Type\TextType;
    use Symfony\Component\Form\FormBuilderInterface;
    use Symfony\Component\OptionsResolver\OptionsResolver;

    class ChangePasswordType extends AbstractType
    {
        public function buildForm(FormBuilderInterface $builder, array $options)
        {
            $builder->add('username', TextType::class)
                ->add('plainPassword', RepeatedType::class, array(
                'type' => PasswordType::class,
                'first_options' => array('label' => 'New Password'),
                'second_options' => array('label' => 'Repeat New Password')
            ));
        }

        public function configureOptions(OptionsResolver $resolver)
        {
            $resolver->setDefaults(array(
                'data_class' => User::class
            ));
        }
    }

forgot.html.twig

{% include 'builder/header.html.twig' %}

<div class="user-container" id="user-content">
    {% block body %}
        {% include 'builder/notices.html.twig' %}

        <div class="user-container">
            <i class="fas fa-user-edit fa-5x"></i>
        </div>

        <hr />

        {{ form_start(form) }}
            {{ form_row(form.username, { 'attr': {'class': 'form-control'} }) }}
            {{ form_row(form.plainPassword.first, { 'attr': {'class': 'form-control'} }) }}
            {{ form_row(form.plainPassword.second, { 'attr': {'class': 'form-control'} }) }}

            <div class="register-btn-container">
                <button class="btn btn-danger" id="return-to-dash-btn" type="button">Cancel!</button>
                <button class="btn btn-primary" type="submit">Update!</button>
            </div>
        {{ form_end(form) }}
    {% endblock %}
</div>

{% include 'builder/footer.html.twig' %}

I'm not sure why email is even being mentioned unless it's trying to insert a new user into a database but it shouldn't be trying to do that based on my controller? How can I go about adding a forgot password form that's identified by username?

like image 667
treyBake Avatar asked Mar 07 '23 03:03

treyBake


2 Answers

Since your change password form only needs two fields we will use an array instead of a user entity. Need a slight tweak to ChangePasswordType:

    // ChangePasswordType
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            //'data_class' => User::class
        ));
    }

Here is a working forgot action:

    public function forgot(
        Request $request, 
        UserPasswordEncoderInterface $encoder, 
        UserRepository $userRepository)
    {

        $userInfo = ['username' => null, 'plainPassword' => null];

        $form = $this->createForm(ChangePasswordType::class, $userInfo);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {

            $userInfo = $form->getData();
            $username = $userInfo['username'];
            $plainPassword = $userInfo['plainPassword'];

            $user = $userRepository->findOneBy(['username' => $username]);
            if ($user === null) {
                $this->addFlash('danger', 'Invalid username');
                return $this->redirectToRoute('forgot');
            }
            $password = $encoder->encodePassword($user, $plainPassword);

            $user->setPassword($password);
            $userRepository->flush();

            return $this->redirectToRoute('login');
        }

        return $this->render('user/forgot.html.twig', array('form' => $form->createView()));
    }

UserRepository is injected which get's rid of all the doctrine nonsense. There is one caveat here that I'll get back to.

We build the userInfo array and let the form processing do it's thing. We really don't want to mess with getting attributes directly from the request object if we don't have to.

Then we get our actual user entity to update. Notice the use of findOneBy instead of findBy. We check to make sure username is valid. If you really wanted to get fancy then you could add a validation constraint to the form to do this check automatically.

I got rid of all the try/catch stuff. It just clutters up your code. By this point if an exception is thrown then it is truly exceptional and can be handled by the default exception handlers.

You got the password encoder stuff just right.

And then instead of $entityManager->flush() I used $userRepository->flush(); Out of the box there is no flush method on repositories so you need to add one:

// UserRepository
public function flush()
{
    $this->_em->flush();
}

I personally like dealing with just repositories and not the entity manager. But if want, you can just go back and inject the manager instead of the repository. Your call.

And as mentioned in the comments, you do want to add some security to prevent users from changing other users passwords.

like image 162
Cerad Avatar answered Mar 08 '23 16:03

Cerad


Implement something along the lines of below - I have left pieces out like the templates and routing. This is just to help you along.

Form 1: ForgottenUserType - use enters only username/email and submits

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
        ->add("username", null, array(
        "label" => "Email",
        "attr" => array(
            "class" => "form-control",
            "id" => "basic-url",
            "placeholder" => "Email address for your account"
        ),
        "constraints" => array(
            new Email(array("message" => "Invalid Email"))
        )
    ));
}

Form 2: ChangePasswordFormType - user enters & repeats new password.

public function buildForm(FormBuilderInterface $builder, array $options)
{
    parent::buildForm($builder, $options);
    $builder
        ->add('plainPassword', RepeatedType::class, array(
            'type'              => PasswordType::class,
            'required'          => false,
            'first_options'     => array('label' => 'New password'),
            'second_options'    => array('label' => 'Confirm new password'),
            'invalid_message' => 'The password fields must match.',
        ))
        ;
}

Controller: ResetPasswordController - handles user lookup request of Form 1 and Password reset request for Form 2:

<?php
namespace App\Controller\User;

use App\Entity\User;
use App\Form\User\ChangePasswordType;
use App\Repository\UserRepository;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;

class ResetPasswordController extends Controller
{
    /**
     * Action for when a user has forgotten their password, to request ForgottenUser form
     *
     * @param Request $request
     */
    public function requestAction(Request $request)
    {
        $tmpUser = new User();
        $entityManager = $this->getDoctrine()->getManager();

        $form = $this->createForm(ForgottenUserType::class, $tmpUser);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {

            $user = $entityManager->getRepository(User::class)->findBy(['username' => $tmpUser->getUsername()]);

            if ($user) {

                //check and set token
                if (null === $user->getConfirmationToken()) {
                    /** @var $tokenGenerator TokenGeneratorInterface */
                    $token = md5(uniqid($user->getUsername(), true)); //some unique token (you can create a nicer token generator in standalone class with a service)
                    $user->setConfirmationToken($token);
                    $user->setPasswordRequestedAt(new \DateTime());
                    $em->persist($user);
                    $em->flush();)

                    $this->addFlash('Info', 'If user is found, you will receive an email with further instructions.');

                    //send email using swiftmailer & include url with token
                }


            } else {
                //return to requestAction.
            }
        }

        //request template contains the ForgottenUserType form
        return $this->render(":path/to/template:request.html.twig", array(
            "forgotten_form" => $form->createView()
        ));

    }

    /**
     * Reset user password.
     *
     * @param Request $request
     * @param $token
     */
    public function resetAction(Request $request, $token)
    {
        $entityManager = $this->getDoctrine()->getManager();
        $user = $entityManager->getRepository(User::class)->findBy(['confirmationToken' => $token]); 

        if (null === $user) {                        
            return new RedirectResponse($this->generateUrl('resetting_request')); //to requestAction above. / create route
        }        

        $form = $this->createForm(ChangePasswordFormType::class, $user);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $user->SetConfirmationToken(null);
            $user->setPasswordRequestedAt(null);
            $entityManager->persist($user);
            $entityManager->flush()

            $this->addFlash("success", "Your password has been reset, log in now.");
            $url = $this->generateUrl('app.login'); //route to login page
            $response = new RedirectResponse($url);
            return $response;            
        }

        //reset template contains the ChangePasswordFormType form
        return $this->render(':path/to/forgottenpasswordtemplate:reset.html.twig', array(
            'token' => $token,
            'form' => $form->createView(),
        ));

    }
}
like image 22
Forer Avatar answered Mar 08 '23 16:03

Forer