If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
Let’s login as user again and surf to /new. Since we have the ROLE_USER role, we’re allowed access. In the access_control section of security.yml, change the role for this page to be ROLE_ADMIN and refresh:
# app/config/security.yml
security:
# ...
access_control:
- { path: ^/new, roles: ROLE_ADMIN }
# ...
This is the access denied page. It means that we are authenticated, but don’t have access. Of course in Symfony’s prod environment, we’ll be able to customize how this looks. We’ll cover how to customize error pages in the next episode.
The access_control section of security.yml is the easiest way to control access, but also the least flexible. Change the access_control entry back to use ROLE_USER and then comment both of them out. We’re going to deny access from inside our controller class instead.
# app/config/security.yml
security:
# ...
access_control:
# - { path: ^/new, roles: ROLE_USER }
# - { path: ^/create, roles: ROLE_USER }
Find the newAction in EventController. To check if the current user has a role, we need to get the “security context”. This is a scary sounding object, which has just one easy method on it: isGranted.
Use it to ask if the user has the ROLE_ADMIN role:
// src/Yoda/EventBundle/Controller/EventController.php
// ...
public function newAction()
{
$securityContext = $this->container->get('security.context');
if (!$securityContext->isGranted('ROLE_ADMIN')) {
// panic?
}
// ...
}
If the user doesn’t have ROLE_ADMIN, we need to throw a very special exception: called AccessDeniedException. Add a use statement for this class and then throw a new instance inside the if block. If you add a message, only the developers will see it:
// src/Yoda/EventBundle/Controller/EventController.php
// ...
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
// ...
public function newAction()
{
$securityContext = $this->container->get('security.context');
if (!$securityContext->isGranted('ROLE_ADMIN')) {
throw new AccessDeniedException('Only an admin can do this!!!!')
}
// ...
}
In Symfony 2.5 and higher, there’s event a shortcut createAccessDeniedException method:
// src/Yoda/EventBundle/Controller/EventController.php
// ...
if (!$securityContext->isGranted('ROLE_ADMIN')) {
// in Symfony 2.5
throw $this->createAccessDeniedException('message!');
}
When we refresh now, we see the access denied page. But if we were logged in as admin, who does have this role, we’d see the page just fine.
Normally, if you throw an exception, it’ll turn into a 500 page. But the AccessDeniedException is special. First, if we’re not already logged in, throwing this causes us to be redirected to the login page. But if we are logged in, we’ll be shown the access denied 403 page. We don’t have to worry about whether the user is logged in or not here, we can just throw this exception.
Phew! Security is hard, but wow, you seriously know almost everything you’ll need to know. You’ll only need to worry about the really hard stuff if you need to create a custom authentication system, like if you’re authenticating users via an API key instead of a login form. If you’re in this situation, make sure you read the Symfony Cookbook entry called How to Authenticate Users with API Keys. It uses a feature that’s new to Symfony 2.4, so you may not see it mentioned in older blog posts.
Ok, let’s unbreak our site. To keep things short, create a new private function in the controller called enforceUserSecurity and copy our security check into this:
private function enforceUserSecurity()
{
$securityContext = $this->container->get('security.context');
if (!$securityContext->isGranted('ROLE_USER')) {
throw new AccessDeniedException('Need ROLE_USER!');
}
}
Now, use this in newAction, createAction, editAction, updateAction and deleteAction:
public function newAction()
{
$this->enforceUserSecurity();
// ...
}
public function createAction(Request $request)
{
$this->enforceUserSecurity();
// ...
}
You can see how sometimes using access_control can be simpler, even if this method is more flexible. Choose whichever works the best for you in each situation.
Tip
You can also use annotations to add security to a controller! Check out SensioFrameworkExtraBundle.
Hey Diego :)
Yes, this behavior is totally by design - it allows the user to get a chance to login to see a secured page. What's your use-case for *not* doing this?
Either way, the whole point of throwing AccessDeniedException is to "redirect to login if they're anonymous OR show them a 403 error page". If you don't want this, you're always free in a controller to simply return a Response object with a 403 status code (e.g. something like "return new Response("get outta here", 403)". Of course, that means you won't be using your *normal* Symfony 403 error page. If you try want to force a 403, but not do the redirect, throw an exception of the class Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException. If you want to know *why* that works, let me know ;).
Cheers!
Great! AccessDeniedHttpException was the key, I'm not getting redirected anymore.
Why is this working :)?
My reason is because my login form is inside an iframe, so I just can't do a redirect. What I need is to show a message saying "You need to login first" and a button which opens the iframe, if login is successful, then redirect to the desired page.
You can see what I want to achieve in my small site ( https://www.esla.pro ) just click where it says "Inicia Sesion" at the top section.
I'm translating the whole site to symfony2 :)
Thanks for your time.
Hi Diego!
For why is this working? There are two things happening:
1) AccessDeniedException is a very special exception. When you throw it, it triggers the part of Symfony that tries to get the user to login (usually by redirecting them to /login). Here's the internals for that: https://github.com/symfony/...
2) If you throw any Exception, it usually triggers a 500. BUT, that's not always true. If the Exception class you're throwing implements a special HttpExceptionInterface (https://github.com/symfony/..., then you will not get a 500, but whatever status code returned by that class's getStatusCode() method. The `AccessDeniedHttpException` is one of these: https://github.com/symfony/.... It causes a 403 status code, but doesn't trigger the special behavior in (1).
About your situation, here's my advice (fwiw):
1) Allow the user to be redirected to /login. This doesn't mean you need to eliminate your iframe popup (keep reading), but *also* allow for the login to be its own page (you could have /login basically be a blank page that has some JS to open your popup, if you really want to - it looks like you're doing something like this already: https://www.esla.pro/soport.... This will make your life much easier. One things Symfony already does is help redirect you back to the original page the user was trying to visit in this situation.
2) On a case-by-case basis, you can make some links automatically open your login popup. For example, when you render a page, if you could check to see if the user is not logged in (in Twig) and render a link to open the popup instead of, for example, taking the user to the registration page. OR, if a link does something with JavaScript, you could make the AJAX request, and if the user is not logged in, then open the popup.
If anything, the piece you need to override is called the "Entry Point" - this is the class that's called when Symfony says "the user is trying to access a protected resource, but they are not logged in, let's help them!". This is the class that causes the redirect - the default one is here: https://github.com/symfony/.... You may use want to use your own, to, for example, redirect on normal requests, but return a normal 403 on AJAX requests. You could extend the FormAuthenticationEntryPoint class, register your new class as a service, then put your service id on the entry_point key under your firewall: http://symfony.com/doc/curr...
Phew! That's a lot of info, but hopefully it's useful!
Cheers!
Wow, great answers, definitely it was useful!
I'll give it a try to use my own Entry Point (I hope it doesn't get too complicated).
Thanks for your time :)
Adding security checks in controller methods could quickly lead to massive code duplication.
Say that I have a few controllers with a few dozens of methods - the code will be almost unmaintainable.
So the question is: does Symphony have something like AOP to deal with cross cutting concerns?
Yo Ivan!
Very good point :). So there are a few things you can do:
1) You will want to keep your security checks very simple - basically one line. In this chapter, we're checking for a ROLE, which is a very simple check. So even if you need to duplicate it, you're basically duplicating one line (in fact, in the latest version of Symfony, it's literally one line: $this->denyAccessUnlessGranted()
). If your security logic is more complex (e.g. you need to check to see if the user is an admin, or if they are the "creator" of some Product to determine if they can edit it), then you'll want to use voters (http://knpuniversity.com/screencast/new-in-symfony3/voter) - this will centralize your logic.
2) If you use voters well, then each controller will only need one line - some version of $this->denyAcessUnlessGranted(...)
. To me, duplicating one line isn't really code duplication. However, you can do even a little bit better, by requiring a role for ALL actions in an entire class. This is done with the @Security annotation - http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/security.html. The examples don't show it (but the docs do mention it): you can put the annotation above your controller class to apply it to all action methods.
Besides that, you can always use access_control
in security.yml, but that has limited flexibility. And to answer your question directly - there is nothing in Symfony specifically for AOP.
Cheers!
Oh, it's interesting, I'll check that voters and controller security annotations. Thanks.
I also found a third way. It's an event system which seems to be the very flexible and looks somewhat like AOP:
http://symfony.com/doc/curr...
How is "throw $this->createAccessDeniedException('message!');" a SHORTCUT to "throw new AccessDeniedException('message!')" ? I mean... come on :P .
Haha :). Yes, but with the AccessDeniedException, you need to add a use statement! That's why :) (and there are 2 AccessDeniedException's in Symfony, which makes it a bit tricky for beginners).
// composer.json
{
"require": {
"php": ">=5.3.3",
"symfony/symfony": "~2.4", // v2.4.2
"doctrine/orm": "~2.2,>=2.2.3", // v2.4.2
"doctrine/doctrine-bundle": "~1.2", // v1.2.0
"twig/extensions": "~1.0", // v1.0.1
"symfony/assetic-bundle": "~2.3", // v2.3.0
"symfony/swiftmailer-bundle": "~2.3", // v2.3.5
"symfony/monolog-bundle": "~2.4", // v2.5.0
"sensio/distribution-bundle": "~2.3", // v2.3.4
"sensio/framework-extra-bundle": "~3.0", // v3.0.0
"sensio/generator-bundle": "~2.3", // v2.3.4
"incenteev/composer-parameter-handler": "~2.0", // v2.1.0
"doctrine/doctrine-fixtures-bundle": "~2.2.0", // v2.2.0
"ircmaxell/password-compat": "~1.0.3", // 1.0.3
"phpunit/phpunit": "~4.1" // 4.1.0
}
}
Hey Weaver!
Like always, great work on this tutorials.
I have a little problem with an automatic redirect by symfony. When I throw that AccessDeniedException to an anonymous user, besides of error page it gets a redirect to Login Page.
Is there a way to disable that redirect or something? I couldn't find a solution on documentation =/
Thanks in advance ;]