Merge AuthenticatorManager and AuthenticatorHandler

The AuthenticatorManager now performs the whole authentication process. This
allows for manual authentication without duplicating or publicly exposing parts
of the process.
This commit is contained in:
Wouter de Jong 2020-03-01 10:21:22 +01:00
parent 44cc76fec2
commit bf1a452e94
31 changed files with 594 additions and 639 deletions

View File

@ -156,8 +156,8 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
->replaceArgument(2, $this->statelessFirewallKeys);
if ($this->authenticatorManagerEnabled) {
$container->getDefinition('security.authenticator_handler')
->replaceArgument(2, $this->statelessFirewallKeys);
$container->getDefinition(SessionListener::class)
->replaceArgument(1, $this->statelessFirewallKeys);
}
if ($config['encoders']) {
@ -444,25 +444,19 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
return new Reference($id);
}, $firewallAuthenticationProviders);
$container
->setDefinition($managerId = 'security.authenticator.manager.'.$id, new ChildDefinition('security.authentication.manager.authenticator'))
->setDefinition($managerId = 'security.authenticator.manager.'.$id, new ChildDefinition('security.authenticator.manager'))
->replaceArgument(0, $authenticators)
->replaceArgument(3, $id)
->addTag('monolog.logger', ['channel' => 'security'])
;
$managerLocator = $container->getDefinition('security.authenticator.managers_locator');
$managerLocator->replaceArgument(0, array_merge($managerLocator->getArgument(0), [$id => new ServiceClosureArgument(new Reference($managerId))]));
// authenticator manager listener
$container
->setDefinition('security.firewall.authenticator.'.$id.'.locator', new ChildDefinition('security.firewall.authenticator.locator'))
->setArguments([$authenticators])
->addTag('container.service_locator')
;
$container
->setDefinition('security.firewall.authenticator.'.$id, new ChildDefinition('security.firewall.authenticator'))
->replaceArgument(0, new Reference($managerId))
->replaceArgument(2, new Reference('security.firewall.authenticator.'.$id.'.locator'))
->replaceArgument(3, $id)
;
$listeners[] = new Reference('security.firewall.authenticator.'.$id);

View File

