Initial commit (but after some polished work) of the new Guard authentication system

This commit is contained in:
Ryan Weaver 2015-05-17 14:39:26 -04:00
parent 330aa7f729
commit 05af97c7f7
14 changed files with 1459 additions and 0 deletions

View File

@ -0,0 +1,113 @@
<?php
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 <weaverryan@gmail.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

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

View File

@ -0,0 +1,180 @@
<?php
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\NonAuthenticatedGuardToken;
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 <weaverryan@gmail.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, $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 getCredentialsFromRequest on guard configurator', array('firewall_key' => $this->providerKey, 'authenticator' => get_class($guardAuthenticator)));
}
// allow the authenticator to fetch authentication info from the request
$credentials = $guardAuthenticator->getCredentialsFromRequest($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 NonAuthenticatedGuardToken($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);
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 (!$guardAuthenticator->supportsRememberMe()) {
return;
}
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 (!$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,122 @@
<?php
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\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 <weaverryan@gmail.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
* @return null|Response
*/
public function handleAuthenticationFailure(AuthenticationException $authenticationException, Request $request, GuardAuthenticatorInterface $guardAuthenticator)
{
$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,119 @@
<?php
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\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 <weaverryan@gmail.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 authenticate()
*
* 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 getCredentialsFromRequest(Request $request);
/**
* Return a UserInterface object based on the credentials OR throw
* an AuthenticationException
*
* The *credentials* are the return value from getCredentialsFromRequest()
*
* @param mixed $credentials
* @param UserProviderInterface $userProvider
* @throws AuthenticationException
* @return UserInterface
*/
public function authenticate($credentials, UserProviderInterface $userProvider);
/**
* 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 TokenInterface
*/
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,106 @@
<?php
namespace Symfony\Component\Security\Guard\Provider;
use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
use Symfony\Component\Security\Guard\GuardAuthenticatorInterface;
use Symfony\Component\Security\Guard\Token\NonAuthenticatedGuardToken;
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;
/**
* Responsible for accepting the NonAuthenticatedGuardToken and calling
* the correct authenticator to retrieve the authenticated token
*
* @author Ryan Weaver <weaverryan@gmail.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 NonAuthenticatedGuardToken $token
* @return TokenInterface
*/
public function authenticate(TokenInterface $token)
{
if (!$token instanceof NonAuthenticatedGuardToken) {
throw new \InvalidArgumentException('GuardAuthenticationProvider only supports NonAuthenticatedGuardToken');
}
// 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);
}
}
throw new \LogicException(sprintf(
'The correct GuardAuthenticator could not be found for unique key "%s". The listener and provider should be passed the same list of authenticators!?',
$token->getGuardProviderKey()
));
}
private function authenticateViaGuard(GuardAuthenticatorInterface $guardAuthenticator, NonAuthenticatedGuardToken $token)
{
// get the user from the GuardAuthenticator
$user = $guardAuthenticator->authenticate($token->getCredentials(), $this->userProvider);
if (!$user instanceof UserInterface) {
throw new \UnexpectedValueException(sprintf(
'The %s::authenticate method must return a UserInterface. You returned %s',
get_class($guardAuthenticator),
is_object($user) ? get_class($user) : gettype($user)
));
}
// check the AdvancedUserInterface methods!
$this->userChecker->checkPreAuth($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 NonAuthenticatedGuardToken;
}
}

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\NonAuthenticatedGuardToken;
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('getCredentialsFromRequest')
->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 NonAuthenticatedGuardToken($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('getCredentialsFromRequest')
->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('getCredentialsFromRequest')
->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);
$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('getCredentialsFromRequest')
->will($this->returnValue(null));
$authenticatorB
->expects($this->once())
->method('getCredentialsFromRequest')
->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,99 @@
<?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;
/**
* @author Ryan Weaver <weaverryan@gmail.com>
*/
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()
{
$this->tokenStorage->expects($this->once())
->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);
$this->assertSame($response, $actualResponse);
}
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,93 @@
<?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;
/**
* @author Ryan Weaver <weaverryan@gmail.com>
*/
class GuardAuthenticationProviderTest extends \PHPUnit_Framework_TestCase
{
private $userProvider;
private $userChecker;
private $nonAuthedToken;
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->nonAuthedToken->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->nonAuthedToken->expects($this->once())
->method('getCredentials')
->will($this->returnValue($enteredCredentials));
// authenticators A and C are never called
$authenticatorA->expects($this->never())
->method('authenticate');
$authenticatorC->expects($this->never())
->method('authenticate');
$mockedUser = $this->getMock('Symfony\Component\Security\Core\User\UserInterface');
$authenticatorB->expects($this->once())
->method('authenticate')
->with($enteredCredentials, $this->userProvider)
->will($this->returnValue($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->nonAuthedToken);
$this->assertSame($authedToken, $actualAuthedToken);
}
protected function setUp()
{
$this->userProvider = $this->getMock('Symfony\Component\Security\Core\User\UserProviderInterface');
$this->userChecker = $this->getMock('Symfony\Component\Security\Core\User\UserCheckerInterface');
$this->nonAuthedToken = $this->getMockBuilder('Symfony\Component\Security\Guard\Token\NonAuthenticatedGuardToken')
->disableOriginalConstructor()
->getMock();
}
protected function tearDown()
{
$this->userProvider = null;
$this->userChecker = null;
$this->nonAuthedToken = null;
}
}

View File

@ -0,0 +1,96 @@
<?php
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;
/**
* A generic token used by the AbstractGuardAuthenticator
*
* This is meant to be used as an "authenticated" token, though it
* could be set to not-authenticated later.
*
* You're free to use this (it's simple) or use any other token for
* your authenticated token
*
* @author Ryan Weaver <weaverryan@gmail.com>
*/
class GenericGuardToken extends AbstractToken
{
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);
}
/**
* {@inheritdoc}
*/
public function setAuthenticated($isAuthenticated)
{
if ($isAuthenticated) {
throw new \LogicException('Cannot set this token to trusted after instantiation.');
}
parent::setAuthenticated(false);
}
/**
* 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,56 @@
<?php
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 <weaverryan@gmail.com>
*/
class NonAuthenticatedGuardToken extends AbstractToken
{
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 \Exception('The NonAuthenticatedGuardToken is *always* not authenticated');
}
}