8000 feature #14673 New Guard Authentication System (e.g. putting the joy … · symfony/symfony@5f2acfd · GitHub
[go: up one dir, main page]

Skip to content

Commit 5f2acfd

Browse files
committed
feature #14673 New Guard Authentication System (e.g. putting the joy back into security) (weaverryan)
This PR was merged into the 2.8 branch. Discussion ---------- New Guard Authentication System (e.g. putting the joy back into security) | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | at least partially: #14300, #11158, #11451, #10035, #10463, #8606, probably more | License | MIT | Doc PR | symfony/symfony-docs#5265 Hi guys! Though it got much easier in 2.4 with `pre_auth`, authentication is a pain in Symfony. This introduces a new authentication provider called guard, with one goal in mind: put everything you need for *any* authentication system into one spot. ### How it works With guard, you can perform custom authentication just by implementing the [GuardAuthenticatorInterface](https://github.com/weaverryan/symfony/blob/guard/src/Symfony/Component/Security/Guard/GuardAuthenticatorInterface.php) and registering it as a service. It has methods for every part of a custom authentication flow I can think of. For a working example, see https://github.com/weaverryan/symfony-demo/tree/guard-auth. This uses 2 authenticators simultaneously, creating a system that handles [form login](https://github.com/weaverryan/symfony-demo/b 8000 lob/guard-auth/src/AppBundle/Security/FormLoginAuthenticator.php) and [api token auth](https://github.com/weaverryan/symfony-demo/blob/guard-auth/src/AppBundle/Security/TokenAuthenticator.php) with a respectable amount of code. The [security.yml](https://github.com/weaverryan/symfony-demo/blob/guard-auth/app/config/security.yml) is also quite simple. This also supports "manual login" without jumping through hoops: https://github.com/weaverryan/symfony-demo/blob/guard-auth/src/AppBundle/Controller/SecurityController.php#L45 I've also tested with "remember me" and "switch user" - no problems with either. I hope you like it :). ### What's Needed 1) **Other Use-Cases?**: Please think about the code and try it. What use-cases are we *not* covering? I want Guard to be simple, but cover the 99.9% use-cases. 2) **Remember me** functionality cannot be triggered via manual login. That's true now, and it's not fixed, and it's tricky. ### Deprecations? This is a new feature, so no deprecations. But, creating a login form with a guard authenticator is a whole heck of a lot easier to understand than `form_login` or even `simple_form`. In a perfect world, we'd either deprecate those or make them use "guard" internally so that we have just **one** way of performing authentication. Thanks! Commits ------- a01ed35 Adding the necessary files so that Guard can be its own installable component d763134 Removing unnecessary override e353833 fabbot dd485f4 Adding a new exception and throwing it when the User changes 302235e Fixing a bug where having an authentication failure would log you out. 396a162 Tweaks thanks to Wouter c9d9430 Adding logging on this step and switching the order - not for any huge reason 31f9cae Adding a base class to assist with form login authentication 0501761 Allowing for other authenticators to be checked 293c8a1 meaningless author and license changes 81432f9 Adding missing factory registration 7a94994 Thanks again fabbot! 7de05be A few more changes thanks to @iltar ffdbc66 Splitting the getting of the user and checking credentials into two steps 6edb9e1 Tweaking docblock on interface thanks to @iltar d693721 Adding periods at the end of exceptions, and changing one class name to LogicException thanks to @iltar eb158cb Updating interface method per suggestion - makes sense to me, Request is redundant c73c32e Thanks fabbot! 6c180c7 Adding an edge case - this should not happen anyways 180e2c7 Properly handles "post auth" tokens that have become not authenticated 873ed28 Renaming the tokens to be clear they are "post" and "pre" auth - also adding an interface a0bceb4 adding Guard tests 05af97c Initial commit (but after some polished work) of the new Guard authentication system 330aa7f Improving phpdoc on AuthenticationEntryPointInterface so people that implement this understand it
2 parents 5b8b429 + a01ed35 commit 5f2acfd

25 files changed

