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/blob/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
This commit is contained in:
Fabien Potencier 2015-09-24 09:21:03 +02:00
commit 5f2acfd4c7
25 changed files with 1932 additions and 3 deletions

View File

@ -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)
));
}
}

View File

@ -65,6 +65,7 @@ class SecurityExtension extends Extension
$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');

View File

@ -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>

View File

@ -23,6 +23,7 @@ use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\RemoteUse
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.
@ -44,6 +45,7 @@ class SecurityBundle extends Bundle
$extension->addSecurityListenerFactory(new RemoteUserFactory());
$extension->addSecurityListenerFactory(new SimplePreAuthenticationFactory());
$extension->addSecurityListenerFactory(new SimpleFormFactory());
$extension->addSecurityListenerFactory(new GuardAuthenticationFactory());
$extension->addUserProviderFactory(new InMemoryFactory());
$container->addCompilerPass(new AddSecurityVotersPass());

View File

@ -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);
}
}

View File

@ -0,0 +1,31 @@
<?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\Core\Exception;
/**
* AuthenticationServiceException is thrown when an authenticated token becomes un-authentcated between requests.
*
* In practice, this is due to the User changing between requests (e.g. password changes),
* causes the token to become un-authenticated.
*
* @author Ryan Weaver <ryan@knpuniversity.com>
*/
class AuthenticationExpiredException extends AccountStatusException
{
/**
* {@inheritdoc}
*/
public function getMessageKey()
{
return 'Authentication expired because your account information has changed.';
}
}

View File

@ -0,0 +1,3 @@
vendor/
composer.lock
phpunit.xml

View File

@ -0,0 +1,41 @@
<?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\Guard;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken;
/**
* An optional base class that creates a PostAuthenticationGuardToken for you.
*
* @author Ryan Weaver <ryan@knpuniversity.com>
*/
abstract class AbstractGuardAuthenticator implements GuardAuthenticatorInterface
{
/**
* Shortcut to create a PostAuthenticationGuardToken for you, if you don't really
* care about which authenticated token you're using.
*
* @param UserInterface $user
* @param string $providerKey
*
* @return PostAuthenticationGuardToken
*/
public function createAuthenticatedToken(UserInterface $user, $providerKey)
{
return new PostAuthenticationGuardToken(
$user,
$providerKey,
$user->getRoles()
);
}
}

View File

@ -0,0 +1,103 @@
<?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\Guard\Authenticator;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Security;
/**
* A base class to make form login authentication easier!
*
* @author Ryan Weaver <ryan@knpuniversity.com>
*/
abstract class AbstractFormLoginAuthenticator extends AbstractGuardAuthenticator
{
/**
* Return the URL to the login page.
*
* @return string
*/
abstract protected function getLoginUrl();
/**
* The user will be redirected to the secure page they originally tried
* to access. But if no such page exists (i.e. the user went to the
* login page directly), this returns the URL the user should be redirected
* to after logging in successfully (e.g. your homepage).
*
* @return string
*/
abstract protected function getDefaultSuccessRedirectUrl();
/**
* Override to change what happens after a bad username/password is submitted.
*
* @param Request $request
* @param AuthenticationException $exception
*
* @return RedirectResponse
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);
$url = $this->getLoginUrl();
return new RedirectResponse($url);
}
/**
* Override to change what happens after successful authentication.
*
* @param Request $request
* @param TokenInterface $token
* @param string $providerKey
*
* @return RedirectResponse
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
// if the user hit a secure page and start() was called, this was
// the URL they were on, and probably where you want to redirect to
$targetPath = $request->getSession()->get('_security.'.$providerKey.'.target_path');
if (!$targetPath) {
$targetPath = $this->getDefaultSuccessRedirectUrl();
}
return new RedirectResponse($targetPath);
}
public function supportsRememberMe()
{
return true;
}
/**
* Override to control what happens when the user hits a secure page
* but isn't logged in yet.
*
* @param Request $request
* @param AuthenticationException|null $authException
*
* @return RedirectResponse
*/
public function start(Request $request, AuthenticationException $authException = null)
{
$url = $this->getLoginUrl();
return new RedirectResponse($url);
}
}

View File

