Live Coding : Activation de compte et récupération de mot de passe avec Symfony 4

Par Nouvelle-Techno.fr le 4 février 2020 - Catégories : MVC Tutoriel Symfony Live-Coding

Lire l'article sur le site d'origine

Lors de l'inscription des utilisateurs, il arrive de vouloir envoyer un code d'activation par e-mail pour s'assurer que son adresse est valide.

En complément, il peut également être utile de permettre aux inscrits de réinitialiser leur mot de passe en cas d'oubli.

Attention : cet article traite spécifiquement de la procédure de réinitialisation de mot de passe, il conviendrait d'ajouter des sécurités complémentaires comme une question de sécurité, par exemple.

Activation du compte de l'utilisateur

Il y a plusieurs façons de procéder à l'activation du compte par l'utilisateur. Nous allons utiliser un token généré aléatoirement.

Si le token existe, le compte ne sera pas activé. Un lien contenant ce token sera envoyé à l'utilisateur.

Création du token

Pour commencer, nous allons modifier notre entité "Users" pour y ajouter la propriété "activation_token". Sa valeur par défaut sera définie également.

Nous allons utiliser la commande ci-dessous pour ajouter le champ de type "string" et pouvant être vide

php bin/console make:entity

Nous effectuons ensuite la migration pour mettre à jour la base de données.

/**
 * @ORM\Column(type="string", length=255, nullable=true)
 */
private $activation_token;

La propriété est créée, nous allons devoir définir sa valeur par défaut lors de l'enregistrement.

Nous allons modifier le fichier "RegistrationController.php" en ajoutant la ligne ci-dessous

// On génère un token et on l'enregistre
$user->setActivationToken(md5(uniqid()));

La méthode "register" contiendra donc ce code

/**
 * @Route("/register", name="app_register")
 */
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder, GuardAuthenticatorHandler $guardHandler, UsersAuthenticator $authenticator): Response
{
    $user = new Users();
    $form = $this->createForm(RegistrationFormType::class, $user);
    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
        // encode the plain password
        $user->setPassword(
            $passwordEncoder->encodePassword(
                $user,
                $form->get('plainPassword')->getData()
            )
        );
        // On génère un token et on l'enregistre
        $user->setActivationToken(md5(uniqid()));

        $entityManager = $this->getDoctrine()->getManager();
        $entityManager->persist($user);
        $entityManager->flush();

        // do anything else you need here, like send an email

        return $guardHandler->authenticateUserAndHandleSuccess(
            $user,
            $request,
            $authenticator,
            'main' // firewall name in security.yaml
        );
    }

    return $this->render('registration/register.html.twig', [
        'registrationForm' => $form->createView(),
    ]);
}

A partir de cet instant, nous allons avoir un token automatiquement généré à chaque inscription.

Nous allons ajouter un envoi d'e-mail pour que l'utilisateur puisse activer son compte.

Création de la méthode d'activation

Nous allons ajouter une méthode dans le fichier "RegistrationController.php". Cette méthode pointera vers une route du type "/activation/{token}"

Le lien dans l'e-mail devra diriger vers cette route qui vérifiera si le token existe et ensuite valider le compte correspondant.

Cette méthode s'écrira comme suit

/**
 * @Route("/activation/{token}", name="activation")
 */
public function activation($token, UsersRepository $users)
{
    // On recherche si un utilisateur avec ce token existe dans la base de données
    $user = $users->findOneBy(['activation_token' => $token]);

    // Si aucun utilisateur n'est associé à ce token
    if(!$user){
        // On renvoie une erreur 404
        throw $this->createNotFoundException('Cet utilisateur n\'existe pas');
    }

    // On supprime le token
    $user->setActivationToken(null);
    $entityManager = $this->getDoctrine()->getManager();
    $entityManager->persist($user);
    $entityManager->flush();

    // On génère un message
    $this->addFlash('message', 'Utilisateur activé avec succès');

    // On retourne à l'accueil
    return $this->redirectToRoute('accueil');
}

Envoi de l'e-mail à l'utilisateur

Notre route étant créée, nous allons préparer l'e-mail à envoyer à l'utilisateur.

Celui-ci devra contenir le lien d'activation.

Nous devrons créer le fichier twig et l'envoi depuis le fichier "RegistrationController.php" de la même manière que dans le tutoriel "Envoyer des e-mails avec Symfony 4"

