Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Authorization: access_control and Roles

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

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.

Denying with access_control

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
}

Many access_control

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.

When you Don't Have Access :(

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.

Leave a comment!

20
Login or Register to join the conversation
Bertin Avatar

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

Reply
Bertin Avatar
Bertin Avatar Bertin | Bertin | posted 5 years ago | edited

Fixed:


    access_control:
         - { path: ^/cms/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
         - { path: ^/cms, roles: ROLE_USER }

Is there a better way?

1 Reply
Victor Avatar Victor | SFCASTS | Bertin | posted 5 years ago | edited

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!

Reply
Bertin Avatar

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

Reply

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!

Reply
Default user avatar
Default user avatar JSThePatriot | posted 5 years ago

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

Reply

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 ;)

1 Reply
Default user avatar

I have successfully login, but in the tool bar below, in the user section, it says "Authentication: No". Any help?

Reply
Default user avatar

Problem solved with EquatableInterface :)

Cheers

Reply

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!

Reply
Default user avatar

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!

Reply

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!

Reply
Default user avatar

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
));
}

Reply
Default user avatar

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

Reply

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!

Reply
Yahya E. Avatar
Yahya E. Avatar Yahya E. | posted 5 years ago

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?

Reply
Yahya E. Avatar

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);
}

Reply

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!

Reply
Yahya E. Avatar
Yahya E. Avatar Yahya E. | MolloKhan | posted 5 years ago | edited

MolloKhan Thank you for the reply. Works. But find out it might be a better way controller wide access controls with @Security :).

Reply

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!

Reply
Cat in space

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

What PHP libraries does this tutorial use?

// 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
    }
}
userVoice