Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Denying Access, access_control & 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

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

We've now talked a lot about authentication: the process of logging in. And... we're even logged in right now. So let's get our first look at authorization. That's the fun part where we get to run around and deny access to different parts of our site.

Hello access_control

The easiest way to kick someone out of your party is actually right inside of config/packages/security.yaml. It's via access_control:

security:
... lines 2 - 38
# 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 }

Un-comment the first entry:

security:
... lines 2 - 40
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }

The path is a regular expression. So this basically says:

If a URL starts with /admin - so /admin or /admin* - then I shall deny access unless the user has ROLE_ADMIN.

We'll talk more about roles in a minute... but I can tell you that our user does not have that role. So... let's try to go to a URL that matches this path. We actually do have a small admin section on our site. Make sure you're logged in... then go to /admin. Access denied! I've never been so happy to be rejected. We get kicked out with a 403 error.

On production, you can customize what this 403 error page looks like... in addition to customizing the 404 error page or 422.

Roles! User::getRoles()

So let's talk about these "roles" thingies. Open up the User class: src/Entity/User.php. Here's how this works. The moment we log in, Symfony calls this getRoles() method, which is part of UserInterface:

... lines 1 - 12
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 15 - 78
/**
* @see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
... lines 90 - 154
}

We return an array of whatever roles this user should have. The make:user command generated this so that we always have a role called ROLE_USER... plus any extra roles stored on the $this->roles property. That property holds an array of strings... which are stored in the database as JSON:

... lines 1 - 12
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 15 - 26
/**
* @ORM\Column(type="json")
*/
private $roles = [];
... lines 31 - 154
}

This means that we can give each user as many roles as we want. So far, when we've created our users, we haven't given them any roles yet... so our roles property is empty. But thanks to how the getRoles() method is written, every user at least has ROLE_USER. The make:user command generated the code like this because all users need to have a least one role... otherwise they wander around our site like half-dead zombie users. It's... not pretty.

So, by convention, we always give a user at least ROLE_USER. Oh, and the only rule about roles - that's a mouthful - is that they must start with ROLE_. Later in the tutorial, we'll learn why.

Anyways, the moment we log in, Symfony calls getRoles(), we return the array of roles, and it stores them. We can actually see this if we click the security icon on the web debug toolbar. Yup! Roles: ROLE_USER.

So then, when we go to /admin, this matches our first access_control entry, it checks to see if we have ROLE_ADMIN, we don't, and it denies access.

Only ONE access_control Matches

Oh, but there's one important detail to know about access_control: only one will ever be matched on a request.

For example, suppose you had two access controls like this:

security:
    # ...
    access_control:
      - { path: ^/admin, roles: ROLE_ADMIN }
      - { path: ^/admin/foo, roles: ROLE_USER }

If we went to /admin, that would match the first rule and only use the first rule. It works like routing: it goes down the access control list one-by-one and as soon as it finds the first match, it stops, and uses only that entry.

This will help us later when we deny access to all of a section except for one URL. But for now, just be aware of it!

And... that's it. Access controls give us a really easy way to secure entire sections of our site. But it's just one way to deny access. Soon we'll talk about how we can deny access on a controller-by-controller basis, which I really like.

But before we do, I know that if I try to access this page without ROLE_ADMIN, I get the 403 forbidden error. But what if I try to access this page as an anonymous user? Go to /logout? We're now not logged in.

Go back to /admin and... whoa! An error!

Full authentication is required to access this resource.

Next, let's talk about the "entry point" of your firewall: the way that you help anonymous users start the login process.

Leave a comment!

9
Login or Register to join the conversation
Ruslan I. Avatar
Ruslan I. Avatar Ruslan I. | posted 1 year ago | edited

It's strange, when I go to /admin it says "no route found (404)" :(
No matter whether I uncommented the first line of access control in security.yaml or not.
And I'm logged in. When I logged out it's the same.

