feature #24260 [Security] Add impersonation support for stateless authentication (chalasr)
This PR was merged into the 3.4 branch.
Discussion
----------
[Security] Add impersonation support for stateless authentication
| Q | A
| ------------- | ---
| Branch? | 3.4
| Bug fix? | no
| New feature? | yes
| BC breaks? | no
| Deprecations? | no
| Tests pass? | yes
| Fixed tickets | https://github.com/lafourchette/SwitchUserStatelessBundle/issues/10#issuecomment-330434589
| License | MIT
| Doc PR | n/a
The `switch_user` listener triggers a redirection in case of success and thus does not play well with stateless authentication which is common nowadays (as opposed to other listeners like the [exception one](https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php#L187..#L189)).
This adds a new `stateless` option to the `switch_user` listener, if set to true then no redirection is triggered during user switching.
This will avoid the need for [lafourchette/SwitchUserStatelessBundle](https://github.com/lafourchette/SwitchUserStatelessBundle) which just duplicated the symfony SwitchUserListener (with config factory) at a given state to avoid the 2 LOC which are causing the redirection.
The bundle is not actively maintained and the listener it provides is out of date due to the missing upstream additions and bug fixes (see https://github.com/lafourchette/SwitchUserStatelessBundle/issues/10).
Commits
-------
e7a5803e2e
[Security] Add user impersonation support for stateless authentication
This commit is contained in:
commit
b1e2d21213
@ -316,6 +316,9 @@ SecurityBundle
|
|||||||
|
|
||||||
* Deprecated the HTTP digest authentication: `HttpDigestFactory` will be removed in 4.0.
|
* Deprecated the HTTP digest authentication: `HttpDigestFactory` will be removed in 4.0.
|
||||||
Use another authentication system like `http_basic` instead.
|
Use another authentication system like `http_basic` instead.
|
||||||
|
|
||||||
|
* Deprecated setting the `switch_user.stateless` option to false when the firewall is `stateless`.
|
||||||
|
Setting it to false will have no effect in 4.0.
|
||||||
|
|
||||||
Translation
|
Translation
|
||||||
-----------
|
-----------
|
||||||
|
@ -693,6 +693,8 @@ SecurityBundle
|
|||||||
|
|
||||||
* Removed the HTTP digest authentication system. The `HttpDigestFactory` class
|
* Removed the HTTP digest authentication system. The `HttpDigestFactory` class
|
||||||
has been removed. Use another authentication system like `http_basic` instead.
|
has been removed. Use another authentication system like `http_basic` instead.
|
||||||
|
|
||||||
|
* The `switch_user.stateless` option is now always true if the firewall is stateless.
|
||||||
|
|
||||||
Serializer
|
Serializer
|
||||||
----------
|
----------
|
||||||
|
@ -17,6 +17,7 @@ CHANGELOG
|
|||||||
* deprecated command `acl:set` along with `SetAclCommand` class
|
* deprecated command `acl:set` along with `SetAclCommand` class
|
||||||
* deprecated command `init:acl` along with `InitAclCommand` class
|
* deprecated command `init:acl` along with `InitAclCommand` class
|
||||||
* Added support for the new Argon2i password encoder
|
* Added support for the new Argon2i password encoder
|
||||||
|
* added `stateless` option to the `switch_user` listener
|
||||||
|
|
||||||
3.3.0
|
3.3.0
|
||||||
-----
|
-----
|
||||||
|
@ -304,6 +304,7 @@ class MainConfiguration implements ConfigurationInterface
|
|||||||
->scalarNode('provider')->end()
|
->scalarNode('provider')->end()
|
||||||
->scalarNode('parameter')->defaultValue('_switch_user')->end()
|
->scalarNode('parameter')->defaultValue('_switch_user')->end()
|
||||||
->scalarNode('role')->defaultValue('ROLE_ALLOWED_TO_SWITCH')->end()
|
->scalarNode('role')->defaultValue('ROLE_ALLOWED_TO_SWITCH')->end()
|
||||||
|
->booleanNode('stateless')->defaultValue(false)->end()
|
||||||
->end()
|
->end()
|
||||||
->end()
|
->end()
|
||||||
;
|
;
|
||||||
|
@ -456,7 +456,7 @@ class SecurityExtension extends Extension
|
|||||||
// Switch user listener
|
// Switch user listener
|
||||||
if (isset($firewall['switch_user'])) {
|
if (isset($firewall['switch_user'])) {
|
||||||
$listenerKeys[] = 'switch_user';
|
$listenerKeys[] = 'switch_user';
|
||||||
$listeners[] = new Reference($this->createSwitchUserListener($container, $id, $firewall['switch_user'], $defaultProvider));
|
$listeners[] = new Reference($this->createSwitchUserListener($container, $id, $firewall['switch_user'], $defaultProvider, $firewall['stateless']));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Access listener
|
// Access listener
|
||||||
@ -699,10 +699,15 @@ class SecurityExtension extends Extension
|
|||||||
return $exceptionListenerId;
|
return $exceptionListenerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createSwitchUserListener($container, $id, $config, $defaultProvider)
|
private function createSwitchUserListener($container, $id, $config, $defaultProvider, $stateless)
|
||||||
{
|
{
|
||||||
$userProvider = isset($config['provider']) ? $this->getUserProviderId($config['provider']) : $defaultProvider;
|
$userProvider = isset($config['provider']) ? $this->getUserProviderId($config['provider']) : $defaultProvider;
|
||||||
|
|
||||||
|
// in 4.0, ignore the `switch_user.stateless` key if $stateless is `true`
|
||||||
|
if ($stateless && false === $config['stateless']) {
|
||||||
|
@trigger_error(sprintf('Firewall "%s" is configured as "stateless" but the "switch_user.stateless" key is set to false. Both should have the same value, the firewall\'s "stateless" value will be used as default value for the "switch_user.stateless" key in 4.0.', $id), E_USER_DEPRECATED);
|
||||||
|
}
|
||||||
|
|
||||||
$switchUserListenerId = 'security.authentication.switchuser_listener.'.$id;
|
$switchUserListenerId = 'security.authentication.switchuser_listener.'.$id;
|
||||||
$listener = $container->setDefinition($switchUserListenerId, new ChildDefinition('security.authentication.switchuser_listener'));
|
$listener = $container->setDefinition($switchUserListenerId, new ChildDefinition('security.authentication.switchuser_listener'));
|
||||||
$listener->replaceArgument(1, new Reference($userProvider));
|
$listener->replaceArgument(1, new Reference($userProvider));
|
||||||
@ -710,6 +715,7 @@ class SecurityExtension extends Extension
|
|||||||
$listener->replaceArgument(3, $id);
|
$listener->replaceArgument(3, $id);
|
||||||
$listener->replaceArgument(6, $config['parameter']);
|
$listener->replaceArgument(6, $config['parameter']);
|
||||||
$listener->replaceArgument(7, $config['role']);
|
$listener->replaceArgument(7, $config['role']);
|
||||||
|
$listener->replaceArgument(9, $config['stateless']);
|
||||||
|
|
||||||
return $switchUserListenerId;
|
return $switchUserListenerId;
|
||||||
}
|
}
|
||||||
|
@ -241,6 +241,7 @@
|
|||||||
<argument>_switch_user</argument>
|
<argument>_switch_user</argument>
|
||||||
<argument>ROLE_ALLOWED_TO_SWITCH</argument>
|
<argument>ROLE_ALLOWED_TO_SWITCH</argument>
|
||||||
<argument type="service" id="event_dispatcher" on-invalid="null"/>
|
<argument type="service" id="event_dispatcher" on-invalid="null"/>
|
||||||
|
<argument>false</argument> <!-- Stateless -->
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<service id="security.access_listener" class="Symfony\Component\Security\Http\Firewall\AccessListener">
|
<service id="security.access_listener" class="Symfony\Component\Security\Http\Firewall\AccessListener">
|
||||||
|
@ -130,6 +130,7 @@ abstract class CompleteConfigurationTest extends TestCase
|
|||||||
array(
|
array(
|
||||||
'parameter' => '_switch_user',
|
'parameter' => '_switch_user',
|
||||||
'role' => 'ROLE_ALLOWED_TO_SWITCH',
|
'role' => 'ROLE_ALLOWED_TO_SWITCH',
|
||||||
|
'stateless' => true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
array(
|
array(
|
||||||
@ -256,6 +257,7 @@ abstract class CompleteConfigurationTest extends TestCase
|
|||||||
array(
|
array(
|
||||||
'parameter' => '_switch_user',
|
'parameter' => '_switch_user',
|
||||||
'role' => 'ROLE_ALLOWED_TO_SWITCH',
|
'role' => 'ROLE_ALLOWED_TO_SWITCH',
|
||||||
|
'stateless' => true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
array(
|
array(
|
||||||
|
@ -65,7 +65,7 @@ $container->loadFromExtension('security', array(
|
|||||||
'http_basic' => true,
|
'http_basic' => true,
|
||||||
'form_login' => true,
|
'form_login' => true,
|
||||||
'anonymous' => true,
|
'anonymous' => true,
|
||||||
'switch_user' => true,
|
'switch_user' => array('stateless' => true),
|
||||||
'x509' => true,
|
'x509' => true,
|
||||||
'remote_user' => true,
|
'remote_user' => true,
|
||||||
'logout' => true,
|
'logout' => true,
|
||||||
|
@ -67,7 +67,7 @@ $container->loadFromExtension('security', array(
|
|||||||
'http_digest' => array('secret' => 'TheSecret'),
|
'http_digest' => array('secret' => 'TheSecret'),
|
||||||
'form_login' => true,
|
'form_login' => true,
|
||||||
'anonymous' => true,
|
'anonymous' => true,
|
||||||
'switch_user' => true,
|
'switch_user' => array('stateless' => true),
|
||||||
'x509' => true,
|
'x509' => true,
|
||||||
'remote_user' => true,
|
'remote_user' => true,
|
||||||
'logout' => true,
|
'logout' => true,
|
||||||
|
@ -67,7 +67,7 @@ $container->loadFromExtension('security', array(
|
|||||||
'http_digest' => array('secret' => 'TheSecret'),
|
'http_digest' => array('secret' => 'TheSecret'),
|
||||||
'form_login' => true,
|
'form_login' => true,
|
||||||
'anonymous' => true,
|
'anonymous' => true,
|
||||||
'switch_user' => true,
|
'switch_user' => array('stateless' => true),
|
||||||
'x509' => true,
|
'x509' => true,
|
||||||
'remote_user' => true,
|
'remote_user' => true,
|
||||||
'logout' => true,
|
'logout' => true,
|
||||||
|
@ -17,7 +17,7 @@ $container->loadFromExtension('security', array(
|
|||||||
'http_basic' => true,
|
'http_basic' => true,
|
||||||
'form_login' => true,
|
'form_login' => true,
|
||||||
'anonymous' => true,
|
'anonymous' => true,
|
||||||
'switch_user' => true,
|
'switch_user' => array('stateless' => true),
|
||||||
'x509' => true,
|
'x509' => true,
|
||||||
'remote_user' => true,
|
'remote_user' => true,
|
||||||
'logout' => true,
|
'logout' => true,
|
||||||
|
@ -49,7 +49,7 @@
|
|||||||
<http-basic />
|
<http-basic />
|
||||||
<form-login />
|
<form-login />
|
||||||
<anonymous />
|
<anonymous />
|
||||||
<switch-user />
|
<switch-user stateless="true" />
|
||||||
<x509 />
|
<x509 />
|
||||||
<remote-user />
|
<remote-user />
|
||||||
<user-checker />
|
<user-checker />
|
||||||
|
@ -51,7 +51,7 @@
|
|||||||
<http-digest secret="TheSecret" />
|
<http-digest secret="TheSecret" />
|
||||||
<form-login />
|
<form-login />
|
||||||
<anonymous />
|
<anonymous />
|
||||||
<switch-user />
|
<switch-user stateless="true" />
|
||||||
<x509 />
|
<x509 />
|
||||||
<remote-user />
|
<remote-user />
|
||||||
<user-checker />
|
<user-checker />
|
||||||
|
@ -52,7 +52,7 @@
|
|||||||
<http-digest secret="TheSecret" />
|
<http-digest secret="TheSecret" />
|
||||||
<form-login />
|
<form-login />
|
||||||
<anonymous />
|
<anonymous />
|
||||||
<switch-user />
|
<switch-user stateless="true" />
|
||||||
<x509 />
|
<x509 />
|
||||||
<remote-user />
|
<remote-user />
|
||||||
<user-checker />
|
<user-checker />
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
<http-basic />
|
<http-basic />
|
||||||
<form-login />
|
<form-login />
|
||||||
<anonymous />
|
<anonymous />
|
||||||
<switch-user />
|
<switch-user stateless="true" />
|
||||||
<x509 />
|
<x509 />
|
||||||
<remote-user />
|
<remote-user />
|
||||||
<user-checker />
|
<user-checker />
|
||||||
|
@ -47,7 +47,8 @@ security:
|
|||||||
http_basic: true
|
http_basic: true
|
||||||
form_login: true
|
form_login: true
|
||||||
anonymous: true
|
anonymous: true
|
||||||
switch_user: true
|
switch_user:
|
||||||
|
stateless: true
|
||||||
x509: true
|
x509: true
|
||||||
remote_user: true
|
remote_user: true
|
||||||
logout: true
|
logout: true
|
||||||
|
@ -50,7 +50,8 @@ security:
|
|||||||
secret: TheSecret
|
secret: TheSecret
|
||||||
form_login: true
|
form_login: true
|
||||||
anonymous: true
|
anonymous: true
|
||||||
switch_user: true
|
switch_user:
|
||||||
|
stateless: true
|
||||||
x509: true
|
x509: true
|
||||||
remote_user: true
|
remote_user: true
|
||||||
logout: true
|
logout: true
|
||||||
|
@ -50,7 +50,8 @@ security:
|
|||||||
secret: TheSecret
|
secret: TheSecret
|
||||||
form_login: true
|
form_login: true
|
||||||
anonymous: true
|
anonymous: true
|
||||||
switch_user: true
|
switch_user:
|
||||||
|
stateless: true
|
||||||
x509: true
|
x509: true
|
||||||
remote_user: true
|
remote_user: true
|
||||||
logout: true
|
logout: true
|
||||||
|
@ -12,7 +12,8 @@ security:
|
|||||||
http_basic: true
|
http_basic: true
|
||||||
form_login: true
|
form_login: true
|
||||||
anonymous: true
|
anonymous: true
|
||||||
switch_user: true
|
switch_user:
|
||||||
|
stateless: true
|
||||||
x509: true
|
x509: true
|
||||||
remote_user: true
|
remote_user: true
|
||||||
logout: true
|
logout: true
|
||||||
|
@ -148,6 +148,32 @@ class SecurityExtensionTest extends TestCase
|
|||||||
$container->compile();
|
$container->compile();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @group legacy
|
||||||
|
* @expectedDeprecation Firewall "some_firewall" is configured as "stateless" but the "switch_user.stateless" key is set to false. Both should have the same value, the firewall's "stateless" value will be used as default value for the "switch_user.stateless" key in 4.0.
|
||||||
|
*/
|
||||||
|
public function testSwitchUserNotStatelessOnStatelessFirewall()
|
||||||
|
{
|
||||||
|
$container = $this->getRawContainer();
|
||||||
|
|
||||||
|
$container->loadFromExtension('security', array(
|
||||||
|
'providers' => array(
|
||||||
|
'default' => array('id' => 'foo'),
|
||||||
|
),
|
||||||
|
|
||||||
|
'firewalls' => array(
|
||||||
|
'some_firewall' => array(
|
||||||
|
'stateless' => true,
|
||||||
|
'http_basic' => null,
|
||||||
|
'switch_user' => array('stateless' => false),
|
||||||
|
'logout_on_user_change' => true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
$container->compile();
|
||||||
|
}
|
||||||
|
|
||||||
protected function getRawContainer()
|
protected function getRawContainer()
|
||||||
{
|
{
|
||||||
$container = new ContainerBuilder();
|
$container = new ContainerBuilder();
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
namespace Symfony\Bundle\SecurityBundle\Tests\Functional;
|
namespace Symfony\Bundle\SecurityBundle\Tests\Functional;
|
||||||
|
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\Security\Http\Firewall\SwitchUserListener;
|
use Symfony\Component\Security\Http\Firewall\SwitchUserListener;
|
||||||
|
|
||||||
class SwitchUserTest extends WebTestCase
|
class SwitchUserTest extends WebTestCase
|
||||||
@ -50,6 +51,18 @@ class SwitchUserTest extends WebTestCase
|
|||||||
$this->assertEquals('user_can_switch', $client->getProfile()->getCollector('security')->getUser());
|
$this->assertEquals('user_can_switch', $client->getProfile()->getCollector('security')->getUser());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testSwitchUserStateless()
|
||||||
|
{
|
||||||
|
$client = $this->createClient(array('test_case' => 'JsonLogin', 'root_config' => 'switchuser_stateless.yml'));
|
||||||
|
$client->request('POST', '/chk', array('_switch_user' => 'dunglas'), array(), array('CONTENT_TYPE' => 'application/json'), '{"user": {"login": "user_can_switch", "password": "test"}}');
|
||||||
|
$response = $client->getResponse();
|
||||||
|
|
||||||
|
$this->assertInstanceOf(JsonResponse::class, $response);
|
||||||
|
$this->assertSame(200, $response->getStatusCode());
|
||||||
|
$this->assertSame(array('message' => 'Welcome @dunglas!'), json_decode($response->getContent(), true));
|
||||||
|
$this->assertSame('dunglas', $client->getProfile()->getCollector('security')->getUser());
|
||||||
|
}
|
||||||
|
|
||||||
public function getTestParameters()
|
public function getTestParameters()
|
||||||
{
|
{
|
||||||
return array(
|
return array(
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
imports:
|
||||||
|
- { resource: ./config.yml }
|
||||||
|
|
||||||
|
security:
|
||||||
|
providers:
|
||||||
|
in_memory:
|
||||||
|
memory:
|
||||||
|
users:
|
||||||
|
user_can_switch: { password: test, roles: [ROLE_USER, ROLE_ALLOWED_TO_SWITCH] }
|
||||||
|
firewalls:
|
||||||
|
main:
|
||||||
|
switch_user:
|
||||||
|
stateless: true
|
@ -49,8 +49,9 @@ class SwitchUserListener implements ListenerInterface
|
|||||||
private $role;
|
private $role;
|
||||||
private $logger;
|
private $logger;
|
||||||
private $dispatcher;
|
private $dispatcher;
|
||||||
|
private $stateless;
|
||||||
|
|
||||||
public function __construct(TokenStorageInterface $tokenStorage, UserProviderInterface $provider, UserCheckerInterface $userChecker, $providerKey, AccessDecisionManagerInterface $accessDecisionManager, LoggerInterface $logger = null, $usernameParameter = '_switch_user', $role = 'ROLE_ALLOWED_TO_SWITCH', EventDispatcherInterface $dispatcher = null)
|
public function __construct(TokenStorageInterface $tokenStorage, UserProviderInterface $provider, UserCheckerInterface $userChecker, $providerKey, AccessDecisionManagerInterface $accessDecisionManager, LoggerInterface $logger = null, $usernameParameter = '_switch_user', $role = 'ROLE_ALLOWED_TO_SWITCH', EventDispatcherInterface $dispatcher = null, $stateless = false)
|
||||||
{
|
{
|
||||||
if (empty($providerKey)) {
|
if (empty($providerKey)) {
|
||||||
throw new \InvalidArgumentException('$providerKey must not be empty.');
|
throw new \InvalidArgumentException('$providerKey must not be empty.');
|
||||||
@ -65,6 +66,7 @@ class SwitchUserListener implements ListenerInterface
|
|||||||
$this->role = $role;
|
$this->role = $role;
|
||||||
$this->logger = $logger;
|
$this->logger = $logger;
|
||||||
$this->dispatcher = $dispatcher;
|
$this->dispatcher = $dispatcher;
|
||||||
|
$this->stateless = $stateless;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -92,12 +94,13 @@ class SwitchUserListener implements ListenerInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$request->query->remove($this->usernameParameter);
|
if (!$this->stateless) {
|
||||||
$request->server->set('QUERY_STRING', http_build_query($request->query->all()));
|
$request->query->remove($this->usernameParameter);
|
||||||
|
$request->server->set('QUERY_STRING', http_build_query($request->query->all()));
|
||||||
|
$response = new RedirectResponse($request->getUri(), 302);
|
||||||
|
|
||||||
$response = new RedirectResponse($request->getUri(), 302);
|
$event->setResponse($response);
|
||||||
|
}
|
||||||
$event->setResponse($response);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -266,4 +266,29 @@ class SwitchUserListenerTest extends TestCase
|
|||||||
|
|
||||||
$this->assertSame($replacedToken, $this->tokenStorage->getToken());
|
$this->assertSame($replacedToken, $this->tokenStorage->getToken());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testSwitchUserStateless()
|
||||||
|
{
|
||||||
|
$token = new UsernamePasswordToken('username', '', 'key', array('ROLE_FOO'));
|
||||||
|
$user = new User('username', 'password', array());
|
||||||
|
|
||||||
|
$this->tokenStorage->setToken($token);
|
||||||
|
$this->request->query->set('_switch_user', 'kuba');
|
||||||
|
|
||||||
|
$this->accessDecisionManager->expects($this->once())
|
||||||
|
->method('decide')->with($token, array('ROLE_ALLOWED_TO_SWITCH'))
|
||||||
|
->will($this->returnValue(true));
|
||||||
|
|
||||||
|
$this->userProvider->expects($this->once())
|
||||||
|
->method('loadUserByUsername')->with('kuba')
|
||||||
|
->will($this->returnValue($user));
|
||||||
|
$this->userChecker->expects($this->once())
|
||||||
|
->method('checkPostAuth')->with($user);
|
||||||
|
|
||||||
|
$listener = new SwitchUserListener($this->tokenStorage, $this->userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager, null, '_switch_user', 'ROLE_ALLOWED_TO_SWITCH', null, true);
|
||||||
|
$listener->handle($this->event);
|
||||||
|
|
||||||
|
$this->assertInstanceOf('Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken', $this->tokenStorage->getToken());
|
||||||
|
$this->assertFalse($this->event->hasResponse());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user