@ -0,0 +1,193 @@
<?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\Guard\Firewall;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;
use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\Security\Guard\GuardAuthenticatorInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Firewall\ListenerInterface;
use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface;
/**
* Authentication listener for the "guard" system.
*
* @author Ryan Weaver <ryan@knpuniversity.com>
*/
class GuardAuthenticationListener implements ListenerInterface
{
private $guardHandler;
private $authenticationManager;
private $providerKey;
private $guardAuthenticators;
private $logger;
private $rememberMeServices;
/**
* @param GuardAuthenticatorHandler $guardHandler The Guard handler
* @param AuthenticationManagerInterface $authenticationManager An AuthenticationManagerInterface instance
* @param string $providerKey The provider (i.e. firewall) key
* @param GuardAuthenticatorInterface[] $guardAuthenticators The authenticators, with keys that match what's passed to GuardAuthenticationProvider
* @param LoggerInterface $logger A LoggerInterface instance
*/
public function __construct(GuardAuthenticatorHandler $guardHandler, AuthenticationManagerInterface $authenticationManager, $providerKey, array $guardAuthenticators, LoggerInterface $logger = null)
{
if (empty($providerKey)) {
throw new \InvalidArgumentException('$providerKey must not be empty.');
}
$this->guardHandler = $guardHandler;
$this->authenticationManager = $authenticationManager;
$this->providerKey = $providerKey;
$this->guardAuthenticators = $guardAuthenticators;
$this->logger = $logger;
}
/**
* Iterates over each authenticator to see if each wants to authenticate the request.
*
* @param GetResponseEvent $event
*/
public function handle(GetResponseEvent $event)
{
if (null !== $this->logger) {
$this->logger->info('Checking for guard authentication credentials.', array('firewall_key' => $this->providerKey, 'authenticators' => count($this->guardAuthenticators)));
}
foreach ($this->guardAuthenticators as $key => $guardAuthenticator) {
// get a key that's unique to *this* guard authenticator
// this MUST be the same as GuardAuthenticationProvider
$uniqueGuardKey = $this->providerKey.'_'.$key;
$this->executeGuardAuthenticator($uniqueGuardKey, $guardAuthenticator, $event);
}
}
private function executeGuardAuthenticator($uniqueGuardKey, GuardAuthenticatorInterface $guardAuthenticator, GetResponseEvent $event)
{
$request = $event->getRequest();
try {
if (null !== $this->logger) {
$this->logger->info('Calling getCredentials on guard configurator.', array('firewall_key' => $this->providerKey, 'authenticator' => get_class($guardAuthenticator)));
}
// allow the authenticator to fetch authentication info from the request
$credentials = $guardAuthenticator->getCredentials($request);
// allow null to be returned to skip authentication
if (null === $credentials) {
return;
}
// create a token with the unique key, so that the provider knows which authenticator to use
$token = new PreAuthenticationGuardToken($credentials, $uniqueGuardKey);
if (null !== $this->logger) {
$this->logger->info('Passing guard token information to the GuardAuthenticationProvider', array('firewall_key' => $this->providerKey, 'authenticator' => get_class($guardAuthenticator)));
}
// pass the token into the AuthenticationManager system
// this indirectly calls GuardAuthenticationProvider::authenticate()
$token = $this->authenticationManager->authenticate($token);
if (null !== $this->logger) {
$this->logger->info('Guard authentication successful!', array('token' => $token, 'authenticator' => get_class($guardAuthenticator)));
}
// sets the token on the token storage, etc
$this->guardHandler->authenticateWithToken($token, $request);
} catch (AuthenticationException $e) {
// oh no! Authentication failed!
if (null !== $this->logger) {
$this->logger->info('Guard authentication failed.', array('exception' => $e, 'authenticator' => get_class($guardAuthenticator)));
}
$response = $this->guardHandler->handleAuthenticationFailure($e, $request, $guardAuthenticator, $this->providerKey);
if ($response instanceof Response) {
$event->setResponse($response);
}
return;
}
// success!
$response = $this->guardHandler->handleAuthenticationSuccess($token, $request, $guardAuthenticator, $this->providerKey);
if ($response instanceof Response) {
if (null !== $this->logger) {
$this->logger->info('Guard authenticator set success response.', array('response' => $response, 'authenticator' => get_class($guardAuthenticator)));
}
$event->setResponse($response);
} else {
if (null !== $this->logger) {
$this->logger->info('Guard authenticator set no success response: request continues.', array('authenticator' => get_class($guardAuthenticator)));
}
}
// attempt to trigger the remember me functionality
$this->triggerRememberMe($guardAuthenticator, $request, $token, $response);
}
/**
* Should be called if this listener will support remember me.
*
* @param RememberMeServicesInterface $rememberMeServices
*/
public function setRememberMeServices(RememberMeServicesInterface $rememberMeServices)
{
$this->rememberMeServices = $rememberMeServices;
}
/**
* Checks to see if remember me is supported in the authenticator and
* on the firewall. If it is, the RememberMeServicesInterface is notified.
*
* @param GuardAuthenticatorInterface $guardAuthenticator
* @param Request $request
* @param TokenInterface $token
* @param Response $response
*/
private function triggerRememberMe(GuardAuthenticatorInterface $guardAuthenticator, Request $request, TokenInterface $token, Response $response = null)
{
if (null === $this->rememberMeServices) {
if (null !== $this->logger) {
$this->logger->info('Remember me skipped: it is not configured for the firewall.', array('authenticator' => get_class($guardAuthenticator)));
}
return;
}
if (!$guardAuthenticator->supportsRememberMe()) {
if (null !== $this->logger) {
$this->logger->info('Remember me skipped: your authenticator does not support it.', array('authenticator' => get_class($guardAuthenticator)));
}
return;
}
if (!$response instanceof Response) {
throw new \LogicException(sprintf(
'%s::onAuthenticationSuccess *must* return a Response if you want to use the remember me functionality. Return a Response, or set remember_me to false under the guard configuration.',
get_class($guardAuthenticator)
));
}
$this->rememberMeServices->loginSuccess($request, $response, $token);
}
}

View File

