If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
I do use access controls to lock down big sections, but, mostly, I handle authorization inside my controllers.
Let's play around: comment out the access_control
:
... lines 1 - 2 | |
security: | |
... lines 4 - 33 | |
access_control: | |
# - { path: ^/admin, roles: ROLE_ADMIN } |
And open up GenusAdminController
. To check if the current user has a role,
you'll always use one service: the authorization checker. It looks like this:
if (!$this->get('security.authorization_checker')->isGranted('ROLE_ADMIN')
. So,
if we do not have ROLE_ADMIN
, then throw $this->createAccessDeniedException()
:
... lines 1 - 13 | |
class GenusAdminController extends Controller | |
{ | |
... lines 16 - 18 | |
public function indexAction() | |
{ | |
if (!$this->get('security.authorization_checker')->isGranted('ROLE_ADMIN')) { | |
throw $this->createAccessDeniedException('GET OUT!'); | |
} | |
... lines 24 - 31 | |
} | |
... lines 33 - 84 | |
} |
That message is just for us developers.
Head back and refresh. Access denied!
So what's the magic behind that createAccessDeniedException()
method? Find out:
hold Command
and click
to open it. Ah, it literally just throws a special exception
called AccessDeniedException
:
... lines 1 - 21 | |
use Symfony\Component\Security\Core\Exception\AccessDeniedException; | |
... lines 23 - 38 | |
abstract class Controller implements ContainerAwareInterface | |
{ | |
... lines 41 - 257 | |
/** | |
* Returns an AccessDeniedException. | |
* | |
* This will result in a 403 response code. Usage example: | |
* | |
* throw $this->createAccessDeniedException('Unable to access this page!'); | |
* | |
* @param string $message A message | |
* @param \Exception|null $previous The previous exception | |
* | |
* @return AccessDeniedException | |
*/ | |
protected function createAccessDeniedException($message = 'Access Denied.', \Exception $previous = null) | |
{ | |
return new AccessDeniedException($message, $previous); | |
} | |
... lines 274 - 396 | |
} |
It turns out - no matter where you are - if you need to deny access for any reason, just throw this exception. Symfony handles everything else.
Simple, but that was too much work. So, you'll probably just do this instead:
$this->denyAccessUnlessGranted('ROLE_ADMIN')
:
... lines 1 - 13 | |
class GenusAdminController extends Controller | |
{ | |
... lines 16 - 18 | |
public function indexAction() | |
{ | |
$this->denyAccessUnlessGranted('ROLE_ADMIN'); | |
... lines 22 - 29 | |
} | |
... lines 31 - 82 | |
} |
Much better: that does the same thing as before.
And, I have another idea! If you love annotations, you can use those to deny
access. Above the controller, add @Security()
then type a little expression:
is_granted('ROLE_ADMIN')
:
... lines 1 - 7 | |
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; | |
... lines 9 - 14 | |
class GenusAdminController extends Controller | |
{ | |
/** | |
* @Route("/genus", name="admin_genus_list") | |
* @Security("is_granted('ROLE_ADMIN')") | |
*/ | |
public function indexAction() | |
{ | |
... lines 23 - 29 | |
} | |
... lines 31 - 82 | |
} |
This has the exact same effect - it just shows us a different message.
But no matter how easy we make it, what we really want to do is lock down this
entire controller. Right now, we could still go to /admin/genus/new
and have
access. We could repeat the security check in every controller... or we could do
something cooler.
Add the annotation above the class itself:
... lines 1 - 7 | |
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; | |
... lines 9 - 11 | |
/** | |
* @Security("is_granted('ROLE_ADMIN')") | |
* @Route("/admin") | |
*/ | |
class GenusAdminController extends Controller | |
{ | |
... lines 18 - 82 | |
} |
As soon as you do that, all of these endpoints are locked down.
Sweet!
Hey Roel,
Have you tried it? Actually, Symfony has this feature out-of-the-box! :) You just need to properly configure your firewall in security.yml file.
Cheers!
Hi Victor,
Yes, I tried it. When I browse as an anonymous user to /user/create I get a 404 No route found for "GET /user/create".
In my security.yml I've added 'access_denied_url: /login' in firewalls > main. Is this the out-of-the-box feature you're referring to?
Cheers,
R.
No, it's something different. How do you restrict access to this page? Is your anonymous user authenticated on /user/create page? You can check it in web debug toolbar, you should see there:
Logged in as: anon.
Authenticated: Yes
Token class: AnonymousToken
Cheers!
Hi Victor,
I've added this to the top of my UserController:
/**
* @Security("is_granted('ROLE_HR')")
* @Route("/user")
*/
class UserController extends Controller
When a go to /user/create with an un-authenticated user, the web debug toolbar shows me "n/a - You are not authenticated".
Cheers,
Roel
Great! That's the point I think. So you need to authenticate user on every page, for it you need to specify "pattern: ^/" in the main firewall:
security:
firewalls:
main:
pattern: ^/
anonymous: true
Do you have these lines in your firewall? Also what's in your access_control list of security.yml?
Cheers!
Thanks! I've added these lines now to my main firewall in security.yml. Still get the 'no route found for "GET /user/create" error. The access_control list is currently empty.
Cheers!
Hey Roel!
Hmm! So, a couple of things to try here:
1) The "404 No Route Found GET /user/create" literally means what it says: this error has nothing to do with security or being denied access. It is actually saying (independent of security) "I don't see any route for /user/create". Try running a bin/console debug:router
to see what URLs you have - something must be slightly misconfigured here in your routing configuration.
2) Once we have (1) figured out (first priority!) then we can "lock" things down with security. The feature that Victor is talking about is mentioned here (it's an older, Symfony 2 tutorial - but this hasn't changed): http://knpuniversity.com/screencast/symfony2-ep2/secure-every-page. Make sure to remove that access_denied_urls thing - that's not needed. Your firewall should look a bit like what Victor posted (https://knpuniversity.com/screencast/symfony-security/deny-access-inside-controller#comment-2982576414) except after anonymous
, you will configure whatever login mechanisms you need (e.g. form_login, guard, http_basic, whatever). You won't have anything under your firewall that actually denies access in any way - that'll happen under access_control. Once you have this, an anonymous user who tries to visit any page that requires login will automatically be redirected to /login: that's a natural feature of Symfony (of course, you can configure exactly where they're redirected: but Symfony always redirects the user to the "login" page if it detects that an anonymous user is trying to access a protected page).
Let us know if that helps! And cheers back!
Yo Ryan!
Thanks for the debug:router
tip! At the top of my UserController
, I somehow added @Route("/user")
which turned my routes into /user/user/create
. Simply removed this and now I'm nicely redirected to /login
when I try to access /user/create
with an unauthenticated user.
Thanks a bunch!
Cheers,
R.
Could some explain to me the difference between : @Security("has_role("ROLE_ADMIN")") and @Security("is_granted("ROLE_ADMIN")") and which one is more recommended?
Hey Ahmedbhs,
Using the "has_role()" function in security expressions is deprecated since Symfony 4.2, use "is_granted()" instead.
Cheers!
hi guys its me again i wanna ask this,so i have a case in some controller they has this line $this->denyAccessUnlessGranted(['ROLE_CUSTOMER_SERVICE', 'ROLE_POINT_OF_SALES']);
but when i test access the module by direct url (let say localhost/app_dev.php/cashier) with user that didnt have role customer service and point of sales it still can access and open the page,so how i make if outside the granted role they meet access denied page
Hey Lavin
By passing an array of ROLES you are saying that it will be granted if the logged in user has any of those roles but not both are required. If you want to check for both, there are many ways to do it, like creating your own voter, or just checking one role at a time
$this->denyAccessUnlessGranted('ROLE_CUSTOMER_SERVICE');
$this->denyAccessUnlessGranted('ROLE_POINT_OF_SALES');
or changing the default "decision strategy" to unanimous
Cheers!
hi thank for the reply im still confuse because right now its still can bypass by access the url,i done changing the $this->denyAccessUnlessGranted
into seperate one like the example you give
ive solved maybe the solution its not the correct way i add this line in angular-jwt.js in code line when user got error message 403 'you got no access' add this code $state.go('dashboard');
It's probably worth pointing out that when you do the top level annotation then all routes are relative to that eg
/**
* @Route("/people")
* @Security("is_granted('ROLE_USER')")
*/
class PersonController extends VollFilmController
{
/**
* @Route("/",name="people_list")
**/
Here people_list url is actually /people
HI Rayan,
in the Class "LoginFormAuthentificator" there is a function named: setUser($credentials, ..)
where is variable $credentials declared? (witch data-type?)
I see in the interface doc that $credentials is a return value from getCredentials() but return variable name is $data and $credentials has in interface also no data-type
is there a magic? :)
Thanks a lot,
Cheers!
Teo
Yo Teolan,
Actually, $credentials can be anything you want, but except the null value. If getCredentials() returns a non-null value, then the getUser() method is called and its return value is passed here as the $credentials argument. So from getCredentials() method you can return just a token string, or an array of entered username and password by user - no matter, you just have to handle this credentials in getUser method. That's why getUser() method doesn't have a typehint for $credentials - it can be any type. So as you see there's no magic ;)
You can see the description of these methods here: http://symfony.com/doc/curr...
Cheers!
For me it always redirects to /login if I try to access /admin/genus.
I access SF 3.3.6 via https://aquanote.local/app_dev.php/admin/genus. Maybe its a new behavior? If I remove the @SECURITY line I can access the site with anon. User.
Yo Mike P.!
When you try to access a protected page as an anonymous user (e.g. /admin/genus), then the "entry point" of your firewall is called to determine what to do next. What the heck is an entry point? Well, each "method" of authentication - e.g. each guard authenticator of if you're using some core authentication methods, then each thing like form_login or http_basic - has an "entry point" - a method that simply returns what Response should be sent to the user in this situation. In our authenticator, the start() method is the entry point.
That's a long way of saying: when you go to /admin/genus as an anonymous user, then the start() method in your authenticator should be called and IT is determining what do send back to the user (in this case, it's redirecting to /login). But, if you have multiple authenticators or 1 authenticator and 1 other authentication mechanism (like form_login), then the entry point is only used from *one* of these. If I remember correctly, the start() method from your authenticator should win over the one in form_login. Let me know if this is your situation - other wise this last rambling is irrelevant :).
Cheers!
Perfect explanation! Ive seen now that start() is calling getLoginUrl() and there it is, the reason thy it redirects to /login:
protected function getLoginUrl()
{
return $this->router->generate('security_login');
}```
Many thanks Ryan!
Hello Ryan,
me again. Thanks first for all the nice tutorials, they help me a lot lerning symfony.
This section is all about denie/grant a ROLE to access a whole homepage. My question:
e.g. I have different roles for my blog, if not logged in, you can only read post/comments. ROLE_USER could read post/comments and post comments, ROLE_AUTHOR can read, post comments AND write posts. What I am planing right now is, logged in authors would see a button "write post" when "normal" users don't. I could do this (similar to login/logout button) by writing something like
{% if is_granted('ROLE_AUTHOR') %}
Write Post
{% endif %}
and I could do the same with comments
{% if (is_granted('ROLE_AUTHOR') or is_granted('ROLE_USER')) %}
{% include 'formwhereIcanwriteacomment.html.twig' %}
{% endif %}
would this work? Is there a better way to do such authorizations?
thx and have a nice weekend
edit: small question: I want to test my theory and add some fixtures, I remember from the beginner tutorial, you used '50% ryan.jpeg: leanna.jpg', I tried
roles: '50%? ['ROLE_AUTHOR'] : ['ROLE_USER']'
but single-quote won't work here, I tried double quote:
roles: "50%? ['ROLE_AUTHOR'] : ['ROLE_USER']"
this seems to put a string into database so I got
"Warning: in_array() expects parameter 2 to be array, string given" at the getRoles()-method.
What should I do?
edit2: solved the "small question" by providing a roles() function in LoadFixtures-class, which just random returns ROLE_USER or ROLE_AUTHOR as an array.
Hey Yang!
Another good question :). Here's what I would do in Twig:
{% if is_granted('ROLE_ADD_COMMENT') %}
Then, you can use the role_hierarchy feature ( https://knpuniversity.com/screencast/symfony-security/role-hierarchy ) to give ROLE_USER and ROLE_AUTHOR this role:
role_hierarchy:
ROLE_USER: [ROLE_ADD_COMMENT]
ROLE_AUTHOR: [ROLE_ADD_COMMENT]
This should help keep things cleaner :).
And also, though you don't need it yet, you may eventually want to use voters: https://knpuniversity.com/screencast/new-in-symfony3/voter. This is needed when the access decision you need to make depends on some data - e.g. deciding whether or not a user can edit a blog post, which in your app, might be allowed only if the author if the blog post matches the current user. So, roles are for global permissions - "Can the user do ABC in general?" - but if you need object-specific permissions - "Can the user EDIT this specific object" - then look into voters.
Cheers!
Hi guys. I just made my first web with login access. How I can secure the images ? I only want that the user gets access to some images after login. I just need to move the images to another folder or do you have a better tip? I hope you can help me.
Hey Cesar
Nice question. I have an idea but it depends on how much security do you need. My idea is to use obfuscation by creating a folder per each user and giving it a very hard guessing name, so only you can find those folders and files.
But, if what you need is something more bullet proof, then you can give it a try to this solution: https://stackoverflow.com/a...
Cheers!
Ah, you can see right bracket ")" at the end of that link. Actually, that was a part of Ryan context that Disqus mistakenly regarded as a part of link. Just remove it and it will work :)
P.S. I fixed the link.
so, now in my security.yml:
security:
role_hierarchy:
ROLE_AUHTOR: [ROLE_WRITE_POST, ROLE_WRITE_COMMENT]
ROLE_USER: [ROLE_WRITE_COMMENT]
(I checked here, its 4 spaces indented)
and in my twig:
{% if is_granted('ROLE_WRITE_POST') %}
New Post
{% endif %}
however, when I log in with a user with 'ROLE_AUTHOR', I can't see the button.
Just for testing,
{% if is_granted('ROLE_WRITE_POST') or is_granted('ROLE_AUTHOR') %}
New Post
{% endif %}
this is working fine. So, did I forget anything?
Ah, it's such a small detail! In your security.yml, you have ROLE_AUHTOR - it's a typo, should be ROLE_AUTHOR.
I bet that'll do it :)
Cheers!
holy shit... of course... the best(worst) ones are always the typos... I would never found it by myself o.O
Hi Guys,
I'm building a web app where users can log in as belonging to different groups.. Currently roles are global - but will end up being group specific but that's for another day.
Today I'm try to ensure that manually switching the group id in the browser bar is blocked in the controller, and I think @security annotations is the way to go. I've checked the symfony docs, and since there are suspicious uses of the name 'Ryan' I'm assuming you guys wrote them.
Can you give me any hints please for the best way of checking against a one to many table linked to the user table - where the one to many linked table has additional characteristics - so can't be handled invisibly by Doctrine?
I'm thinking I need to create a new repository function for this particular query - but I'm not quite sure how the expression engine syntax will reference it.
Thanks,
John
OK - ignore me, wrong question, wrong place.. It's voters isn't it?
Thanks for some of the best courses anywhere.
Hey John,
Nice catch about voters! Yeah, voters are great and very flexible, and looks like it's exactly what you need if you want to flexibly secure something in your app, but the ROLE_* functionality isn't enough for you. However, there's also a concept about groups, if you use FOSUserBundle - check their docs to find more information and use cases about groups:
https://symfony.com/doc/mas...
P.S. If you don't use FOSUserBundle, well, you can get the idea of roles and easily implement them by yourself, just steal some code from the bundle ;)
Cheers!
Hi Victor,
I think after a few hours I've reimplemented what is probably in FOSUserBundle (although I've not gone and looked, and I've probably done it less well). I've created an event subscriber, and am using that to dynamically reload roles and linked it to kernel.controller - based on the group they are acting as a member of. So that deals with a 'group change' whether through a planned action, or through editing the browser bar.
It broke the web profiler for a bit, until I worked out how to exclude triggering my event when the web profiler controller ran.
so all good now, but it was an interesting journey.
Hey John,
As always, security is a complex stuff, but looks like you nailed it ;)
I'm glad to hear it helped you!
Cheers!
Hi Rayan ,
Is it possible to write custom message to @Security annotations? If not how i can do it
Best Regards ,
Hey Boran,
Could you explain a bit what're you trying to do? The @Security annotation throws exception like AccessDeniedHttpException, so users will see an Access Denied error with 403 status code. And you can override this Twig template to show a custom error page for this error. I hope it helps.
Cheers!
I'm trying to use @Security annotations for my routes. Like this:
/**
* @return Response
* @Route("/action")
* @Security("has_role('ROLE_USER')")
* @Template()
*/public function someAction(){
return array();}
When the security restriction fires an exception, I get the message Expression "has_role('ROLE_USER')" denied access.
This is not acceptable to be shown to the end user, so I'm trying to find a way to customize the message for annotation.
Simple workaround is to not to use @Secutity annotations and write code like these:
/**
* @return Response
* @Route("/action")
*
* @Template()
*/public function someAction(){
if (!$this->get('security.context')->isGranted('ROLE_USER')) {
throw new AccessDeniedException('You have to be logged in in
order to use this feature');
}
return array();}
But this is less convenient and less readable.
Is it possible to write custom message to @Security annotations?
Hey Boran,
At first, I see that we can't change this message with @Security annotation, but users won't see this "Expression "has_role('ROLE_USER')" denied access" message in production anyway - you see it only in dev mode. So the better and *easiest* option here is to override the default `error403.html.twig` template (or just error.html.twig for all errors) and customize this page for your users. Please, check out this example: https://knpuniversity.com/s... or look into the docs: https://symfony.com/doc/cur...
Cheers!
Hi there,
I can't use @Security("") :( I am using symfony 3.4 LTS and
I have already run:
$ composer require sensio/framework-extra-bundle
$ composer require symfony/expression-language v3.4.1
But now when I want to add method-wide security like "@Security("is_granted('ROLE_ADMIN')")" it gives an error:
1/1) ClassNotFoundException
Attempted to load interface "ExpressionFunctionProviderInterface" from namespace "Symfony\Component\ExpressionLanguage".
Did you forget a "use" statement for another namespace?
in ExpressionLanguageProvider.php (line 22)
What should I do?
Hey Yahya E.
This may sound silly but did you add the correct use statement for Security annotation?
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
Cheers!
MolloKhan I double checked everything. It seemed fine. However, finally it turned out a bug in filesystem of Vagrant caused some missing files. I remove the dependencies then install again, It works now. Happy that we can ask you so we do not go forward onto darkside (giving up :)). Thanks.
Hey Andjii,
This class was added in the previous episode Symfony Forms: Build, Render & Conquer! of "Starting in Symfony 3!" series. We started working with it at the beginning - check https://knpuniversity.com/s... .
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
}
}
Hi Ryan, me again :)
The web app I'm creating will only allow authenticated users. Is there a way when a non-authenticated user tries to reach e.g. /user/create he is automatically redirected to /login?
Cheers and thanks already!
Roel