Firewalls & Authenticator

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

We built a login form with a traditional route, controller and template. And so you might expect that because the form submits back to this same URL, the submit logic would live right inside this controller:

... lines 1 - 8
class SecurityController extends AbstractController
{
/**
* @Route("/login", name="app_login")
*/
public function login(AuthenticationUtils $authenticationUtils)
{
... lines 16 - 25
}
}

Like, if the request method is POST, we would grab the email, grab the password and do some magic.

What are Authentication Listeners / Authenticators?

Well... we are not going to do that. Symfony's security works in a bit of a "magical" way, at least, it feels like magic at first. At the beginning of every request, Symfony calls a set of "authentication listeners", or "authenticators". The job of each authenticator is to look at the request to see if there is any authentication info on it - like a submitted email & password or maybe an API token that's stored on a header. If an authenticator finds some info, it then tries to use it to find the user, check the password if there is one, and log in the user! Our job is to write these authenticators.

Understanding Firewalls

Open up config/packages/security.yaml. The most important section of this file is the firewalls key:

security:
... lines 2 - 8
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true
# activate different ways to authenticate
# http_basic: true
# https://symfony.com/doc/current/security.html#a-configuring-how-your-users-will-authenticate
# form_login: true
# https://symfony.com/doc/current/security/form_login_setup.html
... lines 23 - 29

Ok, what the heck is a "firewall" in Symfony language? First, let's back up. There are two main parts of security: authentication and authorization. Authentication is all about finding out who you are and making you prove it. It's the login process. Authorization happens after authentication: it's all about determining whether or not you have access to something.

The whole job of the firewall is to authenticate you: to figure out who you are. And, it usually only makes sense to have one firewall in your app, even if you want your users to have many different ways to login - like a login form or API authentication.

But... hmm... Symfony gave us two firewalls by default! What the heck? Here's how it works: at the beginning of each request, Symfony determines the one firewall that matches the current request. It does that by comparing the URL to the regular expression pattern config. And if you look closely... the first firewall is a fake!

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

It becomes the active firewall if the URL starts with /_profiler, /_wdt, /css, /images or /js. When this is the active firewall, it sets security to false. Basically, this firewall exists just to make sure that we don't make our site so secure that we block the web debug toolbar or some of our static assets.

In reality, we only have one real firewall called main:

security:
... lines 2 - 8
firewalls:
... lines 10 - 12
main:
anonymous: true
# activate different ways to authenticate
# http_basic: true
# https://symfony.com/doc/current/security.html#a-configuring-how-your-users-will-authenticate
# form_login: true
# https://symfony.com/doc/current/security/form_login_setup.html
... lines 23 - 29

And because it does not have a pattern key, it will be the active firewall for all URLs, except the ones matched above. Oh, and, in case you're wondering, the names of the firewalls, dev and main are totally meaningless.

Anyways, because the job of a firewall is to authenticate the user, most of the config that goes below a firewall relates to "activating" new authentication listeners - those things that execute at the beginning of Symfony and try to log in the user. We'll add some new config here pretty soon.

Oh, and see this anonymous: true part?

security:
... lines 2 - 8
firewalls:
... lines 10 - 12
main:
anonymous: true
... lines 15 - 29

Tip

Starting with Symfony 4.4.1 and 5.0.1, instead of anonymous: true you will see anonymous: lazy. Both should not behave in any noticeably different way - it's basically the same.

Keep that. This allows anonymous requests to pass through this firewall so that users can access your public pages, without needing to login. Even if you want to require authentication on every page of your site, keep this. There's a different place - access_control - where we can do this better:

security:
... lines 2 - 23
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
# - { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }

Creating the Authentication with make:auth

Ok, let's get to work! To handle the login form submit, we need to create our very first authenticator. Find your terminal and run make:auth:

php bin/console make:auth

Tip

Since MakerBundle v1.8.0 this command asks you to choose between an "Empty authenticator" and a "Login form authenticator". Choose the first option to follow along with the tutorial exactly. Or choose the second to get more generated code than the video!

