9 - Vérification d'adresse email sans bundle (Symfony 6)

Catégories : Symfony

Série : Symfony 6

Fichiers : https://github.com/NouvelleTechno/e-commerce-Symfony-6

Mots-clés : Symfony jwt symfony6 mail email verification token

1261 lectures

Auteur : user Benoit

Date :

Partager

Partager sur Facebook Partager sur Twitter Partager sur LinkedIn

Dans cette 9ème vidéo de la série Symfony 6, nous allons ajouter la vérification d'adresse e-mail sans ajouter de bundle à notre projet.

Dans ce processus, nous allons envoyer un email à l'utilisateur pour vérifier que son adresse existe.

Mise à jour de la base de données

Pour pouvoir stocker le statut “Vérifié” de notre utilisateur, nous devons modifier notre entité Users afin d'y intégrer l'information.

Nous commençons par ajouter une propriété is_verified dans notre entité comme ceci

    #[ORM\Column(type: 'boolean')]
    private $is_verified = false;

Comme vous le constatez, nous initialisons sa valeur à false afin de nous assurer qu'un nouvel utilisateur n'est pas activé par défaut.

Nous devons également ajouter les accesseurs dans notre entité comme ceci

    public function getIsVerified(): ?bool
    {
        return $this->is_verified;
    }

    public function setIsVerified(bool $is_verified): self
    {
        $this->is_verified = $is_verified;

        return $this;
    }

Afin de mettre à jour la base de données, nous exécuterons les commandes suivantes

symfony console make:migration
symfony console doctrine:database:create

Ajout d'une alerte dans le profil utilisateur

Nous allons ajouter une alerte indiquant à l'utilisateur qu'il n'a pas activé son compte.

