Buy

Open up app/config/security.yml. Security - especially authentication - is all configured here. We'll look at this piece-by-piece, but there's one section that's more important than all the rest: firewalls:

25 lines app/config/security.yml
# To get started with security, check out the documentation:
# http://symfony.com/doc/current/book/security.html
security:
... lines 4 - 9
firewalls:
# disables authentication for assets and the profiler, adapt it according to your needs
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: ~
# activate different ways to authenticate
# http_basic: ~
# http://symfony.com/doc/current/book/security.html#a-configuring-how-your-users-will-authenticate
# form_login: ~
# http://symfony.com/doc/current/cookbook/security/form_login_setup.html

All About Firewalls

Your firewall is your authentication system: it's like the security desk you pass when going into a building. Now, there's always only one firewall that's active on any request. You see, if you go to a URL that starts with /_profiler, /_wdt or /css, you hit the dev firewall only:

25 lines app/config/security.yml
... lines 1 - 2
security:
... lines 4 - 9
firewalls:
# disables authentication for assets and the profiler, adapt it according to your needs
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
... lines 15 - 25

This basically turns security off: it's like sneaking through the side door of a building that has no security desk. This is here to prevent us from getting over-excited with security and accidentally securing our debugging tools.

In reality, every real request will activate the main firewall:

25 lines app/config/security.yml
... lines 1 - 2
security:
... lines 4 - 9
firewalls:
... lines 11 - 15
main:
anonymous: ~
# activate different ways to authenticate
# http_basic: ~
# http://symfony.com/doc/current/book/security.html#a-configuring-how-your-users-will-authenticate
# form_login: ~
# http://symfony.com/doc/current/cookbook/security/form_login_setup.html

Because it has no pattern key, it matches all URLs. Oh, and these keys - main and dev, are meaningless.

Our job is to activate different ways to authenticate under this one firewall. We might allow the user to authenticate via a form login, HTTP basic, an API token, Facebook login or all of these.

So - if you ignore the dev firewall, we really only have one firewall, and I want yours to look like mine. There are use-cases for having multiple firewalls, but you probably don't need it. If you're curious, we do set this up on our Symfony REST API course.

We won't use form_login

Ok, we want to activate a system that allows the user to submit their email and password to login. If you look at the official documentation about this, you'll notice they add a key called form_login under their firewall. Then, everything just magically works. I mean, literally: you submit your login form, Symfony intercepts the request and takes care of everything else.

It's really cool because it's quick to set up! But it's super magical and hard to extend and control. If you're using FOSUserBundle, they also recommend that you use this.

But, you have a choice. We won't use this. Instead, we'll use a system that's new in Symfony 2.8 called Guard. It is more work to setup, but you'll have control over everything from day 1.

