Symfony2 Security: Using advanced Request matchers to activate firewalls
Matthias Noback
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="https://symfony.com/schema/dic/services"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://symfony.com/schema/dic/services https://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'],
// ...
),
);