If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeWe'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.
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 hasROLE_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.
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.
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.
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.
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!
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.
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!
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.
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!
// 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
}
}
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