+1932
-3
lines changed
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory;
13+
14+
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
15+
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
use Symfony\Component\DependencyInjection\DefinitionDecorator;
17+
use Symfony\Component\DependencyInjection\Reference;
18+
19+
/**
20+
* Configures the "guard" authentication provider key under a firewall.
21+
*
22+
* @author Ryan Weaver <ryan@knpuniversity.com>
23+
*/
24+
class GuardAuthenticationFactory implements SecurityFactoryInterface
25+
{
26+
public function getPosition()
27+
{
28+
return 'pre_auth';
29+
}
30+
31+
public function getKey()
32+
{
33+
return 'guard';
34+
}
35+
36+
public function addConfiguration(NodeDefinition $node)
37+
{
38+
$node
39+
->fixXmlConfig('authenticator')
40+
->children()
41+
->scalarNode('provider')
42+
->info('A key from the "providers" section of your security config, in case your user provider is different than the firewall')
43+
->end()
44+
->scalarNode('entry_point')
45+
->info('A service id (of one of your authenticators) whose start() method should be called when an anonymous user hits a page that requires authentication')
46+
->defaultValue(null)
47+
->end()
48+
->arrayNode('authenticators')
49+
->info('An array of service ids for all of your "authenticators"')
50+
->requiresAtLeastOneElement()
51+
->prototype('scalar')->end()
52+
->end()
53+
->end()
54+
;
55+
}
56+
57+
public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint)
58+
{
59+
$authenticatorIds = $config['authenticators'];
60+
$authenticatorReferences = array();
61+
foreach ($authenticatorIds as $authenticatorId) {
62+
$authenticatorReferences[] = new Reference($authenticatorId);
63+
}
64+
65+
// configure the GuardAuthenticationFactory to have the dynamic constructor arguments
66+
$providerId = 'security.authentication.provider.guard.'.$id;
67+
$container
68+
->setDefinition($providerId, new DefinitionDecorator('security.authentication.provider.guard'))
69+
->replaceArgument(0, $authenticatorReferences)
70+
->replaceArgument(1, new Reference($userProvider))
71+
->replaceArgument(2, $id)
72+
;
73+
74+
// listener
75+
$listenerId = 'security.authentication.listener.guard.'.$id;
76+
$listener = $container->setDefinition($listenerId, new DefinitionDecorator('security.authentication.listener.guard'));
77+
$listener->replaceArgument(2, $id);
78+
$listener->replaceArgument(3, $authenticatorReferences);
79+
80+
// determine the entryPointId to use
81+
$entryPointId = $this->determineEntryPoint($defaultEntryPoint, $config);
82+
83+
// this is always injected - then the listener decides if it should be used
84+
$container
85+
->getDefinition($listenerId)
86+
->addTag('security.remember_me_aware', array('id' => $id, 'provider' => $userProvider));
87+
88+
return array($providerId, $listenerId, $entryPointId);
89+
}
90+
91+
private function determineEntryPoint($defaultEntryPointId, array $config)
92+
{
93+
if ($defaultEntryPointId) {
94+
// explode if they've configured the entry_point, but there is already one
95+
if ($config['entry_point']) {
96+
throw new \LogicException(sprintf(
97+
'The guard authentication provider cannot use the "%s" entry_point because another entry point is already configured by another provider! Either remove the other provider or move the entry_point configuration as a root key under your firewall',
98+
$config['entry_point']
99+
));
100+
}
101+
102+
return $defaultEntryPointId;
103+
}
104+
105+
if ($config['entry_point']) {
106+
// if it's configured explicitly, use it!
107+
return $config['entry_point'];
108+
}
109+
110+
$authenticatorIds = $config['authenticators'];
111+
if (count($authenticatorIds) == 1) {
112+
// if there is only one authenticator, use that as the entry point
113+
return array_shift($authenticatorIds);
114+
}
115+
116+
// we have multiple entry points - we must ask them to configure one
117+
throw new \LogicException(sprintf(
118+
'Because you have multiple guard configurators, you need to set the "guard.entry_point" key to one of you configurators (%s)',
119+
implode(', ', $authenticatorIds)
120+
));
121+
}
122+
}

src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ public function load(array $configs, ContainerBuilder $container)
6565
$loader->load('templating_php.xml');
6666
$loader->load('templating_twig.xml');
6767
$loader->load('collectors.xml');
68+
$loader->load('guard.xml');
6869

