Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Verificación de la URL firmada del correo electrónico de confirmación

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Ahora estamos generando una URL firmada que normalmente incluiríamos en un correo electrónico de "confirmación de la dirección de correo electrónico" que enviamos al usuario tras el registro. Para simplificar las cosas, sólo vamos a renderizar esa URL en la página después del registro.

Eliminando nuestro Bind no utilizado

Vamos a ver qué aspecto tiene. Refresca y... ¡ah! ¡Un error de aspecto terrible!

Se ha configurado un enlace para un argumento llamado $formLoginAuthenticator en _defaults, pero no se ha encontrado el argumento correspondiente.

Así que, hasta hace unos minutos, teníamos un argumento para nuestra acción register() que se llamaba $formLoginAuthenticator. En config/services.yaml, hemos configurado un "bind" global que decía

Siempre que un servicio autocableado tenga un argumento llamado $formLoginAuthenticator, por favor, pasa este servicio.

... lines 1 - 8
services:
# default configuration for services in *this* file
_defaults:
... lines 12 - 13
bind:
... line 15
$formLoginAuthenticator: '@security.authenticator.form_login.main'
... lines 17 - 32

Una de las cosas buenas de bind es que si no hay un argumento que coincida en ninguna parte de nuestra aplicación, lanza una excepción. Intenta asegurarse de que no estamos cometiendo una errata accidental.

En nuestra situación, ya no necesitamos ese argumento. Así que elimínalo. Y ahora... ¡nuestra página de registro está viva!

Comprobando la URL de verificación

¡Hagamos esto! Introduce un correo electrónico, una contraseña, acepta las condiciones y pulsa registrar. ¡Genial! Aquí está nuestra URL de confirmación por correo electrónico. Puedes ver que va a/verify: que dará a nuestra nueva acción verifyUserEmail(). También incluye una caducidad. Eso es algo que puedes configurar... es el tiempo de validez del enlace. Y tiene un signature: que es algo que ayudará a demostrar que el usuario no se ha inventado esta URL: definitivamente viene de nosotros.

También incluye un id=18: nuestro identificador de usuario.

Verificar la URL firmada

Así que nuestro trabajo ahora es ir al método del controlador verifyUserEmail aquí abajo y validar esa URL firmada. Para ello, necesitamos unos cuantos argumentos: el objeto Request -para poder leer los datos de la URL-, unVerifyEmailHelperInterface para ayudarnos a validar la URL y, por último, nuestro UserRepository -para poder consultar el objeto User:

... lines 1 - 6
use App\Repository\UserRepository;
... line 8
use Symfony\Component\HttpFoundation\Request;
... lines 10 - 13
use SymfonyCasts\Bundle\VerifyEmail\VerifyEmailHelperInterface;
class RegistrationController extends AbstractController
{
... lines 18 - 60
public function verifyUserEmail(Request $request, VerifyEmailHelperInterface $verifyEmailHelper, UserRepository $userRepository): Response
{
... lines 63 - 80
}
}

Y en realidad, ese es nuestro primer trabajo. Digamos que $user = $userRepository->find() y encontrar el usuario al que pertenece este enlace de confirmación leyendo el parámetro de consulta id. Así que, $request->query->get('id'). Y si, por alguna razón, no podemos encontrar el User, vamos a lanzar una página 404 lanzando$this->createNotFoundException():

... lines 1 - 15
class RegistrationController extends AbstractController
{
... lines 18 - 60
public function verifyUserEmail(Request $request, VerifyEmailHelperInterface $verifyEmailHelper, UserRepository $userRepository): Response
{
$user = $userRepository->find($request->query->get('id'));
if (!$user) {
throw $this->createNotFoundException();
}
... lines 67 - 80
}
}

Ahora podemos asegurarnos de que la URL firmada no ha sido manipulada. Para ello, añade un bloque try-catch. Dentro, di $verifyEmailHelper->validateEmailConfirmation()y pasa un par de cosas. Primero, la URL firmada, que... es la URL actual. Obténla con $request->getUri(). A continuación, pasa el identificador del usuario - $user->getId() y luego el correo electrónico del usuario - $user->getEmail():

... lines 1 - 15
class RegistrationController extends AbstractController
{
... lines 18 - 60
public function verifyUserEmail(Request $request, VerifyEmailHelperInterface $verifyEmailHelper, UserRepository $userRepository): Response
{
$user = $userRepository->find($request->query->get('id'));
if (!$user) {
throw $this->createNotFoundException();
}
try {
$verifyEmailHelper->validateEmailConfirmation(
$request->getUri(),
$user->getId(),
$user->getEmail(),
);
... lines 74 - 77
}
... lines 79 - 80
}
}