@ -0,0 +1,139 @@
<?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\Guard;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\SecurityEvents;
/**
* A utility class that does much of the *work* during the guard authentication process.
*
* By having the logic here instead of the listener, more of the process
* can be called directly (e.g. for manual authentication) or overridden.
*
* @author Ryan Weaver <ryan@knpuniversity.com>
*/
class GuardAuthenticatorHandler
{
private $tokenStorage;
private $dispatcher;
public function __construct(TokenStorageInterface $tokenStorage, EventDispatcherInterface $eventDispatcher = null)
{
$this->tokenStorage = $tokenStorage;
$this->dispatcher = $eventDispatcher;
}
/**
* Authenticates the given token in the system.
*
* @param TokenInterface $token
* @param Request $request
*/
public function authenticateWithToken(TokenInterface $token, Request $request)
{
$this->tokenStorage->setToken($token);
if (null !== $this->dispatcher) {
$loginEvent = new InteractiveLoginEvent($request, $token);
$this->dispatcher->dispatch(SecurityEvents::INTERACTIVE_LOGIN, $loginEvent);
}
}
/**
* Returns the "on success" response for the given GuardAuthenticator.
*
* @param TokenInterface $token
* @param Request $request
* @param GuardAuthenticatorInterface $guardAuthenticator
* @param string $providerKey The provider (i.e. firewall) key
*
* @return null|Response
*/
public function handleAuthenticationSuccess(TokenInterface $token, Request $request, GuardAuthenticatorInterface $guardAuthenticator, $providerKey)
{
$response = $guardAuthenticator->onAuthenticationSuccess($request, $token, $providerKey);
// check that it's a Response or null
if ($response instanceof Response || null === $response) {
return $response;
}
throw new \UnexpectedValueException(sprintf(
'The %s::onAuthenticationSuccess method must return null or a Response object. You returned %s.',
get_class($guardAuthenticator),
is_object($response) ? get_class($response) : gettype($response)
));
}
/**
* Convenience method for authenticating the user and returning the
* Response *if any* for success.
*
* @param UserInterface $user
* @param Request $request
* @param GuardAuthenticatorInterface $authenticator
* @param string $providerKey The provider (i.e. firewall) key
*
* @return Response|null
*/
public function authenticateUserAndHandleSuccess(UserInterface $user, Request $request, GuardAuthenticatorInterface $authenticator, $providerKey)
{
// create an authenticated token for the User
$token = $authenticator->createAuthenticatedToken($user, $providerKey);
// authenticate this in the system
$this->authenticateWithToken($token, $request);
// return the success metric
return $this->handleAuthenticationSuccess($token, $request, $authenticator, $providerKey);
}
/**
* Handles an authentication failure and returns the Response for the
* GuardAuthenticator.
*
* @param AuthenticationException $authenticationException
* @param Request $request
* @param GuardAuthenticatorInterface $guardAuthenticator
* @param string $providerKey The key of the firewall
*
* @return null|Response
*/
public function handleAuthenticationFailure(AuthenticationException $authenticationException, Request $request, GuardAuthenticatorInterface $guardAuthenticator, $providerKey)
{
$token = $this->tokenStorage->getToken();
if ($token instanceof PostAuthenticationGuardToken && $providerKey === $token->getProviderKey()) {
$this->tokenStorage->setToken(null);
}
$response = $guardAuthenticator->onAuthenticationFailure($request, $authenticationException);
if ($response instanceof Response || null === $response) {
// returning null is ok, it means they want the request to continue
return $response;
}
throw new \UnexpectedValueException(sprintf(
'The %s::onAuthenticationFailure method must return null or a Response object. You returned %s.',
get_class($guardAuthenticator),
is_object($response) ? get_class($response) : gettype($response)
));
}
}

View File

