Skip to content

New Guard Authentication System (e.g. putting the joy back into security) #14673

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
Sep 24, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
330aa7f
Improving phpdoc on AuthenticationEntryPointInterface so people that …
weaverryan May 17, 2015
05af97c
Initial commit (but after some polished work) of the new Guard authen…
weaverryan May 17, 2015
a0bceb4
adding Guard tests
weaverryan May 17, 2015
873ed28
Renaming the tokens to be clear they are "post" and "pre" auth - also…
weaverryan May 17, 2015
180e2c7
Properly handles "post auth" tokens that have become not authenticated
weaverryan May 17, 2015
6c180c7
Adding an edge case - this should not happen anyways
weaverryan May 17, 2015
c73c32e
Thanks fabbot!
weaverryan May 17, 2015
eb158cb
Updating interface method per suggestion - makes sense to me, Request…
weaverryan May 18, 2015
d693721
Adding periods at the end of exceptions, and changing one class name …
weaverryan May 18, 2015
6edb9e1
Tweaking docblock on interface thanks to @iltar
weaverryan May 18, 2015
ffdbc66
Splitting the getting of the user and checking credentials into two s…
weaverryan May 18, 2015
7de05be
A few more changes thanks to @iltar
weaverryan May 18, 2015
7a94994
Thanks again fabbot!
weaverryan May 18, 2015
81432f9
Adding missing factory registration
weaverryan May 25, 2015
293c8a1
meaningless author and license changes
weaverryan Sep 20, 2015
0501761
Allowing for other authenticators to be checked
weaverryan Sep 20, 2015
31f9cae
Adding a base class to assist with form login authentication
weaverryan Sep 20, 2015
c9d9430
Adding logging on this step and switching the order - not for any hu…
weaverryan Sep 20, 2015
396a162
Tweaks thanks to Wouter
weaverryan Sep 20, 2015
302235e
Fixing a bug where having an authentication failure would log you out.
weaverryan Sep 21, 2015
dd485f4
Adding a new exception and throwing it when the User changes
weaverryan Sep 21, 2015
e353833
fabbot
weaverryan Sep 21, 2015
d763134
Removing unnecessary override
weaverryan Sep 22, 2015
a01ed35
Adding the necessary files so that Guard can be its own installable c…
weaverryan Sep 24, 2015
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<?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\Bundle\SecurityBundle\DependencyInjection\Security\Factory;

use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\DependencyInjection\Reference;

/**
* Configures the "guard" authentication provider key under a firewall.
*
* @author Ryan Weaver <ryan@knpuniversity.com>
*/
class GuardAuthenticationFactory implements SecurityFactoryInterface
{
public function getPosition()
{
return 'pre_auth';
}

public function getKey()
{
return 'guard';
}

public function addConfiguration(NodeDefinition $node)
{
$node
->fixXmlConfig('authenticator')
->children()
->scalarNode('provider')
->info('A key from the "providers" section of your security config, in case your user provider is different than the firewall')
->end()
->scalarNode('entry_point')
->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')
->defaultValue(null)
->end()
->arrayNode('authenticators')
->info('An array of service ids for all of your "authenticators"')
->requiresAtLeastOneElement()
->prototype('scalar')->end()
->end()
->end()
;
}

public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint)
{
$authenticatorIds = $config['authenticators'];
$authenticatorReferences = array();
foreach ($authenticatorIds as $authenticatorId) {
$authenticatorReferences[] = new Reference($authenticatorId);
}

// configure the GuardAuthenticationFactory to have the dynamic constructor arguments
$providerId = 'security.authentication.provider.guard.'.$id;
$container
->setDefinition($providerId, new DefinitionDecorator('security.authentication.provider.guard'))
->replaceArgument(0, $authenticatorReferences)
->replaceArgument(1, new Reference($userProvider))
->replaceArgument(2, $id)
;

// listener
$listenerId = 'security.authentication.listener.guard.'.$id;
$listener = $container->setDefinition($listenerId, new DefinitionDecorator('security.authentication.listener.guard'));
$listener->replaceArgument(2, $id);
$listener->replaceArgument(3, $authenticatorReferences);

// determine the entryPointId to use
$entryPointId = $this->determineEntryPoint($defaultEntryPoint, $config);

// this is always injected - then the listener decides if it should be used
$container
->getDefinition($listenerId)
->addTag('security.remember_me_aware', array('id' => $id, 'provider' => $userProvider));

return array($providerId, $listenerId, $entryPointId);
}