6970
if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) {
7071
$container->removeDefinition('security.expression_language');
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?xml version="1.0" ?>
2+
3+
<container xmlns="http://symfony.com/schema/dic/services"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
6+
7+
<services>
8+
<service id="security.authentication.guard_handler"
9+
class="Symfony\Component\Security\Guard\GuardAuthenticatorHandler"
10+
>
11+
<argument type="service" id="security.token_storage" />
12+
<argument type="service" id="event_dispatcher" on-invalid="null" />
13+
</service>
14+
15+
<!-- See GuardAuthenticationFactory -->
16+
<service id="security.authentication.provider.guard"
17+
class="Symfony\Component\Security\Guard\Provider\GuardAuthenticationProvider"
18+
abstract="true"
19+
public="false"
20+
>
21+
<argument /> <!-- Simple Authenticator -->
22+
<argument /> <!-- User Provider -->
23+
<argument /> <!-- Provider-shared Key -->
24+
<argument type="service" id="security.user_checker" />
25+
</service>
26+
27+
<service id="security.authentication.listener.guard"
28+
class="Symfony\Component\Security\Guard\Firewall\GuardAuthenticationListener"
29+
public="false"
30+
abstract="true"
31+
>
32+
<tag name="monolog.logger" channel="security" />
33+
<argument type="service" id="security.authentication.guard_handler" />
34+
<argument type="service" id="security.authentication.manager" />
35+
<argument /> <!-- Provider-shared Key -->
36+
<argument /> <!-- Authenticator -->
37+
<argument type="service" id="logger" on-invalid="null" />
38+
</service>
39+
</services>
40+
</container>

src/Symfony/Bundle/SecurityBundle/SecurityBundle.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SimplePreAuthenticationFactory;
2424
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SimpleFormFactory;
2525
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\InMemoryFactory;
26+
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\GuardAuthenticationFactory;
2627

2728
/**
2829
* Bundle.
@@ -44,6 +45,7 @@ public function build(ContainerBuilder $container)
4445
$extension->addSecurityListenerFactory(new RemoteUserFactory());
4546
$extension->addSecurityListenerFactory(new SimplePreAuthenticationFactory());
4647
$extension->addSecurityListenerFactory(new SimpleFormFactory());
48+
$extension->addSecurityListenerFactory(new GuardAuthenticationFactory());
4749

4850
$extension->addUserProviderFactory(new InMemoryFactory());
4951
$container->addCompilerPass(new AddSecurityVotersPass());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\SecurityBundle\Tests\DependencyInjection\Security\Factory;
13+
14+
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\GuardAuthenticationFactory;
15+
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
16+
use Symfony\Component\DependencyInjection\ContainerBuilder;
17+
use Symfony\Component\DependencyInjection\Reference;
18+
19+
class GuardAuthenticationFactoryTest extends \PHPUnit_Framework_TestCase
20+
{
21+
/**
22+
* @dataProvider getValidConfigurationTests
23+
*/
24+
public function testAddValidConfiguration(array $inputConfig, array $expectedConfig)
25+
{
26+
$factory = new GuardAuthenticationFactory();
27+
$nodeDefinition = new ArrayNodeDefinition('guard');
28+
$factory->addConfiguration($nodeDefinition);
29+
30+
$node = $nodeDefinition->getNode();
31+
$normalizedConfig = $node->normalize($inputConfig);
32+
$finalizedConfig = $node->finalize($normalizedConfig);
33+
34+
$this->assertEquals($expectedConfig, $finalizedConfig);
35+
}
36+
37+
/**
38+
* @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException
39+
* @dataProvider getInvalidConfigurationTests
40+
*/
41+
public function testAddInvalidConfiguration(array $inputConfig)
42+
{
43+
$factory = new GuardAuthenticationFactory();
44+
$nodeDefinition = new ArrayNodeDefinition('guard');
45+
$factory->addConfiguration($nodeDefinition);
46+
47+
$node = $nodeDefinition->getNode();
48+
$normalizedConfig = $node->normalize($inputConfig);
49+
// will validate and throw an exception on invalid
50+
$node->finalize($normalizedConfig);
51+
}
52+
53+
public function getValidConfigurationTests()
54+
{
55+
$tests = array();
56+
57+
// completely basic
58+
$tests[] = array(
59+
array(
60+
'authenticators' => array('authenticator1', 'authenticator2'),
61+
'provider' => 'some_provider',
62+
'entry_point' => 'the_entry_point',
63+
),
64+
array(
65+
'authenticators' => array('authenticator1', 'authenticator2'),
66+
'provider' => 'some_provider',
67+
'entry_point' => 'the_entry_point',
68+
),
69+
);
70+
71+
// testing xml config fix: authenticator -> authenticators
72+
$tests[] = array(
73+
array(
74+
'authenticator' => array('authenticator1', 'authenticator2'),
75+
),
76+
array(
77+
'authenticators' => array('authenticator1', 'authenticator2'),
78+
'entry_point' => null,
79+
),
80+
);
81+
82+
return $tests;
83+
}
84+
85+
public function getInvalidConfigurationTests()
86+
{
87+
$tests = array();
88+
89+
// testing not empty
90+
$tests[] = array(
91+
array('authenticators' => array()),
92+
);
93+
94+
return $tests;
95+
}
96+
97+
public function testBasicCreate()
98+
{
99+
// simple configuration
100+
$config = array(
101+
'authenticators' => array('authenticator123'),
102+
'entry_point' => null,
103+
);
104+
list($container, $entryPointId) = $this->executeCreate($config, null);
105+
$this->assertEquals('authenticator123', $entryPointId);
106+
107+
$providerDefinition = $container->getDefinition('security.authentication.provider.guard.my_firewall');
108+
$this->assertEquals(array(
109+
'index_0' => array(new Reference('authenticator123')),
110+
'index_1' => new Reference('my_user_provider'),
111+
'index_2' => 'my_firewall',
112+
), $providerDefinition->getArguments());
113+
114+
$listenerDefinition = $container->getDefinition('security.authentication.listener.guard.my_firewall');
115+
$this->assertEquals('my_firewall', $listenerDefinition->getArgument(2));
116+
$this->assertEquals(array(new Reference('authenticator123')), $listenerDefinition->getArgument(3));
117+
}
118+
119+
public function testExistingDefaultEntryPointUsed()
120+
{
121+
// any existing default entry point is used
122+
$config = array(
123+
'authenticators' => array('authenticator123'),
124+
'entry_point' => null,
125+
);
126+
list($container, $entryPointId) = $this->executeCreate($config, 'some_default_entry_point');
127+
$this->assertEquals('some_default_entry_point', $entryPointId);
128+
}
129+
130+
/**
131+
* @expectedException \LogicException
132+
*/
133+
public function testCannotOverrideDefaultEntryPoint()
134+
{
135+
// any existing default entry point is used
136+
$config = array(
137+
'authenticators' => array('authenticator123'),
138+
'entry_point' => 'authenticator123',
139+
);
140+
$this->executeCreate($config, 'some_default_entry_point');
141+
}
142+
143+
/**
144+
* @expectedException \LogicException
145+
*/
146+
public function testMultipleAuthenticatorsRequiresEntryPoint()
147+
{
148+
// any existing default entry point is used
149+
$config = array(
150+
'authenticators' => array('authenticator123', 'authenticatorABC'),
151+
'entry_point' => null,
152+
);
153+
$this->executeCreate($config, null);
154+
}
155+
156+
public function testCreateWithEntryPoint()
157+
{
158+
// any existing default entry point is used
159+
$config = array(
160+
'authenticators' => array('authenticator123', 'authenticatorABC'),
161+
'entry_point' => 'authenticatorABC',
162+
);
163+
list($container, $entryPointId) = $this->executeCreate($config, null);
164+
$this->assertEquals('authenticatorABC', $entryPointId);
165+
}
166+
167+
private function executeCreate(array $config, $defaultEntryPointId)
168+
{
169+
$container = new ContainerBuilder();
170+
$container->register('security.authentication.provider.guard');
171+
$container->register('security.authentication.listener.guard');
172+
$id = 'my_firewall';
173+
$userProviderId = 'my_user_provider';
174+
175+
$factory = new GuardAuthenticationFactory();
176+
list($providerId, $listenerId, $entryPointId) = $factory->create($container, $id, $config, $userProviderId, $defaultEntryPointId);
177+
178+
return array($container, $entryPointId);
179+
}
180+
}

0 commit comments

Comments
 (0)
0