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 SubscribeCuando 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.
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.
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
.
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.
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!
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?
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!
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.
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!
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!
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!
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.
I'm happy to hear that! Thanks that you are staying with us!
Stay safe and keep learning!
Cheers!
// 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
}
}
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").