Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Personalizar los mensajes de error y añadir el cierre de sesió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

Cuando falla el inicio de sesión, almacenamos el AuthenticationException en la sesión -que explica lo que ha ido mal- y luego redirigimos a la página de inicio de sesión:

... lines 1 - 20
class LoginFormAuthenticator extends AbstractAuthenticator
{
... lines 23 - 65
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
$request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);
return new RedirectResponse(
$this->router->generate('app_login')
);
}
... lines 74 - 84
}

En esa página, leemos esa excepción de la sesión utilizando este bonito servicioAuthenticationUtils:

... lines 1 - 9
class SecurityController extends AbstractController
{
... lines 12 - 14
public function login(AuthenticationUtils $authenticationUtils): Response
{
return $this->render('security/login.html.twig', [
'error' => $authenticationUtils->getLastAuthenticationError(),
]);
}
}

Y finalmente, en la plantilla, llamamos al método getMessageKey() para mostrar un mensaje seguro que describa por qué ha fallado la autenticación:

... lines 1 - 4
{% block body %}
<div class="container">
<div class="row">
<div class="login-form bg-light mt-4 p-4">
<form method="post" class="row g-3">
... lines 10 - 11
{% if error %}
<div class="alert alert-danger">{{ error.messageKey }}</div>
{% endif %}
... lines 15 - 29
</form>
</div>
</div>
</div>
{% endblock %}

Por ejemplo, si introducimos un correo electrónico que no existe, veremos

No se pudo encontrar el nombre de usuario.

A nivel técnico, esto significa que no se ha podido encontrar el objeto User. Genial... pero para nosotros no es un gran mensaje porque nos estamos conectando a través de un correo electrónico. Además, si introducimos un usuario válido - abraca_admin@example.com - con una contraseña no válida, vemos

Credenciales no válidas.

Este es un mensaje mejor... pero no es súper amigable.

¿Traducción de los mensajes de error?

Entonces, ¿cómo podemos personalizarlos? La respuesta es sencilla y... quizá un poco sorprendente: los traducimos. Compruébalo: en la plantilla, después de messageKey, añade|trans para traducirlo. Pásale dos argumentos. El primero es error.messageData. No es demasiado importante... pero en el mundo de la traducción, a veces tus traducciones pueden tener valores "comodín"... y aquí pasas los valores de esos comodines. El segundo argumento se llama "dominio de traducción"... que es casi como una categoría de traducción. Pasa security:

... lines 1 - 4
{% block body %}
<div class="container">
<div class="row">
<div class="login-form bg-light mt-4 p-4">
<form method="post" class="row g-3">
... lines 10 - 11
{% if error %}
<div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
{% endif %}
... lines 15 - 29
</form>
</div>
</div>
</div>
{% endblock %}

Si tienes un sitio multilingüe, todos los mensajes centrales de autentificación ya han sido traducidos a otros idiomas... y esas traducciones están disponibles en un dominio llamado security. Así que al utilizar el dominio security aquí, si cambiamos el sitio al español, obtendríamos instantáneamente mensajes de autenticación en español.

Si nos detuviéramos ahora... ¡no cambiaría absolutamente nada! Pero como estamos pasando por el traductor, tenemos la oportunidad de "traducir" estas cadenas del inglés a... ¡un inglés diferente!

En el directorio translations/ -que deberías tener automáticamente porque el componente de traducción ya está instalado- crea un nuevo archivo llamadosecurity.en.yaml: security porque estamos utilizando el dominio de traducción security y en para el inglés. También puedes crear archivos de traducción .xlf - YAML es simplemente más fácil para lo que necesitamos hacer.

Ahora, copia el mensaje de error exacto, incluyendo el punto, pégalo -lo envolveré entre comillas para estar seguro- y pon algo diferente como

¡Contraseña no válida introducida!

"Invalid credentials.": "Invalid password entered!"

¡Genial! Intentémoslo de nuevo. Entra como abraca_admin@example.com con una contraseña no válida y... ¡mucho mejor! Probemos con un correo electrónico incorrecto.

Bien, repite el proceso: copia el mensaje, ve al archivo de traducción, pégalo... y cámbialo por algo un poco más fácil de usar como

¡Email no encontrado!

