Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Security Events & 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

If you've used Symfony for a while, you probably know that Symfony dispatches events during the request-response process and that you can listen to them. To see these events and their listeners, we can run:

symfony console debug:event

I'm not going to go too deeply, but, this kernel.request event is dispatched on every request before the controller is called. This means that all of these listeners are executed before our controller. Listeners to this kernel.response event are called after our controller.

These two events have... nothing to do with the security system. But it turns out that our firewall also dispatches several events during the authentication process. And, we can also listen to those.

To see a list of all of the listeners to these events, we can run debug:event again, but with a special --dispatcher= set to security.event_dispatcher.main:

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

I know, that looks a little funny... but this allows us to list the event listeners for the event dispatcher that's specific to the main firewall.

Looking at the Core Security Events & Listeners

And... awesome! A totally different set of events and listeners. This is so cool. Look back at our custom LoginFormAuthenticator class. We're not using this anymore, but it can help us understand which events are dispatched through the process.

We know that, in our authenticate() method, our job is to return the 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
}

Then, after the authenticate() method is called - on any authenticator - Symfony dispatches CheckPassportEvent. There are a bunch of cool listeners to this.

For example, UserProviderListener is basically responsible for loading the User object, CheckCredentialsListener is responsible for checking the password, CsrfProtectionListener validates the CSRF token and LoginThrottlingListener checks... the login throttling.

If we fail authentication, there's a different event for that: LoginFailureEvent. Right now, our app has just one listener - RememberMeListener - which clears the "remember me" cookie if the user had one.

When login is successful, Symfony dispatches LoginSuccessEvent. This already has 5 listeners in our app, including the listener that sets the "remember me" cookie.

There's also an event that's dispatched when you log out... so you can run code or even control what happens - like where the user is redirected to.

This next one - TokenDeauthenticatedEvent - is a bit more subtle. It's dispatched if the user "loses" authentication... but didn't log out. It's basically dispatched if certain data changes on the user. For example, imagine you're logged in on two computers and then you change your password on the first. When you refresh the site on the second computer, you will be "deauthenticated" because your password changed on another machine. In that case, this event is dispatched.

Oh, and this security.authentication.success isn't too important, it's very similar to LoginSuccessEvent.

Knowing about these events is critical because I want to make it so that if the user tries to log in using an email that has not been verified, we prevent that and show them a nice message.

Let's do that next by bootstrapping our very own shiny event listener that has the ability to cause authentication to fail.

Leave a comment!

2
Login or Register to join the conversation
Trafficmanagertech Avatar
Trafficmanagertech Avatar Trafficmanagertech | posted 8 months ago

In SF 5.3, `symfony console debug:event --dispatcher=security.event_dispatcher.main` doesn't work anymore (got "[ERROR] Event dispatcher "security.event_dispatcher.main" is not available.")
In my case this worked: `symfony console debug:event-dispatcher security`

Reply

Hey Trafficmanagertech

Inside your `security.yaml` file do you have a "main" key defined as your firewall?

Cheers!

Reply
Cat in space

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

This tutorial also works great for 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
    }
}