My Symfony version is 5.3 (all bundles have 5.3.* in composer.json, but by some reason web debug toolbar says 5.4.7)
php 7.4.28.
Windows

Result of symfony console debug:router


_preview_error             ANY      ANY      ANY    /_error/{code}.{_format}
  _wdt                       ANY      ANY      ANY    /_wdt/{token}
  _profiler_home             ANY      ANY      ANY    /_profiler/
  _profiler_search           ANY      ANY      ANY    /_profiler/search
  _profiler_search_bar       ANY      ANY      ANY    /_profiler/search_bar
  _profiler_phpinfo          ANY      ANY      ANY    /_profiler/phpinfo
  _profiler_search_results   ANY      ANY      ANY    /_profiler/{token}/search/results
  _profiler_open_file        ANY      ANY      ANY    /_profiler/open
  _profiler                  ANY      ANY      ANY    /_profiler/{token}
  _profiler_router           ANY      ANY      ANY    /_profiler/{token}/router
  _profiler_exception        ANY      ANY      ANY    /_profiler/{token}/exception
  _profiler_exception_css    ANY      ANY      ANY    /_profiler/{token}/exception.css
  answer_vote                POST     ANY      ANY    /answers/{id}/vote
  app_popular_answers        ANY      ANY      ANY    /answers/popular
  app_homepage               ANY      ANY      ANY    /{page}
  app_question_new           ANY      ANY      ANY    /questions/new
  app_question_show          ANY      ANY      ANY    /questions/{slug}
  app_question_vote          POST     ANY      ANY    /questions/{slug}/vote
  app_login                  ANY      ANY      ANY    /login
  app_logout                 ANY      ANY      ANY    /logout
Reply
Ruslan I. Avatar

Hmmm, when I go to /admin I see in web debug toolbar that I'm logged out.
But when I go to the other pages on the site I'm still logged in.

Reply
Ruslan I. Avatar

Downgraded Symfony version to 5.3.16, but still route is not found.

Reply

Hey Ruslan I.!

Let's see if we can figure this out! I don't immediately see the problem, but I do notice a few things:

1) From your debug:router, it looks like, indeed, there is no route for /admin. So the problem isn't security or access_control... just that, somehow, the admin route isn't being seen! This page isn't something we built in this tutorial, but it should be included (it is, I just double-checked) in the "start/" directory of the code download. It's src/Controller/AdminController.php, and the Route annotation is above the dashboard() method. Do you have this file & method? What does it look like? I'm sure that the root of the issue is centered, somehow, around this routing being missing... or something weird happening.

2) You mentioned that when you go to /admin (the 404 page) you are logged out. But you ARE logged in on other pages. This is unrelated to your problem and is due to a "quirk" in Symfony. In Symfony, if you hit a 404 page, even if you ARE logged in on other pages, you will NOT be logged in on the error page. The reason is that the "listener" that actually "logs you in" at the start of every request runs after the routing system. So the routing system says "Ah! Route not found" and triggers the error page... before the authentication system ever has a chance to log you in. You can actually see this right here on SymfonyCasts: try going to some invented URL on SymfonyCasts and check the upper right: you'll see that you "appear" to not be logged in. It's an annoying quirk of Symfony.

Let me know what you discover!

Cheers!

1 Reply
Ruslan I. Avatar

Oh, I'm sorry, I'm idiot.

1) I thought that /admin page is some sort of basic Symfony's built in dashboard and tried to understand what's wrong with my bundles and configs.
But that's just my bad, because I'm not able to download course code due to payment issues that I already discussed with Victor Bocharsky. I use the same project building it step by step from first tutorial so I have no AdminController. I will try to continue this course as much as I can.
Thank you for quick reply, I was waiting for help. I appreciate that.

2) Hm. Thanks for this fact, it's really interesting. Now I know a little bit more.

Reply

Hey Ruslan I.!