Le fichier twig, que nous stockerons dans "/templates/emails" s'appellera "activation.html.twig" et contiendra le code suivant

<h1>Activation de votre compte</p>
<p>Vous avez créé un compte sur notre site, veuillez cliquer sur le lien ci-dessous pour l'activer</p>
<p><a href="{{ absolute_url(path('activation', {'token': token})) }}">Activer mon compte</a></p>

Pour l'envoi du mail, nous allons modifier les paramètres de la méthode "register" de notre contrôleur comme ceci

public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder, GuardAuthenticatorHandler $guardHandler, UsersAuthenticator $authenticator,\Swift_Mailer $mailer): Response

Nous allons également ajouter les lignes ci-dessous

// do anything else you need here, like send an email
// On crée le message
$message = (new \Swift_Message('Nouveau compte'))
    // On attribue l'expéditeur
    ->setFrom('votre@adresse.fr')
    // On attribue le destinataire
    ->setTo($user->getEmail())
    // On crée le texte avec la vue
    ->setBody(
        $this->renderView(
            'emails/activation.html.twig', ['token' => $user->getActivationToken()]
        ),
        'text/html'
    )
;
$mailer->send($message);

Et voilà, l'activation de compte fonctionne.

Réinitialisation du mot de passe

Lorsque les utilisateurs oublient leur mot de passe, il peut etre utile de donner la possibilité de le réinitialiser.

Le mot de passe étant chiffré en base de donnée, il nous est impossible de le renvoyer, ce serait une faille de sécurité. Nous allons donc proposer à l'utilisateur de saisir son nouveau mot de passe après l'avoir authentifié en lui envoyant un lien par e-mail.

Comme pour l'activation, nous utiliserons un token d'authentification. Il est possible d'utiliser le même, nous allons en créer un nouveau, en utilisant une méthode différente.

Création du token

Pour commencer, nous allons modifier notre entité "Users" pour y ajouter la propriété "reset_token". Sa valeur par défaut sera vide.

Nous allons utiliser la commande ci-dessous pour ajouter le champ de type "string" et pouvant être vide

php bin/console make:entity

Nous effectuons ensuite la migration pour mettre à jour la base de données.

/**
 * @ORM\Column(type="string", length=255, nullable=true)
 */
private $reset_token;

La propriété est créée, nous allons pouvoir l'utiliser.

Création des pages

Pour la réinitialisation du mot de passe, nous aurons besoin de deux pages :

Demande de l'e-mail

Nous allons créer une route "/oubli-pass" qui affichera un champ demandant l'adresse e-mail de l'utilisateur.

Pour ce faire, nous allons créer une méthode dans le fichier "SecurityController.php". Cette méthode fera plusieurs choses :

Commençons par créer le formulaire au moyen de la commande

php bin/console make:form ResetPassType

Ce formulaire ne sera pas lié à une entité. Le fichier contiendra ce code

<?php

namespace App\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class ResetPassType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('email', EmailType::class)
            ->add('envoyer', SubmitType::class)
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            // Configure your form options here
        ]);
    }
}

Le contrôleur, qui va gérer le formulaire, contiendra le code suivant

/**
 * @Route("/oubli-pass", name="app_forgotten_password")
 */