Esto asegura que la identificación y el correo electrónico no han cambiado en la base de datos desde que se envió el correo de verificación. Bueno, el id definitivamente no ha cambiado... ya que lo acabamos de utilizar para la consulta. Esta parte sólo se aplica realmente si confías en que el usuario esté conectado para verificar su correo electrónico.

De todos modos, si esto tiene éxito... ¡no pasará nada! Si falla, lanzará una excepción especial que implementa VerifyEmailExceptionInterface:

... lines 1 - 15
class RegistrationController extends AbstractController
{
... lines 18 - 60
public function verifyUserEmail(Request $request, VerifyEmailHelperInterface $verifyEmailHelper, UserRepository $userRepository): Response
{
... lines 63 - 67
try {
$verifyEmailHelper->validateEmailConfirmation(
$request->getUri(),
$user->getId(),
$user->getEmail(),
);
} catch (VerifyEmailExceptionInterface $e) {
... lines 75 - 77
}
... lines 79 - 80
}
}

Así que, aquí abajo, sabemos que la verificación de la URL ha fallado... tal vez alguien se ha equivocado. O, más probablemente, el enlace ha caducado. Digamos al usuario la razón aprovechando de nuevo el sistema flash. Digamos $this->addFlash(), pero esta vez poniéndolo en una categoría diferente llamada error. Luego, para decir lo que ha ido mal, utiliza $e->getReason(). Por último, utiliza redirectToRoute() para enviarlos a algún sitio. ¿Qué tal la página de registro?

... lines 1 - 15
class RegistrationController extends AbstractController
{
... lines 18 - 60
public function verifyUserEmail(Request $request, VerifyEmailHelperInterface $verifyEmailHelper, UserRepository $userRepository): Response
{
... lines 63 - 67
try {
$verifyEmailHelper->validateEmailConfirmation(
$request->getUri(),
$user->getId(),
$user->getEmail(),
);
} catch (VerifyEmailExceptionInterface $e) {
$this->addFlash('error', $e->getReason());
return $this->redirectToRoute('app_register');
}
dd('TODO');
}
}

Para mostrar el error, vuelve a base.html.twig, duplica todo este bloque, pero busca los mensajes de error y utiliza alert-danger:

<!DOCTYPE html>
<html>
... lines 3 - 14
<body
... lines 16 - 81
{% for flash in app.flashes('success') %}
<div class="alert alert-success">{{ flash }}</div>
{% endfor %}
{% for flash in app.flashes('error') %}
<div class="alert alert-danger">{{ flash }}</div>
{% endfor %}
... lines 88 - 92
</body>
</html>

¡Uf! Probemos el caso del error. Copia la URL y luego abre una nueva pestaña y pégala. Si voy a esta URL real... funciona. Bueno, todavía tenemos que hacer algo más de codificación, pero llega a nuestro TODO en la parte inferior del controlador. Ahora juega con la URL, como eliminar algunos caracteres... o ajustar la caducidad o cambiar el id. Ahora... ¡sí! Ha fallado porque nuestro enlace no es válido. Si el enlace estuviera caducado, verías un mensaje al respecto.

Así que, por fin, ¡acabemos con el caso feliz! En la parte inferior de nuestro controlador, ahora que sabemos que el enlace de verificación es válido, hemos terminado. Para nuestra aplicación, podemos decir $user->isVerified(true) y almacenarlo en la base de datos:

... lines 1 - 16
class RegistrationController extends AbstractController
{
... lines 19 - 61
public function verifyUserEmail(Request $request, VerifyEmailHelperInterface $verifyEmailHelper, UserRepository $userRepository, EntityManagerInterface $entityManager): Response
{
... lines 64 - 68
try {
... lines 70 - 78
}
$user->setIsVerified(true);
... lines 82 - 86
}
}

Veamos... necesitamos un argumento más: EntityManagerInterface $entityManager:

... lines 1 - 7
use Doctrine\ORM\EntityManagerInterface;
... lines 9 - 16
class RegistrationController extends AbstractController
{
... lines 19 - 61
public function verifyUserEmail(Request $request, VerifyEmailHelperInterface $verifyEmailHelper, UserRepository $userRepository, EntityManagerInterface $entityManager): Response
{
... lines 64 - 86
}
}

Aquí abajo, utiliza $entityManager->flush() para guardar ese cambio:

... lines 1 - 16
class RegistrationController extends AbstractController
{
... lines 19 - 61
public function verifyUserEmail(Request $request, VerifyEmailHelperInterface $verifyEmailHelper, UserRepository $userRepository, EntityManagerInterface $entityManager): Response
{
... lines 64 - 80
$user->setIsVerified(true);
$entityManager->flush();
... lines 83 - 86
}
}

Y demos a esto un feliz mensaje de éxito:

¡Cuenta verificada! Ya puedes conectarte.

Bueno, la verdad es que todavía no impedimos que se conecten antes de verificar su correo electrónico. Pero lo haremos pronto. De todos modos, termina redirigiendo a la página de inicio de sesión: app_login:

... lines 1 - 16
class RegistrationController extends AbstractController
{
... lines 19 - 61
public function verifyUserEmail(Request $request, VerifyEmailHelperInterface $verifyEmailHelper, UserRepository $userRepository, EntityManagerInterface $entityManager): Response
{
... lines 64 - 80
$user->setIsVerified(true);
$entityManager->flush();
$this->addFlash('success', 'Account Verified! You can now log in.');
return $this->redirectToRoute('app_login');
}
}

Si quieres ser aún más genial, podrías autenticar manualmente al usuario de la misma manera que lo hicimos antes en nuestro controlador de registro. Eso está totalmente bien y depende de ti.

De vuelta a mi pestaña principal... copia ese enlace de nuevo, pégalo y... ¡estamos verificados! ¡Qué bien!

Lo único que queda por hacer es impedir que el usuario se registre hasta que haya verificado su correo electrónico. Para ello, primero tenemos que conocer los eventos que ocurren dentro del sistema de seguridad. Y para mostrarlos, aprovecharemos una nueva función muy interesante: el estrangulamiento del inicio de sesión.

Leave a comment!

¡Este tutorial también funciona muy bien para Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.4.1 || ^8.0.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.3", // v3.3.0
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "doctrine/annotations": "^1.0", // 1.13.2
        "doctrine/doctrine-bundle": "^2.1", // 2.6.3
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.1.1
        "doctrine/orm": "^2.7", // 2.10.1
        "knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
        "knplabs/knp-time-bundle": "^1.11", // v1.16.1
        "pagerfanta/doctrine-orm-adapter": "^3.3", // v3.3.0
        "pagerfanta/twig": "^3.3", // v3.3.0
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "scheb/2fa-bundle": "^5.12", // v5.12.1
        "scheb/2fa-qr-code": "^5.12", // v5.12.1
        "scheb/2fa-totp": "^5.12", // v5.12.1
        "sensio/framework-extra-bundle": "^6.0", // v6.2.0
        "stof/doctrine-extensions-bundle": "^1.4", // v1.6.0
        "symfony/asset": "5.3.*", // v5.3.4
        "symfony/console": "5.3.*", // v5.3.7
        "symfony/dotenv": "5.3.*", // v5.3.8
        "symfony/flex": "^1.3.1", // v1.17.5
        "symfony/form": "5.3.*", // v5.3.8
        "symfony/framework-bundle": "5.3.*", // v5.3.8
        "symfony/monolog-bundle": "^3.0", // v3.7.0
        "symfony/property-access": "5.3.*", // v5.3.8
        "symfony/property-info": "5.3.*", // v5.3.8
        "symfony/rate-limiter": "5.3.*", // v5.3.4
        "symfony/runtime": "5.3.*", // v5.3.4
        "symfony/security-bundle": "5.3.*", // v5.3.8
        "symfony/serializer": "5.3.*", // v5.3.8
        "symfony/stopwatch": "5.3.*", // v5.3.4
        "symfony/twig-bundle": "5.3.*", // v5.3.4
        "symfony/ux-chartjs": "^1.3", // v1.3.0
        "symfony/validator": "5.3.*", // v5.3.8
        "symfony/webpack-encore-bundle": "^1.7", // v1.12.0
        "symfony/yaml": "5.3.*", // v5.3.6
        "symfonycasts/verify-email-bundle": "^1.5", // v1.5.0
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.3
        "twig/string-extra": "^3.3", // v3.3.3
        "twig/twig": "^2.12|^3.0" // v3.3.3
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
        "symfony/debug-bundle": "5.3.*", // v5.3.4
        "symfony/maker-bundle": "^1.15", // v1.34.0
        "symfony/var-dumper": "5.3.*", // v5.3.8
        "symfony/web-profiler-bundle": "5.3.*", // v5.3.8
        "zenstruck/foundry": "^1.1" // v1.13.3
    }
}