Nous ajoutons cette alerte dans templates/base.html.twig comme ceci (juste après l'include du fichier nav). Attention, le code ci-dessous est adapté à l'utilisation de bootstrap, à personnaliser en fonction du contexte.

{% if app.user and app.user.isVerified == false %}
	<div class="alert alert-warning alert-dismissible" role="alert">
		<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
		<div class="alert-message">
			<strong>Votre compte n'est pas activé</strong>, <a href="{{ path('resend_verif') }}">renvoyer le lien d'activation</a>
		</div>
	</div>
{% endif %}

Création de messages flash

En fonction du contexte, nous aurons à faire passer des messages sur certaines pages.

Nous allons créer un fichier _flash.html.twig dans templates/_partials

Ce fichier contiendra le code suivant

{% for label, messages in app.flashes %}
    {% for message in messages %}
        <div class="alert alert-{{ label }} alert-dismissible" role="alert">
            <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
            <div class="alert-message">
                {{ message|raw }}
            </div>
        </div>
    {% endfor %}
{% endfor %}

Configuration de l'environnement

Nous vérifions que le serveur SMTP est bien configuré dans le fichier .env.local. Dans l'exemple ci-dessous nous utilisons MailHog

MAILER_DSN=smtp://localhost:1025

Nous devons également désactiver le transport des messages par le composant Messenger de Symfony. Pour ce faire nous allons aller dans le fichier config/packages/messenger.yaml et commenter la ligne suivante

Symfony\Component\Mailer\Messenger\SendEmailMessage: async

Création d'un service d'envoi d'emails

Etant donné que nous aurons régulièrement à envoyer des emails depuis différentes parties de notre site, nous allons créer un service permettant de centraliser ces envois.

Pour commencer, nous créons un dossier Service dans src puis à l'intérieur nous créons le fichier SendMailService.php

Dans ce fichier, nous insérons le code d'envoi d'emails comme ceci

<?php
namespace App\Service;

use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\Mailer\MailerInterface;

class SendMailService
{
    private $mailer;

    public function __construct(MailerInterface $mailer)
    {
        $this->mailer = $mailer;
    }

    public function send(
        string $from,
        string $to,
        string $subject,
        string $template,
        array $context
    ): void
    {
        //On crée le mail
        $email = (new TemplatedEmail())
            ->from($from)
            ->to($to)
            ->subject($subject)
            ->htmlTemplate("emails/$template.html.twig")
            ->context($context);

        // On envoie le mail
        $this->mailer->send($email);
    }
}

Utilisation du service depuis le contrôleur

Nous allons maintenant pouvoir utiliser le service dans notre contrôleur d'inscription RegistrationController.

Nous commençons par injecter notre service dans la méthode d'inscription en ajoutant

SendMailService $mail

Nous utilisons ensuite le service comme ceci pour envoyer un email

// On envoie un mail
$mail->send(
    'no-reply@monsite.net',
    $user->getEmail(),
    'Activation de votre compte sur le site e-commerce',
    'register',
    compact('user')
);

Création du template d'email

Nous allons créer le modèle d'email templates/emails/register.html.twig avec ce code

<h1>Activez votre compte</h1>
<p>Bonjour {{ user.firstname }},</p>
<p>Votre inscription sur le site e-commerce est à valider en cliquant sur le lien ci-dessous :</p>
<p><a href="">Lien</a></p>
<p>Ce lien expirera dans 3 heures</p>

A ce moment, le mail est envoyé.

Création du token d'activation

Afin de valider l'utilisateur, nous allons lui envoyer un jeton (token) permettant d'activer son compte. Ce token sera un JSON Web Token (JWT) qui contiendra les informations qui nous semblent intéressantes. Dans cet exemple, nous allons y insérer l'id de l'utilisateur.

Pour commencer nous allons créer un service permettant de générer un JWT. Pour plus de détails, la vidéo https://www.youtube.com/watch?v=dZgHUq-uEGY pourra apporter des précisions.

Dans le dossier src/Service, nous allons créer le fichier JWTService.php qui contiendra le code suivant

<?php
namespace App\Service;

use DateTimeImmutable;

class JWTService
{
    // On génère le token

    /**
     * Génération du JWT
     * @param array $header 
     * @param array $payload 
     * @param string $secret 
     * @param int $validity 
     * @return string 
     */
    public function generate(array $header, array $payload, string $secret, int $validity = 10800): string
    {
        if($validity > 0){
            $now = new DateTimeImmutable();
            $exp = $now->getTimestamp() + $validity;
    
            $payload['iat'] = $now->getTimestamp();
            $payload['exp'] = $exp;
        }

        // On encode en base64
        $base64Header = base64_encode(json_encode($header));
        $base64Payload = base64_encode(json_encode($payload));

        // On "nettoie" les valeurs encodées (retrait des +, / et =)
        $base64Header = str_replace(['+', '/', '='], ['-', '_', ''], $base64Header);
        $base64Payload = str_replace(['+', '/', '='], ['-', '_', ''], $base64Payload);

        // On génère la signature
        $secret = base64_encode($secret);

        $signature = hash_hmac('sha256', $base64Header . '.' . $base64Payload, $secret, true);

        $base64Signature = base64_encode($signature);

        $base64Signature = str_replace(['+', '/', '='], ['-', '_', ''], $base64Signature);

        // On crée le token
        $jwt = $base64Header . '.' . $base64Payload . '.' . $base64Signature;

        return $jwt;
    }

    //On vérifie que le token est valide (correctement formé)

    public function isValid(string $token): bool
    {
        return preg_match(
            '/^[a-zA-Z0-9\-\_\=]+\.[a-zA-Z0-9\-\_\=]+\.[a-zA-Z0-9\-\_\=]+$/',
            $token
        ) === 1;
    }

    // On récupère le Payload
    public function getPayload(string $token): array
    {
        // On démonte le token
        $array = explode('.', $token);

        // On décode le Payload
        $payload = json_decode(base64_decode($array[1]), true);

        return $payload;
    }

    // On récupère le Header
    public function getHeader(string $token): array
    {
        // On démonte le token
        $array = explode('.', $token);

        // On décode le Header
        $header = json_decode(base64_decode($array[0]), true);

        return $header;
    }

    // On vérifie si le token a expiré
    public function isExpired(string $token): bool
    {
        $payload = $this->getPayload($token);

        $now = new DateTimeImmutable();

        return $payload['exp'] < $now->getTimestamp();
    }

    // On vérifie la signature du Token
    public function check(string $token, string $secret)
    {
        // On récupère le header et le payload
        $header = $this->getHeader($token);
        $payload = $this->getPayload($token);

        // On régénère un token
        $verifToken = $this->generate($header, $payload, $secret, 0);

        return $token === $verifToken;
    }
}

Mise à jour du contrôleur et de l'email

Maintenant que notre service est fonctionnel, nous allons modifier le contrôleur RegistrationController afin de générer un token et de l'envoyer à l'utilisateur

            // On génère le JWT de l'utilisateur
            // On crée le Header
            $header = [
                'typ' => 'JWT',
                'alg' => 'HS256'
            ];

            // On crée le Payload
            $payload = [
                'user_id' => $user->getId()
            ];

            // On génère le token
            $token = $jwt->generate($header, $payload, $this->getParameter('app.jwtsecret'));

            // On envoie un mail
            $mail->send(
                'no-reply@monsite.net',
                $user->getEmail(),
                'Activation de votre compte sur le site e-commerce',
                'register',
                compact('user', 'token')
            );

Nous allons également créer une méthode de vérification du token qui validera l'activation du compte

    #[Route('/verif/{token}', name: 'verify_user')]
    public function verifyUser($token, JWTService $jwt, UsersRepository $usersRepository, EntityManagerInterface $em): Response
    {
        //On vérifie si le token est valide, n'a pas expiré et n'a pas été modifié
        if($jwt->isValid($token) && !$jwt->isExpired($token) && $jwt->check($token, $this->getParameter('app.jwtsecret'))){
            // On récupère le payload
            $payload = $jwt->getPayload($token);

            // On récupère le user du token
            $user = $usersRepository->find($payload['user_id']);

            //On vérifie que l'utilisateur existe et n'a pas encore activé son compte
            if($user && !$user->getIsVerified()){
                $user->setIsVerified(true);
                $em->flush($user);
                $this->addFlash('success', 'Utilisateur activé');
                return $this->redirectToRoute('profile_index');
            }
        }
        // Ici un problème se pose dans le token
        $this->addFlash('danger', 'Le token est invalide ou a expiré');
        return $this->redirectToRoute('app_login');
    }

Nous pouvons maintenant mettre à jour le contenu de l'email (templates/emails/register.html.twig)

<h1>Activez votre compte</h1>
<p>Bonjour {{ user.firstname }},</p>
<p>Votre inscription sur le site e-commerce est à valider en cliquant sur le lien ci-dessous :</p>
<p><a href="{{ absolute_url(path('verify_user', {token: token})) }}">{{ absolute_url(path('verify_user', {token: token})) }}</a></p>
<p>Ce lien expirera dans 3 heures</p>

Obtenir de l'aide

Pour obtenir de l'aide, vous pouvez accéder au serveur Guilded pour une entraide par chat.