@ -0,0 +1,150 @@
<?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\Guard;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\Token\GuardTokenInterface;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
/**
* The interface for all "guard" authenticators.
*
* The methods on this interface are called throughout the guard authentication
* process to give you the power to control most parts of the process from
* one location.
*
* @author Ryan Weaver <ryan@knpuniversity.com>
*/
interface GuardAuthenticatorInterface extends AuthenticationEntryPointInterface
{
/**
* Get the authentication credentials from the request and return them
* as any type (e.g. an associate array). If you return null, authentication
* will be skipped.
*
* Whatever value you return here will be passed to getUser() and checkCredentials()
*
* For example, for a form login, you might:
*
* return array(
* 'username' => $request->request->get('_username'),
* 'password' => $request->request->get('_password'),
* );
*
* Or for an API token that's on a header, you might use:
*
* return array('api_key' => $request->headers->get('X-API-TOKEN'));
*
* @param Request $request
*
* @return mixed|null
*/
public function getCredentials(Request $request);
/**
* Return a UserInterface object based on the credentials.
*
* The *credentials* are the return value from getCredentials()
*
* You may throw an AuthenticationException if you wish. If you return
* null, then a UsernameNotFoundException is thrown for you.
*
* @param mixed $credentials
* @param UserProviderInterface $userProvider
*
* @throws AuthenticationException
*
* @return UserInterface|null
*/
public function getUser($credentials, UserProviderInterface $userProvider);
/**
* Throw an AuthenticationException if the credentials are invalid.
*
* The *credentials* are the return value from getCredentials()
*
* @param mixed $credentials
* @param UserInterface $user
*
* @throws AuthenticationException
*/
public function checkCredentials($credentials, UserInterface $user);
/**
* Create an authenticated token for the given user.
*
* If you don't care about which token class is used or don't really
* understand what a "token" is, you can skip this method by extending
* the AbstractGuardAuthenticator class from your authenticator.
*
* @see AbstractGuardAuthenticator
*
* @param UserInterface $user
* @param string $providerKey The provider (i.e. firewall) key
*
* @return GuardTokenInterface
*/
public function createAuthenticatedToken(UserInterface $user, $providerKey);
/**
* Called when authentication executed, but failed (e.g. wrong username password).
*
* This should return the Response sent back to the user, like a
* RedirectResponse to the login page or a 403 response.
*
* If you return null, the request will continue, but the user will
* not be authenticated. This is probably not what you want to do.
*
* @param Request $request
* @param AuthenticationException $exception
*
* @return Response|null
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $exception);
/**
* Called when authentication executed and was successful!
*
* This should return the Response sent back to the user, like a
* RedirectResponse to the last page they visited.
*
* If you return null, the current request will continue, and the user
* will be authenticated. This makes sense, for example, with an API.
*
* @param Request $request
* @param TokenInterface $token
* @param string $providerKey The provider (i.e. firewall) key
*
* @return Response|null
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey);
/**
* Does this method support remember me cookies?
*
* Remember me cookie will be set if *all* of the following are met:
* A) This method returns true
* B) The remember_me key under your firewall is configured
* C) The "remember me" functionality is activated. This is usually
* done by having a _remember_me checkbox in your form, but
* can be configured by the "always_remember_me" and "remember_me_parameter"
* parameters under the "remember_me" firewall key
*
* @return bool
*/
public function supportsRememberMe();
}

View File

@ -0,0 +1,19 @@
Copyright (c) 2004-2015 Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,145 @@
<?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\Guard\Provider;
use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Guard\GuardAuthenticatorInterface;
use Symfony\Component\Security\Guard\Token\GuardTokenInterface;
use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationExpiredException;
/**
* Responsible for accepting the PreAuthenticationGuardToken and calling
* the correct authenticator to retrieve the authenticated token.
*
* @author Ryan Weaver <ryan@knpuniversity.com>
*/
class GuardAuthenticationProvider implements AuthenticationProviderInterface
{
/**
* @var GuardAuthenticatorInterface[]
*/
private $guardAuthenticators;
private $userProvider;
private $providerKey;
private $userChecker;
/**
* @param GuardAuthenticatorInterface[] $guardAuthenticators The authenticators, with keys that match what's passed to GuardAuthenticationListener
* @param UserProviderInterface $userProvider The user provider
* @param string $providerKey The provider (i.e. firewall) key
* @param UserCheckerInterface $userChecker
*/
public function __construct(array $guardAuthenticators, UserProviderInterface $userProvider, $providerKey, UserCheckerInterface $userChecker)
{
$this->guardAuthenticators = $guardAuthenticators;
$this->userProvider = $userProvider;
$this->providerKey = $providerKey;
$this->userChecker = $userChecker;
}
/**
* Finds the correct authenticator for the token and calls it.
*
* @param GuardTokenInterface $token
*
* @return TokenInterface
*/
public function authenticate(TokenInterface $token)
{
if (!$this->supports($token)) {
throw new \InvalidArgumentException('GuardAuthenticationProvider only supports GuardTokenInterface.');
}
if (!$token instanceof PreAuthenticationGuardToken) {
/*
* The listener *only* passes PreAuthenticationGuardToken instances.
* This means that an authenticated token (e.g. PostAuthenticationGuardToken)
* is being passed here, which happens if that token becomes
* "not authenticated" (e.g. happens if the user changes between
* requests). In this case, the user should be logged out, so
* we will return an AnonymousToken to accomplish that.
*/
// this should never happen - but technically, the token is
// authenticated... so it could just be returned
if ($token->isAuthenticated()) {
return $token;
}
// this AccountStatusException causes the user to be logged out
throw new AuthenticationExpiredException();
}
// find the *one* GuardAuthenticator that this token originated from
foreach ($this->guardAuthenticators as $key => $guardAuthenticator) {
// get a key that's unique to *this* guard authenticator
// this MUST be the same as GuardAuthenticationListener
$uniqueGuardKey = $this->providerKey.'_'.$key;
if ($uniqueGuardKey == $token->getGuardProviderKey()) {
return $this->authenticateViaGuard($guardAuthenticator, $token);
}
}
// no matching authenticator found - but there will be multiple GuardAuthenticationProvider
// instances that will be checked if you have multiple firewalls.
}
private function authenticateViaGuard(GuardAuthenticatorInterface $guardAuthenticator, PreAuthenticationGuardToken $token)
{
// get the user from the GuardAuthenticator
$user = $guardAuthenticator->getUser($token->getCredentials(), $this->userProvider);
if (null === $user) {
throw new UsernameNotFoundException(sprintf(
'Null returned from %s::getUser()',
get_class($guardAuthenticator)
));
}
if (!$user instanceof UserInterface) {
throw new \UnexpectedValueException(sprintf(
'The %s::getUser() method must return a UserInterface. You returned %s.',
get_class($guardAuthenticator),
is_object($user) ? get_class($user) : gettype($user)
));
}
$this->userChecker->checkPreAuth($user);
$guardAuthenticator->checkCredentials($token->getCredentials(), $user);
$this->userChecker->checkPostAuth($user);
// turn the UserInterface into a TokenInterface
$authenticatedToken = $guardAuthenticator->createAuthenticatedToken($user, $this->providerKey);
if (!$authenticatedToken instanceof TokenInterface) {
throw new \UnexpectedValueException(sprintf(
'The %s::createAuthenticatedToken() method must return a TokenInterface. You returned %s.',
get_class($guardAuthenticator),
is_object($authenticatedToken) ? get_class($authenticatedToken) : gettype($authenticatedToken)
));
}
return $authenticatedToken;
}
public function supports(TokenInterface $token)
{
return $token instanceof GuardTokenInterface;
}
}

