If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
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
:
# 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 |
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:
... 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:
... 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.
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.
Hey ahmedbhs!
GREAT question :).
* ACL is a complex system that you could *choose* to use if you need it (Symfony does not come with an ACL system by default). Basically, an ACL system gives you MAX flexibility with security, but is almost ALWAYS overkill (so I almost never recommend it). You only need an ACL system if you have the following requirement: an admin user needs to be able to add any permission to any user to do anything dynamically via an admin interface. But basically, forget about ACL - you probably don't need it :).
* Voter: this is the system that allows you to write your own business logic for control. When you call is_granted() (or isGranted in PHP), it calls your voters and asks "can you help me decide if the user has access?". Your custom business logic will do whatever it needs to do to determine this. if you use something like isGranted('ROLE_USER') or isGranted('IS_AUTHENTICATED_FULLY'), there are built-in voters that handle that for you. But you can also invent your own logic, so that you could, for example, determine if the current has permission to "edit" some Product. See https://knpuniversity.com/s... for more details - it's a big topic :).
* access_control: this is an optional, shortcut that allows you to protect pages via URLs, but it's no different than using isGranted. For example, if you had an access_control that required ROLE_ADMIN for all URLs starting with /admin, this would be identical to using isGranted() inside all of the "admin" controllers to make sure the user has ROLE_ADMIN.
I hope that clears things up a little bit! But make sure you go through this entire tutorial - we try to explain this authorization stuff quite a bit :). Basically, I typically ONLY use isGranted() in my controllers for authorization - I don't use access_control (or I don't use it much). When I have more complex rules (beyond just checking to see if a user has a role), I enhance the isGranted() method by adding custom voters.
Cheers!
Hello, I've now this situation: User Entity, Page Entity. I need to restrict a user to view just the pages he created. How to do that? Now I'm using a custom function like isMine() to check in the DB if the Page->getOwner() is the same of the current user, but I think I can do something better. Any hint? Thank you very much :-)
Hey Bettinz,
Take a look at Symfony Voters:
https://symfony.com/doc/cur...
https://knpuniversity.com/s...
https://knpuniversity.com/s...
Though your method still might be useful :)
Cheers!
Hello Victor, just to update, this is how I've solved: created a PageVoter with the "canView" function and this code (a Page can have multiple users) and "canEdit" (only users with "type" 3 can edit)
private function canEdit(Page $page, User $user){
foreach ($page->getUsers() as $userPage){
if ($userPage === $user){
if ($userPage->getType() == '3'){
return true;
}
}
}
return false;
}
private function canView(Page $page, User $user){
if ($page->getUsers()->contains($user)){
return true;
}
}
</code >```
the voteOnAttribute is with this switch:
switch ($attribute){
case self::VIEW:
return $this->canView($page, $user);
case self::EDIT:
return $this->canEdit($page, $user);
}
</code >`
Hope this help someone :-)
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:
There are two areas of the site, which allow for the different categories of editing:
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:
I am wondering what road to go down.
I believe I can implement the second requirement by implementing something similar to <a href="http://stackoverflow.com/questions/18872721/how-to-log-users-off-automatically-after-a-period-of-inactivity">http://stackoverflow.com/questions/18872721/how-to-log-users-off-automatically-after-a-period-of-inactivity</a> 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
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 <a href="https://knpuniversity.com/screencast/new-in-symfony3/voter">New Voter Class</a> 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:
<br />$token->setAttribute('authenticated_by', 'login_form');<br />
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!
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 <a href="http://symfony.com/doc/current/security/impersonating_user.html">here</a>, 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?
Hey Patrick!
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/symfony/blob/f2f9aa61684d867df280473d56c928e6906aa437/src/Symfony/Component/Security/Guard/AbstractGuardAuthenticator.php#L33-L40. 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/symfony/blob/f2f9aa61684d867df280473d56c928e6906aa437/src/Symfony/Component/Security/Guard/AbstractGuardAuthenticator.php#L33-L40. 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!
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
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!
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
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!
What does anonymous: ~
mean?
Is it a wildcard that could be either <strong>true</strong> or <strong>false</strong>?
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/screencast/symfony-security/authorization-roles#many-access-control)
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!
Hi Ryan, thanks for this great tutorial.
I'm using Symfony 4 and I wonder if this is the reason why my guard authenticator doesn't quite work. Everything works well except one thing. When I'm entering correct credentials, the profiler first shows that I am correctly logged in. However, on the next page, zap! (e.g. if I refresh the page, or after being redirected) I become anonymous again…
That's my code if you want to see it:
namespace App\Security;
use App\Entity\Member;
use App\Form\LoginForm;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
use Symfony\Component\HttpFoundation\RedirectResponse;
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
private $formFactory;
private $om;
private $router;
public function __construct(
FormFactoryInterface $formFactory,
ObjectManager $om,
RouterInterface $router)
{
$this->formFactory = $formFactory;
$this->om = $om;
$this->router = $router;
}
public function getCredentials(Request $request)
{
$form = $this->formFactory->create(LoginForm::class);
$form->handleRequest($request);
$data = $form->getData();
return $data;
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$username = $credentials['_username'];
$user = $this
->om
->getRepository(Member::class)->findOneBy(array(
'username' => $username,
));
return $user;
}
public function checkCredentials($credentials, UserInterface $user)
{
$password = $credentials['_password'];
if ('hello' === $password) {
return true;
} else {
return false;
}
}
protected function getLoginUrl()
{
return $this->router->generate('security_login');
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
return new RedirectResponse('/public');
}
public function supports(Request $request): bool
{
$isRouteCorrect = $request->attributes->get('_route') === 'security_login';
$isMethodCorrect = $request->isMethod('POST');
return $isRouteCorrect && $isMethodCorrect;
}
}
Hey Louis,
Hm, probably your application does not have write permissions to write session into "var/sessions/". Where do you store your session?
Cheers!
Hi Victor, thanks for the reply!
I believe I'm using PHP built-in sessions. My framework.yml is:
framework:
secret: '%env(APP_SECRET)%'
#default_locale: en
csrf_protection: true
#http_method_override: true
#trusted_hosts: ~
session:
handler_id: ~
#esi: ~
#fragments: ~
php_errors:
log: true
I'm not sure how to use Symfony sessions. I've also tried session: ~ (without specifying handler_id) and chmod 777 -R to the whole symfony folder, but it did not change anything…
Hey Louis,
Hm, really, not related to file permissions issue because you use PHP built-in sessions. Well, it also could be due to problem of serialization/unserialization of User object. Do you use FOSUserBundle? Do you have serialize()/unserialize() methods in your User entity or any parent classes that you extend?
Cheers!
You're impressive! That was the problem. I actually realized yesterday that my serialization method was wrong (it inverted the username and the password), but I didn't make a connection with the broken authentication.
I was struggling with authentication for more than one week, having tried form_login and the method described here, among many others, but none of them worked. I thought that, because http_basic was working, the problem had to come from the form and not from the entity! Althogh I managed to get a system working by making custom guards intercepting all requests and manually saving the username in the session, it was a bit complex and had a few issues.
Moral of the story: do TDD!
Oh wow, you have done a big work :) But I'm glad we figured out the problem!
Yep, TDD/BDD is great ;)
Cheers!
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!
:)
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!
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.
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!
// composer.json
{
"require": {
"php": ">=5.5.9",
"symfony/symfony": "3.1.*", // v3.1.4
"doctrine/orm": "^2.5", // v2.7.2
"doctrine/doctrine-bundle": "^1.6", // 1.6.4
"doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
"symfony/swiftmailer-bundle": "^2.3", // v2.3.11
"symfony/monolog-bundle": "^2.8", // 2.11.1
"symfony/polyfill-apcu": "^1.0", // v1.2.0
"sensio/distribution-bundle": "^5.0", // v5.0.22
"sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
"incenteev/composer-parameter-handler": "^2.0", // v2.1.2
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"knplabs/knp-markdown-bundle": "^1.4", // 1.4.2
"doctrine/doctrine-migrations-bundle": "^1.1" // 1.1.1
},
"require-dev": {
"sensio/generator-bundle": "^3.0", // v3.0.7
"symfony/phpunit-bridge": "^3.0", // v3.1.3
"nelmio/alice": "^2.1", // 2.1.4
"doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
}
}
what's the difference between ACL, Voters, and access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } or is_granted() function ? wish oone is used to manage API autorization ?