private function determineEntryPoint($defaultEntryPointId, array $config)
{
if ($defaultEntryPointId) {
// explode if they've configured the entry_point, but there is already one
if ($config['entry_point']) {
throw new \LogicException(sprintf(
'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',
$config['entry_point']
));
}

return $defaultEntryPointId;
}

if ($config['entry_point']) {
// if it's configured explicitly, use it!
return $config['entry_point'];
}

$authenticatorIds = $config['authenticators'];
if (count($authenticatorIds) == 1) {
// if there is only one authenticator, use that as the entry point
return array_shift($authenticatorIds);
}

// we have multiple entry points - we must ask them to configure one
throw new \LogicException(sprintf(
'Because you have multiple guard configurators, you need to set the "guard.entry_point" key to one of you configurators (%s)',
implode(', ', $authenticatorIds)
));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ public function load(array $configs, ContainerBuilder $container)
$loader->load('templating_php.xml');
$loader->load('templating_twig.xml');
$loader->load('collectors.xml');
$loader->load('guard.xml');

if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) {
$container->removeDefinition('security.expression_language');
Expand Down
40 changes: 40 additions & 0 deletions src/Symfony/Bundle/SecurityBundle/Resources/config/guard.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?xml version="1.0" ?>

<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="security.authentication.guard_handler"
class="Symfony\Component\Security\Guard\GuardAuthenticatorHandler"
>
<argument type="service" id="security.token_storage" />
<argument type="service" id="event_dispatcher" on-invalid="null" />
</service>

<!-- See GuardAuthenticationFactory -->
<service id="security.authentication.provider.guard"
class="Symfony\Component\Security\Guard\Provider\GuardAuthenticationProvider"
abstract="true"
public="false"
>
<argument /> <!-- Simple Authenticator -->
<argument /> <!-- User Provider -->
<argument /> <!-- Provider-shared Key -->
<argument type="service" id="security.user_checker" />
</service>

<service id="security.authentication.listener.guard"
class="Symfony\Component\Security\Guard\Firewall\GuardAuthenticationListener"
public="false"
abstract="true"
>
<tag name="monolog.logger" channel="security" />
<argument type="service" id="security.authentication.guard_handler" />
<argument type="service" id="security.authentication.manager" />
<argument /> <!-- Provider-shared Key -->
<argument /> <!-- Authenticator -->
<argument type="service" id="logger" on-invalid="null" />
</service>
</services>
</container>
2 changes: 2 additions & 0 deletions src/Symfony/Bundle/SecurityBundle/SecurityBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SimplePreAuthenticationFactory;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SimpleFormFactory;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\InMemoryFactory;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\GuardAuthenticationFactory;

/**
* Bundle.
Expand All @@ -44,6 +45,7 @@ public function build(ContainerBuilder $container)
$extension->addSecurityListenerFactory(new RemoteUserFactory());
$extension->addSecurityListenerFactory(new SimplePreAuthenticationFactory());
$extension->addSecurityListenerFactory(new SimpleFormFactory());
$extension->addSecurityListenerFactory(new GuardAuthenticationFactory());

$extension->addUserProviderFactory(new InMemoryFactory());
$container->addCompilerPass(new AddSecurityVotersPass());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
<?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\Bundle\SecurityBundle\Tests\DependencyInjection\Security\Factory;

use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\GuardAuthenticationFactory;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

class GuardAuthenticationFactoryTest extends \PHPUnit_Framework_TestCase
{
/**
* @dataProvider getValidConfigurationTests
*/
public function testAddValidConfiguration(array $inputConfig, array $expectedConfig)
{
$factory = new GuardAuthenticationFactory();
$nodeDefinition = new ArrayNodeDefinition('guard');
$factory->addConfiguration($nodeDefinition);

$node = $nodeDefinition->getNode();
$normalizedConfig = $node->normalize($inputConfig);
$finalizedConfig = $node->finalize($normalizedConfig);

$this->assertEquals($expectedConfig, $finalizedConfig);
}

/**
* @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException
* @dataProvider getInvalidConfigurationTests
*/
public function testAddInvalidConfiguration(array $inputConfig)
{
$factory = new GuardAuthenticationFactory();
$nodeDefinition = new ArrayNodeDefinition('guard');
$factory->addConfiguration($nodeDefinition);

$node = $nodeDefinition->getNode();
$normalizedConfig = $node->normalize($inputConfig);
// will validate and throw an exception on invalid
$node->finalize($normalizedConfig);
}

public function getValidConfigurationTests()
{
$tests = array();

// completely basic
$tests[] = array(
array(
'authenticators' => array('authenticator1', 'authenticator2'),
'provider' => 'some_provider',
'entry_point' => 'the_entry_point',
),
array(
'authenticators' => array('authenticator1', 'authenticator2'),
'provider' => 'some_provider',
'entry_point' => 'the_entry_point',
),
);

// testing xml config fix: authenticator -> authenticators
$tests[] = array(
array(
'authenticator' => array('authenticator1', 'authenticator2'),
),
array(
'authenticators' => array('authenticator1', 'authenticator2'),
'entry_point' => null,
),
);

return $tests;
}

public function getInvalidConfigurationTests()
{
$tests = array();

// testing not empty
$tests[] = array(
array('authenticators' => array()),
);

return $tests;
}

public function testBasicCreate()
{
// simple configuration
$config = array(
'authenticators' => array('authenticator123'),
'entry_point' => null,
);
list($container, $entryPointId) = $this->executeCreate($config, null);
$this->assertEquals('authenticator123', $entryPointId);

$providerDefinition = $container->getDefinition('security.authentication.provider.guard.my_firewall');
$this->assertEquals(array(
'index_0' => array(new Reference('authenticator123')),
'index_1' => new Reference('my_user_provider'),
'index_2' => 'my_firewall',
), $providerDefinition->getArguments());

$listenerDefinition = $container->getDefinition('security.authentication.listener.guard.my_firewall');
$this->assertEquals('my_firewall', $listenerDefinition->getArgument(2));
$this->assertEquals(array(new Reference('authenticator123')), $listenerDefinition->getArgument(3));
}

public function testExistingDefaultEntryPointUsed()
{
// any existing default entry point is used
$config = array(
'authenticators' => array('authenticator123'),
'entry_point' => null,
);
list($container, $entryPointId) = $this->executeCreate($config, 'some_default_entry_point');
$this->assertEquals('some_default_entry_point', $entryPointId);
}

/**
* @expectedException \LogicException
*/
public function testCannotOverrideDefaultEntryPoint()
{
// any existing default entry point is used
$config = array(
'authenticators' => array('authenticator123'),
'entry_point' => 'authenticator123',
);
$this->executeCreate($config, 'some_default_entry_point');
}

/**
* @expectedException \LogicException
*/
public function testMultipleAuthenticatorsRequiresEntryPoint()
{
// any existing default entry point is used
$config = array(
'authenticators' => array('authenticator123', 'authenticatorABC'),
'entry_point' => null,
);
$this->executeCreate($config, null);
}

public function testCreateWithEntryPoint()
{
// any existing default entry point is used
$config = array(
'authenticators' => array('authenticator123', 'authenticatorABC'),
'entry_point' => 'authenticatorABC',
);
list($container, $entryPointId) = $this->executeCreate($config, null);
$this->assertEquals('authenticatorABC', $entryPointId);
}

private function executeCreate(array $config, $defaultEntryPointId)
{
$container = new ContainerBuilder();
$container->register('security.authentication.provider.guard');
$container->register('security.authentication.listener.guard');
$id = 'my_firewall';
$userProviderId = 'my_user_provider';

$factory = new GuardAuthenticationFactory();
list($providerId, $listenerId, $entryPointId) = $factory->create($container, $id, $config, $userProviderId, $defaultEntryPointId);

return array($container, $entryPointId);
}
}
Loading