... line 1
"Username could not be found.": "Email not found!"

Intentémoslo de nuevo: el mismo correo electrónico, cualquier contraseña y... ¡ya está!

Correo electrónico no encontrado.

¡Muy bien! ¡Nuestro autentificador está hecho! Cargamos el User desde el correo electrónico, comprobamos su contraseña y manejamos tanto el éxito como el fracaso. ¡Booya! Vamos a añadir más cosas a esto más adelante -incluyendo la comprobación de contraseñas de usuarios reales- pero esto es totalmente funcional.

Cerrar la sesión

Vamos a añadir una forma de cerrar la sesión. Así... como si el usuario fuera a /logout, se... ¡se cierra la sesión! Esto empieza exactamente como esperas: necesitamos una ruta y un controlador.

Dentro de SecurityController, copiaré el método login(), lo pegaré, lo cambiaré a /logout, app_logout y llamaré al método logout:

... lines 1 - 9
class SecurityController extends AbstractController
{
... lines 12 - 21
/**
* @Route("/logout")
*/
public function logout()
{
... line 27
}
}

Para realizar el cierre de sesión propiamente dicho... no vamos a poner absolutamente nada de código en este método. En realidad, lanzaré un nuevo \Exception() que diga "logout() nunca debe ser alcanzado":

... lines 1 - 9
class SecurityController extends AbstractController
{
... lines 12 - 21
/**
* @Route("/logout")
*/
public function logout()
{
throw new \Exception('logout() should never be reached');
}
}

Deja que me explique. El cierre de sesión funciona un poco como el inicio de sesión. En lugar de poner alguna lógica en el controlador, vamos a activar algo en nuestro cortafuegos que diga

Si el usuario va a /logout, intercepta esa petición, cierra la sesión del usuario y redirígelo a otro lugar.

Para activar esa magia, abre config/packages/security.yaml. En cualquier lugar de nuestro cortafuegos, añade logout: true:

security:
... lines 2 - 16
firewalls:
... lines 18 - 20
main:
... lines 22 - 25
logout: true
... lines 27 - 39

Internamente, esto activa un "oyente" que busca cualquier petición a /logout.

Configurar el cierre de sesión

Y en realidad, en lugar de decir simplemente logout: true, puedes personalizar cómo funciona esto. Busca tu terminal y ejecuta:

symfony console debug:config security

Como recordatorio, este comando te muestra toda tu configuración actual bajo la clave security. Así que toda nuestra configuración más los valores por defecto.

Si ejecutamos esto... y encontramos el cortafuegos main... mira la sección logout. Todas estas claves son los valores por defecto. Observa que hay una llamadapath: /logout. Por eso está escuchando la URL /logout. Si quisieras cerrar la sesión a través de otra URL, sólo tendrías que modificar esta clave aquí.

Pero como aquí tenemos /logout... y eso coincide con nuestro /logout de aquí, esto debería funcionar. Por cierto, quizá te preguntes por qué necesitamos crear una ruta y un controlador ¡Buena pregunta! En realidad no necesitamos un controlador, nunca será llamado. Pero sí necesitamos una ruta. Si no tuviéramos una, el sistema de rutas provocaría un error 404 antes de que el sistema de cierre de sesión pudiera hacer su magia. Además, es bueno tener una ruta, para poder generar una URL hacia ella.

Bien: ¡probemos esto! Primero inicia sesión: abraca_admin@example.com y contraseña tada. Genial: estamos autentificados. Ve manualmente a /logout y... ¡ya hemos cerrado la sesión! El comportamiento por defecto del sistema es cerrar la sesión y redirigirnos a la página de inicio. Si necesitas personalizarlo, hay algunas opciones. En primer lugar, en la clave logout, puedes cambiar target por alguna otra URL o nombre de ruta.

Pero también podemos engancharnos al proceso de cierre de sesión a través de un oyente de eventos, un tema del que hablaremos hacia el final del tutorial.

Siguiente: vamos a dar a cada usuario una contraseña real. Esto implicará hacer un hash de las contraseñas, para poder almacenarlas de forma segura en la base de datos, y luego comprobar esas contraseñas hash durante la autenticación. Symfony facilita ambas cosas.

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
    }
}