Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Seguridad Eventos y Listeners

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

Si has utilizado Symfony durante un tiempo, probablemente sabrás que Symfony envía eventos durante el proceso de petición-respuesta y que puedes escucharlos. Para ver estos eventos y sus oyentes, podemos ejecutar:

symfony console debug:event

No voy a profundizar demasiado, pero, este evento kernel.request se despacha en cada petición antes de que se llame al controlador. Esto significa que todos estos oyentes se ejecutan antes que nuestro controlador. Los oyentes de este evento kernel.responseson llamados después de nuestro controlador.

Estos dos eventos no tienen... nada que ver con el sistema de seguridad. Pero resulta que nuestro cortafuegos también envía varios eventos durante el proceso de autenticación, y también podemos escucharlos.

Para ver una lista de todos los oyentes de estos eventos, podemos ejecutar de nuevo debug:event, pero con un --dispatcher= especial fijado en security.event_dispatcher.main:

symfony console debug:event --dispatcher=security.event_dispatcher.main

Lo sé, parece un poco raro... pero esto nos permite listar los oyentes de eventos para el despachador de eventos que es específico del cortafuegos main.

Mirando los eventos y oyentes de seguridad del núcleo

Y... ¡impresionante! Un conjunto totalmente diferente de eventos y escuchadores. Esto es genial. Vuelve a mirar nuestra clase personalizada LoginFormAuthenticator. Ya no la utilizamos, pero puede ayudarnos a entender qué eventos se envían a través del proceso.

Sabemos que, en nuestro método authenticate(), nuestro trabajo es devolver el Passport:

... lines 1 - 26
class LoginFormAuthenticator extends AbstractLoginFormAuthenticator
{
... lines 29 - 39
public function authenticate(Request $request): PassportInterface
{
$email = $request->request->get('email');
$password = $request->request->get('password');
return new Passport(
new UserBadge($email, function($userIdentifier) {
// optionally pass a callback to load the User manually
$user = $this->userRepository->findOneBy(['email' => $userIdentifier]);
if (!$user) {
throw new UserNotFoundException();
}
return $user;
}),
new PasswordCredentials($password),
[
new CsrfTokenBadge(
'authenticate',
$request->request->get('_csrf_token')
),
(new RememberMeBadge())->enable(),
]
);
}
... lines 66 - 81
}

Entonces, después de llamar al método authenticate() -en cualquier autentificador- Symfony despacha CheckPassportEvent. Hay un montón de oyentes interesantes para esto.

Por ejemplo, UserProviderListener es básicamente responsable de cargar el objeto User, CheckCredentialsListener es responsable de comprobar la contraseña,CsrfProtectionListener valida el token CSRF y LoginThrottlingListenercomprueba... el estrangulamiento del inicio de sesión.

Si falla la autenticación, hay un evento diferente para eso: LoginFailureEvent. Ahora mismo, nuestra aplicación sólo tiene un oyente - RememberMeListener - que borra la cookie "recuérdame" si el usuario tenía una.

Cuando el inicio de sesión tiene éxito, Symfony envía LoginSuccessEvent. Esto ya tiene 5 oyentes en nuestra aplicación, incluyendo el oyente que establece la cookie "recuérdame".

También hay un evento que se envía cuando se cierra la sesión... para que puedas ejecutar código o incluso controlar lo que ocurre, como a dónde se redirige al usuario.

El siguiente - TokenDeauthenticatedEvent - es un poco más sutil. Se envía si el usuario "pierde" la autenticación... pero no se cierra la sesión. Básicamente se envía si cambian ciertos datos del usuario. Por ejemplo, imagina que estás conectado en dos ordenadores y luego cambias tu contraseña en el primero. Cuando actualices el sitio en el segundo ordenador, serás "desautenticado" porque tu contraseña ha cambiado en otra máquina. En ese caso, se envía este evento.

Ah, y este security.authentication.success no es demasiado importante, es muy similar a LoginSuccessEvent.

Conocer estos eventos es fundamental porque quiero hacer que si el usuario intenta iniciar sesión con un correo electrónico que no ha sido verificado, lo impidamos y le mostremos un bonito mensaje.

Para ello, vamos a poner en marcha nuestro propio receptor de eventos que tiene la capacidad de hacer que la autenticación falle.

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