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
- 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(...);
}
- 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:
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.