Call the new class LoginFormAuthenticator.

Nice! This creates one new file: src/Security/LoginFormAuthenticator.php:

... lines 1 - 2
namespace App\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
class LoginFormAuthenticator extends AbstractGuardAuthenticator
{
public function supports(Request $request)
{
// todo
}
public function getCredentials(Request $request)
{
// todo
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
// todo
}
public function checkCredentials($credentials, UserInterface $user)
{
// todo
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
// todo
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
// todo
}
public function start(Request $request, AuthenticationException $authException = null)
{
// todo
}
public function supportsRememberMe()
{
// todo
}
}

This class is awesome: it basically has a method for each step of the authentication process. Before we walk through each one, because this authenticator will be for a login form, there's a different base class that allows us to... well... do less work!

Instead of extends AbstractGuardAuthenticator use extends AbtractFormLoginAuthenticator:

... lines 1 - 8
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 13 - 41
}

I'll remove the old use statement.

Thanks to this, we no longer need onAuthenticationFailure(), start() or supportsRememberMe(): they're all handled for us:

... lines 1 - 8
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
public function supports(Request $request)
{
// todo
}
public function getCredentials(Request $request)
{
// todo
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
// todo
}
public function checkCredentials($credentials, UserInterface $user)
{
// todo
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
// todo
}
... lines 37 - 41
}

But don't worry, when we create an API token authenticator later, we will learn about these methods. We do now need one new method. Go to the "Code"->"Generate" menu, or Command+N on a Mac, and select "Implement Methods" to generate getLoginUrl():

... lines 1 - 10
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 13 - 37
protected function getLoginUrl()
{
// TODO: Implement getLoginUrl() method.
}
}

Activating the Authenticator in security.yaml

Perfect! Unlike a lot of features in Symfony, this authenticator won't be activated automatically. To tell Symfony about it, go back to security.yaml. Under the main firewall, add a new guard key, a new authenticators key below that, and add one item in that array: App\Security\LoginFormAuthenticator:

security:
... lines 2 - 8
firewalls:
... lines 10 - 12
main:
... lines 14 - 15
guard:
authenticators:
- App\Security\LoginFormAuthenticator
... lines 19 - 33

The whole authenticator system comes from a part of the Security component called "Guard", hence the name. The important part is that, as soon as we add this, at the beginning of every request, Symfony will call the supports() method on our authenticator.

To prove it, add a die() statement with a message:

... lines 1 - 10
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
public function supports(Request $request)
{
die('Our authenticator is alive!');
}
... lines 17 - 41
}

Then, move over and, refresh! Got it! And it doesn't matter what URL we go to: the supports() method is always called at the start of the request.

And now, we're in business! Let's fill in these methods and get our user logged in.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
        "knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
        "knplabs/knp-time-bundle": "^1.8", // 1.8.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.2.0
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.1.4
        "symfony/console": "^4.0", // v4.1.4
        "symfony/flex": "^1.0", // v1.9.10
        "symfony/framework-bundle": "^4.0", // v4.1.4
        "symfony/lts": "^4@dev", // dev-master
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.1.4
        "symfony/serializer-pack": "^1.0", // v1.0.1
        "symfony/twig-bundle": "^4.0", // v4.1.4
        "symfony/web-server-bundle": "^4.0", // v4.1.4
        "symfony/yaml": "^4.0", // v4.1.4
        "twig/extensions": "^1.5" // v1.5.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.7
        "fzaninotto/faker": "^1.7", // v1.8.0
        "symfony/debug-bundle": "^3.3|^4.0", // v4.1.4
        "symfony/dotenv": "^4.0", // v4.1.4
        "symfony/maker-bundle": "^1.0", // v1.7.0
        "symfony/monolog-bundle": "^3.0", // v3.3.0
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.1.4
        "symfony/profiler-pack": "^1.0", // v1.0.3
        "symfony/var-dumper": "^3.3|^4.0" // v4.1.4
    }
}