View File

@ -0,0 +1,22 @@
Security Component - Guard
==========================
The Guard component brings many layers of authentication together, making
it much easier to create complex authentication systems where you have
total control.
Resources
---------
Documentation:
https://symfony.com/doc/2.8/book/security.html
Tests
-----
You can run the unit tests with the following command:
$ cd path/to/Symfony/Component/Security/Guard/
$ composer.phar install --dev
$ phpunit

View File

@ -0,0 +1,222 @@
<?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\Guard\Tests\Firewall;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Guard\Firewall\GuardAuthenticationListener;
use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
/**
* @author Ryan Weaver <weaverryan@gmail.com>
*/
class GuardAuthenticationListenerTest extends \PHPUnit_Framework_TestCase
{
private $authenticationManager;
private $guardAuthenticatorHandler;
private $event;
private $logger;
private $request;
private $rememberMeServices;
public function testHandleSuccess()
{
$authenticator = $this->getMock('Symfony\Component\Security\Guard\GuardAuthenticatorInterface');
$authenticateToken = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface');
$providerKey = 'my_firewall';
$credentials = array('username' => 'weaverryan', 'password' => 'all_your_base');
$authenticator
->expects($this->once())
->method('getCredentials')
->with($this->equalTo($this->request))
->will($this->returnValue($credentials));
// a clone of the token that should be created internally
$uniqueGuardKey = 'my_firewall_0';
$nonAuthedToken = new PreAuthenticationGuardToken($credentials, $uniqueGuardKey);
$this->authenticationManager
->expects($this->once())
->method('authenticate')
->with($this->equalTo($nonAuthedToken))
->will($this->returnValue($authenticateToken));
$this->guardAuthenticatorHandler
->expects($this->once())
->method('authenticateWithToken')
->with($authenticateToken, $this->request);
$this->guardAuthenticatorHandler
->expects($this->once())
->method('handleAuthenticationSuccess')
->with($authenticateToken, $this->request, $authenticator, $providerKey);
$listener = new GuardAuthenticationListener(
$this->guardAuthenticatorHandler,
$this->authenticationManager,
$providerKey,
array($authenticator),
$this->logger
);
$listener->setRememberMeServices($this->rememberMeServices);
// should never be called - our handleAuthenticationSuccess() does not return a Response
$this->rememberMeServices
->expects($this->never())
->method('loginSuccess');
$listener->handle($this->event);
}
public function testHandleSuccessWithRememberMe()
{
$authenticator = $this->getMock('Symfony\Component\Security\Guard\GuardAuthenticatorInterface');
$authenticateToken = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface');
$providerKey = 'my_firewall_with_rememberme';
$authenticator
->expects($this->once())
->method('getCredentials')
->with($this->equalTo($this->request))
->will($this->returnValue(array('username' => 'anything_not_empty')));
$this->authenticationManager
->expects($this->once())
->method('authenticate')
->will($this->returnValue($authenticateToken));
$successResponse = new Response('Success!');
$this->guardAuthenticatorHandler
->expects($this->once())
->method('handleAuthenticationSuccess')
->will($this->returnValue($successResponse));
$listener = new GuardAuthenticationListener(
$this->guardAuthenticatorHandler,
$this->authenticationManager,
$providerKey,
array($authenticator),
$this->logger
);
$listener->setRememberMeServices($this->rememberMeServices);
$authenticator->expects($this->once())
->method('supportsRememberMe')
->will($this->returnValue(true));
// should be called - we do have a success Response
$this->rememberMeServices
->expects($this->once())
->method('loginSuccess');
$listener->handle($this->event);
}
public function testHandleCatchesAuthenticationException()
{
$authenticator = $this->getMock('Symfony\Component\Security\Guard\GuardAuthenticatorInterface');
$providerKey = 'my_firewall2';
$authException = new AuthenticationException('Get outta here crazy user with a bad password!');
$authenticator
->expects($this->once())
->method('getCredentials')
->will($this->throwException($authException));
// this is not called
$this->authenticationManager
->expects($this->never())
->method('authenticate');
$this->guardAuthenticatorHandler
->expects($this->once())
->method('handleAuthenticationFailure')
->with($authException, $this->request, $authenticator, $providerKey);
$listener = new GuardAuthenticationListener(
$this->guardAuthenticatorHandler,
$this->authenticationManager,
$providerKey,
array($authenticator),
$this->logger
);
$listener->handle($this->event);
}
public function testReturnNullToSkipAuth()
{
$authenticatorA = $this->getMock('Symfony\Component\Security\Guard\GuardAuthenticatorInterface');
$authenticatorB = $this->getMock('Symfony\Component\Security\Guard\GuardAuthenticatorInterface');
$providerKey = 'my_firewall3';
$authenticatorA
->expects($this->once())
->method('getCredentials')
->will($this->returnValue(null));
$authenticatorB
->expects($this->once())
->method('getCredentials')
->will($this->returnValue(null));
// this is not called
$this->authenticationManager
->expects($this->never())
->method('authenticate');
$this->guardAuthenticatorHandler
->expects($this->never())
->method('handleAuthenticationSuccess');
$listener = new GuardAuthenticationListener(
$this->guardAuthenticatorHandler,
$this->authenticationManager,
$providerKey,
array($authenticatorA, $authenticatorB),
$this->logger
);
$listener->handle($this->event);
}
protected function setUp()
{
$this->authenticationManager = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager')
->disableOriginalConstructor()
->getMock();
$this->guardAuthenticatorHandler = $this->getMockBuilder('Symfony\Component\Security\Guard\GuardAuthenticatorHandler')
->disableOriginalConstructor()
->getMock();
$this->request = new Request(array(), array(), array(), array(), array(), array());
$this->event = $this->getMock('Symfony\Component\HttpKernel\Event\GetResponseEvent', array(), array(), '', false);
$this->event
->expects($this->any())
->method('getRequest')
->will($this->returnValue($this->request));
$this->logger = $this->getMock('Psr\Log\LoggerInterface');
$this->rememberMeServices = $this->getMock('Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface');
}
protected function tearDown()
{
$this->authenticationManager = null;
$this->guardAuthenticatorHandler = null;
$this->event = null;
$this->logger = null;
$this->request = null;
}
}

