Security Listeners

This Chapter isn't
quite ready...

Rest assured, the gnomes are hard at work
completing this video!

Browse Tutorials

Coming soon...

If you've used Symfony for a while, you probably know that Symfony dispatches events during the request response process, and you can listen to those events, to see some events in their listeners. We can run Symfony console debug event. I'm not going to go into the specifics here, but for example, if you listened to, but if you listened to the kernel dot request event, these are a bunch of listeners that are called before our controller and curl that response is dispatched after our controller sees listeners are called after our controller. Well, it turns out that our firewall also dispatches several events during the authentication process. And we can also listen to those, to see those, to see the listeners attached to that. You need to run debug event dash dash dispatcher = and use a special string here to get, uh, the dispatcher for our firewall.

This is security, not the event_dispatcher dot main, uh, that made because main is the name of our firewall and awesome, a totally different set of events and listeners inside of here. This is really cool. So if we look back at our custom log in form authenticator, we're not using this anymore, but can kind of help us understand what events are dispatched through the process. So in our authenticate method, our job is to return the passport. After we returned to passport from authenticate, after any authenticator, after the authentication method is called on any authenticator Symfony dispatches the Czech passport event, there are a bunch of cool listeners inside of here. The user provider listener is basically responsible for loading the user. The check credentials user is responsible for checking the password. CSRF checks the CSRF and logging and throttling checks the log and throttling. Then if we fail authentication, there's a remember me listening to that clears their mermaid cookie. If we're successful, the log-in success event is dispatched and it also has a number of, uh, listeners on it, including the middle listener that sets the remember me, cookie there's also an event for logging out so you can control what happens on log out. And this is a little more subtle, but one called token D authenticated

And an older one called security authentication success. Knowing about these events is critical because I want to make it so that If I try to log in as a user whose email has not been verified, I want to prevent the user from logging in. So if you want to stop authentication for some reason, then you're going to want to listen to the Czech passport event. So let's create an event subscriber. So any source directory doesn't matter where I put this, but I'm going to create a new directory called events, subscriber, and inside of there. And you class called check verified Abuser subscriber, And I'll make this implement the events subscriber interface, and then I'll go to code generate or Command + N on a Mac and go to "Implement Methods" to generate the one method we need, which is get subscribed events.

And some of here I'm going to return an array of all the events we're going to listen to, or try now is just one. So we can say check the passport event, ::class, and assign that to the method that we want. The methadone's class that should be called when that event is dispatched. I'm gonna say on and check the passport up above this. I'll say public function on check the passport. And this is going to be past this event object. So check the passport event event. And let's just D the, that event to see what it looks like now, just by creating a class and making it implement events, subscriber interface, Symfony is going to use this things to auto configure. And if you want to get in the weeds, it's technically going to listen to the Disney check password event on all firewalls, which for us, we only have one really only have one firewall. So it doesn't matter. But if you did have multiple real firewalls, then if you, you could actually make this listen on just one of the firewalls. Um, but that's beyond our scope. Check the documentation for that.

All right. So let's try something. I don't know, log in as adver up CA admin, at example.com. We did make our fixtures users verify, but I haven't reloaded my database yet. So that user is not going to be verified yet. Now notice by type the wrong password here. Yes, it hit our DD. So it is working, but if I type in invalid Email here, then our listener is not hit. And that's just due to the priority of the listeners. If you go back to our debug console, you can see there that there's a priority priority over here. And the default is zero. So let me actually make this a little bit smaller so we can read it better. There we go. Okay. So by chance, Our listener is kind of happening by chance before check credentials. That's why it was hitting this before the PA that password validation, uh, through, but the, um, kind of user checker stuff was happening before this. No, it's about an expiration check. Credentials is the problem. It actually is the thing that loads the user. So all we needed to do is make our W oh yeah, no, no, no. We want ours to have an after the password, uh, only after the valid email and valid password. So we want both of these to happen. So we're gonna set a negative priority, panic, excuse myself. So do that. We can pass an array in here and say negative 10. So now we're going to have to put in a valid email address and valid password, and only then will our event get called. So I'm going to go back to Avalara CA admin,

At example, dot com, password cutoff and beautiful, because that was a valid login. It stopped in check out this event, and this is awesome. We're past so many good things. We're past the authenticator that's being used in case we need to do something different based on the authenticator. And we're also passed the passport, which is huge because that contains the user. And it also contains any badges that we added to it, because sometimes you need to do different things based on the badges that are on your passport. So inside of our subscriber, let's get to work in order to get the user. We first need to get the passport, the passport = event->get passport. And now I'm gonna say, if not passport, in instance of User interface, then throw an exception.

This is not really important. Um, in reality, if I hit shift, shift my for passport dot PHP, what every authenticated returns is this passport object here, which implements this user passport interface. What, so in reality, all of our passwords are going to implement this. This interface means that the passport is going to have a get user method on it that you can call to fetch the user. That means down here, we can say user = passport arrow, get user, and then I'll do a little sanity check there. If the user's not an instance of our user class, then I'll also throw an exception, Unexpected user type. And that's not really possible, but that's going to make our editor happy. Cause it, now it knows what our user, we have, what our users in instance of. And it's also going to, if you stack analysis like PHP standard Psalm, it's going to make that happen as well.

Finally, we can check it. The user's verified. If not user Aero is verified, get is verified, then let's fail indication. How do we fail on occasion? At any time during the process, you can throw a new authentication exception from security, and that will cause authentication to fail. There are a bunch of subclasses of this like bad credentials, exception. You can throw any of those as long as they all extend authentication exception. That will cause the process to fail. Check it out. Let me refresh here and got it. And authentication exception occurred. That is the generic error message that is tied to authentication exception, not a very good error message, but it did get the job done. How can we customize that error message by throwing a very cool new custom user message, authentication exception, and inside of here, we can say, please verify your account before logging in.

So let me explain this class here. If you all command or control and click to open that you can see that this just extends authentication exception. It's just a special authentication exception, most authentication exceptions and subclasses. If you pass them a custom message, like throw a new authentication exception and pass them a custom exception that except the message is not going to be seen by the end user. Instead every single one of these authentication exception classes has a, where is it getting messaged key method with a hard coded message in it. That's done for security. So we always are going to show the user a nonsensitive message inside of here. However, there are some cases where you do on purpose, want to throw in authentication exception, but control the messaging to your user. You can do that with this class right here. So it's going to fail its indication just like before, but now we can control the message. Exactly beautiful bot to team. We can do even better instead of just saying, please verify your account. And if we're logging in, let's redirect them to another page so that we can better explain why they can't log in and give them an opportunity to rescind the email. If they lost it, let's do that next. It will require a second listener and some teamwork.

Leave a comment!

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