Utilisation du validateur de sécurité Symfony "NotCompromisedPassword"

Publié le 05/06/2019 • Mis à jour le 05/06/2019

Dans cet article nous allons voir comment utiliser le validateur "NotCompromisedPassword" qui a été ajouté dans Symfony 4.3. Celui-ci permet de vérifier si un mot de passe a déjà été exposé publiquement dans une faille de sécurité et est donc compromis. Nous allons voir comment l'utiliser manuellement mais aussi comment offrir la possibililité aux utilisateurs de l'utiliser dans un formulaire de création de compte. C'est parti mon kiki ! 😎

» Publié dans "Une semaine Symfonique 649" (du 3 au 9 juin 2019).

Configuration

C'est parti !

Tout d'abord, créons un formulaire basique afin de tester un mot de passe. Rien de compliqué ici, nous avons un champ de type mot de passe et un bouton afin de valider. Vous pouvez le tester, la valeur entrée est soumise via Ajax et vous aurez le résultat de la validation avec un simple alert Javascript.


Vérifier si un mot de passe a déjà été compromis.

N'utilisez pas ici un mot de passe que vous utilisez pour de vrai.

Jetons un coup d'œil au code utilisé pour tester le mot de passe :

<?php declare(strict_types=1);

// src/Controller/Post/Post26Trait.php

namespace App\Controller\Post;

use App\Type\AccountCreateType;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Validator\Constraints\NotCompromisedPassword;
use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Component\Validator\Validator\ValidatorInterface;

/**
 * Functions for blog 26th post.
 */
trait Post26Trait
{
    /**
     * @Route("/26/check-password", name="check_password", methods={"POST"})
     */
    public function checkPassword(Request $request)
    {
        if (!$this->validator instanceof ValidatorInterface) {
            throw new \RuntimeException("Houston, we've got a problem! 💥");
        }

        $clearPassword = $request->request->get('password');
        $constraints = [
            new NotCompromisedPassword(),
        ];

        $violations = $this->validator->validate($clearPassword, $constraints);
        $messages = [];
        if ($violations->count()) {
            foreach ($violations as $violation) {
                if ($violation instanceof ConstraintViolation) {
                    $messages[] = '❌ '.$violation->getMessage().' ❌';
                }
            }
        } else {
            $messages[] = '✅ This password has NOT been leaked in a data breach. ✅';
        }

        return $this->json(['message' => implode(',', $messages)]);
    }

Comme vous pouvez le voir, nous instancions manuellement la contrainte NotCompromisedPassword et nous lui soumettons le mot de passe entré. Même si nous n'utilisons qu'une seule contrainte, nous bouclons pour construire notre propre message de retour. Vous pouvez éviter ceci en castant l'objet violation en chaîne de caractères : $message = '❌ '.$violations;. Dans ce cas le message contiendra le mot de passe qui a été entré. Le validateur fonctionne comme prévu, voyons comment l'utiliser dans un formulaire de création de compte.

Utilisation du validateur dans un formulaire d'inscription

Je pense que ce type de validation ne devrait pas tout le temps être utilisé. Une bonne pratique est de demander la permission à l'utilisateur afin d'activer une fonctionnalité. (Oui, les fameuses lois RGPD !). Pour ce faire, nous allons ajouter une case à cocher pour activer ou pas le test du mot de passe saisi. Celle-ci doit être décochée par défaut pour respecter les directives RGPD. Ici, nous ne nous appuierons pas sur une entité mais nous construirons le formulaire manuellement. Il sera très simple et contiendra un identifiant, un mot de passe et notre case à cocher. Voici ma proposition, vous pouvez tester. Cette fois-ci la page sera rechargée et vous aurez des retours selon les valeurs que vous avez entrées. Seuls les champs identifiant et mot de passe sont donc obligatoires.


Regardons le code du formulaire correspondant, nous ajoutons nos trois champs. La principale différence avec un formulaire basique réside dans le fait qu'une validation conditionnelle est déclenchée si la case est cochée. Pour ce faire, nous utilisons un validateur de type Callback au niveau du formulaire dans la méthode configureOptions(). (contrairement aux contraintes simples qui sont d'habitude associées à chaque champ). Ce validateur a la particularité d'avoir accès à toutes les valeurs ayant étés soumises, ce qui permet de faire une validation plus complexe dépendant de la valeur de plusieurs champs.