View File

@ -0,0 +1,141 @@
<?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\Guard\Tests;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\SecurityEvents;
class GuardAuthenticatorHandlerTest extends \PHPUnit_Framework_TestCase
{
private $tokenStorage;
private $dispatcher;
private $token;
private $request;
private $guardAuthenticator;
public function testAuthenticateWithToken()
{
$this->tokenStorage->expects($this->once())
->method('setToken')
->with($this->token);
$loginEvent = new InteractiveLoginEvent($this->request, $this->token);
$this->dispatcher
->expects($this->once())
->method('dispatch')
->with($this->equalTo(SecurityEvents::INTERACTIVE_LOGIN), $this->equalTo($loginEvent))
;
$handler = new GuardAuthenticatorHandler($this->tokenStorage, $this->dispatcher);
$handler->authenticateWithToken($this->token, $this->request);
}
public function testHandleAuthenticationSuccess()
{
$providerKey = 'my_handleable_firewall';
$response = new Response('Guard all the things!');
$this->guardAuthenticator->expects($this->once())
->method('onAuthenticationSuccess')
->with($this->request, $this->token, $providerKey)
->will($this->returnValue($response));
$handler = new GuardAuthenticatorHandler($this->tokenStorage, $this->dispatcher);
$actualResponse = $handler->handleAuthenticationSuccess($this->token, $this->request, $this->guardAuthenticator, $providerKey);
$this->assertSame($response, $actualResponse);
}
public function testHandleAuthenticationFailure()
{
// setToken() not called - getToken() will return null, so there's nothing to clear
$this->tokenStorage->expects($this->never())
->method('setToken')
->with(null);
$authException = new AuthenticationException('Bad password!');
$response = new Response('Try again, but with the right password!');
$this->guardAuthenticator->expects($this->once())
->method('onAuthenticationFailure')
->with($this->request, $authException)
->will($this->returnValue($response));
$handler = new GuardAuthenticatorHandler($this->tokenStorage, $this->dispatcher);
$actualResponse = $handler->handleAuthenticationFailure($authException, $this->request, $this->guardAuthenticator, 'firewall_provider_key');
$this->assertSame($response, $actualResponse);
}
/**
* @dataProvider getTokenClearingTests
*/
public function testHandleAuthenticationClearsToken($tokenClass, $tokenProviderKey, $actualProviderKey, $shouldTokenBeCleared)
{
$token = $this->getMockBuilder($tokenClass)
->disableOriginalConstructor()
->getMock();
$token->expects($this->any())
->method('getProviderKey')
->will($this->returnValue($tokenProviderKey));
// make the $token be the current token
$this->tokenStorage->expects($this->once())
->method('getToken')
->will($this->returnValue($token));
$this->tokenStorage->expects($shouldTokenBeCleared ? $this->once() : $this->never())
->method('setToken')
->with(null);
$authException = new AuthenticationException('Bad password!');
$response = new Response('Try again, but with the right password!');
$this->guardAuthenticator->expects($this->once())
->method('onAuthenticationFailure')
->with($this->request, $authException)
->will($this->returnValue($response));
$handler = new GuardAuthenticatorHandler($this->tokenStorage, $this->dispatcher);
$actualResponse = $handler->handleAuthenticationFailure($authException, $this->request, $this->guardAuthenticator, $actualProviderKey);
$this->assertSame($response, $actualResponse);
}
public function getTokenClearingTests()
{
$tests = array();
// correct token class and matching firewall => clear the token
$tests[] = array('Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken', 'the_firewall_key', 'the_firewall_key', true);
$tests[] = array('Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken', 'the_firewall_key', 'different_key', false);
$tests[] = array('Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken', 'the_firewall_key', 'the_firewall_key', false);
return $tests;
}
protected function setUp()
{
$this->tokenStorage = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface');
$this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
$this->token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface');
$this->request = new Request(array(), array(), array(), array(), array(), array());
$this->guardAuthenticator = $this->getMock('Symfony\Component\Security\Guard\GuardAuthenticatorInterface');
}
protected function tearDown()
{
$this->tokenStorage = null;
$this->dispatcher = null;
$this->token = null;
$this->request = null;
$this->guardAuthenticator = null;
}
}

