Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

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 AbstractFormLoginAuthenticator:

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

34
Login or Register to join the conversation
Alex F. Avatar
Alex F. Avatar Alex F. | posted 3 years ago

Somehow "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:" was automagically done for me probably during "make:auth" here? https://github.com/symfony/... I did originally choose the "Login form authenticator" option but cancelled it before answering "The class name of the authenticator to create (e.g. AppCustomAuthenticator):".

1 Reply

Hey Alex,

Most probably so, "make:auth" command might become smarter in your version of MakerBundle :)

Cheers!

Reply
Nathan R. Avatar
Nathan R. Avatar Nathan R. | posted 1 year ago

Hi, I've been digging through Symfony's source code over the past few hours trying to answer a pretty simple question - are ROLE_'s case sensitive? I've noticed in my app that both ROLE_Admin and ROLE_ADMIN both work, so I'm guessing role names are case insensitive, but the convention is to specify them as upper case with underscores between words. We have our projects setup to take roles from SAML attributes, one being "Admin", which we transform into ROLE_<saml_role_name> (ie. ROLE_Admin, in this case). So is ROLE_ADMIN and ROLE_Admin two separate roles from Symfony's point of view, or are they the same?

Reply

Hey Nathan,

Good question! I always thought they should be case sensitive, but if you say that both work for you - maybe they are not case sensitive. Or, it may depend on Symfony version. Or it may just work in some spots and does not work others :) But you're right, the convention is to always uppercase them. About the code, I believe the logic lives in RoleVoter: https://github.com/symfony/... - as you can see it clearly compare strings with strict ===, so I would say different cases won't work... unless you wrote it as ROLE_Admin in all spots that will give you check like "ROLE_Admin" === "ROLE_Admin" there :)

I hope this helps!

Cheers!

Reply
Miky Avatar

Hi, will nice if you're mention in video..not sure from version.. but i see that in my case since 5.3+ lots of changes in security bundle.. no longer is in security.yaml the guard: but there is custom_authenticator: https://symfony.com/doc/cur...

+ also in Sf 5.3+ if i put inside public function supports die('Our authenticator is alive!'); i am getting compile error: about that it requires return bool from AbstractLoginFormAuthenticator

update: i found nice blog which explain everything https://smaine-milianni.med...

Reply
sadikoff Avatar sadikoff | SFCASTS | Miky | posted 1 year ago | edited

Hey Miky

Yeah that's true Symfony 5.3 has a lot of changes in Security component, and it can't be covered by some notes and honestly is not related to this tutorial 'cause it based on Symfony 4.

PS There will be a course dedicated to Symfony 5 + Security component, but I can't say any eta on it :)

Cheers!

Reply
Helmis D. Avatar
Helmis D. Avatar Helmis D. | posted 2 years ago

I don't understand why Symfony allow many firewall and use only one (the first in security.yml file).
Also i discovered that when we put a pattern, something like
pattern: ^/(_(profiler|wdt)|css|images|js)/
The firewall is absent(ignored?) in Symfony Profiler
Cheers!

Reply

Hey Helmis D.!

It's a good question - I was just talking about this exact thing with a friend last night :).

Inside your code (like a controller), you often write code like $this->getUser() to find the ONE user that the "person" using your site is currently authenticated as. The "main" reason to have two firewalls is that it allows you to be logged in as two users at the same time. For example, suppose you have a firewall that matches ^/admin and so when you are under /admin you log in as some "admin" user. But for *any* other URL on the site, you log in using some different "frontend" account. It's not a common use-case... so it's a bit hard to imagine (I would normally recommend that you have just *one* user and give some users admin rights to be able to access things under /admin).

The point is, if you are logged in as some AdminUser and also FrontendUser, when you call $this->getUser(), which *one* should be returned? In Symfony's security, for each request, you can only be authenticated as a *single* user. That's why, when the request starts, Symfony finds the *one* firewall that should be active and asks it "who is logged in?". So if you called $this->getUser() from a URL starting with /admin, you would get that AdminUser. And if you ran that same code on any other URL, you would get the FrontendUser.

That's the long... description. Most of the time, you do *not* need multiple firewalls (not counting the fake "dev" firewall). Inside a single firewall, you can allow the user to authenticate in as *many* ways as you want - like a login form, API token header, LDAP, anything. Basically, the user can then *choose* how to log in. But ultimately, in your code when you call $this->getUser(), you don't care *how* they logged in (e.g. LDAP) - you just care who they are.