<?php declare(strict_types=1);

// src/Type/AccountCreateType.php

namespace App\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormError;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Callback;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\NotCompromisedPassword;
use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Component\Validator\Context\ExecutionContextInterface;

/**
 * Fake account creation form.
 */
class AccountCreateType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder->add('login', TextType::class, ['constraints' => [new NotBlank()]]);
        $builder->add('password', PasswordType::class, ['constraints' => [new NotBlank()]]);
        $builder->add('check_password', CheckboxType::class, ['required' => false]);
    }

    /**
     * Conditional validation depending on the checkbox.
     */
    public function validate(array $data, ExecutionContextInterface $context): void
    {
        // Not checked so continue.
        if (!$data['check_password']) {
            return;
        }

        $violations = $context->getValidator()->validate($data['password'], [
            new NotCompromisedPassword(),
        ]);

        // If compromised assign the error to the password field
        if ($violations instanceof ConstraintViolationList && $violations->count()) {
            $password = $context->getRoot()->get('password');
            if ($password instanceof Form) {
                $password->addError(new FormError((string) $violations));
            }
        }
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'constraints' => [
                new Callback([$this, 'validate']),
            ],
        ]);
    }
}

Un peu plus d'explications à propos de cette fonction validate(). Le premier argument contient toutes les données soumises par le formulaire. Il est important de noter ici que les valeurs ont déjà été validées unitairement. L'identifiant et le mot de passe ne peuvent donc pas être vides. Le deuxième argument est le contexte global du formulaire qui nous permet d'accéder à tous les champs. Tout d'abord nous vérifions si la case a été cochée, nous sommes sûrs que la valeur est un booléen puisque le widget associé est un CheckBoxType. Si faux nous n'avons pas de validation supplémentaire à effectuer. Dans le cas contraire, nous récupérons la valeur en clair du mot de passe que nous validons manuellement avec la contrainte NotCompromisedPassword. Il est retourné un objet ConstraintViolationList qui implémente les interfaces Traversable et Countable. La méthode count() nous permet donc de savoir s'il y a au moins une violation. Si c'est le cas nous extrayons le message d'erreur en castant en chaîne cet objet. Enfin nous assignons le message d'erreur au champ "mot de passe" pour qu'il soit affiché juste en dessous dans le formulaire html.

Et voilà ! J'espère que vous avez aimé. Découvrez d'autres informations en rapport à cet article avec les liens ci-dessous. Comme toujours, feedback, likes et retweets sont les bienvenus. (voir la boîte ci-dessous) À la revoyure ! COil. 😊

 La documentation  Plus sur le web

Ils m'ont donné leurs retours et m'ont aidé à corriger des erreurs et typos dans cet article, un grand merci à : danabrey, greg0ire, jmsche. 👍

Bonus : Le code vue.js utilise dans le premier formulaire de test. (Je suis un tag html <details> ! 🙃)
{% trans_default_domain 'post_26' %}
<script>
    /*global $, console, $http */
    /*jslint browser:true */
    "use strict";
    let vue = new Vue({
        delimiters: ['{', '}'],
        el: '#vue-26',
        data: {
            password: '',
            checkPasswordPath: '{{ path('blog_check_password') }}',
            errorMsg: '{{ 'form_error'|trans }}',
        },
        methods: {
            checkPassword: function () {
                const formData = new FormData();
                formData.append('password', this.password);
                this.$http.post(this.checkPasswordPath, formData).then(response => {
                    alert(response.body.message);
                }, response => {
                    alert(this.errorMsg);
                });
                this.password = '';
            },
        }
    });
</script>


» A vous de jouer !

Ces articles vous ont été utiles ? Vous pouvez m'aider à votre tour de plusieurs manières : (utilisez le Tweet à droite pour commenter / me contacter )

  • Me remonter des erreurs ou typos.
  • Me remonter des choses qui pourraient être améliorées.
  • Aimez et retweetez !
  • Suivez moi sur Twitter
  • Inscrivez-vous au flux RSS.

Merci d'avoir tenu jusque ici et à très bientôt sur Strangebuzz ! 😉

COil