If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
Authentication is done. So how about we tackle the second half of security: authorization. This is all about figuring out whether or not the user has access to do something. For example, right now we have a fancy admin section, but probably not everyone should have access to it.
There are 2 main ways to deny access, and the simplest is right inside of security.yml
.
It's called "access control". Move in 4 spaces - so that you're
at the same level as the firewalls
key, but not inside of it. Add access_control:
,
new line, go out 4 more spaces and add - { path: ^/admin, roles: ROLE_USER }
:
... lines 1 - 2 | |
security: | |
... lines 4 - 33 | |
access_control: | |
- { path: ^/admin, roles: ROLE_USER } |
That path is a regular expression. So, if anyone goes to a URL that starts with
/admin
, the system will kick them out unless they have ROLE_USER
.
Let see it in action. First, make sure you're logged out. Now, go to /admin/genus
.
Boom! That was it! Anonymous users don't have any roles, so the system kicked us
to the login page.
Tip
Our FormLoginAuthenticator
is actually responsible for sending us to /login
.
You can customize and override this behavior if you need to. If you use a built-in
authentication system, like form_login
, then it may be responsible for this.
This functionality is called an "entry point".
Now, login. It redirects us back to /admin/genus
and we do have access. Our
user does have ROLE_USER
- you can see that if you click the security icon in
the web debug toolbar. Remember, that's happening because - in our User
class -
we've hardcoded the roles: every user has a role that I made up: ROLE_USER
:
... lines 1 - 12 | |
class User implements UserInterface | |
{ | |
... lines 15 - 46 | |
public function getRoles() | |
{ | |
return ['ROLE_USER']; | |
} | |
... lines 51 - 93 | |
} |
And at first, that's as complex as Symfony's authorization system gets: you give each user some roles, then check to see if they have those roles. In a minute, we'll make it so each user can have different roles.
But we're not quite done yet with access_control
. We only have one rule, but
you can have many: just create another line below this and secure a different section
of your site. For example, maybe ^/checkout
requires ROLE_ALLOWED_TO_BUY
.
There is one gotcha: Symfony looks for a matching access_control
from top to bottom,
and stops as soon as it finds the first match. We won't talk about it here, but
you can use that fact to lock down every page with an access_control
, and then
white-list the few public pages with access_control
entries above that.
You can also do a few other cool things, like force the user to visit a part of your
site via https
. If they come via http
, they'll be redirected to https
.
Change the role to something we don't have, how about ROLE_ADMIN
:
... lines 1 - 2 | |
security: | |
... lines 4 - 33 | |
access_control: | |
- { path: ^/admin, roles: ROLE_ADMIN } |
Head back and refresh!
Access denied! Ok, two important things.
First, roles can be anything: I didn't have to configure ROLE_ADMIN
before using
it - I just made that up. The only rule about roles is that they must start with
ROLE_
. There's a reason for that, and I'll mention it later.
Second, notice this is an access denied screen: 403, forbidden. We see this because we're in development mode. But your users will see a different error page, which you can customize. In fact, you can have a different error page for 403 errors, 404 errors and 500 errors. It's easy to setup - so just check the docs.
Access controls are super easy to use... but they're a bit inflexible, unless you love writing complex, unreadable regular expressions. Next, let's look at a more precise way to control access: in your controller.
Fixed:
access_control:
- { path: ^/cms/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/cms, roles: ROLE_USER }
Is there a better way?
Hey Bertin,
Your setup looks valid for me, you need to understand why you have those redirects. And Symfony can help with it, In your config_dev.yml file enable intercepting redirects:
web_profiler:
intercept_redirects: true
And try again. You'll see where you're redirected manually following intercepted redirects.
Btw, important question, can you get to the /cms/login page and see the login form? Or are you redirected immediately? And where your login form submit data? What URL?
Cheers!
No i cant go to /cms/login its takes long to load and then it says to many redirects. But with above config it works
Yo Bertin!
Your config is totally the way to handle this :). When you use access_control, there are 2 strategies:
A) Use some access_control to deny access to just a few pages
B) Deny access to the entire site (or some entire section - like /cms) and then "whitelist" a one (or a few) URL under /cms that should be public.
You're using strategy (B), and you're using it just fine :). We talk a bit about this in an older tutorial - but this part is still relevant: https://knpuniversity.com/s...
Cheers!
Okay... so I have a unique issue with the `access_control` still. However, this issue only exists in production. We are unsure how to figure out what's the cause. Any pointers?
Very similar to this comment: https://knpuniversity.com/s... we have a final access_control that basically requires everyone to be logged in. We are trying to set that to an even "higher" role of an Admin as we are only releasing a portion of our functionality, and instead of making a bunch of code changes just wanted to catch everything here, and this would also allow `admins` of a particular level to be able to still see and test the new functionality before we enable it for the "lesser" users. Our `access_control` works in `dev` mode (app_dev.php). However, no amount of cache clearing `rm -rf` seems to make this work in production.
Would love some ideas/thoughts on this!
Thanks!
Jarvis
Jarvis!
Nice to hear from you man! So wow, I hate when things only work in dev. It's rare... but it's bad news when it happens! So first, does the problem exist locally on your machine, or only on the production machine? From your description, it sounds like it exists locally (but of course in the prod environment). I just want to make sure it's not some issue with the specific machine.
Next, when it doesn't work, what does that mean exactly? Do you mean that the access_control
is basically ignored and all users (even anonymous) have access? Or, is it requiring login... but not requiring ROLE_ADMIN? And can you post the full access_control
section? As you know, only one access control is matched per request... so one thing I'm thinking about is whether or not a different access control is being hit, for some reason.
And here are a few other things to try: change logging on prod to log everything (basically, copy the config_dev.yml logging stuff into config_prod.yml temporarily. This may help, because you can see all of the info in the logs. You could also temporarily enable the WebProfilerBundle for all environment in AppKernel, then copy the web_profiler
config from config_dev.yml to config_prod.yml, but change intercept_redirects to true. As you also know, sometimes things get tricky with security because it redirects and you don't see what really happened before the redirect. This might help that.
Phew! Hopefully that gives you something to at least start on :).
Cheers and stay dry ;)
I have successfully login, but in the tool bar below, in the user section, it says "Authentication: No". Any help?
Hey Joe,
Most of the time it's due to a misconfiguration of your firewall, ensure you have a firewall which applies for all your pages, i.e. which has "pattern: ^/".
Cheers!
Hi, Victor!
I have - { path: ^/, roles: ROLE_USER } in security.yml under access_control, despite the fact that it still does not work :(
I was able to fix it by implementing the Entity class (User) with EquatableInferface (class User implements UserInterface, EquatableInterface) and adding the following method:
...
public function isEqualTo(UserInterface $user)
{
return $this->id === $user->getId();
}
Any idea as to why this is happening?
Thanks!
Hey Joe,
Hm, I see. I think somehow your default user equality test is failed. It's difficult to say why this happens, probably you have some problems with serialization/unserialization, but I'm not sure about it. Or maybe the object is modified during the runtime, probably by some event listeners, etc. and that's why default equality test failed. Anyway, with EquatableInterface you override the default implementation of users equality test.
Cheers!
It's strange, but if I change csrf token using developer tools in Chrome, so that it normally should not be valid, I still will be logged in. But if I look into symfony profiler under the tab 'Forms', there will be an error message 'The CSRF token is invalid. Please try to resubmit the form.'
So it looks like Guard detects that csrf token is wrong, but doesn't do anything.
I've tried to add to LoginForm.php following lines, but can't resolve this issue:
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'csrf_protection' => true
));
}
Ok, I think I've found how to make csrf check working. Here is the one of Ryan's commits where he implements csrt check: https://github.com/knpunive...
Hey Ruslan!
Good find! This was an oversight on my part honestly. I also implement CSRF protection (not using forms) here: https://knpuniversity.com/s... (that page isn't released at *this* moment, but will be in a week or so!)
Cheers!
Hi, I am working authentication system throughout this tutorial. And authorization works nice if I navigate `/admin` for example. But when I add this to security.yaml file `access_control: - { path: ^/, roles: ROLE_USER }` and navigate to `/` page it says: `This page isn’t working symfony3.dev redirected you too many times.` Any idea why?
On onAuthenticationFailure @ AbstractGuardAuthenticator it redirects to 'loginUrl'. That's why I think :) We need to override it on LoginFormAuthenticator but how to fill it?
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
if ($request->getSession() instanceof SessionInterface) {
$request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);
}
$url = $this->getLoginUrl();
return new RedirectResponse($url);
}
Hey Yahya E.
That's happening because if you are not logged in, you will be redirected to the login page, but your access control rule says that you must own "ROLE_USER" in order to access to any page, that's why you end up into an infinite loop of redirections.
You have to allow some paths to be accessible to everybody (Like the login page). Try adding this to your access control:
access_control:
- { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
You can read more info about it here: https://symfony.com/doc/current/security/access_control.html
Cheers!
MolloKhan Thank you for the reply. Works. But find out it might be a better way controller wide access controls with @Security :).
Yeah that works too :)
When you want to apply the same rules to a set of paths, I find it easier doing it via "access_control", but actually that's completely up to you
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
}
}
Iam trying to create the following login hierarchy with routes.
Login page is at :example.com/cms/login
Logout page is at: example.com/cms/logout
Actually cms (dashboard) is at: example.com/cms/
How could i setup this hierarchy. Because now i get ERR_TOO_MANY_REDIRECTS