Firewalls & Authenticators

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

We built this log in form by making a route, controller and rendering a template:

... lines 1 - 8
class SecurityController extends AbstractController
{
/**
* @Route("/login", name="app_login")
*/
public function login(): Response
{
return $this->render('security/login.html.twig');
}
}

Dead simple. When we submit the form, it POSTs right back to /login. So, to authenticate the user, you might expect us to put some logic right here: like if this is a POST request, read the POSTed email & password, query for the User object... and eventually check the password. That makes perfect sense! And that is completely not what we're going to do.

Hello Firewalls

Symfony's authentication system works in a... bit of a magic way, which I guess is fitting for our site. At the start of every request, before Symfony calls the controller, the security system executes a set of "authenticators". The job of each authenticator is to look at the request, see if there is any authentication information that it understands - like a submitted email and password, or an API key that's stored on a header - and if there is, use that to query the user and check the password. If all that happens successfully then... boom! Authentication complete.

Our job is to write and activate these authenticators. Open up config/packages/security.yaml. Remember the two parts of security: authentication (who you are) and authorization (what you can do).

The most important part of this file is firewalls:

security:
... lines 2 - 13
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: app_user_provider
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#firewalls-authentication
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
... lines 27 - 33

A firewall is all about authentication: its job is to figure out who you are. And, it usually makes sense to have only one firewall in your app... even if there are multiple different ways to authenticate, like a login form and an API key and OAuth.

The "dev" Firewall

But... woh woh woh. If we almost always want only one firewall... why are there are already two? Here's how this works: at the start of each request, Symfony goes down the list of firewalls, reads the pattern key - which is a regular expression - and finds the first firewall whose pattern matches the current URL. So there's only ever one firewall active per request.

If you look closely, this first firewall is a fake! It basically matches if the URL starts with /_profiler or /_wdt... and then sets security to false:

security:
... lines 2 - 13
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
... lines 18 - 33

In other words, it's basically making sure that you don't create a security system that is so epically awesome that... you block the web debug toolbar and profiler.

So... in reality, we only have one real firewall called main. It has no pattern key, which means that it will match all requests that don't match the dev firewall. Oh, and the names of these firewalls - main and dev? They're totally meaningless.

Activating Authenticators

Most of the config that we're going to put beneath the firewall relates to activating authenticators: those things that execute early in each request and try to authenticate the user. We'll add some of that config soon. But these two top keys do something different. lazy allows the authentication system to not authenticate the user until it needs to and provider ties this firewall to the user provider we talked about earlier. You should have both of these lines... but neither are terribly important:

security:
... lines 2 - 13
firewalls:
... lines 15 - 17
main:
lazy: true
provider: app_user_provider
... lines 21 - 33

Creating a Custom Authenticator Class

Anyways, anytime that we want to authenticate the user - like when we submit a login form - we need an authenticator. There are some core authenticator classes that we can use, including one for login forms.... and I'll show you some of those later. But to start, let's build our own authenticator class from scratch.

To do that, go to terminal and run:

symfony console make:auth

As you can see, you can select "Login form authenticator" to cheat and generate a bunch of code for a login form. But since we're building things from scratch, select "Empty authenticator" and call it LoginFormAuthenticator.

Awesome. This did two things: it created a new authenticator class and also updated security.yaml. Open the class first: src/Security/LoginFormAuthenticator.php:

... lines 1 - 11
class LoginFormAuthenticator extends AbstractAuthenticator
{
public function supports(Request $request): ?bool
{
// TODO: Implement supports() method.
}
public function authenticate(Request $request): PassportInterface
{
// TODO: Implement authenticate() method.
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
// TODO: Implement onAuthenticationSuccess() method.
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
// TODO: Implement onAuthenticationFailure() method.
}
... lines 33 - 43
}

The only rule about an authenticator is that it needs to implement AuthenticatorInterface... though usually you'll extend AbstractAuthenticator... which implements AuthenticatorInterface for you:

... lines 1 - 8
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
... lines 10 - 11
class LoginFormAuthenticator extends AbstractAuthenticator
{
... lines 14 - 43
}

We'll talk about what these methods do one-by-one. Anyways, AbstractAuthenticator is nice because it implements a super boring method for you.

Once we activate this new class in the security system, at the beginning of every request, Symfony will call this supports() method and basically ask:

Do you see authentication information on this request that you understand?

To prove that Symfony will call this, let's just dd('supports'):

... lines 1 - 11
class LoginFormAuthenticator extends AbstractAuthenticator
{
public function supports(Request $request): ?bool
{
dd('supports!');
}
... lines 18 - 43
}

Activating Authenticators with custom_authenticators

Okay, so how do we activate this authenticator? How do we tell our firewall that it should use our new class? Back in security.yaml, we already have the code that does that! This custom_authenticator line was added by the make:auth command:

security:
... lines 2 - 13
firewalls:
... lines 15 - 17
main:
... lines 19 - 20
custom_authenticator: App\Security\LoginFormAuthenticator
... lines 22 - 34

So if you have a custom authenticator class, this is how you activate it. Later, we'll see that you can have multiple custom authenticators if you want.

Anyways, this means that our authenticator is already active! So let's try it. Refresh the login page. It hits the supports() method! In fact, if you go to any URL it will hit our dd(). On every request, before the controller, Symfony now asks our authenticator if it supports authentication on this request.

Next let's fill in the authenticator logic and get our user logged in!

Leave a comment!

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