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!

12
Login or Register to join the conversation
Naglaa-Fouz Avatar
Naglaa-Fouz Avatar Naglaa-Fouz | posted hace 10 meses

hi, when i use lougout true in security.yaml and when i try make debug de security i have this error

Unable to parse at line 23 (near "logout:true").

Reply

Hey Naglaa-Fouz,

Hm, that sounds like you have a syntax error in that security.yaml file. Please, double-check the syntax, in particular, add a space between logout: and true. Also, make sure the indentation in that file is consistent.

If you still cannot solve this error - please, share your security.yaml's content with us in a comment and I'll take a look for you.

Cheers!

Reply
Tomas norre M. Avatar
Tomas norre M. Avatar Tomas norre M. | posted hace 1 año

I find it a little ironic that the course on Symfony Security is suggesting to tell potential hackers, if the username/email exists or not.
By knowing that emails exists but entered a wrong password, you're half way there to hacking the site, this eases the attack a lot.

Invalid credentials message is in my opinion always a better option.

Any takes on this?

Reply

Hey Tomas norre M.!

Fair point to bring up :).

> By knowing that emails exists but entered a wrong password, you're half way there to hacking the site, this eases the attack a lot.

If you know for a fact that an email exists on the site, you've still got quite a lot of work (passwords to try) in order to actually gain access. If you are using a list of "pwned" passwords, then if an account is using that same password on your site, it will help the hacker a bit to know that the account exists... but probably not that much. It would only help if an email had 5 pwned passwords... and then they could have the confidence to try the next 4 or skip the next 4 after trying the first.

> Invalid credentials message is in my opinion always a better option.

I'm not sure. It's easy to say "yes" to this, but I think what you're mostly protecting by always using "invalid credentials" is whether or not someone has an account on your site... if that fact is sensitive. As I mentioned in the video, knowing someone has an account on Symfonycasts probably won't cause any scandal. However, if you run a gambling site or (use your imagination) something much more sensitive, then being able to discover who has accounts could be disastrous.

And, of course, the "email not found" message is a better user experience. That shouldn't trump security, but I'm not convinced there's a real security problem here for most sites. And, a lot of big sites seem to agree. If you try to, for example, log into facebook with a correct email but incorrect password, it DOES tell you that the account exists (and that your password is wrong).

Cheers!

Reply
Tomas norre M. Avatar
Tomas norre M. Avatar Tomas norre M. | weaverryan | posted hace 1 año

Thanks for taking your time to answer. I follow you, no doubt about that.
And I really enjoy watching your courses. I'm 20+ years into PHP Development, and still learn from the videos.
Using them as a brush-up course.

Reply

Thank you for the kind words! ❤️❤️❤️

Reply
Oliver-W Avatar
Oliver-W Avatar Oliver-W | posted hace 1 año

I am very, very glad to see that you will handle the Scheb 2fa!!!!! Can't wait to see this one.

Keep on with your excellent work!

Reply

Hey Oliver,

Thank you for your feedback! Yeah, 2FA is something that was asked a lot lately! And I'm happy to see it's covered in this course :)

Cheers!

Reply
Zaz Avatar

hi when this course will be updated ? I want to continue this symfony track !

Reply

Hey @zarloon,

What do you mean updated? This course is in active release state, so it is releasing now and it should be 1 chapter per work day.

Cheers!

Reply
Zaz Avatar

Oh yes sorry for my english. I wasn't aware about that :D
Just have to wait for the next chapter then. Really good job i'm glad to have suscribe.

Reply

I'm happy to hear that! Thanks that you are staying with us!

Stay safe and keep learning!

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

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

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "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
    }
}
userVoice