Redirección personalizada cuando "Email no verificado"
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeEs genial que podamos escuchar el CheckPassportEvent
y hacer que la autenticación falle lanzando cualquier excepción de autenticación, como estaCustomUserMessageAuthenticationException
:
// ... lines 1 - 8 | |
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; | |
// ... lines 10 - 12 | |
class CheckVerifiedUserSubscriber implements EventSubscriberInterface | |
{ | |
public function onCheckPassport(CheckPassportEvent $event) | |
// ... lines 16 - 26 | |
if (!$user->getIsVerified()) { | |
throw new CustomUserMessageAuthenticationException( | |
'Please verify your account before logging in.' | |
); | |
} | |
} | |
// ... lines 33 - 39 | |
} |
Pero ¿qué pasa si, en lugar del comportamiento normal de fallo -en el que redirigimos a la página de inicio de sesión y mostramos el error-, queremos hacer algo diferente? ¿Qué pasa si, justo en esta situación, queremos redirigir a una página totalmente diferente para poder explicar que su correo electrónico no está verificado... y tal vez incluso permitirles reenviar ese correo electrónico?
Bueno, por desgracia, no hay forma -en este evento- de controlar la respuesta de fallo. No hay $event->setResponse()
ni nada parecido.
Así que no podemos controlar el comportamiento del error desde aquí, pero podemos controlarlo escuchando un evento diferente. Desde este evento "señalaremos" que la cuenta no ha sido verificada, buscaremos esa señal desde un oyente de eventos diferente y redirigiremos a esa otra página. No pasa nada si esto aún no tiene sentido: vamos a verlo en acción.
Crear una clase de excepción personalizada
Para empezar, tenemos que crear una clase de excepción de autenticación personalizada. Esto servirá como "señal" de que estamos en esta situación de "cuenta no verificada".
En el directorio Security/
, añade una nueva clase: ¿qué talAccountNotVerifiedAuthenticationException
. Haz que extienda AuthenticationException
. Y luego... no hagas absolutamente nada más:
// ... lines 1 - 2 | |
namespace App\Security; | |
use Symfony\Component\Security\Core\Exception\AuthenticationException; | |
class AccountNotVerifiedAuthenticationException extends AuthenticationException | |
{ | |
} |
Ésta es sólo una clase marcadora que utilizaremos para indicar que está fallando la autenticación debido a un correo electrónico no verificado.
De vuelta al suscriptor, sustituye el CustomUserMessageAuthenticationException
porAccountNotVerifiedAuthenticationException
. No necesitamos pasarle ningún mensaje:
// ... lines 1 - 5 | |
use App\Security\AccountNotVerifiedAuthenticationException; | |
// ... lines 7 - 13 | |
class CheckVerifiedUserSubscriber implements EventSubscriberInterface | |
{ | |
public function onCheckPassport(CheckPassportEvent $event) | |
{ | |
// ... lines 18 - 27 | |
if (!$user->getIsVerified()) { | |
throw new AccountNotVerifiedAuthenticationException(); | |
} | |
} | |
// ... lines 32 - 38 | |
} |
Si nos detenemos en este momento, esto no será muy interesante. El inicio de sesión sigue fallando, pero volvemos al mensaje genérico:
Se ha producido una excepción de autenticación
Esto se debe a que nuestra nueva clase personalizada extiende AuthenticationException
... y ese es el mensaje genérico que se obtiene de esa clase. Así que esto no es lo que queremos todavía, ¡pero el paso 1 está hecho!
Escuchar el evento LoginFailureEvent
Para el siguiente paso, recuerda del comando debug:event
que uno de los escuchadores que tenemos es para un LoginFailureEvent
, que, como su nombre indica, se llama cada vez que falla la autenticación.
Vamos a añadir otro oyente en esta clase para eso. Digamos queLoginFailureEvent::class
se ajusta a, qué tal, onLoginFailure
. En este caso, la prioridad no importará:
// ... lines 1 - 12 | |
use Symfony\Component\Security\Http\Event\LoginFailureEvent; | |
class CheckVerifiedUserSubscriber implements EventSubscriberInterface | |
{ | |
// ... lines 17 - 38 | |
public static function getSubscribedEvents() | |
{ | |
return [ | |
// ... line 42 | |
LoginFailureEvent::class => 'onLoginFailure', | |
]; | |
} | |
} |
Añade el nuevo método: public function onLoginFailure()
... y sabemos que éste recibirá un argumento LoginFailureEvent
. Al igual que antes, empieza condd($event)
para ver cómo queda:
// ... lines 1 - 12 | |
use Symfony\Component\Security\Http\Event\LoginFailureEvent; | |
class CheckVerifiedUserSubscriber implements EventSubscriberInterface | |
{ | |
// ... lines 17 - 33 | |
public function onLoginFailure(LoginFailureEvent $event) | |
{ | |
dd($event); | |
} | |
// ... lines 38 - 45 | |
} |
Así que, con un poco de suerte, si fallamos en el inicio de sesión -por cualquier motivo- se llamará a nuestro oyente. Por ejemplo, si introduzco una contraseña incorrecta, ¡sí! Se llama. Y fíjate en que elLoginFailureEvent
tiene una propiedad de excepción. En este caso, contiene unBadCredentialsException
.
Ahora entra con la contraseña correcta y... se golpea de nuevo. Pero esta vez, fíjate en la excepción. ¡Es nuestro AccountNotVerifiedAuthenticationException
personalizado! Así que el objeto LoginFailureEvent
contiene la excepción de autenticación que causó el fallo. Podemos utilizarlo para saber -desde este método- si la autenticación falló debido a que la cuenta no está verificada.
Redirigir cuando la cuenta no está verificada
Así que, si no $event->getException()
es una instancia deAccountNotVerifiedAuthenticationException
, entonces simplemente devuelve y permite que el comportamiento de fallo por defecto haga lo suyo:
// ... lines 1 - 14 | |
class CheckVerifiedUserSubscriber implements EventSubscriberInterface | |
{ | |
// ... lines 17 - 33 | |
public function onLoginFailure(LoginFailureEvent $event) | |
{ | |
if (!$event->getException() instanceof AccountNotVerifiedAuthenticationException) { | |
return; | |
} | |
} | |
// ... lines 40 - 47 | |
} |
Finalmente, aquí abajo, sabemos que debemos redirigir a esa página personalizada. Vamos... a crear esa página rápidamente. Hazlo en src/Controller/RegistrationController.php
. En la parte inferior, añade un nuevo método. Lo llamaré resendVerifyEmail()
. Encima de esto, añade @Route()
con, qué tal /verify/resend
y el nombre es igual aapp_verify_resend_email
. Dentro, sólo voy a renderizar una plantilla: return$this->render()
, registration/resend_verify_email.html.twig
:
// ... lines 1 - 16 | |
class RegistrationController extends AbstractController | |
{ | |
// ... lines 19 - 88 | |
/** | |
* @Route("/verify/resend", name="app_verify_resend_email") | |
*/ | |
public function resendVerifyEmail() | |
{ | |
return $this->render('registration/resend_verify_email.html.twig'); | |
} | |
} |
¡Vamos a hacer eso! Dentro de templates/registration/
, crearesend_verify_email.html.twig
. Voy a pegar la plantilla:
{% extends 'base.html.twig' %} | |
{% block title %}Verify Email{% endblock %} | |
{% block body %} | |
<div class="container"> | |
<div class="row"> | |
<div class="login-form bg-light mt-4 p-4"> | |
<h1 class="h3 mb-3 font-weight-normal">Verify your Email</h1> | |
<p> | |
A verification email was sent - please click it to enable your | |
account before logging in. | |
</p> | |
<a href="#" class="btn btn-primary">Re-send Email</a> | |
</div> | |
</div> | |
</div> | |
{% endblock %} |
Aquí no hay nada del otro mundo. Sólo explica la situación.
He incluido un botón para reenviar el correo electrónico, pero te dejo la implementación a ti. Yo probablemente lo rodearía de un formulario que haga un POST a esta URL. Y luego, en el controlador, si el método es POST, utilizaría el paquete de correo electrónico de verificación para generar un nuevo enlace y reenviarlo. Básicamente, el mismo código que utilizamos tras el registro.
De todos modos, ahora que tenemos una página funcional, copia el nombre de la ruta y vuelve a nuestro suscriptor. Para anular el comportamiento normal de los fallos, podemos utilizar un métodosetResponse()
en el evento.
Empieza con $response = new RedirectResponse()
-vamos a generar una URL para la ruta en un minuto- y luego con $event->setResponse($response)
:
// ... lines 1 - 16 | |
class CheckVerifiedUserSubscriber implements EventSubscriberInterface | |
{ | |
// ... lines 19 - 42 | |
public function onLoginFailure(LoginFailureEvent $event) | |
{ | |
if (!$event->getException() instanceof AccountNotVerifiedAuthenticationException) { | |
return; | |
} | |
$response = new RedirectResponse( | |
// ... line 50 | |
); | |
$event->setResponse($response); | |
} | |
// ... lines 54 - 61 | |
} |
Para generar la URL, necesitamos un método __construct()
-permíteme deletrearlo correctamente- con un argumento RouterInterface $router
. Pulsa Alt
+Enter
y ve a "Inicializar propiedades" para crear esa propiedad y establecerla:
// ... lines 1 - 8 | |
use Symfony\Component\Routing\RouterInterface; | |
// ... lines 10 - 16 | |
class CheckVerifiedUserSubscriber implements EventSubscriberInterface | |
{ | |
private RouterInterface $router; | |
public function __construct(RouterInterface $router) | |
{ | |
$this->router = $router; | |
} | |
// ... lines 25 - 61 | |
} |
Aquí abajo, estamos en el negocio: $this->router->generate()
conapp_verify_resend_email
:
// ... lines 1 - 16 | |
class CheckVerifiedUserSubscriber implements EventSubscriberInterface | |
{ | |
// ... lines 19 - 42 | |
public function onLoginFailure(LoginFailureEvent $event) | |
{ | |
// ... lines 45 - 48 | |
$response = new RedirectResponse( | |
$this->router->generate('app_verify_resend_email') | |
); | |
// ... line 52 | |
} | |
// ... lines 54 - 61 | |
} |
¡Donezo! Fallamos la autenticación, nuestro primer oyente lanza la excepción personalizada, buscamos esa excepción desde el oyente de LoginFailureEvent
... y establecemos la redirección.
¡Hora de probar! Refresca y... ¡lo tienes! Nos envían a /verify/resend
. ¡Me encanta!
A continuación: vamos a terminar este tutorial haciendo algo superguay, superdivertido y... un poco friki. Vamos a añadir la autenticación de dos factores, completada con elegantes códigos QR.
Hi! Thanks for this great tutorial!
But at the last step - add resending response to CheckVerifiedUserSubscriber - went something wrong.
The same code (but Symfony 6) - but instead of being redirected to "app_verify_resend_email" in the browser - appears this error:
Typed property App\EventSubscriber\CheckVerifiedUserSubscriber::$router must not be accessed before initialization
Any hint how to fix it?