> Also i discovered that when we put a pattern, something like
> pattern: ^/(_(profiler|wdt)|css|images|js)/
> The firewall is absent(ignored?) in Symfony Profiler

That's correct! Very good attention to detail. If the *one* firewall that's activated has security: false it basically disables the security system entirely. If that's a problem, just remove this firewall. The *only* reason it exists is to help prevent people from accidentally making their app security so tight that they accidentally require authentication for URLs starting with /_profile or /_wdt... which would break the profiler and web debug toolbar... and would probably be difficult to debug. If you know what you're doing and (for some reason) want security active on the profiler, you can totally remove this and take control.

Does that help? Is there a use-case that is making you want to use multiple firewalls?

Cheers!

1 Reply
Helmis D. Avatar

Thank you very much for taking the time for these explanations, I now understand how the firewall works. The ambiguities have disappeared from my head :D ☀

Reply
Patrick M. Avatar
Patrick M. Avatar Patrick M. | posted 2 years ago

When using the 4.4 version of SecurityBundle, it looks like the main firewall defaults to "anonymous: lazy" rather than "anonymous: true" in security.yaml. I'm not sure if that's relevant or not, but I thought I would put it out there.

Reply

Hey Patrick!

Yeah, good catch! In practice (starting with 4.4.1 and 5.0.1 where a fix was done), "anonymous: lazy" should not behave in any noticeably different way than "anonymous: true". So it should be not a big deal - it's basically the same (lazy basically is the same as true). Thank you for mention it here!

Cheers!

Reply

HI! I would like to do my login form accessible in every page, like with a javascript pop-up instead of a login page, how could I manage the request in the controller if I did something like that?
Hint:
I get the error:
Not configuring explicitly the provider for the "guard" listener on "main" firewall is ambiguous as there is more than one registered provider.

The guard is set don't know why. Might be because I had previously install the FOS User Bundle? If it's the reason how can I remove it completly from symfony and make the project work again

Reply

Hey Gballocc7

To remove a bundle from a Symfony app is very easy, if you are using Symfony Flex all you need to do is remove it with composer, unless you added some specific config. The manual way would be to first remove the bundle from composer, remove the line of code which activates the bundle (inside bundles.php), then remove any config file and that would be it.

About your question. Have you checked this documentation? https://symfony.com/doc/cur...

Cheers!

Reply
Roman R. Avatar
Roman R. Avatar Roman R. | posted 3 years ago

Hi! Ihave this error:
The service "security.authentication.provider.guard.main" has a dependency on a non-existent service "App\Security\LoginFormAuthenticator".
The namespace is correct

Reply

Hey Roman R.!

Yep, I know this error! And you've already done the first thing correctly: double-check your namespace.

Basically, this error means that, for some reason, the App\Security\LoginFormAuthenticator service does not exist. Check the spelling of the "namespace" inside that class as well as the class name to be sure. Also, what version of Symfony are you using? If everything is setup correctly with this service, you should see it when you run:


php bin/console debug:container --show-private authenticator

Let me know what you find out - it's most likely a small typo somewhere!

Cheers!

Reply
Roman R. Avatar

Thank you for reply. I double check everything and there is no typo.If running that command the same error occured in console. I'm trying define authenticator as a service and in this case everything looks good but --debug:container --show-private authenticator show only this:
[0] maker.maker.make_authenticator
[1] Symfony\Component\Security\Guard\GuardAuthenticatorHandler

-1 Reply

Hey Roman R.!

Hmm. Can you post your services.yaml code? And what version of Symfony are you using? I'm wondering if you're not using the new service auto-registration stuff and so you're using a different service id. Let me know :).

Cheers!

1 Reply
Roman R. Avatar

I'm using symfony 4.2.3

parameters:

services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.

App\Controller\JsonRpcController:
public: true
bind:
$jsonRpcServer: '@api.jsonrpc_server'

api.jsonrpc_reflection:
class: App\Model\JsonRpcReflection
arguments: ['@service_container', '@api.jsonrpc_serializer', '@annotations.reader']
public: false

