feature #11993 [Security] make it possible to override the default success/failure handler (fabpot)

This PR was merged into the 2.6-dev branch.

Discussion
----------

[Security] make it possible to override the default success/failure handler

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #5432, #9272, #10417, #11926
| License       | MIT
| Doc PR        | symfony/symfony-docs#4258

Overriding the default success/failure handler of the security firewalls is possible via the `success_handler` and `failure_handler` setting but this approach is not flexible as it does not allow you to get the options/provider key.

To sum up the problem:

* Overriding the default success/failure handler is possible via a service;
* When not overridden, the default success/failure handler gets options and the provider key;
* Those options and the provider key are injected by the factory as they are dynamic (they depend on the firewall and the provider key), so getting those options/provider key is not possible for a custom service that is only configured via the container configuration;
* Extending the default handler does not help as the injection mechanism is only triggered when no custom provider is set;
* Wrapping the default handler is not possible as the service id is dynamic.

... and of course we need to keep BC and make it work for people extending the default handler but also for people just using the interface.

Instead of the current PR, I propose this slightly different approach. It's not perfect, but given the above constraint, I think this is an acceptable trade-of.

So, several use cases:

 * Using the default handler (no change);
 * Using a custom handler that implements `AuthenticationSuccessHandlerInterface` directly and does not need any options (no change);
 * Using a custom handler that needs the options/provider key (that's the new use case this PR supports).

This PR introduces 2 new classes that wrap custom handlers. If those classes define the `setOptions()` and/or `setProviderKey()` methods, they are automatically called with the correct arguments. Yours handler does not need to extend the default handler `DefaultAuthentication*Handler`, but doing so helps as the setters are already defined there.

Commits
-------

810eeaf [Security] made it possible to override the default success/failure handler (take 2)
36116fc [Security] made it possible to override the default success/failure handler
This commit is contained in:
Fabien Potencier 2014-09-25 16:21:08 +02:00
commit af0aa501e8
8 changed files with 273 additions and 64 deletions

View File

@ -4,6 +4,8 @@ CHANGELOG
2.6.0
-----
* Added the possibility to override the default success/failure handler
to get the provider key and the options injected
* Deprecated the `security.context` service for the `security.token_storage` and
`security.authorization_checker` services.

View File

@ -12,7 +12,6 @@
namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
@ -21,15 +20,16 @@ use Symfony\Component\DependencyInjection\Reference;
* AbstractFactory is the base class for all classes inheriting from
* AbstractAuthenticationListener
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Lukas Kahwe Smith <smith@pooteeweet.org>
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
abstract class AbstractFactory implements SecurityFactoryInterface
{
protected $options = array(
'check_path' => '/login_check',
'use_forward' => false,
'require_previous_session' => true,
'check_path' => '/login_check',
'use_forward' => false,
'require_previous_session' => true,
);
protected $defaultSuccessHandlerOptions = array(
@ -41,10 +41,10 @@ abstract class AbstractFactory implements SecurityFactoryInterface
);
protected $defaultFailureHandlerOptions = array(
'failure_path' => null,
'failure_forward' => false,
'login_path' => '/login',
'failure_path_parameter' => '_failure_path',
'failure_path' => null,
'failure_forward' => false,
'login_path' => '/login',
'failure_path_parameter' => '_failure_path',
);
public function create(ContainerBuilder $container, $id, $config, $userProviderId, $defaultEntryPointId)
@ -170,29 +170,36 @@ abstract class AbstractFactory implements SecurityFactoryInterface
protected function createAuthenticationSuccessHandler($container, $id, $config)
{
if (isset($config['success_handler'])) {
return $config['success_handler'];
}
$successHandlerId = $this->getSuccessHandlerId($id);
$options = array_intersect_key($config, $this->defaultSuccessHandlerOptions);
$successHandler = $container->setDefinition($successHandlerId, new DefinitionDecorator('security.authentication.success_handler'));
$successHandler->replaceArgument(1, array_intersect_key($config, $this->defaultSuccessHandlerOptions));
$successHandler->addMethodCall('setProviderKey', array($id));
if (isset($config['success_handler'])) {
$successHandler = $container->setDefinition($successHandlerId, new DefinitionDecorator('security.authentication.custom_success_handler'));
$successHandler->replaceArgument(0, new Reference($config['success_handler']));
$successHandler->replaceArgument(1, $options);
$successHandler->replaceArgument(2, $id);
} else {
$successHandler = $container->setDefinition($successHandlerId, new DefinitionDecorator('security.authentication.success_handler'));
$successHandler->addMethodCall('setOptions', array($options));
$successHandler->addMethodCall('setProviderKey', array($id));
}
return $successHandlerId;
}
protected function createAuthenticationFailureHandler($container, $id, $config)
{
if (isset($config['failure_handler'])) {
return $config['failure_handler'];
}
$id = $this->getFailureHandlerId($id);
$options = array_intersect_key($config, $this->defaultFailureHandlerOptions);
$failureHandler = $container->setDefinition($id, new DefinitionDecorator('security.authentication.failure_handler'));
$failureHandler->replaceArgument(2, array_intersect_key($config, $this->defaultFailureHandlerOptions));
if (isset($config['failure_handler'])) {
$failureHandler = $container->setDefinition($id, new DefinitionDecorator('security.authentication.custom_failure_handler'));
$failureHandler->replaceArgument(0, new Reference($config['failure_handler']));
$failureHandler->replaceArgument(1, $options);
} else {
$failureHandler = $container->setDefinition($id, new DefinitionDecorator('security.authentication.failure_handler'));
$failureHandler->addMethodCall('setOptions', array($options));
}
return $id;
}

View File

@ -48,6 +48,8 @@
<parameter key="security.authentication.success_handler.class">Symfony\Component\Security\Http\Authentication\DefaultAuthenticationSuccessHandler</parameter>
<parameter key="security.authentication.failure_handler.class">Symfony\Component\Security\Http\Authentication\DefaultAuthenticationFailureHandler</parameter>
<parameter key="security.authentication.custom_success_handler.class">Symfony\Component\Security\Http\Authentication\CustomAuthenticationSuccessHandler</parameter>
<parameter key="security.authentication.custom_failure_handler.class">Symfony\Component\Security\Http\Authentication\CustomAuthenticationFailureHandler</parameter>
<parameter key="security.authentication.simple_success_failure_handler.class">Symfony\Component\Security\Http\Authentication\SimpleAuthenticationHandler</parameter>
</parameters>
@ -123,11 +125,22 @@
<argument type="service" id="event_dispatcher" on-invalid="null" />
</service>
<service id="security.authentication.custom_success_handler" class="%security.authentication.custom_success_handler.class%" abstract="true" public="false">
<argument /> <!-- The custom success handler service id -->
<argument type="collection" /> <!-- Options -->
<argument /> <!-- Provider-shared Key -->
</service>
<service id="security.authentication.success_handler" class="%security.authentication.success_handler.class%" abstract="true" public="false">
<argument type="service" id="security.http_utils" />
<argument type="collection" /> <!-- Options -->
</service>
<service id="security.authentication.custom_failure_handler" class="%security.authentication.custom_failure_handler.class%" abstract="true" public="false">
<argument /> <!-- The custom failure handler service id -->
<argument type="collection" /> <!-- Options -->
</service>
<service id="security.authentication.failure_handler" class="%security.authentication.failure_handler.class%" abstract="true" public="false">
<tag name="monolog.logger" channel="security" />
<argument type="service" id="http_kernel" />

View File

@ -18,11 +18,13 @@ class AbstractFactoryTest extends \PHPUnit_Framework_TestCase
{
public function testCreate()
{
list($container,
$authProviderId,
$listenerId,
$entryPointId
) = $this->callFactory('foo', array('use_forward' => true, 'failure_path' => '/foo', 'success_handler' => 'qux', 'failure_handler' => 'bar', 'remember_me' => true), 'user_provider', 'entry_point');
list($container, $authProviderId, $listenerId, $entryPointId) = $this->callFactory('foo', array(
'use_forward' => true,
'failure_path' => '/foo',
'success_handler' => 'custom_success_handler',
'failure_handler' => 'custom_failure_handler',
'remember_me' => true,
), 'user_provider', 'entry_point');
// auth provider
$this->assertEquals('auth_provider', $authProviderId);
@ -33,10 +35,10 @@ class AbstractFactoryTest extends \PHPUnit_Framework_TestCase
$definition = $container->getDefinition('abstract_listener.foo');
$this->assertEquals(array(
'index_4' => 'foo',
'index_5' => new Reference('qux'),
'index_6' => new Reference('bar'),
'index_5' => new Reference('security.authentication.success_handler.foo.abstract_factory'),
'index_6' => new Reference('security.authentication.failure_handler.foo.abstract_factory'),
'index_7' => array(
'use_forward' => true,
'use_forward' => true,
),
), $definition->getArguments());
@ -44,30 +46,82 @@ class AbstractFactoryTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('entry_point', $entryPointId, '->create() does not change the default entry point.');
}
public function testDefaultFailureHandler()
/**
* @dataProvider getFailureHandlers
*/
public function testDefaultFailureHandler($serviceId, $defaultHandlerInjection)
{
list($container,
$authProviderId,
$listenerId,
$entryPointId
) = $this->callFactory('foo', array('remember_me' => true), 'user_provider', 'entry_point');
$options = array(
'remember_me' => true,
'login_path' => '/bar',
);
if ($serviceId) {
$options['failure_handler'] = $serviceId;
}
list($container, $authProviderId, $listenerId, $entryPointId) = $this->callFactory('foo', $options, 'user_provider', 'entry_point');
$definition = $container->getDefinition('abstract_listener.foo');
$arguments = $definition->getArguments();
$this->assertEquals(new Reference('security.authentication.failure_handler.foo.abstract_factory'), $arguments['index_6']);
$failureHandler = $container->findDefinition((string) $arguments['index_6']);
$methodCalls = $failureHandler->getMethodCalls();
if ($defaultHandlerInjection) {
$this->assertEquals('setOptions', $methodCalls[0][0]);
$this->assertEquals(array('login_path' => '/bar'), $methodCalls[0][1][0]);
} else {
$this->assertCount(0, $methodCalls);
}
}
public function testDefaultSuccessHandler()
public function getFailureHandlers()
{
list($container,
$authProviderId,
$listenerId,
$entryPointId
) = $this->callFactory('foo', array('remember_me' => true), 'user_provider', 'entry_point');
return array(
array(null, true),
array('custom_failure_handler', false),
);
}
/**
* @dataProvider getSuccessHandlers
*/
public function testDefaultSuccessHandler($serviceId, $defaultHandlerInjection)
{
$options = array(
'remember_me' => true,
'default_target_path' => '/bar',
);
if ($serviceId) {
$options['success_handler'] = $serviceId;
}
list($container, $authProviderId, $listenerId, $entryPointId) = $this->callFactory('foo', $options, 'user_provider', 'entry_point');
$definition = $container->getDefinition('abstract_listener.foo');
$arguments = $definition->getArguments();
$this->assertEquals(new Reference('security.authentication.success_handler.foo.abstract_factory'), $arguments['index_5']);
$successHandler = $container->findDefinition((string) $arguments['index_5']);
$methodCalls = $successHandler->getMethodCalls();
if ($defaultHandlerInjection) {
$this->assertEquals('setOptions', $methodCalls[0][0]);
$this->assertEquals(array('default_target_path' => '/bar'), $methodCalls[0][1][0]);
$this->assertEquals('setProviderKey', $methodCalls[1][0]);
$this->assertEquals(array('foo'), $methodCalls[1][1]);
} else {
$this->assertCount(0, $methodCalls);
}
}
public function getSuccessHandlers()
{
return array(
array(null, true),
array('custom_success_handler', false),
);
}
protected function callFactory($id, $config, $userProviderId, $defaultEntryPointId)
@ -92,11 +146,10 @@ class AbstractFactoryTest extends \PHPUnit_Framework_TestCase
$container = new ContainerBuilder();
$container->register('auth_provider');
$container->register('custom_success_handler');
$container->register('custom_failure_handler');
list($authProviderId,
$listenerId,
$entryPointId
) = $factory->create($container, $id, $config, $userProviderId, $defaultEntryPointId);
list($authProviderId, $listenerId, $entryPointId) = $factory->create($container, $id, $config, $userProviderId, $defaultEntryPointId);
return array($container, $authProviderId, $listenerId, $entryPointId);
}

View File

@ -0,0 +1,45 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authentication;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class CustomAuthenticationFailureHandler implements AuthenticationFailureHandlerInterface
{
private $handler;
/**
* Constructor.
*
* @param AuthenticationFailureHandlerInterface $handler An AuthenticationFailureHandlerInterface instance
* @param array $options Options for processing a successful authentication attempt
*/
public function __construct(AuthenticationFailureHandlerInterface $handler, array $options)
{
$this->handler = $handler;
if (method_exists($handler, 'setOptions')) {
$this->handler->setOptions($options);
}
}
/**
* {@inheritdoc}
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
return $this->handler->onAuthenticationFailure($request, $exception);
}
}

View File

@ -0,0 +1,49 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authentication;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandlerInterface
{
private $handler;
/**
* Constructor.
*
* @param AuthenticationSuccessHandlerInterface $handler An AuthenticationFailureHandlerInterface instance
* @param array $options Options for processing a successful authentication attempt
* @param string $providerKey The provider key
*/
public function __construct(AuthenticationSuccessHandlerInterface $handler, array $options, $providerKey)
{
$this->handler = $handler;
if (method_exists($handler, 'setOptions')) {
$this->handler->setOptions($options);
}
if (method_exists($providerKey, 'setProviderKey')) {
$this->handler->setProviderKey($providerKey);
}
}
/**
* {@inheritdoc}
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token)
{
return $this->handler->onAuthenticationSuccess($request, $token);
}
}

View File

@ -34,6 +34,12 @@ class DefaultAuthenticationFailureHandler implements AuthenticationFailureHandle
protected $httpUtils;
protected $logger;
protected $options;
protected $defaultOptions = array(
'failure_path' => null,
'failure_forward' => false,
'login_path' => '/login',
'failure_path_parameter' => '_failure_path',
);
/**
* Constructor.
@ -43,18 +49,32 @@ class DefaultAuthenticationFailureHandler implements AuthenticationFailureHandle
* @param array $options Options for processing a failed authentication attempt.
* @param LoggerInterface $logger Optional logger
*/
public function __construct(HttpKernelInterface $httpKernel, HttpUtils $httpUtils, array $options, LoggerInterface $logger = null)
public function __construct(HttpKernelInterface $httpKernel, HttpUtils $httpUtils, array $options = array(), LoggerInterface $logger = null)
{
$this->httpKernel = $httpKernel;
$this->httpUtils = $httpUtils;
$this->logger = $logger;
$this->httpUtils = $httpUtils;
$this->logger = $logger;
$this->setOptions($options);
}
$this->options = array_merge(array(
'failure_path' => null,
'failure_forward' => false,
'login_path' => '/login',
'failure_path_parameter' => '_failure_path',
), $options);
/**
* Gets the options.
*
* @return array An array of options
*/
public function getOptions()
{
return $this->options;
}
/**
* Sets the options.
*
* @param array $options An array of options
*/
public function setOptions(array $options)
{
$this->options = array_merge($this->defaultOptions, $options);
}
/**

View File

@ -27,6 +27,13 @@ class DefaultAuthenticationSuccessHandler implements AuthenticationSuccessHandle
protected $httpUtils;
protected $options;
protected $providerKey;
protected $defaultOptions = array(
'always_use_default_target_path' => false,
'default_target_path' => '/',
'login_path' => '/login',
'target_path_parameter' => '_target_path',
'use_referer' => false,
);
/**
* Constructor.
@ -34,17 +41,10 @@ class DefaultAuthenticationSuccessHandler implements AuthenticationSuccessHandle
* @param HttpUtils $httpUtils
* @param array $options Options for processing a successful authentication attempt.
*/
public function __construct(HttpUtils $httpUtils, array $options)
public function __construct(HttpUtils $httpUtils, array $options = array())
{
$this->httpUtils = $httpUtils;
$this->options = array_merge(array(
'always_use_default_target_path' => false,
'default_target_path' => '/',
'login_path' => '/login',
'target_path_parameter' => '_target_path',
'use_referer' => false,
), $options);
$this->httpUtils = $httpUtils;
$this->setOptions($options);
}
/**
@ -55,6 +55,26 @@ class DefaultAuthenticationSuccessHandler implements AuthenticationSuccessHandle
return $this->httpUtils->createRedirectResponse($request, $this->determineTargetUrl($request));
}
/**
* Gets the options.
*
* @return array An array of options
*/
public function getOptions()
{
return $this->options;
}
/**
* Sets the options.
*
* @param array $options An array of options
*/
public function setOptions(array $options)
{
$this->options = array_merge($this->defaultOptions, $options);
}
/**
* Get the provider key.
*