0
votes

Is it possible to apply validation constraints to a Symfony login form?

To me it looks like that won't work. I use Symfony 5.2

I have created a Symfony login form as described on the documentation page "https://symfony.com/doc/current/security/form_login_setup.html".

Now I would like to validate the form and have created the following constraints for this in "validation.yaml".

App \ Entity \ User:
  properties:
    username:
      - NotBlank:
          message: 'form.user.username.not_blank'
    password:
      - NotBlank:
          message: 'form.user.password.not_blank'

Unfortunately the constraints are ignored.

If I leave the username and password fields blank, I get a message that the login failed. In such a case, I would like to receive the constraint message that the username and password cannot be empty.

I didn't get any further during my research on the Internet.

Could it be that no validation constraints can be used in a Symfony login form?

Has anyone of you successfully set up validation constraints in a Symfony 5 login form and can you give me a tip on what to look out for?

4

4 Answers

0
votes

With model
You have to add these constraints to the entitiy (if you are using it). I'd suggest to use the annotations: https://symfony.com/doc/current/validation.html#configuration Just add those to your entity.
In Your Model (Entity):

use Symfony\Component\Validator\Constraints as Assert;

class MyClass
{
    /**
     * @Assert\NotBlank
     */
    private $myVarToBeAsserted;
}

Without model
If you are using a data class (no model behind the form), you can use annotations as well. But in this case you have to add those to your form itself: https://symfony.com/doc/current/form/without_class.html
MyFormType:

        $builder->add(
        'birth',
        DateType::class,
        [
            'required' => true,
            'constraints' =>
                [
                    new NotBlank(),
                    new Date()
                ],
            'label' => $labelBirth,
            'widget' => 'single_text',
            'html5' => true,
            'attr' =>
                [
                ],
        ]
    );

... and in controller:

$form = $this->createForm(
        MyFormType::class,
        [
            'data_class' => true
        ],
);


What you did btw: You defined the message for those possible assertions. Which would be shown if your assertion NotBlank() would be triggered.

0
votes

thank you for your answer to my question.

I have just tested it with annotations, but unfortunately it doesn't work for me even with that. When I submit the "empty" login form, I get the message "Invalid credentials.".

I don't understand why Symfony is checking the login data here even though the form fields are empty.

Before the access data can be validated, it must first be checked whether the form has been filled out correctly.

Here is some sample code to illustrate what I'm trying to do.

User.php

<?php

declare(strict_types=1);

namespace App\Entity;

use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;

/**
 * @ORM\Entity(repositoryClass=UserRepository::class)
 */
class User implements UserInterface
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=180, unique=true)
     */
    private $username;

    /**
     * @var string The hashed password
     * @ORM\Column(type="string")
     */
    private $password;

    /**
     * @ORM\Column(type="json")
     */
    private $roles = [];

    public function getId(): ?int
    {
        return $this->id;
    }

    /**
     * @return string|null
     */
    public function getUsername(): ?string
    {
        return $this->username;
    }

    /**
     * @param string|null $username
     * @return $this
     */
    public function setUsername(?string $username): self
    {
        $this->username = $username;

        return $this;
    }

    /**
     * @return string|null
     */
    public function getPassword(): ?string
    {
        return $this->password;
    }

    /**
     * @param string|null $password
     * @return $this
     */
    public function setPassword(?string $password): self
    {
        $this->password = $password;

        return $this;
    }

    /**
     * @return array
     */
    public function getRoles(): array
    {
        $roles = $this->roles;

        $roles[] = 'ROLE_USER';

        return array_unique($roles);
    }

    /**
     * @param array $roles
     * @return $this
     */
    public function setRoles(array $roles): self
    {
        $this->roles = $roles;

        return $this;
    }

    /**
     * @return string|null
     */
    public function getSalt() :?string
    {
        return null;
    }

    /**
     * @see UserInterface
     */
    public function eraseCredentials()
    {
    }
}

LoginType.php

<?php

declare(strict_types=1);

namespace App\Form;