Leave a comment!

  • 2017-10-02 weaverryan

    Indeed! That's a bit more accurate, and emphasizes the important fact that only 1 firewall matches at one time. I'm not going to make a change to the audio for this. but we will be updating our tuts at the end of 2017/beginning of 2018 for Symfony 4. And I'm going to keep this better wording in mind for that :).

    Cheers!

  • 2017-10-01 maxii123

    Just a small suggestion : "Because it has no pattern key, it matches all URLs" - it would be a little more accurate (I think!) to say "Because it has no pattern key, it matches all URLs not processed by the "dev" firewall discussed earlier.

  • 2017-07-13 weaverryan

    This is AWESOME! What a great setup you've made - probably one of the cleanest (and most necessary) multiple firewall setups that I've seen. Congrats!

  • 2017-07-12 Patrick Vale

    Hey Ryan!

    Many thanks for the further information - it's great to know I've not missed something simple! It's funny how the parts I thought would be easy turn out to be the hard parts, and vica-versa!

    The multiple firewalls are working great, and the impersonation login authenticator does a great job of logging staff users into 'normal' user accounts.

    I've gone with the 2 user providers to give the separation for now, and there might be a time when they get merged later - and thanks to symfony, I know what I'll need to do that!

    All the best,
    Patrick

  • 2017-06-29 weaverryan

    Hey Patrick!

    Welcome back :). Ok, so the *key* requirement that makes this so difficult is the fact that you need staff to remain logged in at the admin area, while at the same time "impersonating" a normal user. That's just not how the "stock" impersonation system works. That's totally ok - in fact, you should feel good that you're not missing some easy solution. This is pretty tough. So:

    A) Yes to 2 firewalls - this is the only way you could be logged in to two different parts of the system as two different users at once. Again, that's the *key* part of the requirement. This is actually a perfect use-case for multiple firewalls.

    B) When you have multiple firewalls, they really do work VERY independently. And that means that there is really no simple way to allow a staff member (who is under the admin firewall) to become a user under the public firewall. You're going to need some sort of hack or complex code to get this to work. Nice job on figuring out the session->set() solution :). Here's the one modification I would make, which will alleviate any potential "What am I skipping or forgetting by doing my hack?" feeling. In that controller, I would simply set a special key in the session - e.g. _impersonation_public_user_id set to the user's id that you want to impersonate. Then, redirect to /users (or wherever) like normal. Then, create a *third* guard authenticator for your public firewall. This authenticator would look for that session key, clear it, and then log in as that user. Ultimately, you'll be using a normal Guard authenticator to login to the public firewall. Doing this might not make any practical difference, but it's probably a "better" way.. and actually doesn't really feel hacky to me :).

    C) About the 2 user providers, that's up to you. Really, the bigger question is, do you want to have 2 totally different User entities (maybe User for the public firewall and StaffUser for staff). The disadvantage is that you would now have 2 different user entities floating around... so you would always need to be thinking "which User object is this" before calling methods on it. That's simple in a controller (you know which firewall you're under), but in theory, could be tricky in a service. But, since the logic and code is probably pretty separated between what the staff and public users can do, it might work out really nicely. Also, with this setup, a StaffUser couldn't also be a public User - they would need 2 different, separate accounts (that could be a pro or a con). The advantage is that you have 2 separate database tables, which is nice if the data on each user entity is very different. And, as you mentioned, there's a bit more separation - there's no way some public User will be able to log into the Staff section.

    Cheers!

  • 2017-06-28 Patrick Vale

    Hi Ryan,

    Thanks for getting back to me - I hope it was a great trip!

    I must apologise for the delay in responding to your very helpful advice - some critical infrastructure work came up that cleared everything else until just now.

    What you suggest regarding the use of different tokens, identifying their type is SUPER interesting and it's really great to get more insight into how Guard issues tokens.

    What I actually ended up implementing since my last post was a two-firewall situation. I'll explain why, and perhaps it is unnecessary, in which case I'd be glad to simplify it!

    What I really needed to allow my staff user to do was:

    1) Be logged into the /admin system
    2) Be able to impersonate a 'normal' user, and be logged into that 'normal' user's /account area.
    3) Remain logged into the /admin system as their staff user account (so that they could eg modify entities and check their results as the 'normal' user would see it in their /account area).

    This would often be while the user in question was on the phone, so speed of workflow is super important.

    I couldn't for the life of me find a way to do it without two firewalls - I tried setting the firewall context to the same key, and trying different user_providers, but nothing I did at the time seemed to allow it - perhaps I should have looked more into ROLE_PREVIOUS_ADMIN, having read the docs once again.

    I also saw some potential benefit in having separate user providers for the different user bases, providing some certainty of separation and protection against accidental privilege escalation, though I might be over-stating the case here.

    I was able to use two firewalls without a logout from one destroying the session of the other by setting the

    invalidate_session: false

    firewall key.

    This somewhat simplified the initial requirements of limiting which auth mechanisms could be used for each user, as each could be configured with just the guard authenticators permitted for that area:


    admin:
    guard:
    entry_point: app.security.admin_login_form_authenticator
    authenticators:
    - app.security.admin_login_form_authenticator
    public:
    guard:
    entry_point: app.security.login_form_authenticator
    authenticators:
    - app.security.login_form_authenticator
    - app.facebook_authenticator

    However, I suspect that apparent simplicity may have come at a high cost now there are two firewalls!

    To allow the admin users to impersonate 'normal' users, I created a route which logged them into the requested account manually:


    /**
    * @Route("/admin/login-as-public-user/{email}", name="admin_login_as_public_user")
    */
    public function loginAsPublicUserAction(User $user)
    {
    $token = new UsernamePasswordToken($user, null, 'public_users', $user->getRoles());
    $this->get('session')->set('_security_public_users',serialize($token));
    return new RedirectResponse('/users');
    }
    }

    This does seem to work, but I wonder if there are other things that I ought to be doing (triggering authentication events) which would happen on a 'regular' authentication process but I'm skipping here by doing it manually.

    Does this all seem a terrible idea to you, or is there some scope for doing it this way?

    Best wishes, and thanks again for the excellent advice,
    Patrick

  • 2017-05-03 weaverryan

    Hey Patrick Vale!

    SO sorry for my slow reply - I was traveling last week! And this is SUCH an interesting situation - I love seeing how you debugged and the questions you've come up with - you're diving into things really really well.

    I definitely have some suggestions. Then, we can iterate and find out what works best:

    1) Use 1 firewall. Your instinct was correct. 2 firewalls is not wrong, but if can complicate things. Setting the firewalls to the same context is a way to workaround these things. So, let's stick with 1 firewall and see if we can get things working.

    2) To identify *how* the user logged in, the "correct" way is to give the user a different token. And fortunately, I see you already discovered the token :) - I try to more-or-less hide it with Guard, because (except in more advanced cases) it's not something you need to worry about. So, with Guard, we create the token for you: https://github.com/symfony/.... But obviously, you can override that method in either of your authenticator classes. I would create 2 tokens (they can probably just extend PostAuthenticationGuardToken and be empty) - one for normal login form login and one for Facebook. Then, in your voter, since you're passed the token, you can check the instance of it:


    if ($token instanceof FacebookGuardToken) {

    3) Giving your user a different role is also a really interesting (arguably better) idea. Once again, the token is the key. We think that the roles are stored on the User. But in reality, at the moment you login, the roles are taken from your user and stored on the token. And for the rest of the session, it's the roles on the *token* that are important, not your User's roles. So, in theory, we *can* add/remove roles from the token, without adding/removing them from the User (because you're correct, these would eventually be accidentally saved to the database).

    How can you do this? It's by overriding the same method as in (2): https://github.com/symfony/.... The 3rd argument - $user->getRoles() - is where we pass what roles we want on the token. Just add a new role right there :).

    Let me know how it all works out!

    Cheers!

  • 2017-05-02 Patrick Vale

    Hey, yet more updates.

    Multiple firewalls work great with user switching! In some cases, it turns out you need to give them the same 'context' value.

    I realise the real tricky situation I'm trying to get working is where I have an admin user who wants to impersonate a normal user, and access public part of the site as the normal user, but continue to access the private /admin part of the site still identified as the original admin user.

    I've found that you can get hold of the original impersonating user by following the guide here, and I've been able to set the session token to that original users by using


    if ($authChecker->isGranted('ROLE_PREVIOUS_ADMIN')) {
    foreach ($tokenStorage->getToken()->getRoles() as $role) {
    if ($role instanceof \Symfony\Component\Security\Core\Role\SwitchUserRole) {
    $user = $role->getSource()->getUser();
    $token = new \Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken($user, $user->getPassword(), "firewallname", $user->getRoles());
    $tokenStorage->setToken($token);
    break;
    }
    }
    }

    But that stops the impersonation of the normal user on the public site - back to square one!

    Is there any way I can get the users logged in to different sections of the site at the same time?

  • 2017-04-28 Patrick Vale

    Hey, I figure I should update this with where I got to so far.

    It seems that voters are totally the way to go with this!

    I followed the New Voter Class tutorial, and created an AdminAreaVoter which is called by the controller in question.


    if (!$this->isGranted('ADMIN_AREA', $user)){
    throw $this->createAccessDeniedException('Access Denied by voter');
    }

    What I've been wondering is how best to identify the authenticator which has been used for the current user session. Looking in the token, I can see 'provider_key', which I guess could be used, if there were two firewalls in play. But other than that, the only way I've come up with is to add a custom attribute to the token in each of the login authenticators (login form and facebook oauth). These both set a 'authenticated_by' attribute:


    $token->setAttribute('authenticated_by', 'login_form');

    in their onAuthenticationSuccess() method.

    This works, as I can check the value of this attribute in the voter, but I wonder if there's a better way to do it? I would consider two firewalls, but that seems a bit overkill, and might stop the user impersonation from working correctly?

    I also considered adding roles to the user of e.g. 'ROLE_AUTH_FACEBOOK' but I couldn't see a way to reliably set a 'dynamic' role on the user, and ensure that it only lasts for the duration of the session - I wouldn't want to persist it to the database by accident.

    What I'm not sure on is:

    1) Is there a better way to identify the authenticator responsible for authenticating a particular user's session other than setting custom attributes?

    2) If there were more than one firewall in play (e.g. to allow for different entry points into the login process) would this cause problems with allowing admin users to impersonate normal users?

    3) Is it possible (or recommended) to attempt to set roles on a user which are only associated with that users session, i.e. they don't persist after a session token no longer exists.

    Many thanks, and sorry for all the questions - I seem to keep finding new possible ways of doing this!

  • 2017-04-27 Patrick Vale

    Hi Ryan,
    Thanks for a great tutorial - it's really helped me to get to grips with Symfony's auth mechanism.

    I wonder if I could ask your advice about firewall setup?

    I am creating a user system for a site which has two categories of user:

    1. Basic user (can edit own content, has ROLE_USER)
    2. Staff user (can edit any user's content, or impersonate any user, has ROLE_USER and ROLE_STAFF)

    There are two areas of the site, which allow for the different categories of editing:

    1. /user - basic user account editing area
    2. /admin - staff user account and content editing area

    Currently, I have one firewall which allows login via login form, or facebook


    main:
    anonymous: ~
    logout:
    path: /logout

    switch_user: ~
    remember_me:
    secret: '%secret%'
    remember_me_parameter: '_remember_me'
    lifetime: 3600
    guard:
    entry_point: app.security.login_form_authenticator
    authenticators:
    - app.security.login_form_authenticator
    - app.facebook_authenticator

    I am managing access to the /admin are via access_control properties in security.yml



    access_control:
    - { path: ^/admin, roles: ROLE_STAFF }

    There are two differences I would like to enforce between the two user categories:

    1. Staff users may not access the /admin area if they have gained their authentication token from a facebook login
    2. Staff users may not access the /admin area if they have been inactive for more than 2 hours (they can still access the rest of the site, and such an access would count as 'activity', should they then try to access the /admin area

    I am wondering what road to go down.

    I believe I can implement the second requirement by implementing something similar to http://stackoverflow.com/questions/18872721/how-to-log-users-off-automatically-after-a-period-of-inactivity although I'm not sure!

    I also believe that I could enforce the first requirement by the use of 'voters', though I'm unsure how exactly they work!

    My primary aim is to keep the system as simple as possible, while still enabling these two features. I don't want to go down a rabbit hole from lack of experience, so, I figured to ask the question first!

    Many thanks again for your tutorials, they really are game-changing.
    Patrick

  • 2016-07-26 weaverryan

    :)

    So, you have 2 options for this, depending on your setup!

    A) If you're using a Guard authenticator (via the AbstractFormLoginAuthenticator) like we are doing in this tutorial, then override the onAuthenticationSuccess method from that class and do whatever you want! This method normally redirects the user. But instead, you can detect if the request is via AJAX ($request->isXmlHttpRequest()) and either send back a nice JSON response or redirect like normal.

    B) If you're using some traditional way of authenticating - e.g. form_login (not covered in this tutorial), then you need to build a custom "authentication success handler". I'm guessing this isn't your situation so I won't include the details here - but you can google for it.

    Let me know if this helps!

  • 2016-07-26 ciudadano82

    I'm trying -unsuccessfully- to implement an ajax login form in the layout, but i dont know how to configure security.yml to return an json response instead of redirecting the browser. Please help me superm... i mean, ryan!

  • 2016-07-16 weaverryan

    Yo Vlad!

    Good question :). Two things here:

    1) The anonymous key means that "anonymous users are allowed into your firewall". Without this key, *no* URL on your site would be accessible without being logged in. Basically, you want this in 99.9% of the cases. Even if you need to require login for almost *every* page on your site (e.g. all pages, except for /login), you can/should accomplish that same thing by keeping anonymous here and using the access_control space (there's a way to require login on all urls with access_control, and then whitelist just a few specific URLs that should be open - I mention it briefly here: https://knpuniversity.com/s...

    2) The ~ is a confusing thing - in YAML this is "null". But, in several places in Symfony's configuration system, if you want to *activate* some system, you simply need the *presence* of some key (e.g. `anonymous`). So, this code basically says `anonymous: null` - but Symfony sees that you've specified the anonymous key and activates that system. You could also say `anonymous: true` and it would have the same effect (and I think would be a little be clearer). Also, often these systems *do* have optional, sub-configuration. You can use `~` to just activate the system, but then later you can add sub-configuration if needed. One example is `switch_user`, which could look like either of the following:


    switch_user: ~

    switch_user:
    parameters: _switch_user_custom_query_param

    Hope that helps! Anonymous is mostly a necessary thing you need and don't need to worry about (unless of course, you're curious!)

    Cheers!

  • 2016-07-16 Vlad

    What does anonymous: ~ mean?
    Is it a wildcard that could be either true or false?