5 minutos de lectura

Implementando verificación de email en Symfony usando VerifyEmailBundle

Puntos a ver ...

Introducción

La verificación de email es un componente crucial en cualquier aplicación moderna que requiera registro de usuarios. El VerifyEmailBundle de Symfony proporciona una solución robusta y segura para implementar este proceso. En este artículo, exploraremos cómo implementar y personalizar la verificación de email en una aplicación Symfony.

Instalación

Primero, necesitamos instalar el bundle usando Composer:

composer require symfonycasts/verify-email-bundle

Configuración Básica

1. Preparación de la Entidad Usuario

Tu entidad Usuario debe implementar la capacidad de rastrear si el email ha sido verificado. Añade los siguientes campos:

use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
class User
{
    #[ORM\Column(type: 'boolean')]
    private bool $isVerified = false;

    #[ORM\Column(type: 'datetime', nullable: true)]
    private ?\DateTimeInterface $verifiedAt = null;

    public function isVerified(): bool
    {
        return $this->isVerified;
    }

    public function setIsVerified(bool $isVerified): self
    {
        $this->isVerified = $isVerified;
        $this->verifiedAt = $isVerified ? new \DateTime() : null;
        return $this;
    }

    public function getVerifiedAt(): ?\DateTimeInterface
    {
        return $this->verifiedAt;
    }
}

2. Implementación del EmailVerifier Service

Crea un servicio que maneje la lógica de verificación:

namespace App\Security;

use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use SymfonyCasts\Bundle\VerifyEmail\Exception\VerifyEmailExceptionInterface;
use SymfonyCasts\Bundle\VerifyEmail\VerifyEmailHelperInterface;

class EmailVerifier
{
    public function __construct(
        private VerifyEmailHelperInterface $verifyEmailHelper,
        private MailerInterface $mailer,
        private EntityManagerInterface $entityManager
    ) {}

    public function sendEmailConfirmation(string $verifyEmailRouteName, UserInterface $user, TemplatedEmail $email): void
    {
        $signatureComponents = $this->verifyEmailHelper->generateSignature(
            $verifyEmailRouteName,
            $user->getId(),
            $user->getEmail(),
            ['id' => $user->getId()]
        );

        $context = $email->getContext();
        $context['signedUrl'] = $signatureComponents->getSignedUrl();
        $context['expiresAtMessageKey'] = $signatureComponents->getExpirationMessageKey();
        $context['expiresAtMessageData'] = $signatureComponents->getExpirationMessageData();

        $email->context($context);

        $this->mailer->send($email);
    }

    /**
     * @throws VerifyEmailExceptionInterface
     */
    public function handleEmailConfirmation(Request $request, UserInterface $user): void
    {
        $this->verifyEmailHelper->validateEmailConfirmation(
            $request->getUri(),
            $user->getId(),
            $user->getEmail()
        );

        $user->setIsVerified(true);
        $this->entityManager->persist($user);
        $this->entityManager->flush();
    }
}

3. Configuración del Controller

Implementa los controllers necesarios para manejar el registro y la verificación:

namespace App\Controller;

use App\Entity\User;
use App\Security\EmailVerifier;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mime\Address;
use Symfony\Component\Routing\Annotation\Route;
use SymfonyCasts\Bundle\VerifyEmail\Exception\VerifyEmailExceptionInterface;

class RegistrationController extends AbstractController
{
    private EmailVerifier $emailVerifier;

    public function __construct(EmailVerifier $emailVerifier)
    {
        $this->emailVerifier = $emailVerifier;
    }

    #[Route('/register', name: 'app_register')]
    public function register(Request $request, EntityManagerInterface $entityManager): Response
    {
        // Lógica de registro aquí...

        // Genera y envía el email de verificación
        $this->emailVerifier->sendEmailConfirmation('app_verify_email',
            $user,
            (new TemplatedEmail())
                ->from(new Address('mailer@your-domain.com', 'Acme Mail Bot'))
                ->to($user->getEmail())
                ->subject('Por favor confirma tu email')
                ->htmlTemplate('registration/confirmation_email.html.twig')
        );

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

    #[Route('/verify/email', name: 'app_verify_email')]
    public function verifyUserEmail(Request $request): Response
    {
        $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
        $user = $this->getUser();

        try {
            $this->emailVerifier->handleEmailConfirmation($request, $user);
        } catch (VerifyEmailExceptionInterface $exception) {
            $this->addFlash('verify_email_error', $exception->getReason());
            return $this->redirectToRoute('app_register');
        }

        $this->addFlash('success', 'Tu dirección de email ha sido verificada.');
        return $this->redirectToRoute('app_home');
    }
}

4. Plantilla de Email

Crea la plantilla para el email de verificación:

{# templates/registration/confirmation_email.html.twig #}
<h1>¡Por favor confirma tu email!</h1>

<p>
    Por favor confirma tu dirección de email haciendo click en el siguiente enlace: <br><br>
    <a href="{{ signedUrl|raw }}">Confirmar mi Email</a>.
    Este enlace expirará en {{ expiresAtMessageKey|trans(expiresAtMessageData, 'VerifyEmailBundle') }}.
</p>

<p>
    ¡Saludos!
</p>

Personalización Avanzada

Configuración del Tiempo de Expiración

Puedes configurar el tiempo de expiración del enlace de verificación en tu archivo config/packages/verify_email.yaml:

symfonycasts_verify_email:
    lifetime: 3600  # El enlace expirará después de 1 hora

Implementación de Middleware de Verificación

Para forzar la verificación de email en ciertas rutas:

namespace App\Security;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\RouterInterface;

class EmailVerificationMiddleware
{
    public function __construct(
        private Security $security,
        private RouterInterface $router
    ) {}

    public function __invoke(Request $request): ?Response
    {
        $user = $this->security->getUser();

        if ($user && !$user->isVerified()) {
            return new RedirectResponse(
                $this->router->generate('app_verify_email_notice')
            );
        }

        return null;
    }
}

Mejores Prácticas y Consideraciones de Seguridad

  1. Manejo de Reintentos: Implementa un límite de reintentos para prevenir abusos:
#[Route('/resend-verification', name: 'app_resend_verification')]
public function resendVerificationEmail(Request $request): Response
{
    $user = $this->getUser();
    
    // Verifica el número de intentos en las últimas 24 horas
    $attempts = $this->rateLimiter->consume($user->getId());
    if (!$attempts->isAccepted()) {
        throw new TooManyRequestsHttpException();
    }
    
    // Reenvía el email
    $this->emailVerifier->sendEmailConfirmation(...);
}
  1. Limpieza de Tokens Expirados: Implementa un comando para limpiar tokens antiguos:
namespace App\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class CleanupVerificationTokensCommand extends Command
{
    protected static $defaultName = 'app:cleanup-verification-tokens';

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        // Implementa la lógica de limpieza aquí
        return Command::SUCCESS;
    }
}

Recursos Adicionales

Para complementar este artículo, te recomiendo el siguiente video tutorial que muestra una implementación práctica del VerifyEmailBundle:

Tutorial

Este video proporciona una demostración visual del proceso de implementación y puede ser especialmente útil para aquellos que prefieren un enfoque más práctico y guiado.

Conclusión

El VerifyEmailBundle proporciona una solución robusta y segura para la verificación de email en aplicaciones Symfony. Al seguir las mejores prácticas y consideraciones de seguridad mencionadas, puedes implementar un sistema de verificación confiable y mantenible.

Recuerda siempre mantener actualizado el bundle y revisar periódicamente la documentación oficial para nuevas características y mejoras de seguridad.