View File

@ -0,0 +1,115 @@
<?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\Guard\Tests\Provider;
use Symfony\Component\Security\Guard\Provider\GuardAuthenticationProvider;
use Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken;
/**
* @author Ryan Weaver <weaverryan@gmail.com>
*/
class GuardAuthenticationProviderTest extends \PHPUnit_Framework_TestCase
{
private $userProvider;
private $userChecker;
private $preAuthenticationToken;
public function testAuthenticate()
{
$providerKey = 'my_cool_firewall';
$authenticatorA = $this->getMock('Symfony\Component\Security\Guard\GuardAuthenticatorInterface');
$authenticatorB = $this->getMock('Symfony\Component\Security\Guard\GuardAuthenticatorInterface');
$authenticatorC = $this->getMock('Symfony\Component\Security\Guard\GuardAuthenticatorInterface');
$authenticators = array($authenticatorA, $authenticatorB, $authenticatorC);
// called 2 times - for authenticator A and B (stops on B because of match)
$this->preAuthenticationToken->expects($this->exactly(2))
->method('getGuardProviderKey')
// it will return the "1" index, which will match authenticatorB
->will($this->returnValue('my_cool_firewall_1'));
$enteredCredentials = array(
'username' => '_weaverryan_test_user',
'password' => 'guard_auth_ftw',
);
$this->preAuthenticationToken->expects($this->atLeastOnce())
->method('getCredentials')
->will($this->returnValue($enteredCredentials));
// authenticators A and C are never called
$authenticatorA->expects($this->never())
->method('getUser');
$authenticatorC->expects($this->never())
->method('getUser');
$mockedUser = $this->getMock('Symfony\Component\Security\Core\User\UserInterface');
$authenticatorB->expects($this->once())
->method('getUser')
->with($enteredCredentials, $this->userProvider)
->will($this->returnValue($mockedUser));
// checkCredentials is called
$authenticatorB->expects($this->once())
->method('checkCredentials')
->with($enteredCredentials, $mockedUser);
$authedToken = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface');
$authenticatorB->expects($this->once())
->method('createAuthenticatedToken')
->with($mockedUser, $providerKey)
->will($this->returnValue($authedToken));
// user checker should be called
$this->userChecker->expects($this->once())
->method('checkPreAuth')
->with($mockedUser);
$this->userChecker->expects($this->once())
->method('checkPostAuth')
->with($mockedUser);
$provider = new GuardAuthenticationProvider($authenticators, $this->userProvider, $providerKey, $this->userChecker);
$actualAuthedToken = $provider->authenticate($this->preAuthenticationToken);
$this->assertSame($authedToken, $actualAuthedToken);
}
/**
* @expectedException \Symfony\Component\Security\Core\Exception\AuthenticationExpiredException
*/
public function testGuardWithNoLongerAuthenticatedTriggersLogout()
{
$providerKey = 'my_firewall_abc';
// create a token and mark it as NOT authenticated anymore
// this mimics what would happen if a user "changed" between request
$mockedUser = $this->getMock('Symfony\Component\Security\Core\User\UserInterface');
$token = new PostAuthenticationGuardToken($mockedUser, $providerKey, array('ROLE_USER'));
$token->setAuthenticated(false);
$provider = new GuardAuthenticationProvider(array(), $this->userProvider, $providerKey, $this->userChecker);
$actualToken = $provider->authenticate($token);
}
protected function setUp()
{
$this->userProvider = $this->getMock('Symfony\Component\Security\Core\User\UserProviderInterface');
$this->userChecker = $this->getMock('Symfony\Component\Security\Core\User\UserCheckerInterface');
$this->preAuthenticationToken = $this->getMockBuilder('Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken')
->disableOriginalConstructor()
->getMock();
}
protected function tearDown()
{
$this->userProvider = null;
$this->userChecker = null;
$this->preAuthenticationToken = null;
}
}

View File

@ -0,0 +1,25 @@
<?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\Guard\Token;
/**
* A marker interface that both guard tokens implement.
*
* Any tokens passed to GuardAuthenticationProvider (i.e. any tokens that
* are handled by the guard auth system) must implement this
* interface.
*
* @author Ryan Weaver <ryan@knpuniversity.com>
*/
interface GuardTokenInterface
{
}