use App\Entity\User;
use Gregwar\CaptchaBundle\Type\CaptchaType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class LoginType extends AbstractType
{
    public const CAPTCHA_REQUIRED = 'captcha_required';

    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('username', TextType::class, ['label' => 'login_form.username'])
            ->add('password', PasswordType::class, ['label' => 'login_form.password'])
            ->add('submit', SubmitType::class, ['label' => 'button.login']);

        if ($options[self::CAPTCHA_REQUIRED]) {
            $builder
                ->add('captcha', CaptchaType::class, ['label' => 'login_form.captcha']);
        }
    }

    /**
     * @param OptionsResolver $resolver
     */
    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => User::class,
            self::CAPTCHA_REQUIRED => false,
        ]);
    }
}

validator.yaml

framework:
    validation:
        enabled: true
        email_validation_mode: html5

validation.yaml

App\Entity\User:
  properties:
    username:
      - NotBlank: ~
    password:
      - NotBlank: ~

Incidentally, I have no problems with any of the other forms.

Only especially with my login form does the validation of the constraints not work.

I suspect because Symfony first validates the access data instead of checking the constraints first.

Have you ever successfully used constraints in a Syfmony login form?

0
votes

I stumbled upon a similar issue - and used the following solution:

Since the authentification happens before the regular form validation I implemented a custom validation in the 'authenticate' method of my LoginFormAuthenticator:

public function authenticate(Request $request): PassportInterface
{
    $credentials = $request->get('login');

    <snip>

    $errors = $this->validateCredentials($credentials);
    if (0 !== $errors->count()) {

        throw new AuthenticationException();
    }

    return new Passport(
        new UserBadge($credentials['email']),
        new PasswordCredentials($credentials['password']),
        [
            new CsrfTokenBadge('login_token', $credentials['_csrf_token']),
            new RememberMeBadge(),
        ]
    );
}

The validateCredentials method which stores the $error-object in the session:

    public function validateCredentials($credentials) {

    $constraints = new Assert\Collection([
        'fields' => [
            'email' =>
                new Assert\Sequentially([
                    new Assert\NotBlank([
                        'message' => 'login.email.not_blank'
                    ]),
                    new Assert\Email([
                        'message' => 'login.email'
                    ])
                ]),

            <snip>

        ],
        'allowExtraFields' => true
    ]);
    $errors = $this->validator->validate(
        $credentials,
        $constraints
    );

    if (0 !== $errors->count()) {
        $this->session->set('login-errors', $errors);
    } else {
        $this->session->remove('login-errors');
    }

    return $errors;
}

The SecurityController fetches the $error-object from the session and adds the respective errors to the login form:

    $loginForm = $this->createForm(LoginType::class, $formData);
    $loginErrors = $request->getSession()->get('login-errors');

    if ($loginErrors) {
        foreach ($loginErrors as $error) {
            $propertyPath = trim($error->getPropertyPath(), '[]');
            $errorMessage = $error->getMessage();
            $loginForm->get($propertyPath)->addError(new FormError($errorMessage));
        }
    }

Most likely not the best approach - but it does the job reasonably well and it's only the login form that makes this extra validation step necessary.

0
votes

thank you for your proposed solution.

Your suggestion helped me a lot.

I have now implemented the validation in my login form in this way.

In my LoginFormAuthenticator class I added a new method "validate". This method validates the login form and saves the errors in the session.

private function validate(array $credentials)
{
    $user = new User();

    $user->setUsername($credentials['username'])
        ->setPassword($credentials['password']);

    $errors = $this->validator->validate($user);

    if (0 !== $errors->count()) {
        $this->session->set(SessionKey::LOGIN_VALIDATION_ERRORS, $errors);
    } else {
        $this->session->remove(SessionKey::LOGIN_VALIDATION_ERRORS);
    }
}

In my SecurityController class, I check whether validation errors are stored in the session. If there are any, I will add them to the login form as you have already described in your post.

$loginErrors = $request->getSession()->get(SessionKey::LOGIN_VALIDATION_ERRORS);
if ($loginErrors) {
    foreach ($loginErrors as $error) {
        $propertyPath = trim($error->getPropertyPath(), '[]');
        $errorMessage = $error->getMessage();
        $form->get($propertyPath)->addError(new FormError($errorMessage));
    }
}

For me this is a workable solution. Maybe not nice but it works.