> But that's just my bad, because I'm not able to download course code due to payment issues that I already discussed with Victor Bocharsky.

Ah, sorry about that! In that case, don't feel bad - I'm glad we could get this sorted out. The /admin page is just a basic, normal Symfony controller that renders a pretty boring page (just so we have a functional page at /admin).

> 2) Hm. Thanks for this fact, it's really interesting. Now I know a little bit more.

Awesome :)

Cheers!

1 Reply
discipolat Avatar
discipolat Avatar discipolat | posted 1 year ago

Hi, I'm facing a weird situation. When a logout from any page and then hit the back button (from the browser), i'm still able to see the last page until i refresh the page.

Reply

Hey discipolat!

Unfortunately... that's not so weird. It is, for better or worse, just how browsers work! Try it on some other site: log out, then click the back button. You'll go back to the previous page. This is because, when you hit back, there is no network request made to the site: your browser simply displays the previous page from cache.

If this is a problem, you could probably write some JavaScript that executes ever few seconds and that checks to see if the user is still authenticated. If they are not, you refresh the page via JavaScript.

Cheers!

Reply
discipolat Avatar

Thank's. I understand...basic indeed !!!

Reply
Cat in space

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

This tutorial also works great for Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.3", // v3.3.0
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "doctrine/annotations": "^1.0", // 1.13.2
        "doctrine/doctrine-bundle": "^2.1", // 2.6.3
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.1.1
        "doctrine/orm": "^2.7", // 2.10.1
        "knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
        "knplabs/knp-time-bundle": "^1.11", // v1.16.1
        "pagerfanta/doctrine-orm-adapter": "^3.3", // v3.3.0
        "pagerfanta/twig": "^3.3", // v3.3.0
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "scheb/2fa-bundle": "^5.12", // v5.12.1
        "scheb/2fa-qr-code": "^5.12", // v5.12.1
        "scheb/2fa-totp": "^5.12", // v5.12.1
        "sensio/framework-extra-bundle": "^6.0", // v6.2.0
        "stof/doctrine-extensions-bundle": "^1.4", // v1.6.0
        "symfony/asset": "5.3.*", // v5.3.4
        "symfony/console": "5.3.*", // v5.3.7
        "symfony/dotenv": "5.3.*", // v5.3.8
        "symfony/flex": "^1.3.1", // v1.17.5
        "symfony/form": "5.3.*", // v5.3.8
        "symfony/framework-bundle": "5.3.*", // v5.3.8
        "symfony/monolog-bundle": "^3.0", // v3.7.0
        "symfony/property-access": "5.3.*", // v5.3.8
        "symfony/property-info": "5.3.*", // v5.3.8
        "symfony/rate-limiter": "5.3.*", // v5.3.4
        "symfony/runtime": "5.3.*", // v5.3.4
        "symfony/security-bundle": "5.3.*", // v5.3.8
        "symfony/serializer": "5.3.*", // v5.3.8
        "symfony/stopwatch": "5.3.*", // v5.3.4
        "symfony/twig-bundle": "5.3.*", // v5.3.4
        "symfony/ux-chartjs": "^1.3", // v1.3.0
        "symfony/validator": "5.3.*", // v5.3.8
        "symfony/webpack-encore-bundle": "^1.7", // v1.12.0
        "symfony/yaml": "5.3.*", // v5.3.6
        "symfonycasts/verify-email-bundle": "^1.5", // v1.5.0
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.3
        "twig/string-extra": "^3.3", // v3.3.3
        "twig/twig": "^2.12|^3.0" // v3.3.3
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
        "symfony/debug-bundle": "5.3.*", // v5.3.4
        "symfony/maker-bundle": "^1.15", // v1.34.0
        "symfony/var-dumper": "5.3.*", // v5.3.8
        "symfony/web-profiler-bundle": "5.3.*", // v5.3.8
        "zenstruck/foundry": "^1.1" // v1.13.3
    }
}
userVoice