If you currently are testing the brand new Symfony2 framework, you might have tried to customize the 403 page. Actually, you have two documented ways to do so, first by overriding the default exception template, second by listening to the onCoreException event and filtering the right exception.

Unfortunately, neither of these two methods satisfied me. The simple customization of the template was not enough for my needs and the addition of an ExceptionListener impose a “maybe” to much complex filtering in order to retrieve just the 403 exception.

Note: Actually, my needs are similar to those I exposed in this blog post on symfony 1.4 : Symfony 1.4 vs. sfGuardUserPlugin : understand credentials and permissions.

What I want to do is to get the unsatisfied requirements and display them to the user.

First secure an action
In order to secure the access to my action with specific requirements, I use the @Secure annotation on my controller’s method. The annotation is provided by the JMSSecurityExtraBundle included in the symfony2 standard edition:

[php]
// /src/MyVendor/MyBundle/Controller/MyController.php

/**
* …
* @extra:Secure(roles="ROLE_MEMBER")
*/
public function newAction()
{
// …
}
[/php]

Here in order to access my action, the user will need to have the role named ROLE_MEMBER. If not, the security layer will throw an Exception resulting in a 403 response.

Change the unauthorized action
You can change the action that will display the 403 page in the security configuration:

[yml]
# /app/config/security.yml
security:
access_denied_url: /unauthorized
[/yml]

Now when dealing with an unauthorized Exception, the security layer will call the action associated to this URL. You need to specify the action that matches this URL in your routing configuration:

[yml]
# /src/MyVendor/MyBundle/Resources/config/routing.yml
unauthorized:
pattern: /unauthorized
defaults: { _controller: FrontendBundle:Default:unauthorized }
[/yml]

Now, the action responsible for rendering a 403 page is the following:

[php]
// /src/MyVendor/MyBundle/Controller/DefaultController.php

class DefaultController extends Controller
{
// …

/**
* @extra:Template()
*/
public function unauthorizedAction()
{
return array();
}
}
[/php]

And the associated template:

[html]
{# /src/MyVendor/MyBundle/Resources/views/Default/unauthorized.html.twig #}

403 error: Unauthorized

[/html]

Display the unmatched requirements
Now, the last things I want to do are to retrieve the required roles of the original request and display them to the user, so that he knows why his request did not succeed.
The tricky now is to obtain the original request, deduce the controller, load its annotations and thus deduce his requirements. Here is what I did:

[php]
// /src/MyVendor/MyBundle/Controller/DefaultController.php

public function unauthorizedAction()
{
// get the previous requests
$requests = $this->container->getCurrentScopedStack(‘request’);

// get the previous controller
$controllerResolver = new SymfonyComponentHttpKernelControllerControllerResolver();
list($controller, $method) = $controllerResolver->getController($requests[‘request’][‘request’]);
// isolate the method
$method = new ReflectionMethod($controller, $method);
// load the metas
$reader = new JMSSecurityExtraBundleMappingDriverAnnotationReader();
$converter = new JMSSecurityExtraBundleMappingDriverAnnotationConverter();
$annotations = $reader->getMethodAnnotations($method);
$metadata = $converter->convertMethodAnnotations($method, $annotations)->getAsArray();
// isolate the required roles
$requiredRoles = $metadata[‘roles’];

return array(‘requiredRoles’ => $requiredRoles);
}
[/php]

The first thing I do is to obtain from the container the request stack that contains the original request. From the request I use the ControllerResolver in order to retrieve the initial controller and then isolate the method. With this method, I can use the AnnotationReader and AnnotationConverter from the JMSSecurityExtraBundle and get back the required roles.
Despite the fact that the method is pretty straightforward, it is not as elegant as I wanted it to be (in the whole process, the controller resolving and annotation extraction will be done twice). But it does the job.

The final thing to do is the show the required roles to the user in the template:

[html]
{# /src/MyVendor/MyBundle/Resources/views/Default/unauthorized.html.twig #}

Required roles:

{% for role in requiredRoles %}
{{ role }}

{% endfor %}

[/html]

11 thoughts on “Advance customization of the 403 error page in Symfony2

  1. Hello,

    I’m trying to customize my “Access denied” page on my site.

    I define the “access_denied_url” parameter in the firewalls part of security.yml. I then define the route and code the controller action with template…

    My problkem is that my page doesn’t extend my layout as it should do (my twig template extends it…).

    Would you have any idea about this issue ?

    Thanks for your reply, and moreover for your blog !

    1. If your twig template extends the layout with the {% extends ‘…’ %} tag at the beginning of the file, there should not be any problem.

      Can you share an extract of your directory arborescence and your twig template ?

      1. My template extends my layout as usual :
        {% extends ‘MyCompanySellerBundle::layout.html.twig’ %}

        The fact is Symfony2 displays my customized 403 page by executing my customized action :

        – Extract of security.yml :

        secured_area:
        pattern: ^
        access_denied_url: /forbidden

        – Extract of my routing.yml :

        forbidden:
        pattern: /forbidden
        defaults: { _controller: MyCompanySellerBundle:Default:forbidden }

        – my forbiddenAction in DefaultController :

        /**
        * Route: _forbidden
        *
        */
        public function forbiddenAction()
        {
        //TODO: Switcher sur le ROLE du user et le renvoyer vers la page correspondante

        $token = $this->get(‘security.context’)->getToken();

        //Si user loggé
        if (isset($token))
        {
        $user = $this->get(‘security.context’)->getToken()->getUser();
        $roles = $user->getRoles();

        switch ($roles[0])
        {
        case User::ROLE_INACTIVE: $mess = ‘Redirection to SignUp form (INACTIVE)’;
        break;
        case User::ROLE_ACTIVE_CC_PROBLEM: $mess = ‘Redirection to CB form (ACTIVE_CC_PROBLEM)’;
        break;
        case User::ROLE_ACTIVE_NO_ACCESS: $mess = ‘Redirection subscription form (ACTIVE_NO_ACCESS)’;
        break;
        default: $mess = ‘Default redirection’;
        break;
        }
        }
        //Sinon, redirection sur login
        else
        {
        return $this->redirect($this->generateUrl(‘_security_logout’));
        }

        return $this->render(‘MyCompanySellerBundle:Default:forbidden.html.twig’, array (‘mess’ => $mess));
        }

        I just discovered that “/forbidden” is not written in my web browser. If I type type it, it’s ok, my layout is loaded, but without it in URL, my CSS are not loaded (the reference link is not ok).

      2. It is normal that your URL does not change because the redirection is done on the server side.

        If I understand well your layout is loading in anycase, it is just that the reference to the css is boken because of the ‘wrong’ url. If so, you might want to check the assetic bundle.

      1. Ok, let me know if you succeed in fixing this issue. The RC1 should come out today or tomorrow.

        Also, you could use a quite strait forward workaround by making a redirection to a dedicated action instead of directly rendering the template. So that you clean the controller/exception stack. It is a bit ugly, but it should do the trick with a less effort.

        Moreover, if the bug manifests to me when upgrading to the RC1 I will surely fix it in the framework and post a fix here and on the git.

  2. Thanks for this; it’s just what I was looking for. I’m loving how much cleaner FOSUserBundle is compared to sfDoctrineGuardPlugin.
    cheers!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s