Symfony 4 was released on November 30th.
Update now to the best Symfony ever!

How to Secure any Service or Method in your Application

How to Secure any Service or Method in your Application

In the security article, you can see how to secure a controller by requesting the security.authorization_checker service from the Service Container and checking the current user's role:

1
2
3
4
5
6
7
8
9
// ...
use Symfony\Component\Security\Core\Exception\AccessDeniedException;

public function helloAction($name)
{
    $this->denyAccessUnlessGranted('ROLE_ADMIN');

    // ...
}

You can also secure any service by injecting the security.authorization_checker service into it. For a general introduction to injecting dependencies into services see the Service Container article. For example, suppose you have a NewsletterManager class that sends out emails and you want to restrict its use to only users who have some ROLE_NEWSLETTER_ADMIN role. Before you add security, the class looks something like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// src/AppBundle/Newsletter/NewsletterManager.php
namespace AppBundle\Newsletter;

class NewsletterManager
{
    public function sendNewsletter()
    {
        // ... where you actually do the work
    }

    // ...
}

Your goal is to check the user's role when the sendNewsletter() method is called. The first step towards this is to inject the security.authorization_checker service into the object. Since it won't make sense not to perform the security check, this is an ideal candidate for constructor injection, which guarantees that the authorization checker object will be available inside the NewsletterManager class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// src/AppBundle/Newsletter/NewsletterManager.php

// ...
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;

class NewsletterManager
{
    protected $authorizationChecker;

    public function __construct(AuthorizationCheckerInterface $authorizationChecker)
    {
        $this->authorizationChecker = $authorizationChecker;
    }

    // ...
}

Then in your service configuration, you can inject the service:

  • YAML
    1
    2
    3
    4
    5
    # app/config/services.yml
    services:
        newsletter_manager:
            class:     AppBundle\Newsletter\NewsletterManager
            arguments: ['@security.authorization_checker']
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    <!-- app/config/services.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="newsletter_manager" class="AppBundle\Newsletter\NewsletterManager">
                <argument type="service" id="security.authorization_checker"/>
            </service>
        </services>
    </container>
    
  • PHP
    1
    2
    3
    4
    5
    6
    // app/config/services.php
    use AppBundle\Newsletter\NewsletterManager;
    use Symfony\Component\DependencyInjection\Reference;
    
    $container->register('newsletter_manager', NewsletterManager::class)
        ->addArgument(new Reference('security.authorization_checker'));
    

The injected service can then be used to perform the security check when the sendNewsletter() method is called:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
namespace AppBundle\Newsletter;

use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
// ...

class NewsletterManager
{
    protected $authorizationChecker;

    public function __construct(AuthorizationCheckerInterface $authorizationChecker)
    {
        $this->authorizationChecker = $authorizationChecker;
    }

    public function sendNewsletter()
    {
        if (false === $this->authorizationChecker->isGranted('ROLE_NEWSLETTER_ADMIN')) {
            throw new AccessDeniedException();
        }

        // ...
    }

    // ...
}

If the current user does not have the ROLE_NEWSLETTER_ADMIN, they will be prompted to log in.

Securing Methods Using Annotations

You can also secure method calls in any service with annotations by using the optional JMSSecurityExtraBundle bundle. This bundle is not included in the Symfony Standard Distribution, but you can choose to install it.

To enable the annotations functionality, tag the service you want to secure with the security.secure_service tag (you can also automatically enable this functionality for all services, see the sidebar below):

  • YAML
    1
    2
    3
    4
    5
    6
    # app/config/services.yml
    services:
        newsletter_manager:
            class: AppBundle\Newsletter\NewsletterManager
            tags:
                - { name: security.secure_service }
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    <!-- app/config/services.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="newsletter_manager" class="AppBundle\Newsletter\NewsletterManager">
                <tag name="security.secure_service" />
            </service>
        </services>
    </container>
    
  • PHP
    1
    2
    3
    4
    5
    6
    // app/config/services.php
    use AppBundle\Newsletter\NewsletterManager;
    use Symfony\Component\DependencyInjection\Reference;
    
    $container->register('newsletter_manager', NewsletterManager::class)
        ->addTag('security.secure_service');
    

You can then achieve the same results as above using an annotation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
namespace AppBundle\Newsletter;

use JMS\SecurityExtraBundle\Annotation\Secure;
// ...

class NewsletterManager
{

    /**
     * @Secure(roles="ROLE_NEWSLETTER_ADMIN")
     */
    public function sendNewsletter()
    {
        // ...
    }

    // ...
}

Note

The annotations work because a proxy class is created for your class which performs the security checks. This means that, whilst you can use annotations on public and protected methods, you cannot use them with private methods or methods marked final.

The JMSSecurityExtraBundle also allows you to secure the parameters and return values of methods. For more information, see the JMSSecurityExtraBundle documentation.

When securing the method of a service (as shown above), you can either tag each service individually, or activate the functionality for all services at once. To do so, set the secure_all_services configuration option to true:

  • YAML
    1
    2
    3
    4
    # app/config/config.yml
    jms_security_extra:
        # ...
        secure_all_services: true
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    <!-- app/config/config.xml -->
    <?xml version="1.0" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:jms-security-extra="http://example.org/schema/dic/jms_security_extra"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <!-- ... -->
        <jms-security-extra:config secure-all-services="true" />
    </container>
    
  • PHP
    1
    2
    3
    4
    5
    // app/config/config.php
    $container->loadFromExtension('jms_security_extra', array(
        // ...
        'secure_all_services' => true,
    ));
    

The disadvantage of this method is that, if activated, the initial page load may be very slow depending on how many services you have defined.

This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.