In the Symfony2 security documentation both the firewalls and the access control rules are demonstrated using the "path" option, which is used to determine if a firewall or rule is applicable to the current URL. Also the "ip" option is demonstrated. The fact of the matter is, the string based configuration options in security.yml are transformed into objects of class RequestMatcher. This is a curious class in the HttpFoundation component which allows you to match a given Request object. The Security component uses it to determine if it should activate a certain firewall for the current request (usually only by checking the request's path info).

In the Security Component's reference an extra option is mentioned (only for firewalls), called "request_matcher". This suggests we can create our own request matcher. And so we can!

Creating an advanced request matcher

A request matcher should implement RequestMatcherInterface, which prescribes just one method, match(). It receives as its only argument the current Request object.

This means, we can match the current request using any information stored in the Request object, like the session, cookies, query parameters, request parameters and server parameters. Lots of opportunities!

For instance, if we would like to activate a firewall only when a certain HTTP header was sent (in the example below: the "X-WSSE" header), we can use the following request matcher:

namespace Matthias\SecurityBundle\Security;

use Symfony\Component\HttpFoundation\RequestMatcherInterface;
use Symfony\Component\HttpFoundation\Request;

class WsseHeaderRequestMatcher implements RequestMatcherInterface
{
    public function matches(Request $request)
    {
        return $request->headers->has('x-wsse');
    }
}

Hook the request matcher in the security configuration

To be able to use the request matcher in the firewall configuration, we have to create a service for it:

<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="matthias_security.wsse_request_matcher"
                 class="Matthias\SecurityBundle\Security\WsseHeaderRequestMatcher">
        </service>
    </services>
</container>

And finally we should add it to the appropriate firewall in security.yml:

security:
    firewalls:
        secured_area:
            request_matcher: matthias_security.wsse_request_matcher

            # we don't use pattern anymore
            #pattern: ^/demo/secured

Configuring access control rules with a custom RequestMatcher

Using a custom request matcher for the access control rules, is a bit more tricky. You need to create a compiler pass for the service container, to modify the security.access_map service definition. The goal is to call the method add() on the AccessMap class, to register the request matcher, its corresponding roles and optionally a required channel (http or https).

First, create the compiler pass:

namespace Matthias\SecurityBundle\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

class AccessControlPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        if (!$container->hasDefinition('security.access_map')) {
            return;
        }

        $requiresChannel = 'https';

        $accessMapDefinition = $container->getDefinition('security.access_map');
        $accessMapDefinition
            ->addMethodCall(
                'add', array(
                    new Reference('matthias_security.wsse_request_matcher'), 
                    array('ROLE_WEBSERVICE'), 
                    $requiresChannel),
                );
    }
}

Register the compiler pass in the build() method of your bundle:

namespace Matthias\SecurityBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Matthias\SecurityBundle\DependencyInjection\Compiler\AccessControlPass;

class MatthiasSecurityBundle extends Bundle
{
    public function build(ContainerBuilder $container)
    {
        parent::build($container);

        $container->addCompilerPass(new AccessControlPass());
    }
}

This is about the easiest it can get. I think a more elegant solution would not be far away, so I might do a pull request some day.

Using request matchers with Silex

When you have registered the SecurityServiceProvider with Silex, you can use a custom request matcher when defining access control rules like this:

use Matthias\SecurityBundle\Security\WsseRequestMatcher;

// ...

$app['wsse_request_matcher'] = new WsseRequestMatcher();

$app['security.access_rules'] = array(
    array($app['wsse_request_matcher'], 'ROLE_WEBSERVICE'),
);

Using the custom request matcher to activate firewalls (like demonstrated above for the Symfony2 case) works like this:

$app['security.firewalls'] = array(
    'secured' => array(
        'pattern' => $app['wsse_request_matcher'],
        // ...
    ),
);
PHP Security Symfony2 compiler pass firewall request service container