public function oubliPass(Request $request, UsersRepository $users, \Swift_Mailer $mailer, TokenGeneratorInterface $tokenGenerator
): Response
{
    // On initialise le formulaire
    $form = $this->createForm(ResetPassType::class);

    // On traite le formulaire
    $form->handleRequest($request);

    // Si le formulaire est valide
    if ($form->isSubmitted() && $form->isValid()) {
        // On récupère les données
        $donnees = $form->getData();

        // On cherche un utilisateur ayant cet e-mail
        $user = $users->findOneByEmail($donnees['email']);

        // Si l'utilisateur n'existe pas
        if ($user === null) {
            // On envoie une alerte disant que l'adresse e-mail est inconnue
            $this->addFlash('danger', 'Cette adresse e-mail est inconnue');
            
            // On retourne sur la page de connexion
            return $this->redirectToRoute('app_login');
        }

        // On génère un token
        $token = $tokenGenerator->generateToken();

        // On essaie d'écrire le token en base de données
        try{
            $user->setResetToken($token);
            $entityManager = $this->getDoctrine()->getManager();
            $entityManager->persist($user);
            $entityManager->flush();
        } catch (\Exception $e) {
            $this->addFlash('warning', $e->getMessage());
            return $this->redirectToRoute('app_login');
        }

        // On génère l'URL de réinitialisation de mot de passe
        $url = $this->generateUrl('app_reset_password', array('token' => $token), UrlGeneratorInterface::ABSOLUTE_URL);

        // On génère l'e-mail
        $message = (new \Swift_Message('Mot de passe oublié'))
            ->setFrom('votre@adresse.fr')
            ->setTo($user->getEmail())
            ->setBody(
                "Bonjour,<br><br>Une demande de réinitialisation de mot de passe a été effectuée pour le site Nouvelle-Techno.fr. Veuillez cliquer sur le lien suivant : " . $url,
                'text/html'
            )
        ;

        // On envoie l'e-mail
        $mailer->send($message);

        // On crée le message flash de confirmation
        $this->addFlash('message', 'E-mail de réinitialisation du mot de passe envoyé !');

        // On redirige vers la page de login
        return $this->redirectToRoute('app_login');
    }

    // On envoie le formulaire à la vue
    return $this->render('security/forgotten_password.html.twig',['emailForm' => $form->createView()]);
}

ATTENTION : à ce stade du tutoriel, vous aurez une erreur indiquant que la route "app_reset_password" n'existe pas, c'est tout à fait normal.

Il reste maintenant le fichier twig correspondant à la page de demande de l'adresse e-mail.

Le fichier "/templates/security/forgotten_password.html.twig" contiendra

{% extends 'base.html.twig' %}
 
{% block title %}Mot de passe oublié{% endblock %}
 
{% block body %}
    <h1>Mot de passe oublié</h1>
    {{ form(emailForm) }}
{% endblock %}

Réinitialiser le mot de passe

Une fois le lien envoyé, nous devons traiter le retour en créant le contrôleur pour la route "app_reset_password". Cette route effectuera les actions suivantes :

On commence par le contrôleur

/**
 * @Route("/reset_pass/{token}", name="app_reset_password")
 */
public function resetPassword(Request $request, string $token, UserPasswordEncoderInterface $passwordEncoder)
{
    // On cherche un utilisateur avec le token donné
    $user = $this->getDoctrine()->getRepository(Users::class)->findOneBy(['reset_token' => $token]);

    // Si l'utilisateur n'existe pas
    if ($user === null) {
        // On affiche une erreur
        $this->addFlash('danger', 'Token Inconnu');
        return $this->redirectToRoute('app_login');
    }

    // Si le formulaire est envoyé en méthode post
    if ($request->isMethod('POST')) {
        // On supprime le token
        $user->setResetToken(null);

        // On chiffre le mot de passe
        $user->setPassword($passwordEncoder->encodePassword($user, $request->request->get('password')));

        // On stocke
        $entityManager = $this->getDoctrine()->getManager();
        $entityManager->persist($user);
        $entityManager->flush();

        // On crée le message flash
        $this->addFlash('message', 'Mot de passe mis à jour');

        // On redirige vers la page de connexion
        return $this->redirectToRoute('app_login');
    }else {
        // Si on n'a pas reçu les données, on affiche le formulaire
        return $this->render('security/reset_password.html.twig', ['token' => $token]);
    }

}

Le fichier twig correspondant à cette page s'appellera "reset_password.html.twig" et contiendra le code suivant

{% extends 'base.html.twig' %}
 
{% block title %}Nouveau mot de passe{% endblock %}
 
{% block body %}
<h1>Réinitialisation du mot de passe</h1>
<p>Veuillez entrer votre nouveau mot de passe ci-dessous.</p>
<form method="post">
    <label for="inputPassword">Password</label>
    <input type="password" name="password" id="inputPassword" placeholder="Mot de passe" required>
    <input type="hidden" name="token" value="{{ token }}">
    <button type="submit" id="valider">
        Valider le mot de passe
    </button>   
</form>
{% endblock %}

Obtenir de l'aide

Pour obtenir de l'aide, vous pouvez accéder aux forums de Nouvelle-Techno.fr ou au serveur Discord pour une entraide par chat

#Tutoriel #MVC #Utilisateurs #Symfony #Live-Coding