@ -1,64 +0,0 @@
<?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\EventListener;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticatorHandler;
use Symfony\Component\Security\Http\Firewall\AuthenticatorManagerListener;
/**
* @author Wouter de Jong <wouter@wouterj.nl>
*
* @experimental in 5.1
*/
class LazyAuthenticatorManagerListener extends AuthenticatorManagerListener
{
private $authenticatorLocator;
public function __construct(
AuthenticationManagerInterface $authenticationManager,
AuthenticatorHandler $authenticatorHandler,
ServiceLocator $authenticatorLocator,
string $providerKey,
EventDispatcherInterface $eventDispatcher,
?LoggerInterface $logger = null
) {
parent::__construct($authenticationManager, $authenticatorHandler, [], $providerKey, $eventDispatcher, $logger);
$this->authenticatorLocator = $authenticatorLocator;
}
protected function getSupportingAuthenticators(Request $request): array
{
$authenticators = [];
$lazy = true;
foreach ($this->authenticatorLocator->getProvidedServices() as $key => $type) {
$authenticator = $this->authenticatorLocator->get($key);
if (null !== $this->logger) {
$this->logger->debug('Checking support on guard authenticator.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($authenticator)]);
}
if (false !== $supports = $authenticator->supports($request)) {
$authenticators[$key] = $authenticator;
$lazy = $lazy && null === $supports;
} elseif (null !== $this->logger) {
$this->logger->debug('Guard authenticator does not support the request.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($authenticator)]);
}
}
return [$authenticators, $lazy];
}
}

View File

@ -8,7 +8,7 @@
<defaults public="false" />
<service id="security.authentication.guard_handler"
class="Symfony\Component\Security\Guard\GuardHandler"
class="Symfony\Component\Security\Guard\GuardAuthenticatorHandler"
>
<argument type="service" id="security.token_storage" />
<argument type="service" id="event_dispatcher" on-invalid="null" />
@ -18,7 +18,7 @@
</call>
</service>
<service id="AuthenticatorHandler" alias="security.authentication.guard_handler" />
<service id="Symfony\Component\Security\Guard\GuardAuthenticatorHandler" alias="security.authentication.guard_handler" />
<!-- See GuardAuthenticationFactory -->
<service id="security.authentication.provider.guard"

View File

@ -6,16 +6,16 @@
<services>
<!-- Manager -->
<service id="security.authentication.manager.authenticator"
<service id="security.authenticator.manager"
class="Symfony\Component\Security\Http\Authentication\AuthenticatorManager"
abstract="true"
>
<argument type="abstract">authenticators</argument>
<argument type="service" id="security.token_storage" />
<argument type="service" id="event_dispatcher" />
<argument type="abstract">provider key</argument>
<argument type="service" id="logger" on-invalid="null" />
<argument>%security.authentication.manager.erase_credentials%</argument>
<call method="setEventDispatcher">
<argument type="service" id="event_dispatcher" />
</call>
</service>
<service id="security.authenticator.managers_locator"
@ -23,39 +23,22 @@
<argument type="collection" />
</service>
<service id="security.authentication.manager"
class="Symfony\Bundle\SecurityBundle\Security\FirewallAwareAuthenticatorManager">
<service id="security.user_authenticator"
class="Symfony\Bundle\SecurityBundle\Security\UserAuthenticator">
<argument type="service" id="security.firewall.map" />
<argument type="service" id="security.authenticator.managers_locator" />
<argument type="service" id="request_stack" />
</service>
<service id="Symfony\Component\Security\Http\Authentication\UserAuthenticatorInterface" alias="security.user_authenticator" />
<service id="security.authentication.manager"
class="Symfony\Component\Security\Http\Authentication\NoopAuthenticationManager"/>
<service id="Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface" alias="security.authentication.manager" />
<service id="security.authenticator_handler"
class="Symfony\Component\Security\Http\Authentication\AuthenticatorHandler"
>
<argument type="service" id="security.token_storage" />
<argument type="service" id="event_dispatcher" on-invalid="null" />
<argument /> <!-- stateless firewall keys -->
<call method="setSessionAuthenticationStrategy">
<argument type="service" id="security.authentication.session_strategy" />
</call>
</service>
<service id="security.firewall.authenticator.locator"
class="Symfony\Component\DependencyInjection\ServiceLocator"
abstract="true" />
<service id="security.firewall.authenticator"
class="Symfony\Bundle\SecurityBundle\EventListener\LazyAuthenticatorManagerListener"
class="Symfony\Component\Security\Http\Firewall\AuthenticatorManagerListener"
abstract="true">
<tag name="monolog.logger" channel="security" />
<argument type="abstract">authenticator manager</argument>
<argument type="service" id="security.authenticator_handler" />
<argument/> <!-- authenticator locator -->
<argument/> <!-- provider key -->
<argument type="service" id="event_dispatcher" />
<argument type="service" id="logger" on-invalid="null" />
</service>
<!-- Listeners -->
@ -75,6 +58,12 @@
<argument type="service" id="Symfony\Component\Security\Core\User\UserCheckerInterface" />
</service>
<service id="security.listener.session" class="Symfony\Component\Security\Http\EventListener\SessionStrategyListener">
<tag name="kernel.event_subscriber" />
<argument type="service" id="security.authentication.session_strategy" />
<argument type="abstract">stateless firewall keys</argument>
</service>
<service id="security.listener.remember_me"
class="Symfony\Component\Security\Http\EventListener\RememberMeListener"
abstract="true">

View File

@ -1,48 +0,0 @@
<?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\Security;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\LogicException;
/**
* A decorator that delegates all method calls to the authenticator
* manager of the current firewall.
*
* @author Wouter de Jong <wouter@wouterj.nl>
*/
class FirewallAwareAuthenticatorManager implements AuthenticationManagerInterface
{
private $firewallMap;
private $authenticatorManagers;
private $requestStack;
public function __construct(FirewallMap $firewallMap, ServiceLocator $authenticatorManagers, RequestStack $requestStack)
{
$this->firewallMap = $firewallMap;
$this->authenticatorManagers = $authenticatorManagers;
$this->requestStack = $requestStack;
}
public function authenticate(TokenInterface $token)
{
$firewallConfig = $this->firewallMap->getFirewallConfig($this->requestStack->getMasterRequest());
if (null === $firewallConfig) {
throw new LogicException('Cannot call authenticate on this request, as it is not behind a firewall.');
}
return $this->authenticatorManagers->get($firewallConfig->getName())->authenticate($token);
}
}

View File

@ -0,0 +1,59 @@
<?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\Security;
use Psr\Container\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\LogicException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Http\Authentication\UserAuthenticatorInterface;
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
/**
* A decorator that delegates all method calls to the authenticator
* manager of the current firewall.
*
* @author Wouter de Jong <wouter@wouterj.nl>
*
* @final
* @experimental in Symfony 5.1
*/
class UserAuthenticator implements UserAuthenticatorInterface
{
private $firewallMap;
private $userAuthenticators;
private $requestStack;
public function __construct(FirewallMap $firewallMap, ContainerInterface $userAuthenticators, RequestStack $requestStack)
{
$this->firewallMap = $firewallMap;
$this->userAuthenticators = $userAuthenticators;
$this->requestStack = $requestStack;
}
public function authenticateUser(UserInterface $user, AuthenticatorInterface $authenticator, Request $request): ?Response
{
return $this->getUserAuthenticator()->authenticateUser($user, $authenticator, $request);
}
private function getUserAuthenticator(): UserAuthenticatorInterface
{
$firewallConfig = $this->firewallMap->getFirewallConfig($this->requestStack->getMasterRequest());
if (null === $firewallConfig) {
throw new LogicException('Cannot call authenticate on this request, as it is not behind a firewall.');
}
return $this->userAuthenticators->get($firewallConfig->getName());
}
}

View File

@ -19,8 +19,8 @@ use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterfac
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Guard\AuthenticatorInterface;
use Symfony\Component\Security\Guard\GuardHandler;
use Symfony\Component\Security\Guard\Token\PreAuthenticationToken as GuardPreAuthenticationGuardToken;
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;
use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken as GuardPreAuthenticationGuardToken;
use Symfony\Component\Security\Http\Firewall\AbstractListener;
use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface;
@ -45,7 +45,7 @@ class GuardAuthenticationListener extends AbstractListener
* @param string $providerKey The provider (i.e. firewall) key
* @param iterable|AuthenticatorInterface[] $guardAuthenticators The authenticators, with keys that match what's passed to GuardAuthenticationProvider
*/
public function __construct(GuardHandler $guardHandler, AuthenticationManagerInterface $authenticationManager, string $providerKey, iterable $guardAuthenticators, LoggerInterface $logger = null)
public function __construct(GuardAuthenticatorHandler $guardHandler, AuthenticationManagerInterface $authenticationManager, string $providerKey, iterable $guardAuthenticators, LoggerInterface $logger = null)
{
if (empty($providerKey)) {
throw new \InvalidArgumentException('$providerKey must not be empty.');
@ -121,7 +121,7 @@ class GuardAuthenticationListener extends AbstractListener
protected function executeGuardAuthenticators(array $guardAuthenticators, RequestEvent $event): void
{
foreach ($guardAuthenticators as $key => $guardAuthenticator) {
$uniqueGuardKey = $this->providerKey.'_'.$key;;
$uniqueGuardKey = $this->providerKey.'_'.$key;
$this->executeGuardAuthenticator($uniqueGuardKey, $guardAuthenticator, $event);

View File

@ -9,32 +9,30 @@
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authentication;
namespace Symfony\Component\Security\Guard;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
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\AuthenticatorInterface as GuardAuthenticatorInterface;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\SecurityEvents;
use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* A utility class that does much of the *work* during the authentication process.
* 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>
*
* @internal
* @final
*/
class AuthenticatorHandler
class GuardAuthenticatorHandler
{
private $tokenStorage;
private $dispatcher;
@ -66,38 +64,26 @@ class AuthenticatorHandler
}
/**
* Returns the "on success" response for the given Authenticator.
*
* @param AuthenticatorInterface|GuardAuthenticatorInterface $authenticator
* Returns the "on success" response for the given GuardAuthenticator.
*/
public function handleAuthenticationSuccess(TokenInterface $token, Request $request, $authenticator, string $providerKey): ?Response
public function handleAuthenticationSuccess(TokenInterface $token, Request $request, AuthenticatorInterface $guardAuthenticator, string $providerKey): ?Response
{
if (!$authenticator instanceof AuthenticatorInterface && !$authenticator instanceof GuardAuthenticatorInterface) {
throw new \UnexpectedValueException('Invalid authenticator passed to '.__METHOD__.'. Expected AuthenticatorInterface of either Security Core or Security Guard.');
}
$response = $authenticator->onAuthenticationSuccess($request, $token, $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($authenticator), \is_object($response) ? \get_class($response) : \gettype($response)));
throw new \UnexpectedValueException(sprintf('The "%s::onAuthenticationSuccess()" method must return null or a Response object. You returned "%s".', \get_class($guardAuthenticator), get_debug_type($response)));
}
/**
* Convenience method for authenticating the user and returning the
* Response *if any* for success.
*
* @param AuthenticatorInterface|GuardAuthenticatorInterface $authenticator
*/
public function authenticateUserAndHandleSuccess(UserInterface $user, Request $request, $authenticator, string $providerKey): ?Response
public function authenticateUserAndHandleSuccess(UserInterface $user, Request $request, AuthenticatorInterface $authenticator, string $providerKey): ?Response
{
if (!$authenticator instanceof AuthenticatorInterface && !$authenticator instanceof GuardAuthenticatorInterface) {
throw new \UnexpectedValueException('Invalid authenticator passed to '.__METHOD__.'. Expected AuthenticatorInterface of either Security Core or Security Guard.');
}
// create an authenticated token for the User
$token = $authenticator->createAuthenticatedToken($user, $providerKey);
// authenticate this in the system
@ -110,22 +96,16 @@ class AuthenticatorHandler
/**
* Handles an authentication failure and returns the Response for the
* GuardAuthenticator.
*
* @param AuthenticatorInterface|GuardAuthenticatorInterface $authenticator
*/
public function handleAuthenticationFailure(AuthenticationException $authenticationException, Request $request, $authenticator, string $providerKey): ?Response
public function handleAuthenticationFailure(AuthenticationException $authenticationException, Request $request, AuthenticatorInterface $guardAuthenticator, string $providerKey): ?Response
{
if (!$authenticator instanceof AuthenticatorInterface && !$authenticator instanceof GuardAuthenticatorInterface) {
throw new \UnexpectedValueException('Invalid authenticator passed to '.__METHOD__.'. Expected AuthenticatorInterface of either Security Core or Security Guard.');
}
$response = $authenticator->onAuthenticationFailure($request, $authenticationException);
$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($authenticator), get_debug_type($response)));
throw new \UnexpectedValueException(sprintf('The "%s::onAuthenticationFailure()" method must return null or a Response object. You returned "%s".', \get_class($guardAuthenticator), get_debug_type($response)));
}
/**

View File

@ -1,28 +0,0 @@
<?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\Http\Authentication\AuthenticatorHandler;
/**
* 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>
*
* @final
*/
class GuardHandler extends AuthenticatorHandler
{
}

View File

@ -11,25 +11,21 @@
namespace Symfony\Component\Security\Guard\Provider;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface;
use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\AuthenticationExpiredException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
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\Guard\AuthenticatorInterface;
use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface;
use Symfony\Component\Security\Guard\Token\GuardTokenInterface;
use Symfony\Component\Security\Guard\Token\PreAuthenticationToken;
use Symfony\Component\Security\Http\Authentication\AuthenticatorManagerTrait;
use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken;
use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface;
/**
@ -40,8 +36,6 @@ use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface;
*/
class GuardAuthenticationProvider implements AuthenticationProviderInterface
{
use AuthenticatorManagerTrait;
/**
* @var AuthenticatorInterface[]
*/
@ -78,7 +72,7 @@ class GuardAuthenticationProvider implements AuthenticationProviderInterface
throw new \InvalidArgumentException('GuardAuthenticationProvider only supports GuardTokenInterface.');
}
if (!$token instanceof PreAuthenticationToken) {
if (!$token instanceof PreAuthenticationGuardToken) {
/*
* The listener *only* passes PreAuthenticationGuardToken instances.
* This means that an authenticated token (e.g. PostAuthenticationGuardToken)
@ -101,7 +95,7 @@ class GuardAuthenticationProvider implements AuthenticationProviderInterface
$guardAuthenticator = $this->findOriginatingAuthenticator($token);
if (null === $guardAuthenticator) {
throw new AuthenticationException(sprintf('Token with provider key "%s" did not originate from any of the guard authenticators of provider "%s".', $token->getAuthenticatorKey(), $this->providerKey));
throw new AuthenticationException(sprintf('Token with provider key "%s" did not originate from any of the guard authenticators of provider "%s".', $token->getGuardProviderKey(), $this->providerKey));
}
return $this->authenticateViaGuard($guardAuthenticator, $token, $this->providerKey);
@ -109,7 +103,7 @@ class GuardAuthenticationProvider implements AuthenticationProviderInterface
public function supports(TokenInterface $token)
{
if ($token instanceof PreAuthenticationToken) {
if ($token instanceof PreAuthenticationGuardToken) {
return null !== $this->findOriginatingAuthenticator($token);
}
@ -121,12 +115,7 @@ class GuardAuthenticationProvider implements AuthenticationProviderInterface
$this->rememberMeServices = $rememberMeServices;
}
protected function getAuthenticatorKey(string $key): string
{
return $this->providerKey.'_'.$key;
}
private function authenticateViaGuard(AuthenticatorInterface $guardAuthenticator, \Symfony\Component\Security\Http\Authenticator\Token\PreAuthenticationToken $token, string $providerKey): TokenInterface
private function authenticateViaGuard(AuthenticatorInterface $guardAuthenticator, PreAuthenticationGuardToken $token, string $providerKey): TokenInterface
{
// get the user from the GuardAuthenticator
$user = $guardAuthenticator->getUser($token->getCredentials(), $this->userProvider);
@ -160,4 +149,21 @@ class GuardAuthenticationProvider implements AuthenticationProviderInterface
return $authenticatedToken;
}
private function findOriginatingAuthenticator(PreAuthenticationGuardToken $token): ?AuthenticatorInterface
{
// find the *one* Authenticator that this token originated from
foreach ($this->authenticators as $key => $authenticator) {
// get a key that's unique to *this* authenticator
// this MUST be the same as AuthenticatorManagerListener
$uniqueAuthenticatorKey = $this->providerKey.'_'.$key;
if ($uniqueAuthenticatorKey === $token->getGuardProviderKey()) {
return $authenticator;
}
}
// no matching authenticator found
return null;
}
}

View File

@ -18,7 +18,7 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Guard\AuthenticatorInterface;
use Symfony\Component\Security\Guard\Firewall\GuardAuthenticationListener;
use Symfony\Component\Security\Guard\Token\PreAuthenticationToken;
use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken;
/**
* @author Ryan Weaver <weaverryan@gmail.com>
@ -53,7 +53,7 @@ class GuardAuthenticationListenerTest extends TestCase
// a clone of the token that should be created internally
$uniqueGuardKey = 'my_firewall_0';
$nonAuthedToken = new PreAuthenticationToken($credentials, $uniqueGuardKey);
$nonAuthedToken = new PreAuthenticationGuardToken($credentials, $uniqueGuardKey);
$this->authenticationManager
->expects($this->once())
@ -267,7 +267,7 @@ class GuardAuthenticationListenerTest extends TestCase
->getMock();
$this->guardAuthenticatorHandler = $this->getMockBuilder(
'Symfony\Component\Security\Guard\GuardHandler'
'Symfony\Component\Security\Guard\GuardAuthenticatorHandler'
)
->disableOriginalConstructor()
->getMock();

View File

@ -18,7 +18,7 @@ use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInt
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Guard\AuthenticatorInterface;
use Symfony\Component\Security\Guard\GuardHandler;
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\SecurityEvents;
use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface;
@ -47,7 +47,7 @@ class GuardAuthenticatorHandlerTest extends TestCase
->with($this->equalTo($loginEvent), $this->equalTo(SecurityEvents::INTERACTIVE_LOGIN))
;
$handler = new GuardHandler($this->tokenStorage, $this->dispatcher);
$handler = new GuardAuthenticatorHandler($this->tokenStorage, $this->dispatcher);
$handler->authenticateWithToken($this->token, $this->request);
}
@ -60,7 +60,7 @@ class GuardAuthenticatorHandlerTest extends TestCase
->with($this->request, $this->token, $providerKey)
->willReturn($response);
$handler = new GuardHandler($this->tokenStorage, $this->dispatcher);
$handler = new GuardAuthenticatorHandler($this->tokenStorage, $this->dispatcher);
$actualResponse = $handler->handleAuthenticationSuccess($this->token, $this->request, $this->guardAuthenticator, $providerKey);
$this->assertSame($response, $actualResponse);
}
@ -79,7 +79,7 @@ class GuardAuthenticatorHandlerTest extends TestCase
->with($this->request, $authException)
->willReturn($response);
$handler = new GuardHandler($this->tokenStorage, $this->dispatcher);
$handler = new GuardAuthenticatorHandler($this->tokenStorage, $this->dispatcher);
$actualResponse = $handler->handleAuthenticationFailure($authException, $this->request, $this->guardAuthenticator, 'firewall_provider_key');
$this->assertSame($response, $actualResponse);
}
@ -100,7 +100,7 @@ class GuardAuthenticatorHandlerTest extends TestCase
->with($this->request, $authException)
->willReturn($response);
$handler = new GuardHandler($this->tokenStorage, $this->dispatcher);
$handler = new GuardAuthenticatorHandler($this->tokenStorage, $this->dispatcher);
$actualResponse = $handler->handleAuthenticationFailure($authException, $this->request, $this->guardAuthenticator, $actualProviderKey);
$this->assertSame($response, $actualResponse);
}
@ -124,7 +124,7 @@ class GuardAuthenticatorHandlerTest extends TestCase
->method('setToken')
->with($this->token);
$handler = new GuardHandler($this->tokenStorage, $this->dispatcher);
$handler = new GuardAuthenticatorHandler($this->tokenStorage, $this->dispatcher);
$handler->authenticateWithToken($this->token, $this->request);
}
@ -136,7 +136,7 @@ class GuardAuthenticatorHandlerTest extends TestCase
->method('onAuthentication')
->with($this->request, $this->token);
$handler = new GuardHandler($this->tokenStorage, $this->dispatcher);
$handler = new GuardAuthenticatorHandler($this->tokenStorage, $this->dispatcher);
$handler->setSessionAuthenticationStrategy($this->sessionStrategy);
$handler->authenticateWithToken($this->token, $this->request);
}
@ -148,7 +148,7 @@ class GuardAuthenticatorHandlerTest extends TestCase
$this->sessionStrategy->expects($this->never())
->method('onAuthentication');
$handler = new GuardHandler($this->tokenStorage, $this->dispatcher, ['some_provider_key']);
$handler = new GuardAuthenticatorHandler($this->tokenStorage, $this->dispatcher, ['some_provider_key']);
$handler->setSessionAuthenticationStrategy($this->sessionStrategy);
$handler->authenticateWithToken($this->token, $this->request, 'some_provider_key');
}

View File

@ -18,7 +18,7 @@ use Symfony\Component\Security\Guard\AuthenticatorInterface;
use Symfony\Component\Security\Guard\Provider\GuardAuthenticationProvider;
use Symfony\Component\Security\Guard\Token\GuardTokenInterface;
use Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken;
use Symfony\Component\Security\Guard\Token\PreAuthenticationToken;
use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken;
/**
* @author Ryan Weaver <weaverryan@gmail.com>
@ -143,11 +143,11 @@ class GuardAuthenticationProviderTest extends TestCase
$mockedUser = $this->getMockBuilder('Symfony\Component\Security\Core\User\UserInterface')->getMock();
$provider = new GuardAuthenticationProvider($authenticators, $this->userProvider, 'first_firewall', $this->userChecker);
$token = new PreAuthenticationToken($mockedUser, 'first_firewall_1');
$token = new PreAuthenticationGuardToken($mockedUser, 'first_firewall_1');
$supports = $provider->supports($token);
$this->assertTrue($supports);
$token = new PreAuthenticationToken($mockedUser, 'second_firewall_0');
$token = new PreAuthenticationGuardToken($mockedUser, 'second_firewall_0');
$supports = $provider->supports($token);
$this->assertFalse($supports);
}
@ -162,7 +162,7 @@ class GuardAuthenticationProviderTest extends TestCase
$mockedUser = $this->getMockBuilder('Symfony\Component\Security\Core\User\UserInterface')->getMock();
$provider = new GuardAuthenticationProvider($authenticators, $this->userProvider, 'first_firewall', $this->userChecker);
$token = new PreAuthenticationToken($mockedUser, 'second_firewall_0');
$token = new PreAuthenticationGuardToken($mockedUser, 'second_firewall_0');
$provider->authenticate($token);
}
@ -171,7 +171,7 @@ class GuardAuthenticationProviderTest extends TestCase
$this->userProvider = $this->getMockBuilder('Symfony\Component\Security\Core\User\UserProviderInterface')->getMock();
$this->userChecker = $this->getMockBuilder('Symfony\Component\Security\Core\User\UserCheckerInterface')->getMock();
$this->preAuthenticationToken = $this->getMockBuilder(
'Symfony\Component\Security\Guard\Token\PreAuthenticationToken'
'Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken'
)
->disableOriginalConstructor()
->getMock();

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 AuthenticatorInterface
*/
public function __construct($credentials, string $guardProviderKey)
{
$this->credentials = $credentials;
$this->guardProviderKey = $guardProviderKey;
parent::__construct([]);
// 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(bool $authenticated)
{
throw new \LogicException('The PreAuthenticationGuardToken is *never* authenticated.');
}
}

View File

@ -1,29 +0,0 @@
<?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;
/**
* 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 PreAuthenticationToken extends \Symfony\Component\Security\Http\Authenticator\Token\CorePreAuthenticationGuardToken implements GuardTokenInterface
{
public function getGuardKey()
{
return $this->getAuthenticatorKey();
}
}

View File

@ -11,109 +11,206 @@
namespace Symfony\Component\Security\Http\Authentication;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Psr\Log\LoggerInterface;
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\AuthenticationEvents;
use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
use Symfony\Component\Security\Http\Authenticator\Token\PreAuthenticationToken;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\AuthenticationEvents;
use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent;
use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\AuthenticationExpiredException;
use Symfony\Component\Security\Core\Exception\ProviderNotFoundException;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\Event\LoginFailureEvent;
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent;
use Symfony\Component\Security\Http\SecurityEvents;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* @author Wouter de Jong <wouter@wouterj.nl>
* @author Ryan Weaver <ryan@knpuniversity.com>
* @author Ryan Weaver <ryan@symfonycasts.com>
* @author Amaury Leroux de Lens <amaury@lerouxdelens.com>
*
* @experimental in 5.1
*/
class AuthenticatorManager implements AuthenticationManagerInterface
class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthenticatorInterface
{
use AuthenticatorManagerTrait;
private $authenticators;
private $tokenStorage;
private $eventDispatcher;
private $eraseCredentials;
private $logger;
private $providerKey;
/**
* @param AuthenticatorInterface[] $authenticators The authenticators, with keys that match what's passed to AuthenticatorManagerListener
* @param AuthenticatorInterface[] $authenticators The authenticators, with their unique providerKey as key
*/
public function __construct(iterable $authenticators, EventDispatcherInterface $eventDispatcher, string $providerKey, bool $eraseCredentials = true)
public function __construct(iterable $authenticators, TokenStorageInterface $tokenStorage, EventDispatcherInterface $eventDispatcher, string $providerKey, ?LoggerInterface $logger = null, bool $eraseCredentials = true)
{
$this->authenticators = $authenticators;
$this->tokenStorage = $tokenStorage;
$this->eventDispatcher = $eventDispatcher;
$this->providerKey = $providerKey;
$this->logger = $logger;
$this->eraseCredentials = $eraseCredentials;
}
public function setEventDispatcher(EventDispatcherInterface $dispatcher)
public function authenticateUser(UserInterface $user, AuthenticatorInterface $authenticator, Request $request): ?Response
{
$this->eventDispatcher = $dispatcher;
// create an authenticated token for the User
$token = $authenticator->createAuthenticatedToken($user, $this->providerKey);
// authenticate this in the system
$this->saveAuthenticatedToken($token, $request);
// return the success metric
return $this->handleAuthenticationSuccess($token, $request, $authenticator);
}
public function authenticate(TokenInterface $token)
public function supports(Request $request): ?bool
{
if (!$token instanceof PreAuthenticationToken) {
/*
* The listener *only* passes PreAuthenticationToken instances.
* This means that an authenticated token (e.g. PostAuthenticationToken)
* 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.
*/
if (null !== $this->logger) {
$context = ['firewall_key' => $this->providerKey];
// this should never happen - but technically, the token is
// authenticated... so it could just be returned
if ($token->isAuthenticated()) {
return $token;
if ($this->authenticators instanceof \Countable || \is_array($this->authenticators)) {
$context['authenticators'] = \count($this->authenticators);
}
// this AccountStatusException causes the user to be logged out
throw new AuthenticationExpiredException();
$this->logger->debug('Checking for guard authentication credentials.', $context);
}
$authenticator = $this->findOriginatingAuthenticator($token);
if (null === $authenticator) {
$this->handleFailure(new ProviderNotFoundException(sprintf('Token with provider key "%s" did not originate from any of the authenticators.', $token->getAuthenticatorKey())), $token);
$authenticators = [];
$lazy = true;
foreach ($this->authenticators as $key => $authenticator) {
if (null !== $this->logger) {
$this->logger->debug('Checking support on authenticator.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($authenticator)]);
}
if (false !== $supports = $authenticator->supports($request)) {
$authenticators[$key] = $authenticator;
$lazy = $lazy && null === $supports;
} elseif (null !== $this->logger) {
$this->logger->debug('Authenticator does not support the request.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($authenticator)]);
}
}
if (!$authenticators) {
return false;
}
$request->attributes->set('_guard_authenticators', $authenticators);
return $lazy ? null : true;
}
public function authenticateRequest(Request $request): ?Response
{
$authenticators = $request->attributes->get('_guard_authenticators');
$request->attributes->remove('_guard_authenticators');
if (!$authenticators) {
return null;
}
return $this->executeAuthenticators($authenticators, $request);
}
/**
* @param AuthenticatorInterface[] $authenticators
*/
private function executeAuthenticators(array $authenticators, Request $request): ?Response
{
foreach ($authenticators as $key => $authenticator) {
// recheck if the authenticator still supports the listener. support() is called
// eagerly (before token storage is initialized), whereas authenticate() is called
// lazily (after initialization). This is important for e.g. the AnonymousAuthenticator
// as its support is relying on the (initialized) token in the TokenStorage.
if (false === $authenticator->supports($request)) {
$this->logger->debug('Skipping the "{authenticator}" authenticator as it did not support the request.', ['authenticator' => \get_class($authenticator)]);
continue;
}
$response = $this->executeAuthenticator($key, $authenticator, $request);
if (null !== $response) {
if (null !== $this->logger) {
$this->logger->debug('The "{authenticator}" authenticator set the response. Any later authenticator will not be called', ['authenticator' => \get_class($authenticator)]);
}
return $response;
}
}
return null;
}
private function executeAuthenticator(string $uniqueAuthenticatorKey, AuthenticatorInterface $authenticator, Request $request): ?Response
{
try {
$result = $this->authenticateViaAuthenticator($authenticator, $token, $token->getProviderKey());
} catch (AuthenticationException $exception) {
$this->handleFailure($exception, $token);
}
if (null !== $result) {
if (true === $this->eraseCredentials) {
$result->eraseCredentials();
if (null !== $this->logger) {
$this->logger->debug('Calling getCredentials() on authenticator.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($authenticator)]);
}
if (null !== $this->eventDispatcher) {
$this->eventDispatcher->dispatch(new AuthenticationSuccessEvent($result), AuthenticationEvents::AUTHENTICATION_SUCCESS);
// allow the authenticator to fetch authentication info from the request
$credentials = $authenticator->getCredentials($request);
if (null === $credentials) {
throw new \UnexpectedValueException(sprintf('The return value of "%1$s::getCredentials()" must not be null. Return false from "%1$s::supports()" instead.', \get_class($authenticator)));
}
if (null !== $this->logger) {
$this->logger->debug('Passing token information to the AuthenticatorManager', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($authenticator)]);
}
// authenticate the credentials (e.g. check password)
$token = $this->authenticateViaAuthenticator($authenticator, $credentials);
if (null !== $this->logger) {
$this->logger->info('Authenticator successful!', ['token' => $token, 'authenticator' => \get_class($authenticator)]);
}
// sets the token on the token storage, etc
$this->saveAuthenticatedToken($token, $request);
} catch (AuthenticationException $e) {
// oh no! Authentication failed!
if (null !== $this->logger) {
$this->logger->info('Authenticator failed.', ['exception' => $e, 'authenticator' => \get_class($authenticator)]);
}
$response = $this->handleAuthenticationFailure($e, $request, $authenticator);
if ($response instanceof Response) {
return $response;
}
return null;
}
return $result;
// success!
$response = $this->handleAuthenticationSuccess($token, $request, $authenticator);
if ($response instanceof Response) {
if (null !== $this->logger) {
$this->logger->debug('Authenticator set success response.', ['response' => $response, 'authenticator' => \get_class($authenticator)]);
}
return $response;
}
if (null !== $this->logger) {
$this->logger->debug('Authenticator set no success response: request continues.', ['authenticator' => \get_class($authenticator)]);
}
return null;
}
protected function getAuthenticatorKey(string $key): string
{
// Authenticators in the AuthenticatorManager are already indexed
// by an unique key
return $key;
}
private function authenticateViaAuthenticator(AuthenticatorInterface $authenticator, PreAuthenticationToken $token, string $providerKey): TokenInterface
private function authenticateViaAuthenticator(AuthenticatorInterface $authenticator, $credentials): TokenInterface
{
// get the user from the Authenticator
$user = $authenticator->getUser($token->getCredentials());
$user = $authenticator->getUser($credentials);
if (null === $user) {
throw new UsernameNotFoundException(sprintf('Null returned from "%s::getUser()".', \get_class($authenticator)));
}
@ -122,22 +219,47 @@ class AuthenticatorManager implements AuthenticationManagerInterface
throw new \UnexpectedValueException(sprintf('The %s::getUser() method must return a UserInterface. You returned %s.', \get_class($authenticator), \is_object($user) ? \get_class($user) : \gettype($user)));
}
$event = new VerifyAuthenticatorCredentialsEvent($authenticator, $token, $user);
$event = new VerifyAuthenticatorCredentialsEvent($authenticator, $credentials, $user);
$this->eventDispatcher->dispatch($event);
if (true !== $event->areCredentialsValid()) {
throw new BadCredentialsException(sprintf('Authentication failed because %s did not approve the credentials.', \get_class($authenticator)));
throw new BadCredentialsException(sprintf('Authentication failed because "%s" did not approve the credentials.', \get_class($authenticator)));
}
// turn the UserInterface into a TokenInterface
$authenticatedToken = $authenticator->createAuthenticatedToken($user, $providerKey);
// turn the UserInterface into a TokenInterface
$authenticatedToken = $authenticator->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($authenticator), \is_object($authenticatedToken) ? \get_class($authenticatedToken) : \gettype($authenticatedToken)));
}
if (true === $this->eraseCredentials) {
$authenticatedToken->eraseCredentials();
}
if (null !== $this->eventDispatcher) {
$this->eventDispatcher->dispatch(new AuthenticationSuccessEvent($authenticatedToken), AuthenticationEvents::AUTHENTICATION_SUCCESS);
}
return $authenticatedToken;
}
private function handleFailure(AuthenticationException $exception, TokenInterface $token)
private function saveAuthenticatedToken(TokenInterface $authenticatedToken, Request $request)
{
$this->tokenStorage->setToken($authenticatedToken);
$loginEvent = new InteractiveLoginEvent($request, $authenticatedToken);
$this->eventDispatcher->dispatch($loginEvent, SecurityEvents::INTERACTIVE_LOGIN);
}
private function handleAuthenticationSuccess(TokenInterface $token, Request $request, AuthenticatorInterface $authenticator): ?Response
{
$response = $authenticator->onAuthenticationSuccess($request, $token, $this->providerKey);
$this->eventDispatcher->dispatch($loginSuccessEvent = new LoginSuccessEvent($authenticator, $token, $request, $response, $this->providerKey));
return $loginSuccessEvent->getResponse();
}
private function handleAuthenticationFailure(AuthenticationException $exception, TokenInterface $token)
{
if (null !== $this->eventDispatcher) {
$this->eventDispatcher->dispatch(new AuthenticationFailureEvent($token, $exception), AuthenticationEvents::AUTHENTICATION_FAILURE);
@ -147,4 +269,17 @@ class AuthenticatorManager implements AuthenticationManagerInterface
throw $exception;
}
/**
* Handles an authentication failure and returns the Response for the authenticator.
*/
private function handleAuthenticatorFailure(AuthenticationException $authenticationException, Request $request, AuthenticatorInterface $authenticator): ?Response
{
$response = $authenticator->onAuthenticationFailure($request, $authenticationException);
$this->eventDispatcher->dispatch($loginFailureEvent = new LoginFailureEvent($authenticationException, $authenticator, $request, $response, $this->providerKey));
// returning null is ok, it means they want the request to continue
return $loginFailureEvent->getResponse();
}
}

View File

@ -0,0 +1,37 @@
<?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\Http\Authentication;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Http\Firewall\AbstractListener;
/**
* @author Wouter de Jong <wouter@wouterj.nl>
* @author Ryan Weaver <ryan@symfonycasts.com>
*
* @experimental in Symfony 5.1
*/
interface AuthenticatorManagerInterface
{
/**
* Called to see if authentication should be attempted on this request.
*
* @see AbstractListener::supports()
*/
public function supports(Request $request): ?bool;
/**
* Tries to authenticate the request and returns a response - if any authenticator set one.
*/
public function authenticateRequest(Request $request): ?Response;
}

View File

@ -1,46 +0,0 @@
<?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\Http\Authentication;
use Symfony\Component\Security\Guard\AuthenticatorInterface as GuardAuthenticatorInterface;
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface as CoreAuthenticatorInterface;
use Symfony\Component\Security\Http\Authenticator\Token\PreAuthenticationToken;
/**
* @author Ryan Weaver <ryan@knpuniversity.com>
*
* @internal
*/
trait AuthenticatorManagerTrait
{
/**
* @return CoreAuthenticatorInterface|GuardAuthenticatorInterface|null
*/
private function findOriginatingAuthenticator(PreAuthenticationToken $token)
{
// find the *one* Authenticator that this token originated from
foreach ($this->authenticators as $key => $authenticator) {
// get a key that's unique to *this* authenticator
// this MUST be the same as AuthenticatorManagerListener
$uniqueAuthenticatorKey = $this->getAuthenticatorKey($key);
if ($uniqueAuthenticatorKey === $token->getAuthenticatorKey()) {
return $authenticator;
}
}
// no matching authenticator found
return null;
}
abstract protected function getAuthenticatorKey(string $key): string;
}

View File

@ -0,0 +1,33 @@
<?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\Http\Authentication;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
/**
* This class is used when the authenticator system is activated.
*
* This is used to not break AuthenticationChecker and ContextListener when
* using the authenticator system. Once the authenticator system is no longer
* experimental, this class can be used trigger deprecation notices.
*
* @internal
*
* @author Wouter de Jong <wouter@wouterj.nl>
*/
class NoopAuthenticationManager implements AuthenticationManagerInterface
{
public function authenticate(TokenInterface $token)
{
}
}

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\Http\Authentication;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
/**
* @author Wouter de Jong <wouter@wouterj.nl>
*
* @experimental in Symfony 5.1
*/
interface UserAuthenticatorInterface
{
/**
* Convenience method to manually login a user and return a
* Response *if any* for success.
*/
public function authenticateUser(UserInterface $user, AuthenticatorInterface $authenticator, Request $request): ?Response;
}

View File

@ -18,7 +18,7 @@ use Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken;
/**
* An optional base class that creates the necessary tokens for you.
*
* @author Ryan Weaver <ryan@knpuniversity.com>
* @author Ryan Weaver <ryan@symfonycasts.com>
*
* @experimental in 5.1
*/

View File

@ -21,7 +21,7 @@ use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface
/**
* A base class to make form login authentication easier!
*
* @author Ryan Weaver <ryan@knpuniversity.com>
* @author Ryan Weaver <ryan@symfonycasts.com>
*
* @experimental in 5.1
*/

View File

@ -20,7 +20,7 @@ use Symfony\Component\Security\Core\User\UserInterface;
/**
* The interface for all authenticators.
*
* @author Ryan Weaver <ryan@knpuniversity.com>
* @author Ryan Weaver <ryan@symfonycasts.com>
* @author Amaury Leroux de Lens <amaury@lerouxdelens.com>
* @author Wouter de Jong <wouter@wouterj.nl>
*
@ -32,6 +32,8 @@ interface AuthenticatorInterface
* Does the authenticator support the given Request?
*
* If this returns false, the authenticator will be skipped.
*
* Returning null means authenticate() can be called lazily when accessing the token storage.
*/
public function supports(Request $request): ?bool;

View File

@ -1,73 +0,0 @@
<?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\Http\Authenticator\Token;
use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
/**
* The token used by the authenticator system before authentication.
*
* The AuthenticatorManagerListener creates this, which is then consumed
* immediately by the AuthenticatorManager. If authentication is
* successful, a different authenticated token is returned
*
* @author Ryan Weaver <ryan@knpuniversity.com>
*/
class PreAuthenticationToken extends AbstractToken
{
private $credentials;
private $authenticatorProviderKey;
private $providerKey;
/**
* @param mixed $credentials
* @param string $authenticatorProviderKey Unique key that bind this token to a specific AuthenticatorInterface
* @param string|null $providerKey The general provider key (when using with HTTP, this is the firewall name)
*/
public function __construct($credentials, string $authenticatorProviderKey, ?string $providerKey = null)
{
$this->credentials = $credentials;
$this->authenticatorProviderKey = $authenticatorProviderKey;
$this->providerKey = $providerKey;
parent::__construct([]);
// never authenticated
parent::setAuthenticated(false);
}
public function getProviderKey(): ?string
{
return $this->providerKey;
}
public function getAuthenticatorKey()
{
return $this->authenticatorProviderKey;
}
/**
* 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(bool $authenticated)
{
throw new \LogicException('The PreAuthenticationToken is *never* authenticated.');
}
}

View File

@ -53,6 +53,11 @@ class LoginFailureEvent extends Event
return $this->request;
}
public function setResponse(?Response $response)
{
$this->response = $response;
}
public function getResponse(): ?Response
{
return $this->response;

View File

@ -50,13 +50,18 @@ class LoginSuccessEvent extends Event
return $this->request;
}
public function getResponse(): ?Response
{
return $this->response;
}
public function getProviderKey(): string
{
return $this->providerKey;
}
public function setResponse(?Response $response): void
{
$this->response = $response;
}
public function getResponse(): ?Response
{
return $this->response;
}
}

View File

@ -19,14 +19,14 @@ use Symfony\Contracts\EventDispatcher\Event;
class VerifyAuthenticatorCredentialsEvent extends Event
{
private $authenticator;
private $preAuthenticatedToken;
private $user;
private $credentials;
private $credentialsValid = false;
public function __construct(AuthenticatorInterface $authenticator, TokenInterface $preAuthenticatedToken, ?UserInterface $user)
public function __construct(AuthenticatorInterface $authenticator, $credentials, ?UserInterface $user)
{
$this->authenticator = $authenticator;
$this->preAuthenticatedToken = $preAuthenticatedToken;
$this->credentials = $credentials;
$this->user = $user;
}
@ -35,9 +35,9 @@ class VerifyAuthenticatorCredentialsEvent extends Event
return $this->authenticator;
}
public function getPreAuthenticatedToken(): TokenInterface
public function getCredentials()
{
return $this->preAuthenticatedToken;
return $this->credentials;
}
public function getUser(): ?UserInterface

View File

@ -41,7 +41,7 @@ class AuthenticatingListener implements EventSubscriberInterface
$user = $event->getUser();
$event->setCredentialsValid($this->encoderFactory->getEncoder($user)->isPasswordValid(
$user->getPassword(),
$authenticator->getPassword($event->getPreAuthenticatedToken()->getCredentials()),
$authenticator->getPassword($event->getCredentials()),
$user->getSalt()
));
@ -58,7 +58,7 @@ class AuthenticatingListener implements EventSubscriberInterface
}
if ($authenticator instanceof CustomAuthenticatedInterface) {
$event->setCredentialsValid($authenticator->checkCredentials($event->getPreAuthenticatedToken()->getCredentials(), $event->getUser()));
$event->setCredentialsValid($authenticator->checkCredentials($event->getCredentials(), $event->getUser()));
return;
}

View File

@ -36,12 +36,11 @@ class PasswordMigratingListener implements EventSubscriberInterface
return;
}
$token = $event->getPreAuthenticatedToken();
if (null !== $password = $authenticator->getPassword($token->getCredentials())) {
if (null !== $password = $authenticator->getPassword($event->getCredentials())) {
return;
}
$user = $token->getUser();
$user = $event->getUser();
if (!$user instanceof UserInterface) {
return;
}

View File

@ -0,0 +1,56 @@
<?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\Http\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy;
use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface;
/**
* Migrates/invalidate the session after successful login.
*
* This should be registered as subscriber to any "stateful" firewalls.
*
* @see SessionAuthenticationStrategy
*
* @author Wouter de Jong <wouter@wouterj.nl>
*/
class SessionStrategyListener implements EventSubscriberInterface
{
private $sessionAuthenticationStrategy;
private $statelessProviderKeys;
public function __construct(SessionAuthenticationStrategyInterface $sessionAuthenticationStrategy, array $statelessProviderKeys = [])
{
$this->sessionAuthenticationStrategy = $sessionAuthenticationStrategy;
$this->statelessProviderKeys = $statelessProviderKeys;
}
public function onSuccessfulLogin(LoginSuccessEvent $event): void
{
$request = $event->getRequest();
$token = $event->getAuthenticatedToken();
$providerKey = $event->getProviderKey();
if (!$request->hasSession() || !$request->hasPreviousSession() || \in_array($providerKey, $this->statelessProviderKeys, true)) {
return;
}
$this->sessionAuthenticationStrategy->onAuthentication($request, $token);
}
public static function getSubscribedEvents(): array
{
return [LoginSuccessEvent::class => 'onSuccessfulLogin'];
}
}

View File

@ -11,192 +11,39 @@
namespace Symfony\Component\Security\Http\Firewall;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authentication\AuthenticatorHandler;
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
use Symfony\Component\Security\Http\Authenticator\Token\PreAuthenticationToken;
use Symfony\Component\Security\Http\Event\LoginFailureEvent;
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
use Symfony\Component\Security\Http\Authentication\AuthenticatorManagerInterface;
/**
* Firewall authentication listener that delegates to the authenticator system.
*
* @author Wouter de Jong <wouter@wouterj.nl>
* @author Ryan Weaver <ryan@knpuniversity.com>
* @author Amaury Leroux de Lens <amaury@lerouxdelens.com>
*
* @experimental in 5.1
*/
class AuthenticatorManagerListener extends AbstractListener
{
private $authenticatorManager;
private $authenticatorHandler;
private $authenticators;
protected $providerKey;
private $eventDispatcher;
protected $logger;
/**
* @param AuthenticatorInterface[] $authenticators
*/
public function __construct(AuthenticationManagerInterface $authenticationManager, AuthenticatorHandler $authenticatorHandler, iterable $authenticators, string $providerKey, EventDispatcherInterface $eventDispatcher, ?LoggerInterface $logger = null)
public function __construct(AuthenticatorManagerInterface $authenticationManager)
{
$this->authenticatorManager = $authenticationManager;
$this->authenticatorHandler = $authenticatorHandler;
$this->authenticators = $authenticators;
$this->providerKey = $providerKey;
$this->logger = $logger;
$this->eventDispatcher = $eventDispatcher;
}
public function supports(Request $request): ?bool
{
if (null !== $this->logger) {
$context = ['firewall_key' => $this->providerKey];
if ($this->authenticators instanceof \Countable || \is_array($this->authenticators)) {
$context['authenticators'] = \count($this->authenticators);
}
$this->logger->debug('Checking for guard authentication credentials.', $context);
}
[$authenticators, $lazy] = $this->getSupportingAuthenticators($request);
if (!$authenticators) {
return false;
}
$request->attributes->set('_guard_authenticators', $authenticators);
return $lazy ? null : true;
return $this->authenticatorManager->supports($request);
}
public function authenticate(RequestEvent $event)
public function authenticate(RequestEvent $event): void
{
$request = $event->getRequest();
$authenticators = $request->attributes->get('_guard_authenticators');
$request->attributes->remove('_guard_authenticators');
if (!$authenticators) {
$response = $this->authenticatorManager->authenticateRequest($request);
if (null === $response) {
return;
}
$this->executeAuthenticators($authenticators, $event);
}
protected function getSupportingAuthenticators(Request $request): array
{
$authenticators = [];
$lazy = true;
foreach ($this->authenticators as $key => $authenticator) {
if (null !== $this->logger) {
$this->logger->debug('Checking support on authenticator.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($authenticator)]);
}
if (false !== $supports = $authenticator->supports($request)) {
$authenticators[$key] = $authenticator;
$lazy = $lazy && null === $supports;
} elseif (null !== $this->logger) {
$this->logger->debug('Authenticator does not support the request.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($authenticator)]);
}
}
return [$authenticators, $lazy];
}
/**
* @param AuthenticatorInterface[] $authenticators
*/
protected function executeAuthenticators(array $authenticators, RequestEvent $event): void
{
foreach ($authenticators as $key => $authenticator) {
// recheck if the authenticator still supports the listener. support() is called
// eagerly (before token storage is initialized), whereas authenticate() is called
// lazily (after initialization). This is important for e.g. the AnonymousAuthenticator
// as its support is relying on the (initialized) token in the TokenStorage.
if (false === $authenticator->supports($event->getRequest())) {
$this->logger->debug('Skipping the "{authenticator}" authenticator as it did not support the request.', ['authenticator' => \get_class($authenticator)]);
continue;
}
$this->executeAuthenticator($key, $authenticator, $event);
if ($event->hasResponse()) {
if (null !== $this->logger) {
$this->logger->debug('The "{authenticator}" authenticator set the response. Any later authenticator will not be called', ['authenticator' => \get_class($authenticator)]);
}
break;
}
}
}
private function executeAuthenticator(string $uniqueAuthenticatorKey, AuthenticatorInterface $authenticator, RequestEvent $event): void
{
$request = $event->getRequest();
try {
if (null !== $this->logger) {
$this->logger->debug('Calling getCredentials() on authenticator.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($authenticator)]);
}
// allow the authenticator to fetch authentication info from the request
$credentials = $authenticator->getCredentials($request);
if (null === $credentials) {
throw new \UnexpectedValueException(sprintf('The return value of "%1$s::getCredentials()" must not be null. Return false from "%1$s::supports()" instead.', \get_class($authenticator)));
}
// create a token with the unique key, so that the provider knows which authenticator to use
$token = new PreAuthenticationToken($credentials, $uniqueAuthenticatorKey, $uniqueAuthenticatorKey);
if (null !== $this->logger) {
$this->logger->debug('Passing token information to the AuthenticatorManager', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($authenticator)]);
}
// pass the token into the AuthenticationManager system
// this indirectly calls AuthenticatorManager::authenticate()
$token = $this->authenticatorManager->authenticate($token);
if (null !== $this->logger) {
$this->logger->info('Authenticator successful!', ['token' => $token, 'authenticator' => \get_class($authenticator)]);
}
// sets the token on the token storage, etc
$this->authenticatorHandler->authenticateWithToken($token, $request, $this->providerKey);
} catch (AuthenticationException $e) {
// oh no! Authentication failed!
if (null !== $this->logger) {
$this->logger->info('Authenticator failed.', ['exception' => $e, 'authenticator' => \get_class($authenticator)]);
}
$response = $this->authenticatorHandler->handleAuthenticationFailure($e, $request, $authenticator, $this->providerKey);
if ($response instanceof Response) {
$event->setResponse($response);
}
$this->eventDispatcher->dispatch(new LoginFailureEvent($e, $authenticator, $request, $response, $this->providerKey));
return;
}
// success!
$response = $this->authenticatorHandler->handleAuthenticationSuccess($token, $request, $authenticator, $this->providerKey);
if ($response instanceof Response) {
if (null !== $this->logger) {
$this->logger->debug('Authenticator set success response.', ['response' => $response, 'authenticator' => \get_class($authenticator)]);
}
$event->setResponse($response);
} else {
if (null !== $this->logger) {
$this->logger->debug('Authenticator set no success response: request continues.', ['authenticator' => \get_class($authenticator)]);
}
}
$this->eventDispatcher->dispatch(new LoginSuccessEvent($authenticator, $token, $request, $response, $this->providerKey));
$event->setResponse($response);
}
}