View File

@ -0,0 +1,90 @@
<?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\Guard\Token;
use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
use Symfony\Component\Security\Core\Role\RoleInterface;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* Used as an "authenticated" token, though it could be set to not-authenticated later.
*
* If you're using Guard authentication, you *must* use a class that implements
* GuardTokenInterface as your authenticated token (like this class).
*
* @author Ryan Weaver <ryan@knpuniversity.com>n@gmail.com>
*/
class PostAuthenticationGuardToken extends AbstractToken implements GuardTokenInterface
{
private $providerKey;
/**
* @param UserInterface $user The user!
* @param string $providerKey The provider (firewall) key
* @param RoleInterface[]|string[] $roles An array of roles
*
* @throws \InvalidArgumentException
*/
public function __construct(UserInterface $user, $providerKey, array $roles)
{
parent::__construct($roles);
if (empty($providerKey)) {
throw new \InvalidArgumentException('$providerKey (i.e. firewall key) must not be empty.');
}
$this->setUser($user);
$this->providerKey = $providerKey;
// this token is meant to be used after authentication success, so it is always authenticated
// you could set it as non authenticated later if you need to
parent::setAuthenticated(true);
}
/**
* This is meant to be only an authenticated token, where credentials
* have already been used and are thus cleared.
*
* {@inheritdoc}
*/
public function getCredentials()
{
return array();
}
/**
* Returns the provider (firewall) key.
*
* @return string
*/
public function getProviderKey()
{
return $this->providerKey;
}
/**
* {@inheritdoc}
*/
public function serialize()
{
return serialize(array($this->providerKey, parent::serialize()));
}
/**
* {@inheritdoc}
*/
public function unserialize($serialized)
{
list($this->providerKey, $parentStr) = unserialize($serialized);
parent::unserialize($parentStr);
}
}

View File

@ -0,0 +1,65 @@
<?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\Guard\Token;
use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
/**
* The token used by the guard auth system before authentication.
*
* The GuardAuthenticationListener creates this, which is then consumed
* immediately by the GuardAuthenticationProvider. If authentication is
* successful, a different authenticated token is returned
*
* @author Ryan Weaver <ryan@knpuniversity.com>
*/
class PreAuthenticationGuardToken extends AbstractToken implements GuardTokenInterface
{
private $credentials;
private $guardProviderKey;
/**
* @param mixed $credentials
* @param string $guardProviderKey Unique key that bind this token to a specific GuardAuthenticatorInterface
*/
public function __construct($credentials, $guardProviderKey)
{
$this->credentials = $credentials;
$this->guardProviderKey = $guardProviderKey;
parent::__construct(array());
// never authenticated
parent::setAuthenticated(false);
}
public function getGuardProviderKey()
{
return $this->guardProviderKey;
}
/**
* Returns the user credentials, which might be an array of anything you
* wanted to put in there (e.g. username, password, favoriteColor).
*
* @return mixed The user credentials
*/
public function getCredentials()
{
return $this->credentials;
}
public function setAuthenticated($authenticated)
{
throw new \LogicException('The PreAuthenticationGuardToken is *never* authenticated.');
}
}

View File

@ -0,0 +1,36 @@
{
"name": "symfony/security-guard",
"type": "library",
"description": "Symfony Security Component - Guard",
"keywords": [],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=5.3.9",
"symfony/security-core": "~2.8|~3.0.0",
"symfony/security-http": "~2.8|~3.0.0"
},
"require-dev": {
"symfony/phpunit-bridge": "~2.8|~3.0.0",
"psr/log": "~1.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Security\\Guard\\": "" }
},
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "2.8-dev"
}
}
}

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
bootstrap="vendor/autoload.php"
>
<php>
<ini name="error_reporting" value="-1" />
</php>
<testsuites>
<testsuite name="Symfony Security Component Guard Suite">
<directory>./Tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./</directory>
<exclude>
<directory>./vendor</directory>
<directory>./Tests</directory>
</exclude>
</whitelist>
</filter>
</phpunit>

View File

@ -16,15 +16,25 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* AuthenticationEntryPointInterface is the interface used to start the
* authentication scheme.
* Implement this interface for any classes that will be called to "start"
* the authentication process (see method for more details).
*
* @author Fabien Potencier <fabien@symfony.com>
*/
interface AuthenticationEntryPointInterface
{
/**
* Starts the authentication scheme.
* Returns a response that directs the user to authenticate.
*
* This is called when an anonymous request accesses a resource that
* requires authentication. The job of this method is to return some
* response that "helps" the user start into the authentication process.
*
* Examples:
* A) For a form login, you might redirect to the login page
* return new Response('/login');
* B) For an API token authentication system, you return a 401 response
* return new Response('Auth header required', 401);
*
* @param Request $request The request that resulted in an AuthenticationException
* @param AuthenticationException $authException The exception that started the authentication process

View File

@ -15,6 +15,7 @@
<directory>./Acl/Tests/</directory>
<directory>./Core/Tests/</directory>
<directory>./Http/Tests/</directory>
<directory>./Guard/Tests/</directory>
</testsuite>
</testsuites>