api.jsonrpc_serializer:
class: App\Model\JsonRpcSerializer
arguments: ['@jms_serializer', '@jms_serializer.serialization_context_factory']
public: false

api.jsonrpc_server:
class: App\Service\JsonRpcServer
arguments: ['@api.jsonrpc_serializer', '@api.jsonrpc_reflection']
public: true

api.form.login:
class: App\Security\LoginFormAuthenticator

Reply

Hey Roman R.

You are missing this piece of configuration:


services:
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/*'
exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'

# controllers are imported separately to make sure services can be injected
# as action arguments even if you don't extend any base controller class
App\Controller\:
resource: '../src/Controller'
tags: ['controller.service_arguments']

So almost everything inside "src/" will be auto registered as a service. And because of that you don't need to define "api.form.login"

Cheers!

Reply
Roman R. Avatar

Thank you! That's works!!! But now my ide didnt know about Controllers methotds, I'm using php storm.

Reply

Have you installed Symfony plugin? And a couple others like Twig and annotations support?

Reply
Roman R. Avatar

Yes. I have its both and its enabled.

Reply

Hmm, try clearing the index. Go to settings -> symfony plugin -> clear index
It may do the trick :)

Reply
Roman R. Avatar

Already trying(((

Reply

And nothing? Sometimes by running "composer install" it get fixed. If not, try restarting PHPStorm... I'm running out of options =S

Reply
Mohamed K. Avatar
Mohamed K. Avatar Mohamed K. | posted 3 years ago

Hey Ryan it seems that the make:auth command has an upgrade now it give you 2 options
1: empty authenticator
2: login form authenticator
if you choose option 2 you get the name the security/autheticator
and your controller. will you update this later in the future when building and rendering forms?

Reply

Hey @Bobby!

Haha, like your name :). Yes, the new make:auth command awesomely now gives you the new option (2) - the login form authenticator. In this tutorial (before that option existed), we were effectively choosing option (1). And then we basically build (manually) a login form system. The final result of this tutorial actually looks quite similar to what you get if you choose option (2) (but we also wanted to build it manually so we could learn along the way).

Anyways, I'm not sure I've answered your question yet :).

> will you update this later in the future when building and rendering forms

In our forms tutorial, we will *not* do any more work on the login form system. Both in this tutorial and in the updated generator, it does not use the form system. There is nothing wrong with using the form system for the login form, it's just such a simple form (and its validation is a bit different) that I prefer just using HTML forms for it. The only thing you need to remember (which we do in this tutorial and the generator also does it) is to add CSRF protection to your form.

If I still haven't answered your question - let me know!

Cheers!

Reply
akincer Avatar

So if I understand correctly, when following the tutorial for learning purposes, we should choose "Empty authenticator" when running make:auth? Is there a simple instruction set to stay in line with the course at this point with the new wizard?

Thanks!

Reply
akincer Avatar

Nevermind. Should have just watched the video and seen the instructions added. Anyone else wondering -- just keep going.

Reply

haha, no worries Aaron, sometimes we all speak too fast :p

Reply

Hello,

After make the command make:auth, in the LoginFormAuthenticator class extends AbstractGuardAuthenticator and no AbstractAuthenticator. This is due to the update of Sf4? Why you don't use it ?

Reply

Hey Stephane!

Ah, typo! Yes, it should be AbstractGuardAuthenticator. It's not a Sf4 thing - it's just me saying the wrong class! You will see AbstractGuardAuthenticator in the video, even if I say something different in the script. Sorry for the confusion!

Cheers!

Reply

Hey Ryan,

Thank for your reply. But why you don't use AbstractGuardAuthenticator ? It's about simplicity ?

Reply

Hey Stephane!

Good question :). We change from AbstractGuardAuthenticator to AbstractFormLoginAuthenticator because, when you're building a login form, AbstractFormLoginAuthenticator requires you to do less work. If you look, AbstractFormLoginAuthenticator extends AbstractGuardAuthenticator, but it implements a few methods for you so that you don't have to. For example, when you're building a login form, we *know* that on failure, the user should be redirected to the login page. So, AbstractFormLoginAuthenticator implements the onAuthenticationError() method so that you don't have to.

In other words, AbstractFormLoginAuthenticator is just a "helper" sub-class that allows you to do less work when you're building a login form :).

Cheers!

1 Reply
Cat in space

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

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