From c321f4d73a33598792164788d8618c8de02e008b Mon Sep 17 00:00:00 2001 From: Wouter J Date: Sun, 8 Sep 2019 15:54:23 +0200 Subject: [PATCH 01/30] Created GuardAuthenticationManager to make Guard first-class Security --- .../GuardAuthenticationManager.php | 117 ++++++++++++++++++ .../Component/Security/Core/composer.json | 1 + .../Provider/GuardAuthenticationProvider.php | 66 ++-------- .../GuardAuthenticationProviderTrait.php | 86 +++++++++++++ 4 files changed, 211 insertions(+), 59 deletions(-) create mode 100644 src/Symfony/Component/Security/Core/Authentication/GuardAuthenticationManager.php create mode 100644 src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProviderTrait.php diff --git a/src/Symfony/Component/Security/Core/Authentication/GuardAuthenticationManager.php b/src/Symfony/Component/Security/Core/Authentication/GuardAuthenticationManager.php new file mode 100644 index 0000000000..0afa2121aa --- /dev/null +++ b/src/Symfony/Component/Security/Core/Authentication/GuardAuthenticationManager.php @@ -0,0 +1,117 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authentication; + +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\Core\User\UserCheckerInterface; +use Symfony\Component\Security\Guard\AuthenticatorInterface; +use Symfony\Component\Security\Guard\Provider\GuardAuthenticationProviderTrait; +use Symfony\Component\Security\Guard\Token\GuardTokenInterface; +use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; + +class GuardAuthenticationManager implements AuthenticationManagerInterface +{ + use GuardAuthenticationProviderTrait; + + private $guardAuthenticators; + private $userChecker; + private $eraseCredentials; + /** @var EventDispatcherInterface */ + private $eventDispatcher; + + /** + * @param iterable|AuthenticatorInterface[] $guardAuthenticators The authenticators, with keys that match what's passed to GuardAuthenticationListener + */ + public function __construct($guardAuthenticators, UserCheckerInterface $userChecker, bool $eraseCredentials = true) + { + $this->guardAuthenticators = $guardAuthenticators; + $this->userChecker = $userChecker; + $this->eraseCredentials = $eraseCredentials; + } + + public function setEventDispatcher(EventDispatcherInterface $dispatcher) + { + $this->eventDispatcher = $dispatcher; + } + + public function authenticate(TokenInterface $token) + { + if (!$token instanceof GuardTokenInterface) { + throw new \InvalidArgumentException('GuardAuthenticationManager only supports GuardTokenInterface.'); + } + + if (!$token instanceof PreAuthenticationGuardToken) { + /* + * The listener *only* passes PreAuthenticationGuardToken instances. + * This means that an authenticated token (e.g. PostAuthenticationGuardToken) + * is being passed here, which happens if that token becomes + * "not authenticated" (e.g. happens if the user changes between + * requests). In this case, the user should be logged out. + */ + + // this should never happen - but technically, the token is + // authenticated... so it could just be returned + if ($token->isAuthenticated()) { + return $token; + } + + // this AccountStatusException causes the user to be logged out + throw new AuthenticationExpiredException(); + } + + $guard = $this->findOriginatingAuthenticator($token); + if (null === $guard) { + $this->handleFailure(new ProviderNotFoundException(sprintf('Token with provider key "%s" did not originate from any of the guard authenticators.', $token->getGuardProviderKey())), $token); + } + + try { + $result = $this->authenticateViaGuard($guard, $token); + } catch (AuthenticationException $exception) { + $this->handleFailure($exception, $token); + } + + if (true === $this->eraseCredentials) { + $result->eraseCredentials(); + } + + if (null !== $this->eventDispatcher) { + $this->eventDispatcher->dispatch(new AuthenticationSuccessEvent($result), AuthenticationEvents::AUTHENTICATION_SUCCESS); + } + + return $result; + } + + private function handleFailure(AuthenticationException $exception, TokenInterface $token) + { + if (null !== $this->eventDispatcher) { + $this->eventDispatcher->dispatch(new AuthenticationFailureEvent($token, $exception), AuthenticationEvents::AUTHENTICATION_FAILURE); + } + + $exception->setToken($token); + + throw $exception; + } + + protected function getGuardKey(string $key): string + { + // Guard authenticators in the GuardAuthenticationManager are already indexed + // by an unique key + return $key; + } +} diff --git a/src/Symfony/Component/Security/Core/composer.json b/src/Symfony/Component/Security/Core/composer.json index fc500b285f..83b082bdde 100644 --- a/src/Symfony/Component/Security/Core/composer.json +++ b/src/Symfony/Component/Security/Core/composer.json @@ -20,6 +20,7 @@ "symfony/event-dispatcher-contracts": "^1.1|^2", "symfony/polyfill-php80": "^1.15", "symfony/service-contracts": "^1.1.6|^2", + "symfony/security-guard": "^4.4", "symfony/deprecation-contracts": "^2.1" }, "require-dev": { diff --git a/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php b/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php index 7e9258a9c5..ac5c4cc2d4 100644 --- a/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php +++ b/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php @@ -16,14 +16,9 @@ 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\PreAuthenticationGuardToken; @@ -35,6 +30,8 @@ use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken; */ class GuardAuthenticationProvider implements AuthenticationProviderInterface { + use GuardAuthenticationProviderTrait; + /** * @var AuthenticatorInterface[] */ @@ -99,60 +96,6 @@ class GuardAuthenticationProvider implements AuthenticationProviderInterface return $this->authenticateViaGuard($guardAuthenticator, $token); } - private function authenticateViaGuard(AuthenticatorInterface $guardAuthenticator, PreAuthenticationGuardToken $token): GuardTokenInterface - { - // get the user from the GuardAuthenticator - $user = $guardAuthenticator->getUser($token->getCredentials(), $this->userProvider); - - if (null === $user) { - throw new UsernameNotFoundException(sprintf('Null returned from "%s::getUser()".', get_debug_type($guardAuthenticator))); - } - - if (!$user instanceof UserInterface) { - throw new \UnexpectedValueException(sprintf('The "%s::getUser()" method must return a UserInterface. You returned "%s".', get_debug_type($guardAuthenticator), get_debug_type($user))); - } - - $this->userChecker->checkPreAuth($user); - if (true !== $checkCredentialsResult = $guardAuthenticator->checkCredentials($token->getCredentials(), $user)) { - if (false !== $checkCredentialsResult) { - throw new \TypeError(sprintf('"%s::checkCredentials()" must return a boolean value.', get_debug_type($guardAuthenticator))); - } - - throw new BadCredentialsException(sprintf('Authentication failed because "%s::checkCredentials()" did not return true.', get_debug_type($guardAuthenticator))); - } - if ($this->userProvider instanceof PasswordUpgraderInterface && $guardAuthenticator instanceof PasswordAuthenticatedInterface && null !== $this->passwordEncoder && (null !== $password = $guardAuthenticator->getPassword($token->getCredentials())) && method_exists($this->passwordEncoder, 'needsRehash') && $this->passwordEncoder->needsRehash($user)) { - $this->userProvider->upgradePassword($user, $this->passwordEncoder->encodePassword($user, $password)); - } - $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_debug_type($guardAuthenticator), get_debug_type($authenticatedToken))); - } - - return $authenticatedToken; - } - - private function findOriginatingAuthenticator(PreAuthenticationGuardToken $token): ?AuthenticatorInterface - { - // 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 $guardAuthenticator; - } - } - - // no matching authenticator found - but there will be multiple GuardAuthenticationProvider - // instances that will be checked if you have multiple firewalls. - - return null; - } - public function supports(TokenInterface $token) { if ($token instanceof PreAuthenticationGuardToken) { @@ -161,4 +104,9 @@ class GuardAuthenticationProvider implements AuthenticationProviderInterface return $token instanceof GuardTokenInterface; } + + protected function getGuardKey(string $key): string + { + return $this->providerKey.'_'.$key; + } } diff --git a/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProviderTrait.php b/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProviderTrait.php new file mode 100644 index 0000000000..33e82eb022 --- /dev/null +++ b/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProviderTrait.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Guard\Provider; + +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +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\AuthenticatorInterface; +use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface; +use Symfony\Component\Security\Guard\Token\GuardTokenInterface; +use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken; + +/** + * @author Ryan Weaver + * + * @internal + */ +trait GuardAuthenticationProviderTrait +{ + private function authenticateViaGuard(AuthenticatorInterface $guardAuthenticator, PreAuthenticationGuardToken $token): GuardTokenInterface + { + // get the user from the GuardAuthenticator + $user = $guardAuthenticator->getUser($token->getCredentials(), $this->userProvider); + + if (null === $user) { + throw new UsernameNotFoundException(sprintf('Null returned from "%s::getUser()".', get_debug_type($guardAuthenticator))); + } + + if (!$user instanceof UserInterface) { + throw new \UnexpectedValueException(sprintf('The "%s::getUser()" method must return a UserInterface. You returned "%s".', get_debug_type($guardAuthenticator), get_debug_type($user))); + } + + $this->userChecker->checkPreAuth($user); + if (true !== $checkCredentialsResult = $guardAuthenticator->checkCredentials($token->getCredentials(), $user)) { + if (false !== $checkCredentialsResult) { + throw new \TypeError(sprintf('"%s::checkCredentials()" must return a boolean value.', get_debug_type($guardAuthenticator))); + } + + throw new BadCredentialsException(sprintf('Authentication failed because "%s::checkCredentials()" did not return true.', get_debug_type($guardAuthenticator))); + } + if ($this->userProvider instanceof PasswordUpgraderInterface && $guardAuthenticator instanceof PasswordAuthenticatedInterface && null !== $this->passwordEncoder && (null !== $password = $guardAuthenticator->getPassword($token->getCredentials())) && method_exists($this->passwordEncoder, 'needsRehash') && $this->passwordEncoder->needsRehash($user)) { + $this->userProvider->upgradePassword($user, $this->passwordEncoder->encodePassword($user, $password)); + } + $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_debug_type($guardAuthenticator), get_debug_type($authenticatedToken))); + } + + return $authenticatedToken; + } + + private function findOriginatingAuthenticator(PreAuthenticationGuardToken $token): ?AuthenticatorInterface + { + // 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->getGuardKey($key); + + if ($uniqueGuardKey === $token->getGuardProviderKey()) { + return $guardAuthenticator; + } + } + + // no matching authenticator found - but there will be multiple GuardAuthenticationProvider + // instances that will be checked if you have multiple firewalls. + + return null; + } + + abstract protected function getGuardKey(string $key): string; +} From a6890dbcf056d13b1dc5361d75bf96aa1603d8eb Mon Sep 17 00:00:00 2001 From: Wouter J Date: Sun, 8 Sep 2019 15:55:11 +0200 Subject: [PATCH 02/30] Created HttpBasicAuthenticator and some Guard traits --- .../Authenticator/HttpBasicAuthenticator.php | 91 ++++++++++++++ .../Authenticator/UserProviderTrait.php | 26 ++++ .../Authenticator/UsernamePasswordTrait.php | 48 ++++++++ .../Token/UsernamePasswordToken.php | 3 +- .../HttpBasicAuthenticatorTest.php | 114 ++++++++++++++++++ 5 files changed, 281 insertions(+), 1 deletion(-) create mode 100644 src/Symfony/Component/Security/Core/Authentication/Authenticator/HttpBasicAuthenticator.php create mode 100644 src/Symfony/Component/Security/Core/Authentication/Authenticator/UserProviderTrait.php create mode 100644 src/Symfony/Component/Security/Core/Authentication/Authenticator/UsernamePasswordTrait.php create mode 100644 src/Symfony/Component/Security/Core/Tests/Authentication/Authenticator/HttpBasicAuthenticatorTest.php diff --git a/src/Symfony/Component/Security/Core/Authentication/Authenticator/HttpBasicAuthenticator.php b/src/Symfony/Component/Security/Core/Authentication/Authenticator/HttpBasicAuthenticator.php new file mode 100644 index 0000000000..9ba11d0ddb --- /dev/null +++ b/src/Symfony/Component/Security/Core/Authentication/Authenticator/HttpBasicAuthenticator.php @@ -0,0 +1,91 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authentication\Authenticator; + +use Psr\Log\LoggerInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; +use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Guard\AuthenticatorInterface; + +/** + * @author Wouter de Jong + */ +class HttpBasicAuthenticator implements AuthenticatorInterface +{ + use UserProviderTrait, UsernamePasswordTrait { + UserProviderTrait::getUser as getUserTrait; + } + + private $realmName; + private $userProvider; + private $encoderFactory; + private $logger; + + public function __construct(string $realmName, UserProviderInterface $userProvider, EncoderFactoryInterface $encoderFactory, ?LoggerInterface $logger = null) + { + $this->realmName = $realmName; + $this->userProvider = $userProvider; + $this->encoderFactory = $encoderFactory; + $this->logger = $logger; + } + + public function start(Request $request, AuthenticationException $authException = null) + { + $response = new Response(); + $response->headers->set('WWW-Authenticate', sprintf('Basic realm="%s"', $this->realmName)); + $response->setStatusCode(401); + + return $response; + } + + public function supports(Request $request): bool + { + return $request->headers->has('PHP_AUTH_USER'); + } + + public function getUser($credentials, UserProviderInterface $userProvider): ?UserInterface + { + return $this->getUserTrait($credentials, $this->userProvider); + } + + public function getCredentials(Request $request) + { + return [ + 'username' => $request->headers->get('PHP_AUTH_USER'), + 'password' => $request->headers->get('PHP_AUTH_PW', ''), + ]; + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey): ?Response + { + return null; + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response + { + if (null !== $this->logger) { + $this->logger->info('Basic authentication failed for user.', ['username' => $request->headers->get('PHP_AUTH_USER'), 'exception' => $exception]); + } + + return $this->start($request, $exception); + } + + public function supportsRememberMe(): bool + { + return false; + } +} diff --git a/src/Symfony/Component/Security/Core/Authentication/Authenticator/UserProviderTrait.php b/src/Symfony/Component/Security/Core/Authentication/Authenticator/UserProviderTrait.php new file mode 100644 index 0000000000..b0bad3844e --- /dev/null +++ b/src/Symfony/Component/Security/Core/Authentication/Authenticator/UserProviderTrait.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authentication\Authenticator; + +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Core\User\UserProviderInterface; + +/** + * @author Wouter de Jong + */ +trait UserProviderTrait +{ + public function getUser($credentials, UserProviderInterface $userProvider): ?UserInterface + { + return $userProvider->loadUserByUsername($credentials['username']); + } +} diff --git a/src/Symfony/Component/Security/Core/Authentication/Authenticator/UsernamePasswordTrait.php b/src/Symfony/Component/Security/Core/Authentication/Authenticator/UsernamePasswordTrait.php new file mode 100644 index 0000000000..e791d52405 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Authentication/Authenticator/UsernamePasswordTrait.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authentication\Authenticator; + +use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; +use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; +use Symfony\Component\Security\Core\Exception\BadCredentialsException; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Guard\Token\GuardTokenInterface; + +/** + * @author Wouter de Jong + * + * @property EncoderFactoryInterface $encoderFactory + */ +trait UsernamePasswordTrait +{ + public function checkCredentials($credentials, UserInterface $user): bool + { + if (!$this->encoderFactory instanceof EncoderFactoryInterface) { + throw new \LogicException(\get_class($this).' uses the '.__CLASS__.' trait, which requires an $encoderFactory property to be initialized with an '.EncoderFactoryInterface::class.' implementation.'); + } + + if ('' === $credentials['password']) { + throw new BadCredentialsException('The presented password cannot be empty.'); + } + + if (!$this->encoderFactory->getEncoder($user)->isPasswordValid($user->getPassword(), $credentials['password'], null)) { + throw new BadCredentialsException('The presented password is invalid.'); + } + + return true; + } + + public function createAuthenticatedToken(UserInterface $user, $providerKey): GuardTokenInterface + { + return new UsernamePasswordToken($user, null, $providerKey, $user->getRoles()); + } +} diff --git a/src/Symfony/Component/Security/Core/Authentication/Token/UsernamePasswordToken.php b/src/Symfony/Component/Security/Core/Authentication/Token/UsernamePasswordToken.php index b9eaa68246..b751bde7f1 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Token/UsernamePasswordToken.php +++ b/src/Symfony/Component/Security/Core/Authentication/Token/UsernamePasswordToken.php @@ -12,13 +12,14 @@ namespace Symfony\Component\Security\Core\Authentication\Token; use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Guard\Token\GuardTokenInterface; /** * UsernamePasswordToken implements a username and password token. * * @author Fabien Potencier */ -class UsernamePasswordToken extends AbstractToken +class UsernamePasswordToken extends AbstractToken implements GuardTokenInterface { private $credentials; private $providerKey; diff --git a/src/Symfony/Component/Security/Core/Tests/Authentication/Authenticator/HttpBasicAuthenticatorTest.php b/src/Symfony/Component/Security/Core/Tests/Authentication/Authenticator/HttpBasicAuthenticatorTest.php new file mode 100644 index 0000000000..9e923364ea --- /dev/null +++ b/src/Symfony/Component/Security/Core/Tests/Authentication/Authenticator/HttpBasicAuthenticatorTest.php @@ -0,0 +1,114 @@ +userProvider = $this->getMockBuilder(UserProviderInterface::class)->getMock(); + $this->encoderFactory = $this->getMockBuilder(EncoderFactoryInterface::class)->getMock(); + $this->encoder = $this->getMockBuilder(PasswordEncoderInterface::class)->getMock(); + $this->encoderFactory + ->expects($this->any()) + ->method('getEncoder') + ->willReturn($this->encoder); + } + + public function testValidUsernameAndPasswordServerParameters() + { + $request = new Request([], [], [], [], [], [ + 'PHP_AUTH_USER' => 'TheUsername', + 'PHP_AUTH_PW' => 'ThePassword', + ]); + + $guard = new HttpBasicAuthenticator('test', $this->userProvider, $this->encoderFactory); + $credentials = $guard->getCredentials($request); + $this->assertEquals([ + 'username' => 'TheUsername', + 'password' => 'ThePassword', + ], $credentials); + + $mockedUser = $this->getMockBuilder(UserInterface::class)->getMock(); + $mockedUser->expects($this->any())->method('getPassword')->willReturn('ThePassword'); + + $this->userProvider + ->expects($this->any()) + ->method('loadUserByUsername') + ->with('TheUsername') + ->willReturn($mockedUser); + + $user = $guard->getUser($credentials, $this->userProvider); + $this->assertSame($mockedUser, $user); + + $this->encoder + ->expects($this->any()) + ->method('isPasswordValid') + ->with('ThePassword', 'ThePassword', null) + ->willReturn(true); + + $checkCredentials = $guard->checkCredentials($credentials, $user); + $this->assertTrue($checkCredentials); + } + + /** @dataProvider provideInvalidPasswords */ + public function testInvalidPassword($presentedPassword, $exceptionMessage) + { + $guard = new HttpBasicAuthenticator('test', $this->userProvider, $this->encoderFactory); + + $this->encoder + ->expects($this->any()) + ->method('isPasswordValid') + ->willReturn(false); + + $this->expectException(BadCredentialsException::class); + $this->expectExceptionMessage($exceptionMessage); + + $guard->checkCredentials([ + 'username' => 'TheUsername', + 'password' => $presentedPassword, + ], $this->getMockBuilder(UserInterface::class)->getMock()); + } + + public function provideInvalidPasswords() + { + return [ + ['InvalidPassword', 'The presented password is invalid.'], + ['', 'The presented password cannot be empty.'], + ]; + } + + /** @dataProvider provideMissingHttpBasicServerParameters */ + public function testHttpBasicServerParametersMissing(array $serverParameters) + { + $request = new Request([], [], [], [], [], $serverParameters); + + $guard = new HttpBasicAuthenticator('test', $this->userProvider, $this->encoderFactory); + $this->assertFalse($guard->supports($request)); + } + + public function provideMissingHttpBasicServerParameters() + { + return [ + [[]], + [['PHP_AUTH_PW' => 'ThePassword']], + ]; + } +} From 9b7fddd10c1ded1e19ccb3bd625c178b2128d15f Mon Sep 17 00:00:00 2001 From: Wouter J Date: Sun, 8 Sep 2019 15:55:27 +0200 Subject: [PATCH 03/30] Integrated GuardAuthenticationManager in the SecurityBundle --- .../DependencyInjection/MainConfiguration.php | 1 + .../Factory/CustomAuthenticatorFactory.php | 56 ++++++++++++ .../Factory/GuardFactoryInterface.php | 27 ++++++ .../Security/Factory/HttpBasicFactory.php | 13 ++- .../DependencyInjection/SecurityExtension.php | 86 ++++++++++++++----- .../Resources/config/authenticators.xml | 16 ++++ .../Resources/config/security.xml | 11 ++- .../Bundle/SecurityBundle/SecurityBundle.php | 2 + 8 files changed, 188 insertions(+), 24 deletions(-) create mode 100644 src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/CustomAuthenticatorFactory.php create mode 100644 src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardFactoryInterface.php create mode 100644 src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php index 15ff8246f7..b0d7e5c342 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -73,6 +73,7 @@ class MainConfiguration implements ConfigurationInterface ->booleanNode('hide_user_not_found')->defaultTrue()->end() ->booleanNode('always_authenticate_before_granting')->defaultFalse()->end() ->booleanNode('erase_credentials')->defaultTrue()->end() + ->booleanNode('guard_authentication_manager')->defaultFalse()->end() ->arrayNode('access_decision_manager') ->addDefaultsIfNotSet() ->children() diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/CustomAuthenticatorFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/CustomAuthenticatorFactory.php new file mode 100644 index 0000000000..43c236fcfa --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/CustomAuthenticatorFactory.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory; + +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\Config\Definition\Builder\NodeDefinition; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +class CustomAuthenticatorFactory implements AuthenticatorFactoryInterface, SecurityFactoryInterface +{ + public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint) + { + throw new \LogicException('Custom authenticators are not supported when "security.enable_authenticator_manager" is not set to true.'); + } + + public function getPosition(): string + { + return 'pre_auth'; + } + + public function getKey(): string + { + return 'custom_authenticator'; + } + + /** + * @param ArrayNodeDefinition $builder + */ + public function addConfiguration(NodeDefinition $builder) + { + $builder + ->fixXmlConfig('service') + ->children() + ->arrayNode('services') + ->info('An array of service ids for all of your "authenticators"') + ->requiresAtLeastOneElement() + ->prototype('scalar')->end() + ->end() + ->end() + ; + } + + public function createAuthenticator(ContainerBuilder $container, string $id, array $config, string $userProviderId): array + { + return $config['services']; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardFactoryInterface.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardFactoryInterface.php new file mode 100644 index 0000000000..312f73499a --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardFactoryInterface.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory; + +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * @author Wouter de Jong + */ +interface GuardFactoryInterface +{ + /** + * Creates the Guard service(s) for the provided configuration. + * + * @return string|string[] The Guard service ID(s) to be used by the firewall + */ + public function createGuard(ContainerBuilder $container, string $id, array $config, string $userProviderId); +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php index f731469520..f50698fc67 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php @@ -21,7 +21,7 @@ use Symfony\Component\DependencyInjection\Reference; * * @author Fabien Potencier */ -class HttpBasicFactory implements SecurityFactoryInterface +class HttpBasicFactory implements SecurityFactoryInterface, GuardFactoryInterface { public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint) { @@ -46,6 +46,17 @@ class HttpBasicFactory implements SecurityFactoryInterface return [$provider, $listenerId, $entryPointId]; } + public function createGuard(ContainerBuilder $container, string $id, array $config, string $userProviderId): string + { + $authenticatorId = 'security.authenticator.http_basic.'.$id; + $container + ->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.http_basic')) + ->replaceArgument(0, $config['realm']) + ->replaceArgument(1, new Reference($userProviderId)); + + return $authenticatorId; + } + public function getPosition() { return 'http'; diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 9240133065..73b9a55a7c 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\SecurityBundle\DependencyInjection; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\GuardFactoryInterface; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\RememberMeFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\UserProviderFactoryInterface; @@ -52,6 +53,8 @@ class SecurityExtension extends Extension implements PrependExtensionInterface private $userProviderFactories = []; private $statelessFirewallKeys = []; + private $guardAuthenticationManagerEnabled = false; + public function __construct() { foreach ($this->listenerPositions as $position) { @@ -135,6 +138,8 @@ class SecurityExtension extends Extension implements PrependExtensionInterface $container->setParameter('security.access.always_authenticate_before_granting', $config['always_authenticate_before_granting']); $container->setParameter('security.authentication.hide_user_not_found', $config['hide_user_not_found']); + $this->guardAuthenticationManagerEnabled = $config['guard_authentication_manager']; + $this->createFirewalls($config, $container); $this->createAuthorization($config, $container); $this->createRoleHierarchy($config, $container); @@ -258,8 +263,13 @@ class SecurityExtension extends Extension implements PrependExtensionInterface $authenticationProviders = array_map(function ($id) { return new Reference($id); }, array_values(array_unique($authenticationProviders))); + $authenticationManagerId = 'security.authentication.manager.provider'; + if ($this->guardAuthenticationManagerEnabled) { + $authenticationManagerId = 'security.authentication.manager.guard'; + $container->setAlias('security.authentication.manager', new Alias($authenticationManagerId)); + } $container - ->getDefinition('security.authentication.manager') + ->getDefinition($authenticationManagerId) ->replaceArgument(0, new IteratorArgument($authenticationProviders)) ; @@ -467,31 +477,27 @@ class SecurityExtension extends Extension implements PrependExtensionInterface $key = str_replace('-', '_', $factory->getKey()); if (isset($firewall[$key])) { - if (isset($firewall[$key]['provider'])) { - if (!isset($providerIds[$normalizedName = str_replace('-', '_', $firewall[$key]['provider'])])) { - throw new InvalidConfigurationException(sprintf('Invalid firewall "%s": user provider "%s" not found.', $id, $firewall[$key]['provider'])); - } - $userProvider = $providerIds[$normalizedName]; - } elseif ('remember_me' === $key || 'anonymous' === $key) { - // RememberMeFactory will use the firewall secret when created, AnonymousAuthenticationListener does not load users. - $userProvider = null; + $userProvider = $this->getUserProvider($container, $id, $firewall, $key, $defaultProvider, $providerIds, $contextListenerId); - if ('remember_me' === $key && $contextListenerId) { - $container->getDefinition($contextListenerId)->addTag('security.remember_me_aware', ['id' => $id, 'provider' => 'none']); + if ($this->guardAuthenticationManagerEnabled) { + if (!$factory instanceof GuardFactoryInterface) { + throw new InvalidConfigurationException(sprintf('Cannot configure GuardAuthenticationManager as %s authentication does not support it, set security.guard_authentication_manager to `false`.', $key)); + } + + $authenticators = $factory->createGuard($container, $id, $firewall[$key], $userProvider); + if (\is_array($authenticators)) { + foreach ($authenticators as $i => $authenticator) { + $authenticationProviders[$id.'_'.$key.$i] = $authenticator; + } + } else { + $authenticationProviders[$id.'_'.$key] = $authenticators; } - } elseif ($defaultProvider) { - $userProvider = $defaultProvider; - } elseif (empty($providerIds)) { - $userProvider = sprintf('security.user.provider.missing.%s', $key); - $container->setDefinition($userProvider, (new ChildDefinition('security.user.provider.missing'))->replaceArgument(0, $id)); } else { - throw new InvalidConfigurationException(sprintf('Not configuring explicitly the provider for the "%s" listener on "%s" firewall is ambiguous as there is more than one registered provider.', $key, $id)); + list($provider, $listenerId, $defaultEntryPoint) = $factory->create($container, $id, $firewall[$key], $userProvider, $defaultEntryPoint); + + $listeners[] = new Reference($listenerId); + $authenticationProviders[] = $provider; } - - list($provider, $listenerId, $defaultEntryPoint) = $factory->create($container, $id, $firewall[$key], $userProvider, $defaultEntryPoint); - - $listeners[] = new Reference($listenerId); - $authenticationProviders[] = $provider; $hasListeners = true; } } @@ -504,6 +510,42 @@ class SecurityExtension extends Extension implements PrependExtensionInterface return [$listeners, $defaultEntryPoint]; } + private function getUserProvider(ContainerBuilder $container, string $id, array $firewall, string $factoryKey, ?string $defaultProvider, array $providerIds, ?string $contextListenerId): ?string + { + if (isset($firewall[$factoryKey]['provider'])) { + if (!isset($providerIds[$normalizedName = str_replace('-', '_', $firewall[$factoryKey]['provider'])])) { + throw new InvalidConfigurationException(sprintf('Invalid firewall "%s": user provider "%s" not found.', $id, $firewall[$factoryKey]['provider'])); + } + + return $providerIds[$normalizedName]; + } + + if ('remember_me' === $factoryKey || 'anonymous' === $factoryKey) { + if ('remember_me' === $factoryKey && $contextListenerId) { + $container->getDefinition($contextListenerId)->addTag('security.remember_me_aware', ['id' => $id, 'provider' => 'none']); + } + + // RememberMeFactory will use the firewall secret when created + return null; + } + + if ($defaultProvider) { + return $defaultProvider; + } + + if (!$providerIds) { + $userProvider = sprintf('security.user.provider.missing.%s', $factoryKey); + $container->setDefinition( + $userProvider, + (new ChildDefinition('security.user.provider.missing'))->replaceArgument(0, $id) + ); + + return $userProvider; + } + + throw new InvalidConfigurationException(sprintf('Not configuring explicitly the provider for the "%s" listener on "%s" firewall is ambiguous as there is more than one registered provider.', $factoryKey, $id)); + } + private function createEncoders(array $encoders, ContainerBuilder $container) { $encoderMap = []; diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml new file mode 100644 index 0000000000..4022eafd9d --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml @@ -0,0 +1,16 @@ + + + + + + realm name + user provider + + + + + diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml index 7219210597..0992a92499 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml @@ -45,13 +45,22 @@ - + %security.authentication.manager.erase_credentials% + + + + %security.authentication.manager.erase_credentials% + + + + + diff --git a/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php b/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php index b3243c83d7..d8e6590736 100644 --- a/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php +++ b/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php @@ -17,6 +17,7 @@ use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddSessionDomainC use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterCsrfTokenClearingLogoutHandlerPass; use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterTokenUsageTrackingPass; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AnonymousFactory; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\CustomAuthenticatorFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FormLoginFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FormLoginLdapFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\GuardAuthenticationFactory; @@ -63,6 +64,7 @@ class SecurityBundle extends Bundle $extension->addSecurityListenerFactory(new RemoteUserFactory()); $extension->addSecurityListenerFactory(new GuardAuthenticationFactory()); $extension->addSecurityListenerFactory(new AnonymousFactory()); + $extension->addSecurityListenerFactory(new CustomAuthenticatorFactory()); $extension->addUserProviderFactory(new InMemoryFactory()); $extension->addUserProviderFactory(new LdapFactory()); From a172bacaa6525b6fb14d77cf985731b9bd842ace Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Fri, 13 Dec 2019 17:31:14 +0100 Subject: [PATCH 04/30] Added FormLogin and Anonymous authenticators --- .../Security/Factory/AnonymousFactory.php | 16 +- .../Security/Factory/FormLoginFactory.php | 15 +- .../Factory/GuardFactoryInterface.php | 2 +- .../Security/Factory/HttpBasicFactory.php | 2 +- .../DependencyInjection/SecurityExtension.php | 4 +- .../Resources/config/authenticators.xml | 15 ++ .../Resources/config/security.xml | 2 +- .../Authenticator/AnonymousAuthenticator.php | 70 +++++++++ .../Authenticator/FormLoginAuthenticator.php | 142 ++++++++++++++++++ 9 files changed, 262 insertions(+), 6 deletions(-) create mode 100644 src/Symfony/Component/Security/Core/Authentication/Authenticator/AnonymousAuthenticator.php create mode 100644 src/Symfony/Component/Security/Core/Authentication/Authenticator/FormLoginAuthenticator.php diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php index eb3c930afe..2479cff3ac 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php @@ -19,7 +19,7 @@ use Symfony\Component\DependencyInjection\Parameter; /** * @author Wouter de Jong */ -class AnonymousFactory implements SecurityFactoryInterface +class AnonymousFactory implements SecurityFactoryInterface, GuardFactoryInterface { public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint) { @@ -42,6 +42,20 @@ class AnonymousFactory implements SecurityFactoryInterface return [$providerId, $listenerId, $defaultEntryPoint]; } + public function createGuard(ContainerBuilder $container, string $id, array $config, ?string $userProviderId): string + { + if (null === $config['secret']) { + $config['secret'] = new Parameter('container.build_hash'); + } + + $authenticatorId = 'security.authenticator.anonymous.'.$id; + $container + ->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.anonymous')) + ->replaceArgument(0, $config['secret']); + + return $authenticatorId; + } + public function getPosition() { return 'anonymous'; diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php index af20026406..2a773b34ad 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php @@ -22,7 +22,7 @@ use Symfony\Component\DependencyInjection\Reference; * @author Fabien Potencier * @author Johannes M. Schmitt */ -class FormLoginFactory extends AbstractFactory +class FormLoginFactory extends AbstractFactory implements GuardFactoryInterface { public function __construct() { @@ -96,4 +96,17 @@ class FormLoginFactory extends AbstractFactory return $entryPointId; } + + public function createGuard(ContainerBuilder $container, string $id, array $config, ?string $userProviderId): string + { + $authenticatorId = 'security.authenticator.form_login.'.$id; + $defaultOptions = array_merge($this->defaultSuccessHandlerOptions, $this->options); + $options = array_merge($defaultOptions, array_intersect_key($config, $defaultOptions)); + $container + ->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.form_login')) + ->replaceArgument(1, isset($config['csrf_token_generator']) ? new Reference($config['csrf_token_generator']) : null) + ->replaceArgument(3, $options); + + return $authenticatorId; + } } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardFactoryInterface.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardFactoryInterface.php index 312f73499a..0d1dcb0fad 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardFactoryInterface.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardFactoryInterface.php @@ -23,5 +23,5 @@ interface GuardFactoryInterface * * @return string|string[] The Guard service ID(s) to be used by the firewall */ - public function createGuard(ContainerBuilder $container, string $id, array $config, string $userProviderId); + public function createGuard(ContainerBuilder $container, string $id, array $config, ?string $userProviderId); } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php index f50698fc67..c632ebf587 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php @@ -46,7 +46,7 @@ class HttpBasicFactory implements SecurityFactoryInterface, GuardFactoryInterfac return [$provider, $listenerId, $entryPointId]; } - public function createGuard(ContainerBuilder $container, string $id, array $config, string $userProviderId): string + public function createGuard(ContainerBuilder $container, string $id, array $config, ?string $userProviderId): string { $authenticatorId = 'security.authenticator.http_basic.'.$id; $container diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 73b9a55a7c..5a707a9f26 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -138,7 +138,9 @@ class SecurityExtension extends Extension implements PrependExtensionInterface $container->setParameter('security.access.always_authenticate_before_granting', $config['always_authenticate_before_granting']); $container->setParameter('security.authentication.hide_user_not_found', $config['hide_user_not_found']); - $this->guardAuthenticationManagerEnabled = $config['guard_authentication_manager']; + if ($this->guardAuthenticationManagerEnabled = $config['guard_authentication_manager']) { + $loader->load('authenticators.xml'); + } $this->createFirewalls($config, $container); $this->createAuthorization($config, $container); diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml index 4022eafd9d..588f4d1567 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml @@ -12,5 +12,20 @@ + + + + + + options + + + + + diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml index 0992a92499..99d8550e1b 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml @@ -54,7 +54,7 @@ - + %security.authentication.manager.erase_credentials% diff --git a/src/Symfony/Component/Security/Core/Authentication/Authenticator/AnonymousAuthenticator.php b/src/Symfony/Component/Security/Core/Authentication/Authenticator/AnonymousAuthenticator.php new file mode 100644 index 0000000000..e173792dba --- /dev/null +++ b/src/Symfony/Component/Security/Core/Authentication/Authenticator/AnonymousAuthenticator.php @@ -0,0 +1,70 @@ + + */ +class AnonymousAuthenticator implements AuthenticatorInterface +{ + private $secret; + + public function __construct(string $secret) + { + $this->secret = $secret; + } + + public function start(Request $request, AuthenticationException $authException = null) + { + return new Response(null, Response::HTTP_UNAUTHORIZED); + } + + public function supports(Request $request): ?bool + { + return true; + } + + public function getCredentials(Request $request) + { + return []; + } + + public function getUser($credentials, UserProviderInterface $userProvider) + { + return new User('anon.', null); + } + + public function checkCredentials($credentials, UserInterface $user) + { + return true; + } + + public function createAuthenticatedToken(UserInterface $user, string $providerKey) + { + return new AnonymousToken($this->secret, 'anon.', []); + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception) + { + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey) + { + } + + public function supportsRememberMe(): bool + { + return false; + } +} diff --git a/src/Symfony/Component/Security/Core/Authentication/Authenticator/FormLoginAuthenticator.php b/src/Symfony/Component/Security/Core/Authentication/Authenticator/FormLoginAuthenticator.php new file mode 100644 index 0000000000..72e2bc5ff1 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Authentication/Authenticator/FormLoginAuthenticator.php @@ -0,0 +1,142 @@ + + */ +class FormLoginAuthenticator extends AbstractFormLoginAuthenticator +{ + use TargetPathTrait, UsernamePasswordTrait, UserProviderTrait { + UsernamePasswordTrait::checkCredentials as checkPassword; + } + + private $options; + private $httpUtils; + private $csrfTokenManager; + private $encoderFactory; + + public function __construct(HttpUtils $httpUtils, ?CsrfTokenManagerInterface $csrfTokenManager, EncoderFactoryInterface $encoderFactory, array $options) + { + $this->httpUtils = $httpUtils; + $this->csrfTokenManager = $csrfTokenManager; + $this->encoderFactory = $encoderFactory; + $this->options = array_merge([ + 'username_parameter' => '_username', + 'password_parameter' => '_password', + 'csrf_parameter' => '_csrf_token', + 'csrf_token_id' => 'authenticate', + 'post_only' => true, + + 'always_use_default_target_path' => false, + 'default_target_path' => '/', + 'login_path' => '/login', + 'target_path_parameter' => '_target_path', + 'use_referer' => false, + ], $options); + } + + protected function getLoginUrl(): string + { + return $this->options['login_path']; + } + + public function supports(Request $request): bool + { + return ($this->options['post_only'] ? $request->isMethod('POST') : true) + && $this->httpUtils->checkRequestPath($request, $this->options['check_path']); + } + + public function getCredentials(Request $request): array + { + $credentials = []; + + if (null !== $this->csrfTokenManager) { + $credentials['csrf_token'] = ParameterBagUtils::getRequestParameterValue($request, $this->options['csrf_parameter']); + } + + if ($this->options['post_only']) { + $credentials['username'] = ParameterBagUtils::getParameterBagValue($request->request, $this->options['username_parameter']); + $credentials['password'] = ParameterBagUtils::getParameterBagValue($request->request, $this->options['password_parameter']); + } else { + $credentials['username'] = ParameterBagUtils::getRequestParameterValue($request, $this->options['username_parameter']); + $credentials['password'] = ParameterBagUtils::getRequestParameterValue($request, $this->options['password_parameter']); + } + + if (!\is_string($credentials['username']) && (!\is_object($credentials['username']) || !method_exists($credentials['username'], '__toString'))) { + throw new BadRequestHttpException(sprintf('The key "%s" must be a string, "%s" given.', $this->options['username_parameter'], \gettype($credentials['username']))); + } + + $credentials['username'] = trim($credentials['username']); + + if (\strlen($credentials['username']) > Security::MAX_USERNAME_LENGTH) { + throw new BadCredentialsException('Invalid username.'); + } + + $request->getSession()->set(Security::LAST_USERNAME, $username); + + return $credentials; + } + + public function checkCredentials($credentials, UserInterface $user): bool + { + if (null !== $this->csrfTokenManager) { + if (false === $this->csrfTokenManager->isTokenValid(new CsrfToken($this->options['csrf_token_id'], $credentials['csrf_token']))) { + throw new InvalidCsrfTokenException('Invalid CSRF token.'); + } + } + + return $this->checkPassword($credentials, $user); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey) + { + return $this->httpUtils->createRedirectResponse($request, $this->determineTargetUrl($request, $providerKey)); + } + + private function determineTargetUrl(Request $request, string $providerKey) + { + if ($this->options['always_use_default_target_path']) { + return $this->options['default_target_path']; + } + + if ($targetUrl = ParameterBagUtils::getRequestParameterValue($request, $this->options['target_path_parameter'])) { + return $targetUrl; + } + + if ($targetUrl = $this->getTargetPath($request->getSession(), $providerKey)) { + $this->removeTargetPath($request->getSession(), $providerKey); + + return $targetUrl; + } + + if ($this->options['use_referer'] && $targetUrl = $request->headers->get('Referer')) { + if (false !== $pos = strpos($targetUrl, '?')) { + $targetUrl = substr($targetUrl, 0, $pos); + } + if ($targetUrl && $targetUrl !== $this->httpUtils->generateUri($request, $this->options['login_path'])) { + return $targetUrl; + } + } + + return $this->options['default_target_path']; + } +} From 526f75608b2d5a1bc4041c0361dcb85ef2b4cb22 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Fri, 13 Dec 2019 17:31:35 +0100 Subject: [PATCH 05/30] Added GuardManagerListener This replaces all individual authentication listeners when guard authentication manager is enabled. --- .../DependencyInjection/SecurityExtension.php | 17 +- .../LazyGuardManagerListener.php | 58 +++++++ .../Resources/config/authenticators.xml | 15 ++ .../Firewall/GuardAuthenticationListener.php | 128 +-------------- .../GuardAuthenticatorListenerTrait.php | 154 ++++++++++++++++++ .../Http/Firewall/GuardManagerListener.php | 64 ++++++++ 6 files changed, 314 insertions(+), 122 deletions(-) create mode 100644 src/Symfony/Bundle/SecurityBundle/EventListener/LazyGuardManagerListener.php create mode 100644 src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticatorListenerTrait.php create mode 100644 src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 5a707a9f26..55ebd0d62f 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -264,11 +264,24 @@ class SecurityExtension extends Extension implements PrependExtensionInterface // add authentication providers to authentication manager $authenticationProviders = array_map(function ($id) { return new Reference($id); - }, array_values(array_unique($authenticationProviders))); + }, array_unique($authenticationProviders)); $authenticationManagerId = 'security.authentication.manager.provider'; if ($this->guardAuthenticationManagerEnabled) { $authenticationManagerId = 'security.authentication.manager.guard'; $container->setAlias('security.authentication.manager', new Alias($authenticationManagerId)); + + // guard authentication manager listener + $container + ->setDefinition('security.firewall.guard.'.$name.'locator', new ChildDefinition('security.firewall.guard.locator')) + ->setArguments([$authenticationProviders]) + ->addTag('container.service_locator') + ; + $container + ->setDefinition('security.firewall.guard.'.$name, new ChildDefinition('security.firewall.guard')) + ->replaceArgument(2, new Reference('security.firewall.guard.'.$name.'locator')) + ->replaceArgument(3, $name) + ->addTag('kernel.event_listener', ['event' => KernelEvents::REQUEST]) + ; } $container ->getDefinition($authenticationManagerId) @@ -498,7 +511,7 @@ class SecurityExtension extends Extension implements PrependExtensionInterface list($provider, $listenerId, $defaultEntryPoint) = $factory->create($container, $id, $firewall[$key], $userProvider, $defaultEntryPoint); $listeners[] = new Reference($listenerId); - $authenticationProviders[] = $provider; + $authenticationProviders[$id.'_'.$key] = $provider; } $hasListeners = true; } diff --git a/src/Symfony/Bundle/SecurityBundle/EventListener/LazyGuardManagerListener.php b/src/Symfony/Bundle/SecurityBundle/EventListener/LazyGuardManagerListener.php new file mode 100644 index 0000000000..63b201cb66 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/EventListener/LazyGuardManagerListener.php @@ -0,0 +1,58 @@ + + * + * 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\HttpFoundation\Request; +use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; +use Symfony\Component\Security\Guard\GuardAuthenticatorHandler; +use Symfony\Component\Security\Http\Firewall\GuardManagerListener; + +/** + * @author Wouter de Jong + */ +class LazyGuardManagerListener extends GuardManagerListener +{ + private $guardLocator; + + public function __construct( + AuthenticationManagerInterface $authenticationManager, + GuardAuthenticatorHandler $guardHandler, + ServiceLocator $guardLocator, + string $providerKey, + ?LoggerInterface $logger = null + ) { + parent::__construct($authenticationManager, $guardHandler, [], $providerKey, $logger); + + $this->guardLocator = $guardLocator; + } + + protected function getSupportingGuardAuthenticators(Request $request): array + { + $guardAuthenticators = []; + foreach ($this->guardLocator->getProvidedServices() as $key => $type) { + $guardAuthenticator = $this->guardLocator->get($key); + if (null !== $this->logger) { + $this->logger->debug('Checking support on guard authenticator.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)]); + } + + if ($guardAuthenticator->supports($request)) { + $guardAuthenticators[$key] = $guardAuthenticator; + } elseif (null !== $this->logger) { + $this->logger->debug('Guard authenticator does not support the request.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)]); + } + } + + return $guardAuthenticators; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml index 588f4d1567..f9268c380e 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml @@ -4,6 +4,21 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd"> + + + + + + + + + + + diff --git a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php index 022538731d..35c4bda103 100644 --- a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php +++ b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php @@ -34,6 +34,8 @@ use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; */ class GuardAuthenticationListener extends AbstractListener { + use GuardAuthenticatorListenerTrait; + private $guardHandler; private $authenticationManager; private $providerKey; @@ -73,20 +75,7 @@ class GuardAuthenticationListener extends AbstractListener $this->logger->debug('Checking for guard authentication credentials.', $context); } - $guardAuthenticators = []; - - foreach ($this->guardAuthenticators as $key => $guardAuthenticator) { - if (null !== $this->logger) { - $this->logger->debug('Checking support on guard authenticator.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)]); - } - - if ($guardAuthenticator->supports($request)) { - $guardAuthenticators[$key] = $guardAuthenticator; - } elseif (null !== $this->logger) { - $this->logger->debug('Guard authenticator does not support the request.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)]); - } - } - + $guardAuthenticators = $this->getSupportingGuardAuthenticators($request); if (!$guardAuthenticators) { return false; } @@ -105,86 +94,7 @@ class GuardAuthenticationListener extends AbstractListener $guardAuthenticators = $request->attributes->get('_guard_authenticators'); $request->attributes->remove('_guard_authenticators'); - foreach ($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); - - 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($guardAuthenticator)]); - } - - break; - } - } - } - - private function executeGuardAuthenticator(string $uniqueGuardKey, AuthenticatorInterface $guardAuthenticator, RequestEvent $event) - { - $request = $event->getRequest(); - try { - if (null !== $this->logger) { - $this->logger->debug('Calling getCredentials() on guard authenticator.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)]); - } - - // allow the authenticator to fetch authentication info from the request - $credentials = $guardAuthenticator->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_debug_type($guardAuthenticator))); - } - - // create a token with the unique key, so that the provider knows which authenticator to use - $token = new PreAuthenticationGuardToken($credentials, $uniqueGuardKey); - - if (null !== $this->logger) { - $this->logger->debug('Passing guard token information to the GuardAuthenticationProvider', ['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!', ['token' => $token, 'authenticator' => \get_class($guardAuthenticator)]); - } - - // sets the token on the token storage, etc - $this->guardHandler->authenticateWithToken($token, $request, $this->providerKey); - } catch (AuthenticationException $e) { - // oh no! Authentication failed! - - if (null !== $this->logger) { - $this->logger->info('Guard authentication failed.', ['exception' => $e, 'authenticator' => \get_class($guardAuthenticator)]); - } - - $response = $this->guardHandler->handleAuthenticationFailure($e, $request, $guardAuthenticator, $this->providerKey); - - if ($response instanceof Response) { - $event->setResponse($response); - } - - return; - } - - // success! - $response = $this->guardHandler->handleAuthenticationSuccess($token, $request, $guardAuthenticator, $this->providerKey); - if ($response instanceof Response) { - if (null !== $this->logger) { - $this->logger->debug('Guard authenticator set success response.', ['response' => $response, 'authenticator' => \get_class($guardAuthenticator)]); - } - - $event->setResponse($response); - } else { - if (null !== $this->logger) { - $this->logger->debug('Guard authenticator set no success response: request continues.', ['authenticator' => \get_class($guardAuthenticator)]); - } - } - - // attempt to trigger the remember me functionality - $this->triggerRememberMe($guardAuthenticator, $request, $token, $response); + $this->executeGuardAuthenticators($guardAuthenticators, $event); } /** @@ -195,32 +105,10 @@ class GuardAuthenticationListener extends AbstractListener $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. - */ - private function triggerRememberMe(AuthenticatorInterface $guardAuthenticator, Request $request, TokenInterface $token, Response $response = null) + protected function getGuardKey(string $key): string { - if (null === $this->rememberMeServices) { - if (null !== $this->logger) { - $this->logger->debug('Remember me skipped: it is not configured for the firewall.', ['authenticator' => \get_class($guardAuthenticator)]); - } - - return; - } - - if (!$guardAuthenticator->supportsRememberMe()) { - if (null !== $this->logger) { - $this->logger->debug('Remember me skipped: your authenticator does not support it.', ['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_debug_type($guardAuthenticator))); - } - - $this->rememberMeServices->loginSuccess($request, $response, $token); + // get a key that's unique to *this* guard authenticator + // this MUST be the same as GuardAuthenticationProvider + return $this->providerKey.'_'.$key; } } diff --git a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticatorListenerTrait.php b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticatorListenerTrait.php new file mode 100644 index 0000000000..935f8fa064 --- /dev/null +++ b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticatorListenerTrait.php @@ -0,0 +1,154 @@ + + * @author Amaury Leroux de Lens + * + * @internal + */ +trait GuardAuthenticatorListenerTrait +{ + protected function getSupportingGuardAuthenticators(Request $request): array + { + $guardAuthenticators = []; + foreach ($this->guardAuthenticators as $key => $guardAuthenticator) { + if (null !== $this->logger) { + $this->logger->debug('Checking support on guard authenticator.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)]); + } + + if ($guardAuthenticator->supports($request)) { + $guardAuthenticators[$key] = $guardAuthenticator; + } elseif (null !== $this->logger) { + $this->logger->debug('Guard authenticator does not support the request.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)]); + } + } + + return $guardAuthenticators; + } + + /** + * @param AuthenticatorInterface[] $guardAuthenticators + */ + protected function executeGuardAuthenticators(array $guardAuthenticators, RequestEvent $event): void + { + foreach ($guardAuthenticators as $key => $guardAuthenticator) { + $uniqueGuardKey = $this->getGuardKey($key); + + $this->executeGuardAuthenticator($uniqueGuardKey, $guardAuthenticator, $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($guardAuthenticator)]); + } + + break; + } + } + } + + private function executeGuardAuthenticator(string $uniqueGuardKey, AuthenticatorInterface $guardAuthenticator, RequestEvent $event) + { + $request = $event->getRequest(); + try { + if (null !== $this->logger) { + $this->logger->debug('Calling getCredentials() on guard authenticator.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)]); + } + + // allow the authenticator to fetch authentication info from the request + $credentials = $guardAuthenticator->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_debug_type($guardAuthenticator))); + } + + // create a token with the unique key, so that the provider knows which authenticator to use + $token = new PreAuthenticationGuardToken($credentials, $uniqueGuardKey); + + if (null !== $this->logger) { + $this->logger->debug('Passing guard token information to the GuardAuthenticationProvider', ['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!', ['token' => $token, 'authenticator' => \get_class($guardAuthenticator)]); + } + + // sets the token on the token storage, etc + $this->guardHandler->authenticateWithToken($token, $request, $this->providerKey); + } catch (AuthenticationException $e) { + // oh no! Authentication failed! + + if (null !== $this->logger) { + $this->logger->info('Guard authentication failed.', ['exception' => $e, 'authenticator' => \get_class($guardAuthenticator)]); + } + + $response = $this->guardHandler->handleAuthenticationFailure($e, $request, $guardAuthenticator, $this->providerKey); + + if ($response instanceof Response) { + $event->setResponse($response); + } + + return; + } + + // success! + $response = $this->guardHandler->handleAuthenticationSuccess($token, $request, $guardAuthenticator, $this->providerKey); + if ($response instanceof Response) { + if (null !== $this->logger) { + $this->logger->debug('Guard authenticator set success response.', ['response' => $response, 'authenticator' => \get_class($guardAuthenticator)]); + } + + $event->setResponse($response); + } else { + if (null !== $this->logger) { + $this->logger->debug('Guard authenticator set no success response: request continues.', ['authenticator' => \get_class($guardAuthenticator)]); + } + } + + // attempt to trigger the remember me functionality + $this->triggerRememberMe($guardAuthenticator, $request, $token, $response); + } + + /** + * Checks to see if remember me is supported in the authenticator and + * on the firewall. If it is, the RememberMeServicesInterface is notified. + */ + private function triggerRememberMe(AuthenticatorInterface $guardAuthenticator, Request $request, TokenInterface $token, Response $response = null) + { + if (null === $this->rememberMeServices) { + if (null !== $this->logger) { + $this->logger->debug('Remember me skipped: it is not configured for the firewall.', ['authenticator' => \get_class($guardAuthenticator)]); + } + + return; + } + + if (!$guardAuthenticator->supportsRememberMe()) { + if (null !== $this->logger) { + $this->logger->debug('Remember me skipped: your authenticator does not support it.', ['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_debug_type($guardAuthenticator))); + } + + $this->rememberMeServices->loginSuccess($request, $response, $token); + } + + abstract protected function getGuardKey(string $key): string; +} diff --git a/src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php b/src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php new file mode 100644 index 0000000000..2cfa86d420 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Firewall; + +use Psr\Log\LoggerInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; +use Symfony\Component\Security\Guard\AuthenticatorInterface; +use Symfony\Component\Security\Guard\Firewall\GuardAuthenticatorListenerTrait; +use Symfony\Component\Security\Guard\GuardAuthenticatorHandler; + +/** + * @author Wouter de Jong + */ +class GuardManagerListener +{ + use GuardAuthenticatorListenerTrait; + + private $authenticationManager; + private $guardHandler; + private $guardAuthenticators; + protected $providerKey; + protected $logger; + + /** + * @param AuthenticatorInterface[] $guardAuthenticators + */ + public function __construct(AuthenticationManagerInterface $authenticationManager, GuardAuthenticatorHandler $guardHandler, iterable $guardAuthenticators, string $providerKey, ?LoggerInterface $logger = null) + { + $this->authenticationManager = $authenticationManager; + $this->guardHandler = $guardHandler; + $this->guardAuthenticators = $guardAuthenticators; + $this->providerKey = $providerKey; + $this->logger = $logger; + } + + public function __invoke(RequestEvent $requestEvent) + { + $request = $requestEvent->getRequest(); + $guardAuthenticators = $this->getSupportingGuardAuthenticators($request); + if (!$guardAuthenticators) { + return; + } + + $this->executeGuardAuthenticators($guardAuthenticators, $requestEvent); + } + + protected function getGuardKey(string $key): string + { + // Guard authenticators in the GuardAuthenticationManager are already indexed + // by an unique key + return $key; + } +} From 50132587a186347ec288f85f43e158cb3b4273da Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sun, 26 Jan 2020 15:37:32 +0100 Subject: [PATCH 06/30] Add provider key in PreAuthenticationGuardToken This is required to create the correct authenticated token in the GuardAuthenticationManager. --- .../DependencyInjection/SecurityExtension.php | 37 ++++++++++++------- .../GuardAuthenticationManager.php | 2 +- .../GuardAuthenticatorListenerTrait.php | 2 +- .../Provider/GuardAuthenticationProvider.php | 2 +- .../GuardAuthenticationProviderTrait.php | 4 +- .../Token/PreAuthenticationGuardToken.php | 14 +++++-- .../Http/Firewall/GuardManagerListener.php | 2 +- 7 files changed, 40 insertions(+), 23 deletions(-) diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 55ebd0d62f..94450d2461 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -269,19 +269,6 @@ class SecurityExtension extends Extension implements PrependExtensionInterface if ($this->guardAuthenticationManagerEnabled) { $authenticationManagerId = 'security.authentication.manager.guard'; $container->setAlias('security.authentication.manager', new Alias($authenticationManagerId)); - - // guard authentication manager listener - $container - ->setDefinition('security.firewall.guard.'.$name.'locator', new ChildDefinition('security.firewall.guard.locator')) - ->setArguments([$authenticationProviders]) - ->addTag('container.service_locator') - ; - $container - ->setDefinition('security.firewall.guard.'.$name, new ChildDefinition('security.firewall.guard')) - ->replaceArgument(2, new Reference('security.firewall.guard.'.$name.'locator')) - ->replaceArgument(3, $name) - ->addTag('kernel.event_listener', ['event' => KernelEvents::REQUEST]) - ; } $container ->getDefinition($authenticationManagerId) @@ -431,7 +418,29 @@ class SecurityExtension extends Extension implements PrependExtensionInterface $configuredEntryPoint = isset($firewall['entry_point']) ? $firewall['entry_point'] : null; // Authentication listeners - list($authListeners, $defaultEntryPoint) = $this->createAuthenticationListeners($container, $id, $firewall, $authenticationProviders, $defaultProvider, $providerIds, $configuredEntryPoint, $contextListenerId); + $firewallAuthenticationProviders = []; + list($authListeners, $defaultEntryPoint) = $this->createAuthenticationListeners($container, $id, $firewall, $firewallAuthenticationProviders, $defaultProvider, $providerIds, $configuredEntryPoint, $contextListenerId); + + $authenticationProviders = array_merge($authenticationProviders, $firewallAuthenticationProviders); + + if ($this->guardAuthenticationManagerEnabled) { + // guard authentication manager listener + $container + ->setDefinition('security.firewall.guard.'.$id.'.locator', new ChildDefinition('security.firewall.guard.locator')) + ->setArguments([array_map(function ($id) { + return new Reference($id); + }, $firewallAuthenticationProviders)]) + ->addTag('container.service_locator') + ; + $container + ->setDefinition('security.firewall.guard.'.$id, new ChildDefinition('security.firewall.guard')) + ->replaceArgument(2, new Reference('security.firewall.guard.'.$id.'.locator')) + ->replaceArgument(3, $id) + ->addTag('kernel.event_listener', ['event' => KernelEvents::REQUEST]) + ; + + $listeners[] = new Reference('security.firewall.guard.'.$id); + } $config->replaceArgument(7, $configuredEntryPoint ?: $defaultEntryPoint); diff --git a/src/Symfony/Component/Security/Core/Authentication/GuardAuthenticationManager.php b/src/Symfony/Component/Security/Core/Authentication/GuardAuthenticationManager.php index 0afa2121aa..624b0a678c 100644 --- a/src/Symfony/Component/Security/Core/Authentication/GuardAuthenticationManager.php +++ b/src/Symfony/Component/Security/Core/Authentication/GuardAuthenticationManager.php @@ -81,7 +81,7 @@ class GuardAuthenticationManager implements AuthenticationManagerInterface } try { - $result = $this->authenticateViaGuard($guard, $token); + $result = $this->authenticateViaGuard($guard, $token, $token->getProviderKey()); } catch (AuthenticationException $exception) { $this->handleFailure($exception, $token); } diff --git a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticatorListenerTrait.php b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticatorListenerTrait.php index 935f8fa064..043c51c7a8 100644 --- a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticatorListenerTrait.php +++ b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticatorListenerTrait.php @@ -72,7 +72,7 @@ trait GuardAuthenticatorListenerTrait } // create a token with the unique key, so that the provider knows which authenticator to use - $token = new PreAuthenticationGuardToken($credentials, $uniqueGuardKey); + $token = new PreAuthenticationGuardToken($credentials, $uniqueGuardKey, $this->providerKey); if (null !== $this->logger) { $this->logger->debug('Passing guard token information to the GuardAuthenticationProvider', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)]); diff --git a/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php b/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php index ac5c4cc2d4..04085aaa05 100644 --- a/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php +++ b/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php @@ -93,7 +93,7 @@ class GuardAuthenticationProvider implements AuthenticationProviderInterface 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); + return $this->authenticateViaGuard($guardAuthenticator, $token, $this->providerKey); } public function supports(TokenInterface $token) diff --git a/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProviderTrait.php b/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProviderTrait.php index 33e82eb022..0112256b85 100644 --- a/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProviderTrait.php +++ b/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProviderTrait.php @@ -28,7 +28,7 @@ use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken; */ trait GuardAuthenticationProviderTrait { - private function authenticateViaGuard(AuthenticatorInterface $guardAuthenticator, PreAuthenticationGuardToken $token): GuardTokenInterface + private function authenticateViaGuard(AuthenticatorInterface $guardAuthenticator, PreAuthenticationGuardToken $token, string $providerKey): TokenInterface { // get the user from the GuardAuthenticator $user = $guardAuthenticator->getUser($token->getCredentials(), $this->userProvider); @@ -55,7 +55,7 @@ trait GuardAuthenticationProviderTrait $this->userChecker->checkPostAuth($user); // turn the UserInterface into a TokenInterface - $authenticatedToken = $guardAuthenticator->createAuthenticatedToken($user, $this->providerKey); + $authenticatedToken = $guardAuthenticator->createAuthenticatedToken($user, $providerKey); if (!$authenticatedToken instanceof TokenInterface) { throw new \UnexpectedValueException(sprintf('The "%s::createAuthenticatedToken()" method must return a TokenInterface. You returned "%s".', get_debug_type($guardAuthenticator), get_debug_type($authenticatedToken))); } diff --git a/src/Symfony/Component/Security/Guard/Token/PreAuthenticationGuardToken.php b/src/Symfony/Component/Security/Guard/Token/PreAuthenticationGuardToken.php index 451d96c6ee..460dcf9bda 100644 --- a/src/Symfony/Component/Security/Guard/Token/PreAuthenticationGuardToken.php +++ b/src/Symfony/Component/Security/Guard/Token/PreAuthenticationGuardToken.php @@ -26,15 +26,18 @@ class PreAuthenticationGuardToken extends AbstractToken implements GuardTokenInt { private $credentials; private $guardProviderKey; + private $providerKey; /** - * @param mixed $credentials - * @param string $guardProviderKey Unique key that bind this token to a specific AuthenticatorInterface + * @param mixed $credentials + * @param string $guardProviderKey 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 $guardProviderKey) + public function __construct($credentials, string $guardProviderKey, ?string $providerKey = null) { $this->credentials = $credentials; $this->guardProviderKey = $guardProviderKey; + $this->providerKey = $providerKey; parent::__construct([]); @@ -42,6 +45,11 @@ class PreAuthenticationGuardToken extends AbstractToken implements GuardTokenInt parent::setAuthenticated(false); } + public function getProviderKey(): ?string + { + return $this->providerKey; + } + public function getGuardProviderKey() { return $this->guardProviderKey; diff --git a/src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php b/src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php index 2cfa86d420..b1261bf2b1 100644 --- a/src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php @@ -57,7 +57,7 @@ class GuardManagerListener protected function getGuardKey(string $key): string { - // Guard authenticators in the GuardAuthenticationManager are already indexed + // Guard authenticators in the GuardManagerListener are already indexed // by an unique key return $key; } From 5efa89239550057ff87edd3926562869f179626d Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sun, 26 Jan 2020 15:31:40 +0100 Subject: [PATCH 07/30] Create a new core AuthenticatorInterface This is an iteration on the AuthenticatorInterface of the Guard, to allow more flexibility so it can be used as a real replaced of the authentication providers and listeners. --- .../Factory/EntryPointFactoryInterface.php | 25 ++++ .../Security/Factory/FormLoginFactory.php | 7 +- .../DependencyInjection/SecurityExtension.php | 5 + .../Resources/config/authenticators.xml | 1 + .../Authenticator/AbstractAuthenticator.php | 35 +++++ .../AbstractFormLoginAuthenticator.php | 62 +++++++++ .../Authenticator/AnonymousAuthenticator.php | 29 ++-- .../Authenticator/AuthenticatorInterface.php | 129 ++++++++++++++++++ .../Authenticator/FormLoginAuthenticator.php | 26 +++- .../Authenticator/HttpBasicAuthenticator.php | 20 ++- .../Authenticator/UserProviderTrait.php | 26 ---- .../Authenticator/UsernamePasswordTrait.php | 4 +- .../GuardAuthenticationManager.php | 2 +- .../Token/UsernamePasswordToken.php | 3 +- .../Firewall/GuardAuthenticationListener.php | 4 - .../GuardAuthenticatorListenerTrait.php | 29 +++- .../Guard/GuardAuthenticatorHandler.php | 25 +++- .../GuardAuthenticationProviderTrait.php | 24 +++- .../Http/Firewall/GuardManagerListener.php | 3 +- 19 files changed, 379 insertions(+), 80 deletions(-) create mode 100644 src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/EntryPointFactoryInterface.php create mode 100644 src/Symfony/Component/Security/Core/Authentication/Authenticator/AbstractAuthenticator.php create mode 100644 src/Symfony/Component/Security/Core/Authentication/Authenticator/AbstractFormLoginAuthenticator.php create mode 100644 src/Symfony/Component/Security/Core/Authentication/Authenticator/AuthenticatorInterface.php delete mode 100644 src/Symfony/Component/Security/Core/Authentication/Authenticator/UserProviderTrait.php diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/EntryPointFactoryInterface.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/EntryPointFactoryInterface.php new file mode 100644 index 0000000000..804399ad51 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/EntryPointFactoryInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory; + +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * @author Wouter de Jong + */ +interface EntryPointFactoryInterface +{ + /** + * Creates the entry point and returns the service ID. + */ + public function createEntryPoint(ContainerBuilder $container, string $id, array $config, ?string $defaultEntryPointId): string; +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php index 2a773b34ad..386ba8e462 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php @@ -22,7 +22,7 @@ use Symfony\Component\DependencyInjection\Reference; * @author Fabien Potencier * @author Johannes M. Schmitt */ -class FormLoginFactory extends AbstractFactory implements GuardFactoryInterface +class FormLoginFactory extends AbstractFactory implements GuardFactoryInterface, EntryPointFactoryInterface { public function __construct() { @@ -84,7 +84,7 @@ class FormLoginFactory extends AbstractFactory implements GuardFactoryInterface return $listenerId; } - protected function createEntryPoint(ContainerBuilder $container, string $id, array $config, ?string $defaultEntryPoint) + public function createEntryPoint(ContainerBuilder $container, string $id, array $config, ?string $defaultEntryPoint): string { $entryPointId = 'security.authentication.form_entry_point.'.$id; $container @@ -105,7 +105,8 @@ class FormLoginFactory extends AbstractFactory implements GuardFactoryInterface $container ->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.form_login')) ->replaceArgument(1, isset($config['csrf_token_generator']) ? new Reference($config['csrf_token_generator']) : null) - ->replaceArgument(3, $options); + ->replaceArgument(2, new Reference($userProviderId)) + ->replaceArgument(4, $options); return $authenticatorId; } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 94450d2461..54403cfa4a 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\SecurityBundle\DependencyInjection; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\EntryPointFactoryInterface; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\GuardFactoryInterface; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\RememberMeFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface; @@ -516,6 +517,10 @@ class SecurityExtension extends Extension implements PrependExtensionInterface } else { $authenticationProviders[$id.'_'.$key] = $authenticators; } + + if ($factory instanceof EntryPointFactoryInterface) { + $defaultEntryPoint = $factory->createEntryPoint($container, $id, $firewall[$key], $defaultEntryPoint); + } } else { list($provider, $listenerId, $defaultEntryPoint) = $factory->create($container, $id, $firewall[$key], $userProvider, $defaultEntryPoint); diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml index f9268c380e..9da2d3b8a5 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml @@ -33,6 +33,7 @@ abstract="true"> + user provider options diff --git a/src/Symfony/Component/Security/Core/Authentication/Authenticator/AbstractAuthenticator.php b/src/Symfony/Component/Security/Core/Authentication/Authenticator/AbstractAuthenticator.php new file mode 100644 index 0000000000..8e9bee6f07 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Authentication/Authenticator/AbstractAuthenticator.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authentication\Authenticator; + +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken; + +/** + * An optional base class that creates the necessary tokens for you. + * + * @author Ryan Weaver + */ +abstract class AbstractAuthenticator implements AuthenticatorInterface +{ + /** + * Shortcut to create a PostAuthenticationGuardToken for you, if you don't really + * care about which authenticated token you're using. + * + * @return PostAuthenticationGuardToken + */ + public function createAuthenticatedToken(UserInterface $user, string $providerKey): TokenInterface + { + return new PostAuthenticationGuardToken($user, $providerKey, $user->getRoles()); + } +} diff --git a/src/Symfony/Component/Security/Core/Authentication/Authenticator/AbstractFormLoginAuthenticator.php b/src/Symfony/Component/Security/Core/Authentication/Authenticator/AbstractFormLoginAuthenticator.php new file mode 100644 index 0000000000..1f4b3352e7 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Authentication/Authenticator/AbstractFormLoginAuthenticator.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authentication\Authenticator; + +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Core\Security; +use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface; + +/** + * A base class to make form login authentication easier! + * + * @author Ryan Weaver + */ +abstract class AbstractFormLoginAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface +{ + /** + * Return the URL to the login page. + */ + abstract protected function getLoginUrl(): string; + + /** + * Override to change what happens after a bad username/password is submitted. + */ + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response + { + if ($request->hasSession()) { + $request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception); + } + + $url = $this->getLoginUrl(); + + return new RedirectResponse($url); + } + + public function supportsRememberMe(): bool + { + return true; + } + + /** + * Override to control what happens when the user hits a secure page + * but isn't logged in yet. + */ + public function start(Request $request, AuthenticationException $authException = null): Response + { + $url = $this->getLoginUrl(); + + return new RedirectResponse($url); + } +} diff --git a/src/Symfony/Component/Security/Core/Authentication/Authenticator/AnonymousAuthenticator.php b/src/Symfony/Component/Security/Core/Authentication/Authenticator/AnonymousAuthenticator.php index e173792dba..78c80800aa 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Authenticator/AnonymousAuthenticator.php +++ b/src/Symfony/Component/Security/Core/Authentication/Authenticator/AnonymousAuthenticator.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\Security\Core\Authentication\Authenticator; use Symfony\Component\HttpFoundation\Request; @@ -9,9 +18,6 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\User\User; 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\Token\GuardTokenInterface; /** * @author Wouter de Jong @@ -25,11 +31,6 @@ class AnonymousAuthenticator implements AuthenticatorInterface $this->secret = $secret; } - public function start(Request $request, AuthenticationException $authException = null) - { - return new Response(null, Response::HTTP_UNAUTHORIZED); - } - public function supports(Request $request): ?bool { return true; @@ -40,27 +41,29 @@ class AnonymousAuthenticator implements AuthenticatorInterface return []; } - public function getUser($credentials, UserProviderInterface $userProvider) + public function getUser($credentials): ?UserInterface { return new User('anon.', null); } - public function checkCredentials($credentials, UserInterface $user) + public function checkCredentials($credentials, UserInterface $user): bool { return true; } - public function createAuthenticatedToken(UserInterface $user, string $providerKey) + public function createAuthenticatedToken(UserInterface $user, string $providerKey): TokenInterface { return new AnonymousToken($this->secret, 'anon.', []); } - public function onAuthenticationFailure(Request $request, AuthenticationException $exception) + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response { + return null; } - public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey) + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response { + return null; } public function supportsRememberMe(): bool diff --git a/src/Symfony/Component/Security/Core/Authentication/Authenticator/AuthenticatorInterface.php b/src/Symfony/Component/Security/Core/Authentication/Authenticator/AuthenticatorInterface.php new file mode 100644 index 0000000000..c4a9965381 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Authentication/Authenticator/AuthenticatorInterface.php @@ -0,0 +1,129 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authentication\Authenticator; + +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; + +/** + * The interface for all authenticators. + * + * @author Ryan Weaver + * @author Amaury Leroux de Lens + * @author Wouter de Jong + */ +interface AuthenticatorInterface +{ + /** + * Does the authenticator support the given Request? + * + * If this returns false, the authenticator will be skipped. + */ + public function supports(Request $request): ?bool; + + /** + * Get the authentication credentials from the request and return them + * as any type (e.g. an associate array). + * + * Whatever value you return here will be passed to getUser() and checkCredentials() + * + * For example, for a form login, you might: + * + * return [ + * 'username' => $request->request->get('_username'), + * 'password' => $request->request->get('_password'), + * ]; + * + * Or for an API token that's on a header, you might use: + * + * return ['api_key' => $request->headers->get('X-API-TOKEN')]; + * + * @return mixed Any non-null value + * + * @throws \UnexpectedValueException If null is returned + */ + public function getCredentials(Request $request); + + /** + * Return a UserInterface object based on the credentials. + * + * You may throw an AuthenticationException if you wish. If you return + * null, then a UsernameNotFoundException is thrown for you. + * + * @param mixed $credentials the value returned from getCredentials() + * + * @throws AuthenticationException + */ + public function getUser($credentials): ?UserInterface; + + /** + * Returns true if the credentials are valid. + * + * If false is returned, authentication will fail. You may also throw + * an AuthenticationException if you wish to cause authentication to fail. + * + * @param mixed $credentials the value returned from getCredentials() + * + * @throws AuthenticationException + */ + public function checkCredentials($credentials, UserInterface $user): bool; + + /** + * 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 AbstractAuthenticator class from your authenticator. + * + * @see AbstractAuthenticator + */ + public function createAuthenticatedToken(UserInterface $user, string $providerKey): TokenInterface; + + /** + * 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. + */ + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response; + + /** + * 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. + */ + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response; + + /** + * 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 + * D) The onAuthenticationSuccess method returns a Response object + */ + public function supportsRememberMe(): bool; +} diff --git a/src/Symfony/Component/Security/Core/Authentication/Authenticator/FormLoginAuthenticator.php b/src/Symfony/Component/Security/Core/Authentication/Authenticator/FormLoginAuthenticator.php index 72e2bc5ff1..06f400242c 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Authenticator/FormLoginAuthenticator.php +++ b/src/Symfony/Component/Security/Core/Authentication/Authenticator/FormLoginAuthenticator.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\Security\Core\Authentication\Authenticator; use Symfony\Component\HttpFoundation\Request; @@ -7,7 +16,6 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; -use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException; use Symfony\Component\Security\Core\Security; @@ -15,7 +23,6 @@ use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Csrf\CsrfToken; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; -use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator; use Symfony\Component\Security\Http\HttpUtils; use Symfony\Component\Security\Http\ParameterBagUtils; use Symfony\Component\Security\Http\Util\TargetPathTrait; @@ -25,16 +32,17 @@ use Symfony\Component\Security\Http\Util\TargetPathTrait; */ class FormLoginAuthenticator extends AbstractFormLoginAuthenticator { - use TargetPathTrait, UsernamePasswordTrait, UserProviderTrait { + use TargetPathTrait, UsernamePasswordTrait { UsernamePasswordTrait::checkCredentials as checkPassword; } private $options; private $httpUtils; private $csrfTokenManager; + private $userProvider; private $encoderFactory; - public function __construct(HttpUtils $httpUtils, ?CsrfTokenManagerInterface $csrfTokenManager, EncoderFactoryInterface $encoderFactory, array $options) + public function __construct(HttpUtils $httpUtils, ?CsrfTokenManagerInterface $csrfTokenManager, UserProviderInterface $userProvider, EncoderFactoryInterface $encoderFactory, array $options) { $this->httpUtils = $httpUtils; $this->csrfTokenManager = $csrfTokenManager; @@ -52,6 +60,7 @@ class FormLoginAuthenticator extends AbstractFormLoginAuthenticator 'target_path_parameter' => '_target_path', 'use_referer' => false, ], $options); + $this->userProvider = $userProvider; } protected function getLoginUrl(): string @@ -91,11 +100,16 @@ class FormLoginAuthenticator extends AbstractFormLoginAuthenticator throw new BadCredentialsException('Invalid username.'); } - $request->getSession()->set(Security::LAST_USERNAME, $username); + $request->getSession()->set(Security::LAST_USERNAME, $credentials['username']); return $credentials; } + public function getUser($credentials): ?UserInterface + { + return $this->userProvider->loadUserByUsername($credentials['username']); + } + public function checkCredentials($credentials, UserInterface $user): bool { if (null !== $this->csrfTokenManager) { @@ -107,7 +121,7 @@ class FormLoginAuthenticator extends AbstractFormLoginAuthenticator return $this->checkPassword($credentials, $user); } - public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey) + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): Response { return $this->httpUtils->createRedirectResponse($request, $this->determineTargetUrl($request, $providerKey)); } diff --git a/src/Symfony/Component/Security/Core/Authentication/Authenticator/HttpBasicAuthenticator.php b/src/Symfony/Component/Security/Core/Authentication/Authenticator/HttpBasicAuthenticator.php index 9ba11d0ddb..78e6d91cc2 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Authenticator/HttpBasicAuthenticator.php +++ b/src/Symfony/Component/Security/Core/Authentication/Authenticator/HttpBasicAuthenticator.php @@ -19,16 +19,14 @@ use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; -use Symfony\Component\Security\Guard\AuthenticatorInterface; +use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface; /** * @author Wouter de Jong */ -class HttpBasicAuthenticator implements AuthenticatorInterface +class HttpBasicAuthenticator implements AuthenticatorInterface, AuthenticationEntryPointInterface { - use UserProviderTrait, UsernamePasswordTrait { - UserProviderTrait::getUser as getUserTrait; - } + use UsernamePasswordTrait; private $realmName; private $userProvider; @@ -52,16 +50,11 @@ class HttpBasicAuthenticator implements AuthenticatorInterface return $response; } - public function supports(Request $request): bool + public function supports(Request $request): ?bool { return $request->headers->has('PHP_AUTH_USER'); } - public function getUser($credentials, UserProviderInterface $userProvider): ?UserInterface - { - return $this->getUserTrait($credentials, $this->userProvider); - } - public function getCredentials(Request $request) { return [ @@ -70,6 +63,11 @@ class HttpBasicAuthenticator implements AuthenticatorInterface ]; } + public function getUser($credentials): ?UserInterface + { + return $this->userProvider->loadUserByUsername($credentials['username']); + } + public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey): ?Response { return null; diff --git a/src/Symfony/Component/Security/Core/Authentication/Authenticator/UserProviderTrait.php b/src/Symfony/Component/Security/Core/Authentication/Authenticator/UserProviderTrait.php deleted file mode 100644 index b0bad3844e..0000000000 --- a/src/Symfony/Component/Security/Core/Authentication/Authenticator/UserProviderTrait.php +++ /dev/null @@ -1,26 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Security\Core\Authentication\Authenticator; - -use Symfony\Component\Security\Core\User\UserInterface; -use Symfony\Component\Security\Core\User\UserProviderInterface; - -/** - * @author Wouter de Jong - */ -trait UserProviderTrait -{ - public function getUser($credentials, UserProviderInterface $userProvider): ?UserInterface - { - return $userProvider->loadUserByUsername($credentials['username']); - } -} diff --git a/src/Symfony/Component/Security/Core/Authentication/Authenticator/UsernamePasswordTrait.php b/src/Symfony/Component/Security/Core/Authentication/Authenticator/UsernamePasswordTrait.php index e791d52405..05f340a68f 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Authenticator/UsernamePasswordTrait.php +++ b/src/Symfony/Component/Security/Core/Authentication/Authenticator/UsernamePasswordTrait.php @@ -11,11 +11,11 @@ namespace Symfony\Component\Security\Core\Authentication\Authenticator; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\User\UserInterface; -use Symfony\Component\Security\Guard\Token\GuardTokenInterface; /** * @author Wouter de Jong @@ -41,7 +41,7 @@ trait UsernamePasswordTrait return true; } - public function createAuthenticatedToken(UserInterface $user, $providerKey): GuardTokenInterface + public function createAuthenticatedToken(UserInterface $user, $providerKey): TokenInterface { return new UsernamePasswordToken($user, null, $providerKey, $user->getRoles()); } diff --git a/src/Symfony/Component/Security/Core/Authentication/GuardAuthenticationManager.php b/src/Symfony/Component/Security/Core/Authentication/GuardAuthenticationManager.php index 624b0a678c..68b542af97 100644 --- a/src/Symfony/Component/Security/Core/Authentication/GuardAuthenticationManager.php +++ b/src/Symfony/Component/Security/Core/Authentication/GuardAuthenticationManager.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Security\Core\Authentication; +use Symfony\Component\Security\Core\Authentication\Authenticator\AuthenticatorInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\AuthenticationEvents; use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent; @@ -19,7 +20,6 @@ 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\Core\User\UserCheckerInterface; -use Symfony\Component\Security\Guard\AuthenticatorInterface; use Symfony\Component\Security\Guard\Provider\GuardAuthenticationProviderTrait; use Symfony\Component\Security\Guard\Token\GuardTokenInterface; use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken; diff --git a/src/Symfony/Component/Security/Core/Authentication/Token/UsernamePasswordToken.php b/src/Symfony/Component/Security/Core/Authentication/Token/UsernamePasswordToken.php index b751bde7f1..b9eaa68246 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Token/UsernamePasswordToken.php +++ b/src/Symfony/Component/Security/Core/Authentication/Token/UsernamePasswordToken.php @@ -12,14 +12,13 @@ namespace Symfony\Component\Security\Core\Authentication\Token; use Symfony\Component\Security\Core\User\UserInterface; -use Symfony\Component\Security\Guard\Token\GuardTokenInterface; /** * UsernamePasswordToken implements a username and password token. * * @author Fabien Potencier */ -class UsernamePasswordToken extends AbstractToken implements GuardTokenInterface +class UsernamePasswordToken extends AbstractToken { private $credentials; private $providerKey; diff --git a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php index 35c4bda103..d30a95fdd7 100644 --- a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php +++ b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php @@ -13,14 +13,10 @@ namespace Symfony\Component\Security\Guard\Firewall; use Psr\Log\LoggerInterface; 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\Authentication\Token\TokenInterface; -use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Guard\AuthenticatorInterface; use Symfony\Component\Security\Guard\GuardAuthenticatorHandler; -use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken; use Symfony\Component\Security\Http\Firewall\AbstractListener; use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; diff --git a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticatorListenerTrait.php b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticatorListenerTrait.php index 043c51c7a8..245f02c906 100644 --- a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticatorListenerTrait.php +++ b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticatorListenerTrait.php @@ -1,10 +1,20 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\Security\Guard\Firewall; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\Security\Core\Authentication\Authenticator\AuthenticatorInterface as CoreAuthenticatorInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Guard\AuthenticatorInterface; @@ -37,7 +47,7 @@ trait GuardAuthenticatorListenerTrait } /** - * @param AuthenticatorInterface[] $guardAuthenticators + * @param (CoreAuthenticatorInterface|AuthenticatorInterface)[] $guardAuthenticators */ protected function executeGuardAuthenticators(array $guardAuthenticators, RequestEvent $event): void { @@ -56,8 +66,15 @@ trait GuardAuthenticatorListenerTrait } } - private function executeGuardAuthenticator(string $uniqueGuardKey, AuthenticatorInterface $guardAuthenticator, RequestEvent $event) + /** + * @param CoreAuthenticatorInterface|AuthenticatorInterface $guardAuthenticator + */ + private function executeGuardAuthenticator(string $uniqueGuardKey, $guardAuthenticator, RequestEvent $event) { + if (!$guardAuthenticator instanceof AuthenticatorInterface && !$guardAuthenticator instanceof CoreAuthenticatorInterface) { + throw new \UnexpectedValueException('Invalid guard authenticator passed to '.__METHOD__.'. Expected AuthenticatorInterface of either Security Core or Security Guard.'); + } + $request = $event->getRequest(); try { if (null !== $this->logger) { @@ -124,9 +141,15 @@ trait GuardAuthenticatorListenerTrait /** * Checks to see if remember me is supported in the authenticator and * on the firewall. If it is, the RememberMeServicesInterface is notified. + * + * @param CoreAuthenticatorInterface|AuthenticatorInterface $guardAuthenticator */ - private function triggerRememberMe(AuthenticatorInterface $guardAuthenticator, Request $request, TokenInterface $token, Response $response = null) + private function triggerRememberMe($guardAuthenticator, Request $request, TokenInterface $token, Response $response = null) { + if (!$guardAuthenticator instanceof AuthenticatorInterface && !$guardAuthenticator instanceof CoreAuthenticatorInterface) { + throw new \UnexpectedValueException('Invalid guard authenticator passed to '.__METHOD__.'. Expected AuthenticatorInterface of either Security Core or Security Guard.'); + } + if (null === $this->rememberMeServices) { if (null !== $this->logger) { $this->logger->debug('Remember me skipped: it is not configured for the firewall.', ['authenticator' => \get_class($guardAuthenticator)]); diff --git a/src/Symfony/Component/Security/Guard/GuardAuthenticatorHandler.php b/src/Symfony/Component/Security/Guard/GuardAuthenticatorHandler.php index 11f207a9ab..d2c0d298d2 100644 --- a/src/Symfony/Component/Security/Guard/GuardAuthenticatorHandler.php +++ b/src/Symfony/Component/Security/Guard/GuardAuthenticatorHandler.php @@ -13,6 +13,7 @@ namespace Symfony\Component\Security\Guard; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\Authentication\Authenticator\AuthenticatorInterface as CoreAuthenticatorInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; @@ -65,9 +66,15 @@ class GuardAuthenticatorHandler /** * Returns the "on success" response for the given GuardAuthenticator. + * + * @param CoreAuthenticatorInterface|AuthenticatorInterface $guardAuthenticator */ - public function handleAuthenticationSuccess(TokenInterface $token, Request $request, AuthenticatorInterface $guardAuthenticator, string $providerKey): ?Response + public function handleAuthenticationSuccess(TokenInterface $token, Request $request, $guardAuthenticator, string $providerKey): ?Response { + if (!$guardAuthenticator instanceof AuthenticatorInterface && !$guardAuthenticator instanceof CoreAuthenticatorInterface) { + throw new \UnexpectedValueException('Invalid guard authenticator passed to '.__METHOD__.'. Expected AuthenticatorInterface of either Security Core or Security Guard.'); + } + $response = $guardAuthenticator->onAuthenticationSuccess($request, $token, $providerKey); // check that it's a Response or null @@ -81,9 +88,15 @@ class GuardAuthenticatorHandler /** * Convenience method for authenticating the user and returning the * Response *if any* for success. + * + * @param CoreAuthenticatorInterface|AuthenticatorInterface $authenticator */ - public function authenticateUserAndHandleSuccess(UserInterface $user, Request $request, AuthenticatorInterface $authenticator, string $providerKey): ?Response + public function authenticateUserAndHandleSuccess(UserInterface $user, Request $request, $authenticator, string $providerKey): ?Response { + if (!$authenticator instanceof AuthenticatorInterface && !$authenticator instanceof CoreAuthenticatorInterface) { + throw new \UnexpectedValueException('Invalid guard 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 @@ -96,9 +109,15 @@ class GuardAuthenticatorHandler /** * Handles an authentication failure and returns the Response for the * GuardAuthenticator. + * + * @param CoreAuthenticatorInterface|AuthenticatorInterface $guardAuthenticator */ - public function handleAuthenticationFailure(AuthenticationException $authenticationException, Request $request, AuthenticatorInterface $guardAuthenticator, string $providerKey): ?Response + public function handleAuthenticationFailure(AuthenticationException $authenticationException, Request $request, $guardAuthenticator, string $providerKey): ?Response { + if (!$guardAuthenticator instanceof AuthenticatorInterface && !$guardAuthenticator instanceof CoreAuthenticatorInterface) { + throw new \UnexpectedValueException('Invalid guard authenticator passed to '.__METHOD__.'. Expected AuthenticatorInterface of either Security Core or Security Guard.'); + } + $response = $guardAuthenticator->onAuthenticationFailure($request, $authenticationException); if ($response instanceof Response || null === $response) { // returning null is ok, it means they want the request to continue diff --git a/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProviderTrait.php b/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProviderTrait.php index 0112256b85..0d25f167db 100644 --- a/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProviderTrait.php +++ b/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProviderTrait.php @@ -11,14 +11,15 @@ namespace Symfony\Component\Security\Guard\Provider; +use Symfony\Component\Security\Core\Authentication\Authenticator\AuthenticatorInterface as CoreAuthenticatorInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\BadCredentialsException; +use Symfony\Component\Security\Core\Exception\LogicException; 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\AuthenticatorInterface; use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface; -use Symfony\Component\Security\Guard\Token\GuardTokenInterface; use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken; /** @@ -28,10 +29,22 @@ use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken; */ trait GuardAuthenticationProviderTrait { - private function authenticateViaGuard(AuthenticatorInterface $guardAuthenticator, PreAuthenticationGuardToken $token, string $providerKey): TokenInterface + /** + * @param CoreAuthenticatorInterface|AuthenticatorInterface $guardAuthenticator + */ + private function authenticateViaGuard($guardAuthenticator, PreAuthenticationGuardToken $token, string $providerKey): TokenInterface { // get the user from the GuardAuthenticator - $user = $guardAuthenticator->getUser($token->getCredentials(), $this->userProvider); + if ($guardAuthenticator instanceof AuthenticatorInterface) { + if (!isset($this->userProvider)) { + throw new LogicException(sprintf('%s only supports authenticators implementing "%s", update "%s" or use the legacy guard integration instead.', __CLASS__, CoreAuthenticatorInterface::class, \get_class($guardAuthenticator))); + } + $user = $guardAuthenticator->getUser($token->getCredentials(), $this->userProvider); + } elseif ($guardAuthenticator instanceof CoreAuthenticatorInterface) { + $user = $guardAuthenticator->getUser($token->getCredentials()); + } else { + throw new \UnexpectedValueException('Invalid guard authenticator passed to '.__METHOD__.'. Expected AuthenticatorInterface of either Security Core or Security Guard.'); + } if (null === $user) { throw new UsernameNotFoundException(sprintf('Null returned from "%s::getUser()".', get_debug_type($guardAuthenticator))); @@ -63,7 +76,10 @@ trait GuardAuthenticationProviderTrait return $authenticatedToken; } - private function findOriginatingAuthenticator(PreAuthenticationGuardToken $token): ?AuthenticatorInterface + /** + * @return CoreAuthenticatorInterface|\Symfony\Component\Security\Guard\AuthenticatorInterface|null + */ + private function findOriginatingAuthenticator(PreAuthenticationGuardToken $token) { // find the *one* GuardAuthenticator that this token originated from foreach ($this->guardAuthenticators as $key => $guardAuthenticator) { diff --git a/src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php b/src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php index b1261bf2b1..564f60d31b 100644 --- a/src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php @@ -12,10 +12,9 @@ namespace Symfony\Component\Security\Http\Firewall; use Psr\Log\LoggerInterface; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; -use Symfony\Component\Security\Guard\AuthenticatorInterface; +use Symfony\Component\Security\Core\Authentication\Authenticator\AuthenticatorInterface; use Symfony\Component\Security\Guard\Firewall\GuardAuthenticatorListenerTrait; use Symfony\Component\Security\Guard\GuardAuthenticatorHandler; From fa4b3ec2135d3a1682cfaa52c87c03fb4eb7b3ef Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sun, 26 Jan 2020 15:37:44 +0100 Subject: [PATCH 08/30] Implemented password migration for the new authenticators --- .../Guard/PasswordAuthenticatedInterface.php | 4 ++++ .../GuardAuthenticationProviderTrait.php | 16 ++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Security/Guard/PasswordAuthenticatedInterface.php b/src/Symfony/Component/Security/Guard/PasswordAuthenticatedInterface.php index dd2eeba33d..b6b26cbd31 100644 --- a/src/Symfony/Component/Security/Guard/PasswordAuthenticatedInterface.php +++ b/src/Symfony/Component/Security/Guard/PasswordAuthenticatedInterface.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Security\Guard; +use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; + /** * An optional interface for "guard" authenticators that deal with user passwords. */ @@ -22,4 +24,6 @@ interface PasswordAuthenticatedInterface * @param mixed $credentials The user credentials */ public function getPassword($credentials): ?string; + + /* public function getPasswordEncoder(): ?UserPasswordEncoderInterface; */ } diff --git a/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProviderTrait.php b/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProviderTrait.php index 0d25f167db..667c35d05e 100644 --- a/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProviderTrait.php +++ b/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProviderTrait.php @@ -62,8 +62,20 @@ trait GuardAuthenticationProviderTrait throw new BadCredentialsException(sprintf('Authentication failed because "%s::checkCredentials()" did not return true.', get_debug_type($guardAuthenticator))); } - if ($this->userProvider instanceof PasswordUpgraderInterface && $guardAuthenticator instanceof PasswordAuthenticatedInterface && null !== $this->passwordEncoder && (null !== $password = $guardAuthenticator->getPassword($token->getCredentials())) && method_exists($this->passwordEncoder, 'needsRehash') && $this->passwordEncoder->needsRehash($user)) { - $this->userProvider->upgradePassword($user, $this->passwordEncoder->encodePassword($user, $password)); + + if ($guardAuthenticator instanceof PasswordAuthenticatedInterface + && null !== $password = $guardAuthenticator->getPassword($token->getCredentials()) + && null !== $passwordEncoder = $this->passwordEncoder ?? (method_exists($guardAuthenticator, 'getPasswordEncoder') ? $guardAuthenticator->getPasswordEncoder() : null) + ) { + if (method_exists($passwordEncoder, 'needsRehash') && $passwordEncoder->needsRehash($user)) { + if (!isset($this->userProvider)) { + if ($guardAuthenticator instanceof PasswordUpgraderInterface) { + $guardAuthenticator->upgradePassword($user, $guardAuthenticator->getPasswordEncoder()->encodePassword($user, $password)); + } + } elseif ($this->userProvider instanceof PasswordUpgraderInterface) { + $this->userProvider->upgradePassword($user, $passwordEncoder->encodePassword($user, $password)); + } + } } $this->userChecker->checkPostAuth($user); From 4c06236933545f2186b75ab1b2e7f20471504821 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sun, 26 Jan 2020 15:33:15 +0100 Subject: [PATCH 09/30] Fixes after testing in Demo application --- .../Resources/config/authenticators.xml | 1 + .../Authenticator/AnonymousAuthenticator.php | 8 ++++++-- .../Authentication/GuardAuthenticationManager.php | 12 +++++++----- .../Firewall/GuardAuthenticatorListenerTrait.php | 5 +++++ 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml index 9da2d3b8a5..e4fa9008dd 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml @@ -42,6 +42,7 @@ class="Symfony\Component\Security\Core\Authentication\Authenticator\AnonymousAuthenticator" abstract="true"> + diff --git a/src/Symfony/Component/Security/Core/Authentication/Authenticator/AnonymousAuthenticator.php b/src/Symfony/Component/Security/Core/Authentication/Authenticator/AnonymousAuthenticator.php index 78c80800aa..227981c696 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Authenticator/AnonymousAuthenticator.php +++ b/src/Symfony/Component/Security/Core/Authentication/Authenticator/AnonymousAuthenticator.php @@ -14,6 +14,7 @@ namespace Symfony\Component\Security\Core\Authentication\Authenticator; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken; +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\User; @@ -25,15 +26,18 @@ use Symfony\Component\Security\Core\User\UserInterface; class AnonymousAuthenticator implements AuthenticatorInterface { private $secret; + private $tokenStorage; - public function __construct(string $secret) + public function __construct(string $secret, TokenStorageInterface $tokenStorage) { $this->secret = $secret; + $this->tokenStorage = $tokenStorage; } public function supports(Request $request): ?bool { - return true; + // do not overwrite already stored tokens (i.e. from the session) + return null === $this->tokenStorage->getToken(); } public function getCredentials(Request $request) diff --git a/src/Symfony/Component/Security/Core/Authentication/GuardAuthenticationManager.php b/src/Symfony/Component/Security/Core/Authentication/GuardAuthenticationManager.php index 68b542af97..a836353b61 100644 --- a/src/Symfony/Component/Security/Core/Authentication/GuardAuthenticationManager.php +++ b/src/Symfony/Component/Security/Core/Authentication/GuardAuthenticationManager.php @@ -86,12 +86,14 @@ class GuardAuthenticationManager implements AuthenticationManagerInterface $this->handleFailure($exception, $token); } - if (true === $this->eraseCredentials) { - $result->eraseCredentials(); - } + if (null !== $result) { + if (true === $this->eraseCredentials) { + $result->eraseCredentials(); + } - if (null !== $this->eventDispatcher) { - $this->eventDispatcher->dispatch(new AuthenticationSuccessEvent($result), AuthenticationEvents::AUTHENTICATION_SUCCESS); + if (null !== $this->eventDispatcher) { + $this->eventDispatcher->dispatch(new AuthenticationSuccessEvent($result), AuthenticationEvents::AUTHENTICATION_SUCCESS); + } } return $result; diff --git a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticatorListenerTrait.php b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticatorListenerTrait.php index 245f02c906..ac1cb8200c 100644 --- a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticatorListenerTrait.php +++ b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticatorListenerTrait.php @@ -150,6 +150,11 @@ trait GuardAuthenticatorListenerTrait throw new \UnexpectedValueException('Invalid guard authenticator passed to '.__METHOD__.'. Expected AuthenticatorInterface of either Security Core or Security Guard.'); } + // @todo implement remember me functionality + if (!isset($this->rememberMeServices)) { + return; + } + if (null === $this->rememberMeServices) { if (null !== $this->logger) { $this->logger->debug('Remember me skipped: it is not configured for the firewall.', ['authenticator' => \get_class($guardAuthenticator)]); From 873b949cf9723285419e88dffade4c78b941806d Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sun, 26 Jan 2020 15:51:46 +0100 Subject: [PATCH 10/30] Mark new core authenticators as experimental --- .../Security/Factory/EntryPointFactoryInterface.php | 2 ++ .../Security/Factory/GuardFactoryInterface.php | 2 ++ .../EventListener/LazyGuardManagerListener.php | 2 ++ .../Authentication/Authenticator/AbstractAuthenticator.php | 2 ++ .../Authenticator/AbstractFormLoginAuthenticator.php | 2 ++ .../Authentication/Authenticator/AnonymousAuthenticator.php | 4 ++++ .../Authentication/Authenticator/AuthenticatorInterface.php | 2 ++ .../Authentication/Authenticator/FormLoginAuthenticator.php | 4 ++++ .../Authentication/Authenticator/HttpBasicAuthenticator.php | 4 ++++ .../Authentication/Authenticator/UsernamePasswordTrait.php | 2 ++ .../Core/Authentication/GuardAuthenticationManager.php | 6 ++++++ .../Security/Http/Firewall/GuardManagerListener.php | 4 ++++ 12 files changed, 36 insertions(+) diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/EntryPointFactoryInterface.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/EntryPointFactoryInterface.php index 804399ad51..bf0e625f0a 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/EntryPointFactoryInterface.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/EntryPointFactoryInterface.php @@ -15,6 +15,8 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; /** * @author Wouter de Jong + * + * @experimental in 5.1 */ interface EntryPointFactoryInterface { diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardFactoryInterface.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardFactoryInterface.php index 0d1dcb0fad..34314e5a43 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardFactoryInterface.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardFactoryInterface.php @@ -15,6 +15,8 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; /** * @author Wouter de Jong + * + * @experimental in 5.1 */ interface GuardFactoryInterface { diff --git a/src/Symfony/Bundle/SecurityBundle/EventListener/LazyGuardManagerListener.php b/src/Symfony/Bundle/SecurityBundle/EventListener/LazyGuardManagerListener.php index 63b201cb66..958ca5d4bb 100644 --- a/src/Symfony/Bundle/SecurityBundle/EventListener/LazyGuardManagerListener.php +++ b/src/Symfony/Bundle/SecurityBundle/EventListener/LazyGuardManagerListener.php @@ -20,6 +20,8 @@ use Symfony\Component\Security\Http\Firewall\GuardManagerListener; /** * @author Wouter de Jong + * + * @experimental in 5.1 */ class LazyGuardManagerListener extends GuardManagerListener { diff --git a/src/Symfony/Component/Security/Core/Authentication/Authenticator/AbstractAuthenticator.php b/src/Symfony/Component/Security/Core/Authentication/Authenticator/AbstractAuthenticator.php index 8e9bee6f07..1127fb6781 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Authenticator/AbstractAuthenticator.php +++ b/src/Symfony/Component/Security/Core/Authentication/Authenticator/AbstractAuthenticator.php @@ -19,6 +19,8 @@ use Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken; * An optional base class that creates the necessary tokens for you. * * @author Ryan Weaver + * + * @experimental in 5.1 */ abstract class AbstractAuthenticator implements AuthenticatorInterface { diff --git a/src/Symfony/Component/Security/Core/Authentication/Authenticator/AbstractFormLoginAuthenticator.php b/src/Symfony/Component/Security/Core/Authentication/Authenticator/AbstractFormLoginAuthenticator.php index 1f4b3352e7..27df412d28 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Authenticator/AbstractFormLoginAuthenticator.php +++ b/src/Symfony/Component/Security/Core/Authentication/Authenticator/AbstractFormLoginAuthenticator.php @@ -22,6 +22,8 @@ use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface * A base class to make form login authentication easier! * * @author Ryan Weaver + * + * @experimental in 5.1 */ abstract class AbstractFormLoginAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface { diff --git a/src/Symfony/Component/Security/Core/Authentication/Authenticator/AnonymousAuthenticator.php b/src/Symfony/Component/Security/Core/Authentication/Authenticator/AnonymousAuthenticator.php index 227981c696..26a7d3102b 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Authenticator/AnonymousAuthenticator.php +++ b/src/Symfony/Component/Security/Core/Authentication/Authenticator/AnonymousAuthenticator.php @@ -22,6 +22,10 @@ use Symfony\Component\Security\Core\User\UserInterface; /** * @author Wouter de Jong + * @author Fabien Potencier + * + * @final + * @experimental in 5.1 */ class AnonymousAuthenticator implements AuthenticatorInterface { diff --git a/src/Symfony/Component/Security/Core/Authentication/Authenticator/AuthenticatorInterface.php b/src/Symfony/Component/Security/Core/Authentication/Authenticator/AuthenticatorInterface.php index c4a9965381..cf84ce1609 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Authenticator/AuthenticatorInterface.php +++ b/src/Symfony/Component/Security/Core/Authentication/Authenticator/AuthenticatorInterface.php @@ -23,6 +23,8 @@ use Symfony\Component\Security\Core\User\UserInterface; * @author Ryan Weaver * @author Amaury Leroux de Lens * @author Wouter de Jong + * + * @experimental in 5.1 */ interface AuthenticatorInterface { diff --git a/src/Symfony/Component/Security/Core/Authentication/Authenticator/FormLoginAuthenticator.php b/src/Symfony/Component/Security/Core/Authentication/Authenticator/FormLoginAuthenticator.php index 06f400242c..19c5b69029 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Authenticator/FormLoginAuthenticator.php +++ b/src/Symfony/Component/Security/Core/Authentication/Authenticator/FormLoginAuthenticator.php @@ -29,6 +29,10 @@ use Symfony\Component\Security\Http\Util\TargetPathTrait; /** * @author Wouter de Jong + * @author Fabien Potencier + * + * @final + * @experimental in 5.1 */ class FormLoginAuthenticator extends AbstractFormLoginAuthenticator { diff --git a/src/Symfony/Component/Security/Core/Authentication/Authenticator/HttpBasicAuthenticator.php b/src/Symfony/Component/Security/Core/Authentication/Authenticator/HttpBasicAuthenticator.php index 78e6d91cc2..6ce74c6809 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Authenticator/HttpBasicAuthenticator.php +++ b/src/Symfony/Component/Security/Core/Authentication/Authenticator/HttpBasicAuthenticator.php @@ -23,6 +23,10 @@ use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface /** * @author Wouter de Jong + * @author Fabien Potencier + * + * @final + * @experimental in 5.1 */ class HttpBasicAuthenticator implements AuthenticatorInterface, AuthenticationEntryPointInterface { diff --git a/src/Symfony/Component/Security/Core/Authentication/Authenticator/UsernamePasswordTrait.php b/src/Symfony/Component/Security/Core/Authentication/Authenticator/UsernamePasswordTrait.php index 05f340a68f..292ec370f8 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Authenticator/UsernamePasswordTrait.php +++ b/src/Symfony/Component/Security/Core/Authentication/Authenticator/UsernamePasswordTrait.php @@ -21,6 +21,8 @@ use Symfony\Component\Security\Core\User\UserInterface; * @author Wouter de Jong * * @property EncoderFactoryInterface $encoderFactory + * + * @experimental in 5.1 */ trait UsernamePasswordTrait { diff --git a/src/Symfony/Component/Security/Core/Authentication/GuardAuthenticationManager.php b/src/Symfony/Component/Security/Core/Authentication/GuardAuthenticationManager.php index a836353b61..8b4e2e6393 100644 --- a/src/Symfony/Component/Security/Core/Authentication/GuardAuthenticationManager.php +++ b/src/Symfony/Component/Security/Core/Authentication/GuardAuthenticationManager.php @@ -25,6 +25,12 @@ use Symfony\Component\Security\Guard\Token\GuardTokenInterface; use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +/** + * @author Wouter de Jong + * @author Ryan Weaver + * + * @experimental in 5.1 + */ class GuardAuthenticationManager implements AuthenticationManagerInterface { use GuardAuthenticationProviderTrait; diff --git a/src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php b/src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php index 564f60d31b..e2a80c9888 100644 --- a/src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php @@ -20,6 +20,10 @@ use Symfony\Component\Security\Guard\GuardAuthenticatorHandler; /** * @author Wouter de Jong + * @author Ryan Weaver + * @author Amaury Leroux de Lens + * + * @experimental in 5.1 */ class GuardManagerListener { From b923e4c4f6adde63f829d315214a23e8435351a7 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sun, 26 Jan 2020 21:36:07 +0100 Subject: [PATCH 11/30] Enabled remember me for the GuardManagerListener --- .../DependencyInjection/SecurityExtension.php | 21 +++++++++++++------ .../GuardAuthenticatorListenerTrait.php | 5 ----- .../Http/Firewall/GuardManagerListener.php | 7 +++++++ 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 54403cfa4a..f1bf246d8d 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -418,6 +418,19 @@ class SecurityExtension extends Extension implements PrependExtensionInterface // Determine default entry point $configuredEntryPoint = isset($firewall['entry_point']) ? $firewall['entry_point'] : null; + if ($this->guardAuthenticationManagerEnabled) { + // guard authentication manager listener (must be before calling createAuthenticationListeners() to inject remember me services) + $container + ->setDefinition('security.firewall.guard.'.$id, new ChildDefinition('security.firewall.guard')) + ->replaceArgument(2, new Reference('security.firewall.guard.'.$id.'.locator')) + ->replaceArgument(3, $id) + ->addTag('kernel.event_listener', ['event' => KernelEvents::REQUEST]) + ->addTag('security.remember_me_aware', ['id' => $id, 'provider' => 'none']) + ; + + $listeners[] = new Reference('security.firewall.guard.'.$id); + } + // Authentication listeners $firewallAuthenticationProviders = []; list($authListeners, $defaultEntryPoint) = $this->createAuthenticationListeners($container, $id, $firewall, $firewallAuthenticationProviders, $defaultProvider, $providerIds, $configuredEntryPoint, $contextListenerId); @@ -425,7 +438,7 @@ class SecurityExtension extends Extension implements PrependExtensionInterface $authenticationProviders = array_merge($authenticationProviders, $firewallAuthenticationProviders); if ($this->guardAuthenticationManagerEnabled) { - // guard authentication manager listener + // add authentication providers for this firewall to the GuardManagerListener (if guard is enabled) $container ->setDefinition('security.firewall.guard.'.$id.'.locator', new ChildDefinition('security.firewall.guard.locator')) ->setArguments([array_map(function ($id) { @@ -434,13 +447,9 @@ class SecurityExtension extends Extension implements PrependExtensionInterface ->addTag('container.service_locator') ; $container - ->setDefinition('security.firewall.guard.'.$id, new ChildDefinition('security.firewall.guard')) + ->getDefinition('security.firewall.guard.'.$id) ->replaceArgument(2, new Reference('security.firewall.guard.'.$id.'.locator')) - ->replaceArgument(3, $id) - ->addTag('kernel.event_listener', ['event' => KernelEvents::REQUEST]) ; - - $listeners[] = new Reference('security.firewall.guard.'.$id); } $config->replaceArgument(7, $configuredEntryPoint ?: $defaultEntryPoint); diff --git a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticatorListenerTrait.php b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticatorListenerTrait.php index ac1cb8200c..245f02c906 100644 --- a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticatorListenerTrait.php +++ b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticatorListenerTrait.php @@ -150,11 +150,6 @@ trait GuardAuthenticatorListenerTrait throw new \UnexpectedValueException('Invalid guard authenticator passed to '.__METHOD__.'. Expected AuthenticatorInterface of either Security Core or Security Guard.'); } - // @todo implement remember me functionality - if (!isset($this->rememberMeServices)) { - return; - } - if (null === $this->rememberMeServices) { if (null !== $this->logger) { $this->logger->debug('Remember me skipped: it is not configured for the firewall.', ['authenticator' => \get_class($guardAuthenticator)]); diff --git a/src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php b/src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php index e2a80c9888..78681bd1e8 100644 --- a/src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php @@ -17,6 +17,7 @@ use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterfac use Symfony\Component\Security\Core\Authentication\Authenticator\AuthenticatorInterface; use Symfony\Component\Security\Guard\Firewall\GuardAuthenticatorListenerTrait; use Symfony\Component\Security\Guard\GuardAuthenticatorHandler; +use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; /** * @author Wouter de Jong @@ -34,6 +35,7 @@ class GuardManagerListener private $guardAuthenticators; protected $providerKey; protected $logger; + private $rememberMeServices; /** * @param AuthenticatorInterface[] $guardAuthenticators @@ -58,6 +60,11 @@ class GuardManagerListener $this->executeGuardAuthenticators($guardAuthenticators, $requestEvent); } + public function setRememberMeServices(RememberMeServicesInterface $rememberMeServices) + { + $this->rememberMeServices = $rememberMeServices; + } + protected function getGuardKey(string $key): string { // Guard authenticators in the GuardManagerListener are already indexed From b14a5e8c523ad758e9a0ff5a678b414f54e0826d Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sun, 26 Jan 2020 22:07:27 +0100 Subject: [PATCH 12/30] Moved new authenticator to the HTTP namespace This removes the introduced dependency on Guard from core. It also allows an easier migration path, as the complete Guard subcomponent can now be deprecated later in the 5.x life. --- .../Resources/config/authenticators.xml | 6 +- .../Resources/config/security.xml | 2 +- .../Token/PreAuthenticationGuardToken.php | 71 +++++++++ .../HttpBasicAuthenticatorTest.php | 2 +- .../Component/Security/Core/composer.json | 1 - .../Firewall/GuardAuthenticationListener.php | 10 +- .../Guard/GuardAuthenticatorHandler.php | 124 +-------------- .../Provider/GuardAuthenticationProvider.php | 3 +- .../Token/PreAuthenticationGuardToken.php | 50 +----- .../Component/Security/Guard/composer.json | 2 +- .../Authenticator/AbstractAuthenticator.php | 2 +- .../AbstractFormLoginAuthenticator.php | 2 +- .../Authenticator/AnonymousAuthenticator.php | 2 +- .../Authenticator/AuthenticatorInterface.php | 2 +- .../Authenticator/FormLoginAuthenticator.php | 2 +- .../Authenticator/HttpBasicAuthenticator.php | 2 +- .../Authenticator/UsernamePasswordTrait.php | 2 +- .../GuardAuthenticationManager.php | 15 +- .../GuardAuthenticationManagerTrait.php} | 8 +- .../GuardAuthenticatorHandler.php | 149 ++++++++++++++++++ .../Http/Firewall/GuardManagerListener.php | 11 +- .../Firewall/GuardManagerListenerTrait.php} | 12 +- .../Component/Security/Http/composer.json | 2 +- 23 files changed, 273 insertions(+), 209 deletions(-) create mode 100644 src/Symfony/Component/Security/Core/Authentication/Token/PreAuthenticationGuardToken.php rename src/Symfony/Component/Security/{Core => Http}/Authentication/Authenticator/AbstractAuthenticator.php (94%) rename src/Symfony/Component/Security/{Core => Http}/Authentication/Authenticator/AbstractFormLoginAuthenticator.php (96%) rename src/Symfony/Component/Security/{Core => Http}/Authentication/Authenticator/AnonymousAuthenticator.php (97%) rename src/Symfony/Component/Security/{Core => Http}/Authentication/Authenticator/AuthenticatorInterface.php (98%) rename src/Symfony/Component/Security/{Core => Http}/Authentication/Authenticator/FormLoginAuthenticator.php (98%) rename src/Symfony/Component/Security/{Core => Http}/Authentication/Authenticator/HttpBasicAuthenticator.php (97%) rename src/Symfony/Component/Security/{Core => Http}/Authentication/Authenticator/UsernamePasswordTrait.php (96%) rename src/Symfony/Component/Security/{Core => Http}/Authentication/GuardAuthenticationManager.php (88%) rename src/Symfony/Component/Security/{Guard/Provider/GuardAuthenticationProviderTrait.php => Http/Authentication/GuardAuthenticationManagerTrait.php} (95%) create mode 100644 src/Symfony/Component/Security/Http/Authentication/GuardAuthenticatorHandler.php rename src/Symfony/Component/Security/{Guard/Firewall/GuardAuthenticatorListenerTrait.php => Http/Firewall/GuardManagerListenerTrait.php} (94%) diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml index e4fa9008dd..f752f923ca 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml @@ -20,7 +20,7 @@ realm name user provider @@ -29,7 +29,7 @@ @@ -39,7 +39,7 @@ diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml index 99d8550e1b..5e31b492f0 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml @@ -52,7 +52,7 @@ - + %security.authentication.manager.erase_credentials% diff --git a/src/Symfony/Component/Security/Core/Authentication/Token/PreAuthenticationGuardToken.php b/src/Symfony/Component/Security/Core/Authentication/Token/PreAuthenticationGuardToken.php new file mode 100644 index 0000000000..b19b82e066 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Authentication/Token/PreAuthenticationGuardToken.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authentication\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 + */ +class PreAuthenticationGuardToken extends AbstractToken +{ + private $credentials; + private $guardProviderKey; + private $providerKey; + + /** + * @param mixed $credentials + * @param string $guardProviderKey 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 $guardProviderKey, ?string $providerKey = null) + { + $this->credentials = $credentials; + $this->guardProviderKey = $guardProviderKey; + $this->providerKey = $providerKey; + + parent::__construct([]); + + // never authenticated + parent::setAuthenticated(false); + } + + public function getProviderKey(): ?string + { + return $this->providerKey; + } + + 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.'); + } +} diff --git a/src/Symfony/Component/Security/Core/Tests/Authentication/Authenticator/HttpBasicAuthenticatorTest.php b/src/Symfony/Component/Security/Core/Tests/Authentication/Authenticator/HttpBasicAuthenticatorTest.php index 9e923364ea..c0265cd55a 100644 --- a/src/Symfony/Component/Security/Core/Tests/Authentication/Authenticator/HttpBasicAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authentication/Authenticator/HttpBasicAuthenticatorTest.php @@ -5,7 +5,7 @@ namespace Symfony\Component\Security\Core\Tests\Authentication\Authenticator; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\Security\Core\Authentication\Authenticator\HttpBasicAuthenticator; +use Symfony\Component\Security\Http\Authentication\Authenticator\HttpBasicAuthenticator; use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface; use Symfony\Component\Security\Core\Exception\BadCredentialsException; diff --git a/src/Symfony/Component/Security/Core/composer.json b/src/Symfony/Component/Security/Core/composer.json index 83b082bdde..fc500b285f 100644 --- a/src/Symfony/Component/Security/Core/composer.json +++ b/src/Symfony/Component/Security/Core/composer.json @@ -20,7 +20,6 @@ "symfony/event-dispatcher-contracts": "^1.1|^2", "symfony/polyfill-php80": "^1.15", "symfony/service-contracts": "^1.1.6|^2", - "symfony/security-guard": "^4.4", "symfony/deprecation-contracts": "^2.1" }, "require-dev": { diff --git a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php index d30a95fdd7..7ffad32454 100644 --- a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php +++ b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php @@ -15,9 +15,12 @@ use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; +use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticationGuardToken; use Symfony\Component\Security\Guard\AuthenticatorInterface; 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\Firewall\GuardManagerListenerTrait; use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; /** @@ -30,7 +33,7 @@ use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; */ class GuardAuthenticationListener extends AbstractListener { - use GuardAuthenticatorListenerTrait; + use GuardManagerListenerTrait; private $guardHandler; private $authenticationManager; @@ -101,6 +104,11 @@ class GuardAuthenticationListener extends AbstractListener $this->rememberMeServices = $rememberMeServices; } + protected function createPreAuthenticatedToken($credentials, string $uniqueGuardKey, string $providerKey): PreAuthenticationGuardToken + { + return new GuardPreAuthenticationGuardToken($credentials, $uniqueGuardKey, $providerKey); + } + protected function getGuardKey(string $key): string { // get a key that's unique to *this* guard authenticator diff --git a/src/Symfony/Component/Security/Guard/GuardAuthenticatorHandler.php b/src/Symfony/Component/Security/Guard/GuardAuthenticatorHandler.php index d2c0d298d2..2f16dfa140 100644 --- a/src/Symfony/Component/Security/Guard/GuardAuthenticatorHandler.php +++ b/src/Symfony/Component/Security/Guard/GuardAuthenticatorHandler.php @@ -11,17 +11,7 @@ namespace Symfony\Component\Security\Guard; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Security\Core\Authentication\Authenticator\AuthenticatorInterface as CoreAuthenticatorInterface; -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; -use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface; -use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Security\Http\Authentication\GuardAuthenticatorHandler as CoreAuthenticatorHandlerAlias; /** * A utility class that does much of the *work* during the guard authentication process. @@ -33,116 +23,6 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; * * @final */ -class GuardAuthenticatorHandler +class GuardAuthenticatorHandler extends CoreAuthenticatorHandlerAlias { - private $tokenStorage; - private $dispatcher; - private $sessionStrategy; - private $statelessProviderKeys; - - /** - * @param array $statelessProviderKeys An array of provider/firewall keys that are "stateless" and so do not need the session migrated on success - */ - public function __construct(TokenStorageInterface $tokenStorage, EventDispatcherInterface $eventDispatcher = null, array $statelessProviderKeys = []) - { - $this->tokenStorage = $tokenStorage; - $this->dispatcher = $eventDispatcher; - $this->statelessProviderKeys = $statelessProviderKeys; - } - - /** - * Authenticates the given token in the system. - */ - public function authenticateWithToken(TokenInterface $token, Request $request, string $providerKey = null) - { - $this->migrateSession($request, $token, $providerKey); - $this->tokenStorage->setToken($token); - - if (null !== $this->dispatcher) { - $loginEvent = new InteractiveLoginEvent($request, $token); - $this->dispatcher->dispatch($loginEvent, SecurityEvents::INTERACTIVE_LOGIN); - } - } - - /** - * Returns the "on success" response for the given GuardAuthenticator. - * - * @param CoreAuthenticatorInterface|AuthenticatorInterface $guardAuthenticator - */ - public function handleAuthenticationSuccess(TokenInterface $token, Request $request, $guardAuthenticator, string $providerKey): ?Response - { - if (!$guardAuthenticator instanceof AuthenticatorInterface && !$guardAuthenticator instanceof CoreAuthenticatorInterface) { - throw new \UnexpectedValueException('Invalid guard authenticator passed to '.__METHOD__.'. Expected AuthenticatorInterface of either Security Core or Security Guard.'); - } - - $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), get_debug_type($response))); - } - - /** - * Convenience method for authenticating the user and returning the - * Response *if any* for success. - * - * @param CoreAuthenticatorInterface|AuthenticatorInterface $authenticator - */ - public function authenticateUserAndHandleSuccess(UserInterface $user, Request $request, $authenticator, string $providerKey): ?Response - { - if (!$authenticator instanceof AuthenticatorInterface && !$authenticator instanceof CoreAuthenticatorInterface) { - throw new \UnexpectedValueException('Invalid guard 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 - $this->authenticateWithToken($token, $request, $providerKey); - - // return the success metric - return $this->handleAuthenticationSuccess($token, $request, $authenticator, $providerKey); - } - - /** - * Handles an authentication failure and returns the Response for the - * GuardAuthenticator. - * - * @param CoreAuthenticatorInterface|AuthenticatorInterface $guardAuthenticator - */ - public function handleAuthenticationFailure(AuthenticationException $authenticationException, Request $request, $guardAuthenticator, string $providerKey): ?Response - { - if (!$guardAuthenticator instanceof AuthenticatorInterface && !$guardAuthenticator instanceof CoreAuthenticatorInterface) { - throw new \UnexpectedValueException('Invalid guard authenticator passed to '.__METHOD__.'. Expected AuthenticatorInterface of either Security Core or Security Guard.'); - } - - $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), get_debug_type($response))); - } - - /** - * Call this method if your authentication token is stored to a session. - * - * @final - */ - public function setSessionAuthenticationStrategy(SessionAuthenticationStrategyInterface $sessionStrategy) - { - $this->sessionStrategy = $sessionStrategy; - } - - private function migrateSession(Request $request, TokenInterface $token, ?string $providerKey) - { - if (\in_array($providerKey, $this->statelessProviderKeys, true) || !$this->sessionStrategy || !$request->hasSession() || !$request->hasPreviousSession()) { - return; - } - - $this->sessionStrategy->onAuthentication($request, $token); - } } diff --git a/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php b/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php index 04085aaa05..01f70e9b4e 100644 --- a/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php +++ b/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Security\Guard\Provider; +use Symfony\Component\Security\Http\Authentication\GuardAuthenticationManagerTrait; use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; @@ -30,7 +31,7 @@ use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken; */ class GuardAuthenticationProvider implements AuthenticationProviderInterface { - use GuardAuthenticationProviderTrait; + use GuardAuthenticationManagerTrait; /** * @var AuthenticatorInterface[] diff --git a/src/Symfony/Component/Security/Guard/Token/PreAuthenticationGuardToken.php b/src/Symfony/Component/Security/Guard/Token/PreAuthenticationGuardToken.php index 460dcf9bda..69013599f3 100644 --- a/src/Symfony/Component/Security/Guard/Token/PreAuthenticationGuardToken.php +++ b/src/Symfony/Component/Security/Guard/Token/PreAuthenticationGuardToken.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Security\Guard\Token; -use Symfony\Component\Security\Core\Authentication\Token\AbstractToken; +use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticationGuardToken as CorePreAuthenticationGuardToken; /** * The token used by the guard auth system before authentication. @@ -22,52 +22,6 @@ use Symfony\Component\Security\Core\Authentication\Token\AbstractToken; * * @author Ryan Weaver */ -class PreAuthenticationGuardToken extends AbstractToken implements GuardTokenInterface +class PreAuthenticationGuardToken extends CorePreAuthenticationGuardToken implements GuardTokenInterface { - private $credentials; - private $guardProviderKey; - private $providerKey; - - /** - * @param mixed $credentials - * @param string $guardProviderKey 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 $guardProviderKey, ?string $providerKey = null) - { - $this->credentials = $credentials; - $this->guardProviderKey = $guardProviderKey; - $this->providerKey = $providerKey; - - parent::__construct([]); - - // never authenticated - parent::setAuthenticated(false); - } - - public function getProviderKey(): ?string - { - return $this->providerKey; - } - - 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.'); - } } diff --git a/src/Symfony/Component/Security/Guard/composer.json b/src/Symfony/Component/Security/Guard/composer.json index 1b2337f829..f129233640 100644 --- a/src/Symfony/Component/Security/Guard/composer.json +++ b/src/Symfony/Component/Security/Guard/composer.json @@ -17,7 +17,7 @@ ], "require": { "php": "^7.2.5", - "symfony/security-core": "^5.0", + "symfony/security-core": "^5.1", "symfony/security-http": "^4.4.1|^5.0.1", "symfony/polyfill-php80": "^1.15" }, diff --git a/src/Symfony/Component/Security/Core/Authentication/Authenticator/AbstractAuthenticator.php b/src/Symfony/Component/Security/Http/Authentication/Authenticator/AbstractAuthenticator.php similarity index 94% rename from src/Symfony/Component/Security/Core/Authentication/Authenticator/AbstractAuthenticator.php rename to src/Symfony/Component/Security/Http/Authentication/Authenticator/AbstractAuthenticator.php index 1127fb6781..ce22dce368 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Authenticator/AbstractAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authentication/Authenticator/AbstractAuthenticator.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Core\Authentication\Authenticator; +namespace Symfony\Component\Security\Http\Authentication\Authenticator; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\User\UserInterface; diff --git a/src/Symfony/Component/Security/Core/Authentication/Authenticator/AbstractFormLoginAuthenticator.php b/src/Symfony/Component/Security/Http/Authentication/Authenticator/AbstractFormLoginAuthenticator.php similarity index 96% rename from src/Symfony/Component/Security/Core/Authentication/Authenticator/AbstractFormLoginAuthenticator.php rename to src/Symfony/Component/Security/Http/Authentication/Authenticator/AbstractFormLoginAuthenticator.php index 27df412d28..5cc2f95414 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Authenticator/AbstractFormLoginAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authentication/Authenticator/AbstractFormLoginAuthenticator.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Core\Authentication\Authenticator; +namespace Symfony\Component\Security\Http\Authentication\Authenticator; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; diff --git a/src/Symfony/Component/Security/Core/Authentication/Authenticator/AnonymousAuthenticator.php b/src/Symfony/Component/Security/Http/Authentication/Authenticator/AnonymousAuthenticator.php similarity index 97% rename from src/Symfony/Component/Security/Core/Authentication/Authenticator/AnonymousAuthenticator.php rename to src/Symfony/Component/Security/Http/Authentication/Authenticator/AnonymousAuthenticator.php index 26a7d3102b..bec859e7a7 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Authenticator/AnonymousAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authentication/Authenticator/AnonymousAuthenticator.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Core\Authentication\Authenticator; +namespace Symfony\Component\Security\Http\Authentication\Authenticator; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; diff --git a/src/Symfony/Component/Security/Core/Authentication/Authenticator/AuthenticatorInterface.php b/src/Symfony/Component/Security/Http/Authentication/Authenticator/AuthenticatorInterface.php similarity index 98% rename from src/Symfony/Component/Security/Core/Authentication/Authenticator/AuthenticatorInterface.php rename to src/Symfony/Component/Security/Http/Authentication/Authenticator/AuthenticatorInterface.php index cf84ce1609..8bf38ac85a 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Authenticator/AuthenticatorInterface.php +++ b/src/Symfony/Component/Security/Http/Authentication/Authenticator/AuthenticatorInterface.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Core\Authentication\Authenticator; +namespace Symfony\Component\Security\Http\Authentication\Authenticator; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; diff --git a/src/Symfony/Component/Security/Core/Authentication/Authenticator/FormLoginAuthenticator.php b/src/Symfony/Component/Security/Http/Authentication/Authenticator/FormLoginAuthenticator.php similarity index 98% rename from src/Symfony/Component/Security/Core/Authentication/Authenticator/FormLoginAuthenticator.php rename to src/Symfony/Component/Security/Http/Authentication/Authenticator/FormLoginAuthenticator.php index 19c5b69029..2ff37f987b 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Authenticator/FormLoginAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authentication/Authenticator/FormLoginAuthenticator.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Core\Authentication\Authenticator; +namespace Symfony\Component\Security\Http\Authentication\Authenticator; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; diff --git a/src/Symfony/Component/Security/Core/Authentication/Authenticator/HttpBasicAuthenticator.php b/src/Symfony/Component/Security/Http/Authentication/Authenticator/HttpBasicAuthenticator.php similarity index 97% rename from src/Symfony/Component/Security/Core/Authentication/Authenticator/HttpBasicAuthenticator.php rename to src/Symfony/Component/Security/Http/Authentication/Authenticator/HttpBasicAuthenticator.php index 6ce74c6809..92cb130ec9 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Authenticator/HttpBasicAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authentication/Authenticator/HttpBasicAuthenticator.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Core\Authentication\Authenticator; +namespace Symfony\Component\Security\Http\Authentication\Authenticator; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\Request; diff --git a/src/Symfony/Component/Security/Core/Authentication/Authenticator/UsernamePasswordTrait.php b/src/Symfony/Component/Security/Http/Authentication/Authenticator/UsernamePasswordTrait.php similarity index 96% rename from src/Symfony/Component/Security/Core/Authentication/Authenticator/UsernamePasswordTrait.php rename to src/Symfony/Component/Security/Http/Authentication/Authenticator/UsernamePasswordTrait.php index 292ec370f8..bbfbc5af02 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Authenticator/UsernamePasswordTrait.php +++ b/src/Symfony/Component/Security/Http/Authentication/Authenticator/UsernamePasswordTrait.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Core\Authentication\Authenticator; +namespace Symfony\Component\Security\Http\Authentication\Authenticator; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; diff --git a/src/Symfony/Component/Security/Core/Authentication/GuardAuthenticationManager.php b/src/Symfony/Component/Security/Http/Authentication/GuardAuthenticationManager.php similarity index 88% rename from src/Symfony/Component/Security/Core/Authentication/GuardAuthenticationManager.php rename to src/Symfony/Component/Security/Http/Authentication/GuardAuthenticationManager.php index 8b4e2e6393..b62516168b 100644 --- a/src/Symfony/Component/Security/Core/Authentication/GuardAuthenticationManager.php +++ b/src/Symfony/Component/Security/Http/Authentication/GuardAuthenticationManager.php @@ -9,9 +9,11 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Core\Authentication; +namespace Symfony\Component\Security\Http\Authentication; -use Symfony\Component\Security\Core\Authentication\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; +use Symfony\Component\Security\Http\Authentication\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticationGuardToken; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\AuthenticationEvents; use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent; @@ -20,9 +22,6 @@ 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\Core\User\UserCheckerInterface; -use Symfony\Component\Security\Guard\Provider\GuardAuthenticationProviderTrait; -use Symfony\Component\Security\Guard\Token\GuardTokenInterface; -use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** @@ -33,7 +32,7 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; */ class GuardAuthenticationManager implements AuthenticationManagerInterface { - use GuardAuthenticationProviderTrait; + use GuardAuthenticationManagerTrait; private $guardAuthenticators; private $userChecker; @@ -58,10 +57,6 @@ class GuardAuthenticationManager implements AuthenticationManagerInterface public function authenticate(TokenInterface $token) { - if (!$token instanceof GuardTokenInterface) { - throw new \InvalidArgumentException('GuardAuthenticationManager only supports GuardTokenInterface.'); - } - if (!$token instanceof PreAuthenticationGuardToken) { /* * The listener *only* passes PreAuthenticationGuardToken instances. diff --git a/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProviderTrait.php b/src/Symfony/Component/Security/Http/Authentication/GuardAuthenticationManagerTrait.php similarity index 95% rename from src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProviderTrait.php rename to src/Symfony/Component/Security/Http/Authentication/GuardAuthenticationManagerTrait.php index 667c35d05e..7de91a75a3 100644 --- a/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProviderTrait.php +++ b/src/Symfony/Component/Security/Http/Authentication/GuardAuthenticationManagerTrait.php @@ -9,9 +9,10 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Guard\Provider; +namespace Symfony\Component\Security\Http\Authentication; -use Symfony\Component\Security\Core\Authentication\Authenticator\AuthenticatorInterface as CoreAuthenticatorInterface; +use Symfony\Component\Security\Http\Authentication\Authenticator\AuthenticatorInterface as CoreAuthenticatorInterface; +use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticationGuardToken; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Exception\LogicException; @@ -20,14 +21,13 @@ use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Guard\AuthenticatorInterface; use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface; -use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken; /** * @author Ryan Weaver * * @internal */ -trait GuardAuthenticationProviderTrait +trait GuardAuthenticationManagerTrait { /** * @param CoreAuthenticatorInterface|AuthenticatorInterface $guardAuthenticator diff --git a/src/Symfony/Component/Security/Http/Authentication/GuardAuthenticatorHandler.php b/src/Symfony/Component/Security/Http/Authentication/GuardAuthenticatorHandler.php new file mode 100644 index 0000000000..d930df1896 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authentication/GuardAuthenticatorHandler.php @@ -0,0 +1,149 @@ + + * + * 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\Authentication\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 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 + * + * @internal + */ +class GuardAuthenticatorHandler +{ + private $tokenStorage; + private $dispatcher; + private $sessionStrategy; + private $statelessProviderKeys; + + /** + * @param array $statelessProviderKeys An array of provider/firewall keys that are "stateless" and so do not need the session migrated on success + */ + public function __construct(TokenStorageInterface $tokenStorage, EventDispatcherInterface $eventDispatcher = null, array $statelessProviderKeys = []) + { + $this->tokenStorage = $tokenStorage; + $this->dispatcher = $eventDispatcher; + $this->statelessProviderKeys = $statelessProviderKeys; + } + + /** + * Authenticates the given token in the system. + */ + public function authenticateWithToken(TokenInterface $token, Request $request, string $providerKey = null) + { + $this->migrateSession($request, $token, $providerKey); + $this->tokenStorage->setToken($token); + + if (null !== $this->dispatcher) { + $loginEvent = new InteractiveLoginEvent($request, $token); + $this->dispatcher->dispatch($loginEvent, SecurityEvents::INTERACTIVE_LOGIN); + } + } + + /** + * Returns the "on success" response for the given GuardAuthenticator. + * + * @param AuthenticatorInterface|GuardAuthenticatorInterface $guardAuthenticator + */ + public function handleAuthenticationSuccess(TokenInterface $token, Request $request, $guardAuthenticator, string $providerKey): ?Response + { + if (!$guardAuthenticator instanceof AuthenticatorInterface && !$guardAuthenticator instanceof GuardAuthenticatorInterface) { + throw new \UnexpectedValueException('Invalid guard authenticator passed to '.__METHOD__.'. Expected AuthenticatorInterface of either Security Core or Security Guard.'); + } + + $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), 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 + { + if (!$authenticator instanceof AuthenticatorInterface && !$authenticator instanceof GuardAuthenticatorInterface) { + throw new \UnexpectedValueException('Invalid guard 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 + $this->authenticateWithToken($token, $request, $providerKey); + + // return the success metric + return $this->handleAuthenticationSuccess($token, $request, $authenticator, $providerKey); + } + + /** + * Handles an authentication failure and returns the Response for the + * GuardAuthenticator. + * + * @param AuthenticatorInterface|GuardAuthenticatorInterface $guardAuthenticator + */ + public function handleAuthenticationFailure(AuthenticationException $authenticationException, Request $request, $guardAuthenticator, string $providerKey): ?Response + { + if (!$guardAuthenticator instanceof AuthenticatorInterface && !$guardAuthenticator instanceof GuardAuthenticatorInterface) { + throw new \UnexpectedValueException('Invalid guard authenticator passed to '.__METHOD__.'. Expected AuthenticatorInterface of either Security Core or Security Guard.'); + } + + $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), get_debug_type($response))); + } + + /** + * Call this method if your authentication token is stored to a session. + * + * @final + */ + public function setSessionAuthenticationStrategy(SessionAuthenticationStrategyInterface $sessionStrategy) + { + $this->sessionStrategy = $sessionStrategy; + } + + private function migrateSession(Request $request, TokenInterface $token, ?string $providerKey) + { + if (\in_array($providerKey, $this->statelessProviderKeys, true) || !$this->sessionStrategy || !$request->hasSession() || !$request->hasPreviousSession()) { + return; + } + + $this->sessionStrategy->onAuthentication($request, $token); + } +} diff --git a/src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php b/src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php index 78681bd1e8..2367223657 100644 --- a/src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php @@ -14,8 +14,8 @@ namespace Symfony\Component\Security\Http\Firewall; use Psr\Log\LoggerInterface; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; -use Symfony\Component\Security\Core\Authentication\Authenticator\AuthenticatorInterface; -use Symfony\Component\Security\Guard\Firewall\GuardAuthenticatorListenerTrait; +use Symfony\Component\Security\Http\Authentication\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticationGuardToken; use Symfony\Component\Security\Guard\GuardAuthenticatorHandler; use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; @@ -28,7 +28,7 @@ use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; */ class GuardManagerListener { - use GuardAuthenticatorListenerTrait; + use GuardManagerListenerTrait; private $authenticationManager; private $guardHandler; @@ -65,6 +65,11 @@ class GuardManagerListener $this->rememberMeServices = $rememberMeServices; } + protected function createPreAuthenticatedToken($credentials, string $uniqueGuardKey, string $providerKey): PreAuthenticationGuardToken + { + return new PreAuthenticationGuardToken($credentials, $uniqueGuardKey, $providerKey); + } + protected function getGuardKey(string $key): string { // Guard authenticators in the GuardManagerListener are already indexed diff --git a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticatorListenerTrait.php b/src/Symfony/Component/Security/Http/Firewall/GuardManagerListenerTrait.php similarity index 94% rename from src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticatorListenerTrait.php rename to src/Symfony/Component/Security/Http/Firewall/GuardManagerListenerTrait.php index 245f02c906..794d1dd133 100644 --- a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticatorListenerTrait.php +++ b/src/Symfony/Component/Security/Http/Firewall/GuardManagerListenerTrait.php @@ -9,16 +9,16 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Guard\Firewall; +namespace Symfony\Component\Security\Http\Firewall; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\RequestEvent; -use Symfony\Component\Security\Core\Authentication\Authenticator\AuthenticatorInterface as CoreAuthenticatorInterface; +use Symfony\Component\Security\Http\Authentication\Authenticator\AuthenticatorInterface as CoreAuthenticatorInterface; +use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticationGuardToken; 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\Token\PreAuthenticationGuardToken; /** * @author Ryan Weaver @@ -26,7 +26,7 @@ use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken; * * @internal */ -trait GuardAuthenticatorListenerTrait +trait GuardManagerListenerTrait { protected function getSupportingGuardAuthenticators(Request $request): array { @@ -89,7 +89,7 @@ trait GuardAuthenticatorListenerTrait } // create a token with the unique key, so that the provider knows which authenticator to use - $token = new PreAuthenticationGuardToken($credentials, $uniqueGuardKey, $this->providerKey); + $token = $this->createPreAuthenticatedToken($credentials, $uniqueGuardKey, $this->providerKey); if (null !== $this->logger) { $this->logger->debug('Passing guard token information to the GuardAuthenticationProvider', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)]); @@ -174,4 +174,6 @@ trait GuardAuthenticatorListenerTrait } abstract protected function getGuardKey(string $key): string; + + abstract protected function createPreAuthenticatedToken($credentials, string $uniqueGuardKey, string $providerKey): PreAuthenticationGuardToken; } diff --git a/src/Symfony/Component/Security/Http/composer.json b/src/Symfony/Component/Security/Http/composer.json index 376ee410fa..77a16c50ce 100644 --- a/src/Symfony/Component/Security/Http/composer.json +++ b/src/Symfony/Component/Security/Http/composer.json @@ -18,7 +18,7 @@ "require": { "php": "^7.2.5", "symfony/deprecation-contracts": "^2.1", - "symfony/security-core": "^4.4.8|^5.0.8", + "symfony/security-core": "^5.1", "symfony/http-foundation": "^4.4.7|^5.0.7", "symfony/http-kernel": "^4.4|^5.0", "symfony/polyfill-php80": "^1.15", From 999ec2795fcd6bfa1ff31c6c6646ff42ca61ee06 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Thu, 6 Feb 2020 15:06:07 +0100 Subject: [PATCH 13/30] Refactor to an event based authentication approach This allows more flexibility for the authentication manager (to e.g. implement login throttling, easier remember me, etc). It is also a known design pattern in Symfony HttpKernel. --- .../Security/Factory/FormLoginFactory.php | 2 +- .../DependencyInjection/SecurityExtension.php | 20 +-- .../LazyGuardManagerListener.php | 4 +- .../Resources/config/authenticators.xml | 35 ++++- .../Resources/config/security.xml | 2 +- .../Firewall/GuardAuthenticationListener.php | 124 ++++++++++++++++- .../Guard/PasswordAuthenticatedInterface.php | 4 - .../Provider/GuardAuthenticationProvider.php | 50 +++++++ .../Authenticator/AnonymousAuthenticator.php | 13 +- .../Authenticator/AuthenticatorInterface.php | 12 -- .../CustomAuthenticatedInterface.php | 27 ++++ .../Authenticator/FormLoginAuthenticator.php | 23 +++- .../Authenticator/HttpBasicAuthenticator.php | 16 ++- .../TokenAuthenticatedInterface.php | 24 ++++ .../Authenticator/UsernamePasswordTrait.php | 50 ------- .../GuardAuthenticationManager.php | 54 ++++++-- .../GuardAuthenticationManagerTrait.php | 59 -------- .../Security/Http/Event/LoginFailureEvent.php | 60 ++++++++ .../Security/Http/Event/LoginSuccessEvent.php | 62 +++++++++ .../VerifyAuthenticatorCredentialsEvent.php | 57 ++++++++ .../EventListener/AuthenticatingListener.php | 68 +++++++++ .../PasswordMigratingListener.php | 65 +++++++++ .../Http/EventListener/RememberMeListener.php | 88 ++++++++++++ .../EventListener/UserCheckerListener.php | 43 ++++++ .../Http/Firewall/GuardManagerListener.php | 103 ++++++++++++-- .../Firewall/GuardManagerListenerTrait.php | 129 ------------------ 26 files changed, 876 insertions(+), 318 deletions(-) create mode 100644 src/Symfony/Component/Security/Http/Authentication/Authenticator/CustomAuthenticatedInterface.php create mode 100644 src/Symfony/Component/Security/Http/Authentication/Authenticator/TokenAuthenticatedInterface.php delete mode 100644 src/Symfony/Component/Security/Http/Authentication/Authenticator/UsernamePasswordTrait.php create mode 100644 src/Symfony/Component/Security/Http/Event/LoginFailureEvent.php create mode 100644 src/Symfony/Component/Security/Http/Event/LoginSuccessEvent.php create mode 100644 src/Symfony/Component/Security/Http/Event/VerifyAuthenticatorCredentialsEvent.php create mode 100644 src/Symfony/Component/Security/Http/EventListener/AuthenticatingListener.php create mode 100644 src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php create mode 100644 src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php create mode 100644 src/Symfony/Component/Security/Http/EventListener/UserCheckerListener.php diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php index 386ba8e462..cfed004d86 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php @@ -106,7 +106,7 @@ class FormLoginFactory extends AbstractFactory implements GuardFactoryInterface, ->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.form_login')) ->replaceArgument(1, isset($config['csrf_token_generator']) ? new Reference($config['csrf_token_generator']) : null) ->replaceArgument(2, new Reference($userProviderId)) - ->replaceArgument(4, $options); + ->replaceArgument(3, $options); return $authenticatorId; } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index f1bf246d8d..d67682e883 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -419,16 +419,13 @@ class SecurityExtension extends Extension implements PrependExtensionInterface $configuredEntryPoint = isset($firewall['entry_point']) ? $firewall['entry_point'] : null; if ($this->guardAuthenticationManagerEnabled) { - // guard authentication manager listener (must be before calling createAuthenticationListeners() to inject remember me services) + // Remember me listener (must be before calling createAuthenticationListeners() to inject remember me services) $container - ->setDefinition('security.firewall.guard.'.$id, new ChildDefinition('security.firewall.guard')) - ->replaceArgument(2, new Reference('security.firewall.guard.'.$id.'.locator')) - ->replaceArgument(3, $id) - ->addTag('kernel.event_listener', ['event' => KernelEvents::REQUEST]) + ->setDefinition('security.listener.remember_me.'.$id, new ChildDefinition('security.listener.remember_me')) + ->replaceArgument(0, $id) + ->addTag('kernel.event_subscriber') ->addTag('security.remember_me_aware', ['id' => $id, 'provider' => 'none']) ; - - $listeners[] = new Reference('security.firewall.guard.'.$id); } // Authentication listeners @@ -438,7 +435,7 @@ class SecurityExtension extends Extension implements PrependExtensionInterface $authenticationProviders = array_merge($authenticationProviders, $firewallAuthenticationProviders); if ($this->guardAuthenticationManagerEnabled) { - // add authentication providers for this firewall to the GuardManagerListener (if guard is enabled) + // guard authentication manager listener $container ->setDefinition('security.firewall.guard.'.$id.'.locator', new ChildDefinition('security.firewall.guard.locator')) ->setArguments([array_map(function ($id) { @@ -446,10 +443,15 @@ class SecurityExtension extends Extension implements PrependExtensionInterface }, $firewallAuthenticationProviders)]) ->addTag('container.service_locator') ; + $container - ->getDefinition('security.firewall.guard.'.$id) + ->setDefinition('security.firewall.guard.'.$id, new ChildDefinition('security.firewall.guard')) ->replaceArgument(2, new Reference('security.firewall.guard.'.$id.'.locator')) + ->replaceArgument(3, $id) + ->addTag('kernel.event_listener', ['event' => KernelEvents::REQUEST]) ; + + $listeners[] = new Reference('security.firewall.guard.'.$id); } $config->replaceArgument(7, $configuredEntryPoint ?: $defaultEntryPoint); diff --git a/src/Symfony/Bundle/SecurityBundle/EventListener/LazyGuardManagerListener.php b/src/Symfony/Bundle/SecurityBundle/EventListener/LazyGuardManagerListener.php index 958ca5d4bb..4cea805737 100644 --- a/src/Symfony/Bundle/SecurityBundle/EventListener/LazyGuardManagerListener.php +++ b/src/Symfony/Bundle/SecurityBundle/EventListener/LazyGuardManagerListener.php @@ -13,6 +13,7 @@ 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\Guard\GuardAuthenticatorHandler; @@ -32,9 +33,10 @@ class LazyGuardManagerListener extends GuardManagerListener GuardAuthenticatorHandler $guardHandler, ServiceLocator $guardLocator, string $providerKey, + EventDispatcherInterface $eventDispatcher, ?LoggerInterface $logger = null ) { - parent::__construct($authenticationManager, $guardHandler, [], $providerKey, $logger); + parent::__construct($authenticationManager, $guardHandler, [], $providerKey, $eventDispatcher, $logger); $this->guardLocator = $guardLocator; } diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml index f752f923ca..a6b1a0a9f5 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml @@ -12,13 +12,41 @@ class="Symfony\Bundle\SecurityBundle\EventListener\LazyGuardManagerListener" abstract="true"> - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -34,14 +62,13 @@ user provider - options - + secret diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml index 5e31b492f0..f3da0349b2 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml @@ -54,7 +54,7 @@ - + %security.authentication.manager.erase_credentials% diff --git a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php index 7ffad32454..50b42990c5 100644 --- a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php +++ b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php @@ -13,9 +13,12 @@ namespace Symfony\Component\Security\Guard\Firewall; use Psr\Log\LoggerInterface; 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\Authentication\Token\PreAuthenticationGuardToken; +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\GuardAuthenticatorHandler; use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken as GuardPreAuthenticationGuardToken; @@ -104,15 +107,122 @@ class GuardAuthenticationListener extends AbstractListener $this->rememberMeServices = $rememberMeServices; } + /** + * @param AuthenticatorInterface[] $guardAuthenticators + */ + protected function executeGuardAuthenticators(array $guardAuthenticators, RequestEvent $event): void + { + foreach ($guardAuthenticators as $key => $guardAuthenticator) { + $uniqueGuardKey = $this->providerKey.'_'.$key;; + + $this->executeGuardAuthenticator($uniqueGuardKey, $guardAuthenticator, $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($guardAuthenticator)]); + } + + break; + } + } + } + + private function executeGuardAuthenticator(string $uniqueGuardKey, AuthenticatorInterface $guardAuthenticator, RequestEvent $event) + { + $request = $event->getRequest(); + try { + if (null !== $this->logger) { + $this->logger->debug('Calling getCredentials() on guard authenticator.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)]); + } + + // allow the authenticator to fetch authentication info from the request + $credentials = $guardAuthenticator->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_debug_type($guardAuthenticator))); + } + + // create a token with the unique key, so that the provider knows which authenticator to use + $token = $this->createPreAuthenticatedToken($credentials, $uniqueGuardKey, $this->providerKey); + + if (null !== $this->logger) { + $this->logger->debug('Passing guard token information to the GuardAuthenticationProvider', ['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!', ['token' => $token, 'authenticator' => \get_class($guardAuthenticator)]); + } + + // sets the token on the token storage, etc + $this->guardHandler->authenticateWithToken($token, $request, $this->providerKey); + } catch (AuthenticationException $e) { + // oh no! Authentication failed! + + if (null !== $this->logger) { + $this->logger->info('Guard authentication failed.', ['exception' => $e, 'authenticator' => \get_class($guardAuthenticator)]); + } + + $response = $this->guardHandler->handleAuthenticationFailure($e, $request, $guardAuthenticator, $this->providerKey); + + if ($response instanceof Response) { + $event->setResponse($response); + } + + return; + } + + // success! + $response = $this->guardHandler->handleAuthenticationSuccess($token, $request, $guardAuthenticator, $this->providerKey); + if ($response instanceof Response) { + if (null !== $this->logger) { + $this->logger->debug('Guard authenticator set success response.', ['response' => $response, 'authenticator' => \get_class($guardAuthenticator)]); + } + + $event->setResponse($response); + } else { + if (null !== $this->logger) { + $this->logger->debug('Guard authenticator set no success response: request continues.', ['authenticator' => \get_class($guardAuthenticator)]); + } + } + + // attempt to trigger the remember me functionality + $this->triggerRememberMe($guardAuthenticator, $request, $token, $response); + } + + protected function triggerRememberMe($guardAuthenticator, Request $request, TokenInterface $token, Response $response = null) + { + if (!$guardAuthenticator instanceof AuthenticatorInterface && !$guardAuthenticator instanceof CoreAuthenticatorInterface) { + throw new \UnexpectedValueException('Invalid guard authenticator passed to '.__METHOD__.'. Expected AuthenticatorInterface of either Security Core or Security Guard.'); + } + + if (null === $this->rememberMeServices) { + if (null !== $this->logger) { + $this->logger->debug('Remember me skipped: it is not configured for the firewall.', ['authenticator' => \get_class($guardAuthenticator)]); + } + + return; + } + + if (!$guardAuthenticator->supportsRememberMe()) { + if (null !== $this->logger) { + $this->logger->debug('Remember me skipped: your authenticator does not support it.', ['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_debug_type($guardAuthenticator))); + } + + $this->rememberMeServices->loginSuccess($request, $response, $token); + } + protected function createPreAuthenticatedToken($credentials, string $uniqueGuardKey, string $providerKey): PreAuthenticationGuardToken { return new GuardPreAuthenticationGuardToken($credentials, $uniqueGuardKey, $providerKey); } - - protected function getGuardKey(string $key): string - { - // get a key that's unique to *this* guard authenticator - // this MUST be the same as GuardAuthenticationProvider - return $this->providerKey.'_'.$key; - } } diff --git a/src/Symfony/Component/Security/Guard/PasswordAuthenticatedInterface.php b/src/Symfony/Component/Security/Guard/PasswordAuthenticatedInterface.php index b6b26cbd31..dd2eeba33d 100644 --- a/src/Symfony/Component/Security/Guard/PasswordAuthenticatedInterface.php +++ b/src/Symfony/Component/Security/Guard/PasswordAuthenticatedInterface.php @@ -11,8 +11,6 @@ namespace Symfony\Component\Security\Guard; -use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; - /** * An optional interface for "guard" authenticators that deal with user passwords. */ @@ -24,6 +22,4 @@ interface PasswordAuthenticatedInterface * @param mixed $credentials The user credentials */ public function getPassword($credentials): ?string; - - /* public function getPasswordEncoder(): ?UserPasswordEncoderInterface; */ } diff --git a/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php b/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php index 01f70e9b4e..9733584119 100644 --- a/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php +++ b/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php @@ -11,6 +11,14 @@ 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\Http\Authentication\GuardAuthenticationManagerTrait; use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; @@ -22,6 +30,7 @@ use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Guard\AuthenticatorInterface; use Symfony\Component\Security\Guard\Token\GuardTokenInterface; use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken; +use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; /** * Responsible for accepting the PreAuthenticationGuardToken and calling @@ -41,6 +50,7 @@ class GuardAuthenticationProvider implements AuthenticationProviderInterface private $providerKey; private $userChecker; private $passwordEncoder; + private $rememberMeServices; /** * @param iterable|AuthenticatorInterface[] $guardAuthenticators The authenticators, with keys that match what's passed to GuardAuthenticationListener @@ -106,8 +116,48 @@ class GuardAuthenticationProvider implements AuthenticationProviderInterface return $token instanceof GuardTokenInterface; } + public function setRememberMeServices(RememberMeServicesInterface $rememberMeServices) + { + $this->rememberMeServices = $rememberMeServices; + } + protected function getGuardKey(string $key): string { return $this->providerKey.'_'.$key; } + + private function authenticateViaGuard(AuthenticatorInterface $guardAuthenticator, \Symfony\Component\Security\Core\Authentication\Token\PreAuthenticationGuardToken $token, string $providerKey): TokenInterface + { + // get the user from the GuardAuthenticator + $user = $guardAuthenticator->getUser($token->getCredentials(), $this->userProvider); + if (null === $user) { + throw new UsernameNotFoundException(sprintf('Null returned from "%s::getUser()".', get_debug_type($guardAuthenticator))); + } + + if (!$user instanceof UserInterface) { + throw new \UnexpectedValueException(sprintf('The "%s::getUser()" method must return a UserInterface. You returned "%s".', get_debug_type($guardAuthenticator), get_debug_type($user))); + } + + $this->userChecker->checkPreAuth($user); + if (true !== $checkCredentialsResult = $guardAuthenticator->checkCredentials($token->getCredentials(), $user)) { + if (false !== $checkCredentialsResult) { + throw new \TypeError(sprintf('"%s::checkCredentials()" must return a boolean value.', get_debug_type($guardAuthenticator))); + } + + throw new BadCredentialsException(sprintf('Authentication failed because "%s::checkCredentials()" did not return true.', get_debug_type($guardAuthenticator))); + } + + if ($this->userProvider instanceof PasswordUpgraderInterface && $guardAuthenticator instanceof PasswordAuthenticatedInterface && null !== $this->passwordEncoder && (null !== $password = $guardAuthenticator->getPassword($token->getCredentials())) && method_exists($this->passwordEncoder, 'needsRehash') && $this->passwordEncoder->needsRehash($user)) { + $this->userProvider->upgradePassword($user, $this->passwordEncoder->encodePassword($user, $password)); + } + $this->userChecker->checkPostAuth($user); + + // turn the UserInterface into a TokenInterface + $authenticatedToken = $guardAuthenticator->createAuthenticatedToken($user, $providerKey); + if (!$authenticatedToken instanceof TokenInterface) { + throw new \UnexpectedValueException(sprintf('The "%s::createAuthenticatedToken()" method must return a TokenInterface. You returned "%s".', get_debug_type($guardAuthenticator), get_debug_type($authenticatedToken))); + } + + return $authenticatedToken; + } } diff --git a/src/Symfony/Component/Security/Http/Authentication/Authenticator/AnonymousAuthenticator.php b/src/Symfony/Component/Security/Http/Authentication/Authenticator/AnonymousAuthenticator.php index bec859e7a7..c6b9427fce 100644 --- a/src/Symfony/Component/Security/Http/Authentication/Authenticator/AnonymousAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authentication/Authenticator/AnonymousAuthenticator.php @@ -27,7 +27,7 @@ use Symfony\Component\Security\Core\User\UserInterface; * @final * @experimental in 5.1 */ -class AnonymousAuthenticator implements AuthenticatorInterface +class AnonymousAuthenticator implements AuthenticatorInterface, CustomAuthenticatedInterface { private $secret; private $tokenStorage; @@ -49,16 +49,17 @@ class AnonymousAuthenticator implements AuthenticatorInterface return []; } + public function checkCredentials($credentials, UserInterface $user): bool + { + // anonymous users do not have credentials + return true; + } + public function getUser($credentials): ?UserInterface { return new User('anon.', null); } - public function checkCredentials($credentials, UserInterface $user): bool - { - return true; - } - public function createAuthenticatedToken(UserInterface $user, string $providerKey): TokenInterface { return new AnonymousToken($this->secret, 'anon.', []); diff --git a/src/Symfony/Component/Security/Http/Authentication/Authenticator/AuthenticatorInterface.php b/src/Symfony/Component/Security/Http/Authentication/Authenticator/AuthenticatorInterface.php index 8bf38ac85a..e2ca2e2e0c 100644 --- a/src/Symfony/Component/Security/Http/Authentication/Authenticator/AuthenticatorInterface.php +++ b/src/Symfony/Component/Security/Http/Authentication/Authenticator/AuthenticatorInterface.php @@ -70,18 +70,6 @@ interface AuthenticatorInterface */ public function getUser($credentials): ?UserInterface; - /** - * Returns true if the credentials are valid. - * - * If false is returned, authentication will fail. You may also throw - * an AuthenticationException if you wish to cause authentication to fail. - * - * @param mixed $credentials the value returned from getCredentials() - * - * @throws AuthenticationException - */ - public function checkCredentials($credentials, UserInterface $user): bool; - /** * Create an authenticated token for the given user. * diff --git a/src/Symfony/Component/Security/Http/Authentication/Authenticator/CustomAuthenticatedInterface.php b/src/Symfony/Component/Security/Http/Authentication/Authenticator/CustomAuthenticatedInterface.php new file mode 100644 index 0000000000..69ec6da097 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authentication/Authenticator/CustomAuthenticatedInterface.php @@ -0,0 +1,27 @@ + + */ +interface CustomAuthenticatedInterface +{ + /** + * Returns true if the credentials are valid. + * + * If false is returned, authentication will fail. You may also throw + * an AuthenticationException if you wish to cause authentication to fail. + * + * @param mixed $credentials the value returned from getCredentials() + * + * @throws AuthenticationException + */ + public function checkCredentials($credentials, UserInterface $user): bool; +} diff --git a/src/Symfony/Component/Security/Http/Authentication/Authenticator/FormLoginAuthenticator.php b/src/Symfony/Component/Security/Http/Authentication/Authenticator/FormLoginAuthenticator.php index 2ff37f987b..acdb5e257a 100644 --- a/src/Symfony/Component/Security/Http/Authentication/Authenticator/FormLoginAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authentication/Authenticator/FormLoginAuthenticator.php @@ -15,6 +15,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException; @@ -23,6 +24,7 @@ use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Csrf\CsrfToken; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; +use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface; use Symfony\Component\Security\Http\HttpUtils; use Symfony\Component\Security\Http\ParameterBagUtils; use Symfony\Component\Security\Http\Util\TargetPathTrait; @@ -34,23 +36,19 @@ use Symfony\Component\Security\Http\Util\TargetPathTrait; * @final * @experimental in 5.1 */ -class FormLoginAuthenticator extends AbstractFormLoginAuthenticator +class FormLoginAuthenticator extends AbstractFormLoginAuthenticator implements PasswordAuthenticatedInterface { - use TargetPathTrait, UsernamePasswordTrait { - UsernamePasswordTrait::checkCredentials as checkPassword; - } + use TargetPathTrait; private $options; private $httpUtils; private $csrfTokenManager; private $userProvider; - private $encoderFactory; - public function __construct(HttpUtils $httpUtils, ?CsrfTokenManagerInterface $csrfTokenManager, UserProviderInterface $userProvider, EncoderFactoryInterface $encoderFactory, array $options) + public function __construct(HttpUtils $httpUtils, ?CsrfTokenManagerInterface $csrfTokenManager, UserProviderInterface $userProvider, array $options) { $this->httpUtils = $httpUtils; $this->csrfTokenManager = $csrfTokenManager; - $this->encoderFactory = $encoderFactory; $this->options = array_merge([ 'username_parameter' => '_username', 'password_parameter' => '_password', @@ -109,11 +107,17 @@ class FormLoginAuthenticator extends AbstractFormLoginAuthenticator return $credentials; } + public function getPassword($credentials): ?string + { + return $credentials['password']; + } + public function getUser($credentials): ?UserInterface { return $this->userProvider->loadUserByUsername($credentials['username']); } + /* @todo How to do CSRF protection? public function checkCredentials($credentials, UserInterface $user): bool { if (null !== $this->csrfTokenManager) { @@ -123,6 +127,11 @@ class FormLoginAuthenticator extends AbstractFormLoginAuthenticator } return $this->checkPassword($credentials, $user); + }*/ + + public function createAuthenticatedToken(UserInterface $user, $providerKey): TokenInterface + { + return new UsernamePasswordToken($user, null, $providerKey, $user->getRoles()); } public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): Response diff --git a/src/Symfony/Component/Security/Http/Authentication/Authenticator/HttpBasicAuthenticator.php b/src/Symfony/Component/Security/Http/Authentication/Authenticator/HttpBasicAuthenticator.php index 92cb130ec9..c3ff43f01c 100644 --- a/src/Symfony/Component/Security/Http/Authentication/Authenticator/HttpBasicAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authentication/Authenticator/HttpBasicAuthenticator.php @@ -15,10 +15,12 @@ use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface; use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface; /** @@ -28,10 +30,8 @@ use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface * @final * @experimental in 5.1 */ -class HttpBasicAuthenticator implements AuthenticatorInterface, AuthenticationEntryPointInterface +class HttpBasicAuthenticator implements AuthenticatorInterface, AuthenticationEntryPointInterface, PasswordAuthenticatedInterface { - use UsernamePasswordTrait; - private $realmName; private $userProvider; private $encoderFactory; @@ -67,11 +67,21 @@ class HttpBasicAuthenticator implements AuthenticatorInterface, AuthenticationEn ]; } + public function getPassword($credentials): ?string + { + return $credentials['password']; + } + public function getUser($credentials): ?UserInterface { return $this->userProvider->loadUserByUsername($credentials['username']); } + public function createAuthenticatedToken(UserInterface $user, $providerKey): TokenInterface + { + return new UsernamePasswordToken($user, null, $providerKey, $user->getRoles()); + } + public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey): ?Response { return null; diff --git a/src/Symfony/Component/Security/Http/Authentication/Authenticator/TokenAuthenticatedInterface.php b/src/Symfony/Component/Security/Http/Authentication/Authenticator/TokenAuthenticatedInterface.php new file mode 100644 index 0000000000..4630c57ae9 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authentication/Authenticator/TokenAuthenticatedInterface.php @@ -0,0 +1,24 @@ + + */ +interface TokenAuthenticatedInterface +{ + /** + * Extracts the token from the credentials. + * + * If you return null, the credentials will not be marked as + * valid and a BadCredentialsException is thrown. + * + * @param mixed $credentials The user credentials + * + * @return mixed|null the token - if any - or null otherwise + */ + public function getToken($credentials); +} diff --git a/src/Symfony/Component/Security/Http/Authentication/Authenticator/UsernamePasswordTrait.php b/src/Symfony/Component/Security/Http/Authentication/Authenticator/UsernamePasswordTrait.php deleted file mode 100644 index bbfbc5af02..0000000000 --- a/src/Symfony/Component/Security/Http/Authentication/Authenticator/UsernamePasswordTrait.php +++ /dev/null @@ -1,50 +0,0 @@ - - * - * 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\Authenticator; - -use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; -use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; -use Symfony\Component\Security\Core\Exception\BadCredentialsException; -use Symfony\Component\Security\Core\User\UserInterface; - -/** - * @author Wouter de Jong - * - * @property EncoderFactoryInterface $encoderFactory - * - * @experimental in 5.1 - */ -trait UsernamePasswordTrait -{ - public function checkCredentials($credentials, UserInterface $user): bool - { - if (!$this->encoderFactory instanceof EncoderFactoryInterface) { - throw new \LogicException(\get_class($this).' uses the '.__CLASS__.' trait, which requires an $encoderFactory property to be initialized with an '.EncoderFactoryInterface::class.' implementation.'); - } - - if ('' === $credentials['password']) { - throw new BadCredentialsException('The presented password cannot be empty.'); - } - - if (!$this->encoderFactory->getEncoder($user)->isPasswordValid($user->getPassword(), $credentials['password'], null)) { - throw new BadCredentialsException('The presented password is invalid.'); - } - - return true; - } - - public function createAuthenticatedToken(UserInterface $user, $providerKey): TokenInterface - { - return new UsernamePasswordToken($user, null, $providerKey, $user->getRoles()); - } -} diff --git a/src/Symfony/Component/Security/Http/Authentication/GuardAuthenticationManager.php b/src/Symfony/Component/Security/Http/Authentication/GuardAuthenticationManager.php index b62516168b..29bb5476ed 100644 --- a/src/Symfony/Component/Security/Http/Authentication/GuardAuthenticationManager.php +++ b/src/Symfony/Component/Security/Http/Authentication/GuardAuthenticationManager.php @@ -12,6 +12,9 @@ namespace Symfony\Component\Security\Http\Authentication; use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; +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\Authentication\Authenticator\AuthenticatorInterface; use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticationGuardToken; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; @@ -21,7 +24,7 @@ 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\Core\User\UserCheckerInterface; +use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** @@ -35,18 +38,16 @@ class GuardAuthenticationManager implements AuthenticationManagerInterface use GuardAuthenticationManagerTrait; private $guardAuthenticators; - private $userChecker; - private $eraseCredentials; - /** @var EventDispatcherInterface */ private $eventDispatcher; + private $eraseCredentials; /** * @param iterable|AuthenticatorInterface[] $guardAuthenticators The authenticators, with keys that match what's passed to GuardAuthenticationListener */ - public function __construct($guardAuthenticators, UserCheckerInterface $userChecker, bool $eraseCredentials = true) + public function __construct(iterable $guardAuthenticators, EventDispatcherInterface $eventDispatcher, bool $eraseCredentials = true) { $this->guardAuthenticators = $guardAuthenticators; - $this->userChecker = $userChecker; + $this->eventDispatcher = $eventDispatcher; $this->eraseCredentials = $eraseCredentials; } @@ -100,6 +101,40 @@ class GuardAuthenticationManager implements AuthenticationManagerInterface return $result; } + protected function getGuardKey(string $key): string + { + // Guard authenticators in the GuardAuthenticationManager are already indexed + // by an unique key + return $key; + } + + private function authenticateViaGuard(AuthenticatorInterface $authenticator, PreAuthenticationGuardToken $token, string $providerKey): TokenInterface + { + // get the user from the Authenticator + $user = $authenticator->getUser($token->getCredentials()); + if (null === $user) { + throw new UsernameNotFoundException(sprintf('Null returned from "%s::getUser()".', \get_class($authenticator))); + } + + if (!$user instanceof UserInterface) { + 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); + $this->eventDispatcher->dispatch($event); + if (true !== $event->areCredentialsValid()) { + 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); + 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))); + } + + return $authenticatedToken; + } + private function handleFailure(AuthenticationException $exception, TokenInterface $token) { if (null !== $this->eventDispatcher) { @@ -110,11 +145,4 @@ class GuardAuthenticationManager implements AuthenticationManagerInterface throw $exception; } - - protected function getGuardKey(string $key): string - { - // Guard authenticators in the GuardAuthenticationManager are already indexed - // by an unique key - return $key; - } } diff --git a/src/Symfony/Component/Security/Http/Authentication/GuardAuthenticationManagerTrait.php b/src/Symfony/Component/Security/Http/Authentication/GuardAuthenticationManagerTrait.php index 7de91a75a3..3808d79be1 100644 --- a/src/Symfony/Component/Security/Http/Authentication/GuardAuthenticationManagerTrait.php +++ b/src/Symfony/Component/Security/Http/Authentication/GuardAuthenticationManagerTrait.php @@ -29,65 +29,6 @@ use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface; */ trait GuardAuthenticationManagerTrait { - /** - * @param CoreAuthenticatorInterface|AuthenticatorInterface $guardAuthenticator - */ - private function authenticateViaGuard($guardAuthenticator, PreAuthenticationGuardToken $token, string $providerKey): TokenInterface - { - // get the user from the GuardAuthenticator - if ($guardAuthenticator instanceof AuthenticatorInterface) { - if (!isset($this->userProvider)) { - throw new LogicException(sprintf('%s only supports authenticators implementing "%s", update "%s" or use the legacy guard integration instead.', __CLASS__, CoreAuthenticatorInterface::class, \get_class($guardAuthenticator))); - } - $user = $guardAuthenticator->getUser($token->getCredentials(), $this->userProvider); - } elseif ($guardAuthenticator instanceof CoreAuthenticatorInterface) { - $user = $guardAuthenticator->getUser($token->getCredentials()); - } else { - throw new \UnexpectedValueException('Invalid guard authenticator passed to '.__METHOD__.'. Expected AuthenticatorInterface of either Security Core or Security Guard.'); - } - - if (null === $user) { - throw new UsernameNotFoundException(sprintf('Null returned from "%s::getUser()".', get_debug_type($guardAuthenticator))); - } - - if (!$user instanceof UserInterface) { - throw new \UnexpectedValueException(sprintf('The "%s::getUser()" method must return a UserInterface. You returned "%s".', get_debug_type($guardAuthenticator), get_debug_type($user))); - } - - $this->userChecker->checkPreAuth($user); - if (true !== $checkCredentialsResult = $guardAuthenticator->checkCredentials($token->getCredentials(), $user)) { - if (false !== $checkCredentialsResult) { - throw new \TypeError(sprintf('"%s::checkCredentials()" must return a boolean value.', get_debug_type($guardAuthenticator))); - } - - throw new BadCredentialsException(sprintf('Authentication failed because "%s::checkCredentials()" did not return true.', get_debug_type($guardAuthenticator))); - } - - if ($guardAuthenticator instanceof PasswordAuthenticatedInterface - && null !== $password = $guardAuthenticator->getPassword($token->getCredentials()) - && null !== $passwordEncoder = $this->passwordEncoder ?? (method_exists($guardAuthenticator, 'getPasswordEncoder') ? $guardAuthenticator->getPasswordEncoder() : null) - ) { - if (method_exists($passwordEncoder, 'needsRehash') && $passwordEncoder->needsRehash($user)) { - if (!isset($this->userProvider)) { - if ($guardAuthenticator instanceof PasswordUpgraderInterface) { - $guardAuthenticator->upgradePassword($user, $guardAuthenticator->getPasswordEncoder()->encodePassword($user, $password)); - } - } elseif ($this->userProvider instanceof PasswordUpgraderInterface) { - $this->userProvider->upgradePassword($user, $passwordEncoder->encodePassword($user, $password)); - } - } - } - $this->userChecker->checkPostAuth($user); - - // turn the UserInterface into a TokenInterface - $authenticatedToken = $guardAuthenticator->createAuthenticatedToken($user, $providerKey); - if (!$authenticatedToken instanceof TokenInterface) { - throw new \UnexpectedValueException(sprintf('The "%s::createAuthenticatedToken()" method must return a TokenInterface. You returned "%s".', get_debug_type($guardAuthenticator), get_debug_type($authenticatedToken))); - } - - return $authenticatedToken; - } - /** * @return CoreAuthenticatorInterface|\Symfony\Component\Security\Guard\AuthenticatorInterface|null */ diff --git a/src/Symfony/Component/Security/Http/Event/LoginFailureEvent.php b/src/Symfony/Component/Security/Http/Event/LoginFailureEvent.php new file mode 100644 index 0000000000..6a5cf03e01 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Event/LoginFailureEvent.php @@ -0,0 +1,60 @@ + + */ +class LoginFailureEvent extends Event +{ + private $exception; + private $authenticator; + private $request; + private $response; + private $providerKey; + + public function __construct(AuthenticationException $exception, AuthenticatorInterface $authenticator, Request $request, ?Response $response, string $providerKey) + { + $this->exception = $exception; + $this->authenticator = $authenticator; + $this->request = $request; + $this->response = $response; + $this->providerKey = $providerKey; + } + + public function getException(): AuthenticationException + { + return $this->exception; + } + + public function getAuthenticator(): AuthenticatorInterface + { + return $this->authenticator; + } + + public function getProviderKey(): string + { + return $this->providerKey; + } + + public function getRequest(): Request + { + return $this->request; + } + + public function getResponse(): ?Response + { + return $this->response; + } +} diff --git a/src/Symfony/Component/Security/Http/Event/LoginSuccessEvent.php b/src/Symfony/Component/Security/Http/Event/LoginSuccessEvent.php new file mode 100644 index 0000000000..de93b3a78c --- /dev/null +++ b/src/Symfony/Component/Security/Http/Event/LoginSuccessEvent.php @@ -0,0 +1,62 @@ + + */ +class LoginSuccessEvent extends Event +{ + private $authenticator; + private $authenticatedToken; + private $request; + private $response; + private $providerKey; + + public function __construct(AuthenticatorInterface $authenticator, TokenInterface $authenticatedToken, Request $request, ?Response $response, string $providerKey) + { + $this->authenticator = $authenticator; + $this->authenticatedToken = $authenticatedToken; + $this->request = $request; + $this->response = $response; + $this->providerKey = $providerKey; + } + + public function getAuthenticator(): AuthenticatorInterface + { + return $this->authenticator; + } + + public function getAuthenticatedToken(): TokenInterface + { + return $this->authenticatedToken; + } + + public function getRequest(): Request + { + return $this->request; + } + + public function getResponse(): ?Response + { + return $this->response; + } + + public function getProviderKey(): string + { + return $this->providerKey; + } +} diff --git a/src/Symfony/Component/Security/Http/Event/VerifyAuthenticatorCredentialsEvent.php b/src/Symfony/Component/Security/Http/Event/VerifyAuthenticatorCredentialsEvent.php new file mode 100644 index 0000000000..173f448048 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Event/VerifyAuthenticatorCredentialsEvent.php @@ -0,0 +1,57 @@ + + */ +class VerifyAuthenticatorCredentialsEvent extends Event +{ + private $authenticator; + private $preAuthenticatedToken; + private $user; + private $credentialsValid = false; + + public function __construct(AuthenticatorInterface $authenticator, TokenInterface $preAuthenticatedToken, ?UserInterface $user) + { + $this->authenticator = $authenticator; + $this->preAuthenticatedToken = $preAuthenticatedToken; + $this->user = $user; + } + + public function getAuthenticator(): AuthenticatorInterface + { + return $this->authenticator; + } + + public function getPreAuthenticatedToken(): TokenInterface + { + return $this->preAuthenticatedToken; + } + + public function getUser(): ?UserInterface + { + return $this->user; + } + + public function setCredentialsValid(bool $validated = true): void + { + $this->credentialsValid = $validated; + } + + public function areCredentialsValid(): bool + { + return $this->credentialsValid; + } +} diff --git a/src/Symfony/Component/Security/Http/EventListener/AuthenticatingListener.php b/src/Symfony/Component/Security/Http/EventListener/AuthenticatingListener.php new file mode 100644 index 0000000000..738142bc05 --- /dev/null +++ b/src/Symfony/Component/Security/Http/EventListener/AuthenticatingListener.php @@ -0,0 +1,68 @@ + + * + * @final + * @experimental in 5.1 + */ +class AuthenticatingListener implements EventSubscriberInterface +{ + private $encoderFactory; + + public function __construct(EncoderFactoryInterface $encoderFactory) + { + $this->encoderFactory = $encoderFactory; + } + + public static function getSubscribedEvents(): array + { + return [VerifyAuthenticatorCredentialsEvent::class => ['onAuthenticating', 128]]; + } + + public function onAuthenticating(VerifyAuthenticatorCredentialsEvent $event): void + { + $authenticator = $event->getAuthenticator(); + if ($authenticator instanceof PasswordAuthenticatedInterface) { + // Use the password encoder to validate the credentials + $user = $event->getUser(); + $event->setCredentialsValid($this->encoderFactory->getEncoder($user)->isPasswordValid( + $user->getPassword(), + $authenticator->getPassword($event->getPreAuthenticatedToken()->getCredentials()), + $user->getSalt() + )); + + return; + } + + if ($authenticator instanceof TokenAuthenticatedInterface) { + if (null !== $authenticator->getToken($event->getCredentials())) { + // Token based authenticators do not have a credential validation step + $event->setCredentialsValid(); + } + + return; + } + + if ($authenticator instanceof CustomAuthenticatedInterface) { + $event->setCredentialsValid($authenticator->checkCredentials($event->getPreAuthenticatedToken()->getCredentials(), $event->getUser())); + + return; + } + + throw new LogicException(sprintf('Authenticator %s does not have valid credentials. Authenticators must implement one of the authenticated interfaces (%s, %s or %s).', \get_class($authenticator), PasswordAuthenticatedInterface::class, TokenAuthenticatedInterface::class, CustomAuthenticatedInterface::class)); + } +} diff --git a/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php b/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php new file mode 100644 index 0000000000..f981c983fe --- /dev/null +++ b/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php @@ -0,0 +1,65 @@ + + * + * @final + * @experimental in 5.1 + */ +class PasswordMigratingListener implements EventSubscriberInterface +{ + private $encoderFactory; + + public function __construct(EncoderFactoryInterface $encoderFactory) + { + $this->encoderFactory = $encoderFactory; + } + + public function onCredentialsVerification(VerifyAuthenticatorCredentialsEvent $event): void + { + if (!$event->areCredentialsValid()) { + // Do not migrate password that are not validated + return; + } + + $authenticator = $event->getAuthenticator(); + if (!$authenticator instanceof PasswordAuthenticatedInterface) { + return; + } + + $token = $event->getPreAuthenticatedToken(); + if (null !== $password = $authenticator->getPassword($token->getCredentials())) { + return; + } + + $user = $token->getUser(); + if (!$user instanceof UserInterface) { + return; + } + + $passwordEncoder = $this->encoderFactory->getEncoder($user); + if (!method_exists($passwordEncoder, 'needsRehash') || !$passwordEncoder->needsRehash($user)) { + return; + } + + if (!$authenticator instanceof PasswordUpgraderInterface) { + return; + } + + $authenticator->upgradePassword($user, $passwordEncoder->encodePassword($user, $password)); + } + + public static function getSubscribedEvents(): array + { + return [VerifyAuthenticatorCredentialsEvent::class => ['onCredentialsVerification', -128]]; + } +} diff --git a/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php b/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php new file mode 100644 index 0000000000..9e612d7778 --- /dev/null +++ b/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php @@ -0,0 +1,88 @@ + + * + * @final + * @experimental in 5.1 + */ +class RememberMeListener implements EventSubscriberInterface +{ + private $providerKey; + private $logger; + /** @var RememberMeServicesInterface|null */ + private $rememberMeServices; + + public function __construct(string $providerKey, ?LoggerInterface $logger = null) + { + $this->providerKey = $providerKey; + $this->logger = $logger; + } + + + public function setRememberMeServices(RememberMeServicesInterface $rememberMeServices): void + { + $this->rememberMeServices = $rememberMeServices; + } + + public function onSuccessfulLogin(LoginSuccessEvent $event): void + { + if (!$this->isRememberMeEnabled($event->getAuthenticator(), $event->getProviderKey())) { + return; + } + + $this->rememberMeServices->loginSuccess($event->getRequest(), $event->getResponse(), $event->getAuthenticatedToken()); + } + + public function onFailedLogin(LoginFailureEvent $event): void + { + if (!$this->isRememberMeEnabled($event->getAuthenticator(), $event->getProviderKey())) { + return; + } + + $this->rememberMeServices->loginFail($event->getRequest(), $event->getException()); + } + + private function isRememberMeEnabled(AuthenticatorInterface $authenticator, string $providerKey): bool + { + if ($providerKey !== $this->providerKey) { + // This listener is created for a different firewall. + return false; + } + + if (null === $this->rememberMeServices) { + if (null !== $this->logger) { + $this->logger->debug('Remember me skipped: it is not configured for the firewall.', ['authenticator' => \get_class($authenticator)]); + } + + return false; + } + + if (!$authenticator->supportsRememberMe()) { + if (null !== $this->logger) { + $this->logger->debug('Remember me skipped: your authenticator does not support it.', ['authenticator' => \get_class($authenticator)]); + } + + return false; + } + + return true; + } + + public static function getSubscribedEvents(): array + { + return [ + LoginSuccessEvent::class => 'onSuccessfulLogin', + LoginFailureEvent::class => 'onFailedLogin', + ]; + } +} diff --git a/src/Symfony/Component/Security/Http/EventListener/UserCheckerListener.php b/src/Symfony/Component/Security/Http/EventListener/UserCheckerListener.php new file mode 100644 index 0000000000..c0c6c6895d --- /dev/null +++ b/src/Symfony/Component/Security/Http/EventListener/UserCheckerListener.php @@ -0,0 +1,43 @@ + + * + * @final + * @experimental in 5.1 + */ +class UserCheckerListener implements EventSubscriberInterface +{ + private $userChecker; + + public function __construct(UserCheckerInterface $userChecker) + { + $this->userChecker = $userChecker; + } + + public function preCredentialsVerification(VerifyAuthenticatorCredentialsEvent $event): void + { + $this->userChecker->checkPreAuth($event->getUser()); + } + + public function postCredentialsVerification(VerifyAuthenticatorCredentialsEvent $event): void + { + $this->userChecker->checkPostAuth($event->getUser()); + } + + public static function getSubscribedEvents(): array + { + return [ + VerifyAuthenticatorCredentialsEvent::class => [ + ['preCredentialsVerification', 256], + ['preCredentialsVerification', 32] + ], + ]; + } +} diff --git a/src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php b/src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php index 2367223657..71a448384d 100644 --- a/src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php @@ -12,12 +12,18 @@ 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\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Http\Authentication\Authenticator\AuthenticatorInterface; use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticationGuardToken; use Symfony\Component\Security\Guard\GuardAuthenticatorHandler; -use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; +use Symfony\Component\Security\Http\Event\LoginFailureEvent; +use Symfony\Component\Security\Http\Event\LoginSuccessEvent; /** * @author Wouter de Jong @@ -34,19 +40,20 @@ class GuardManagerListener private $guardHandler; private $guardAuthenticators; protected $providerKey; + private $eventDispatcher; protected $logger; - private $rememberMeServices; /** * @param AuthenticatorInterface[] $guardAuthenticators */ - public function __construct(AuthenticationManagerInterface $authenticationManager, GuardAuthenticatorHandler $guardHandler, iterable $guardAuthenticators, string $providerKey, ?LoggerInterface $logger = null) + public function __construct(AuthenticationManagerInterface $authenticationManager, GuardAuthenticatorHandler $guardHandler, iterable $guardAuthenticators, string $providerKey, EventDispatcherInterface $eventDispatcher, ?LoggerInterface $logger = null) { $this->authenticationManager = $authenticationManager; $this->guardHandler = $guardHandler; $this->guardAuthenticators = $guardAuthenticators; $this->providerKey = $providerKey; $this->logger = $logger; + $this->eventDispatcher = $eventDispatcher; } public function __invoke(RequestEvent $requestEvent) @@ -57,23 +64,95 @@ class GuardManagerListener return; } - $this->executeGuardAuthenticators($guardAuthenticators, $requestEvent); + $this->executeAuthenticators($guardAuthenticators, $requestEvent); } - public function setRememberMeServices(RememberMeServicesInterface $rememberMeServices) + /** + * @param AuthenticatorInterface[] $authenticators + */ + protected function executeAuthenticators(array $authenticators, RequestEvent $event): void { - $this->rememberMeServices = $rememberMeServices; + foreach ($authenticators as $key => $guardAuthenticator) { + $this->executeAuthenticator($key, $guardAuthenticator, $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($guardAuthenticator)]); + } + + break; + } + } } - protected function createPreAuthenticatedToken($credentials, string $uniqueGuardKey, string $providerKey): PreAuthenticationGuardToken + private function executeAuthenticator(string $uniqueAuthenticatorKey, AuthenticatorInterface $authenticator, RequestEvent $event): void { - return new PreAuthenticationGuardToken($credentials, $uniqueGuardKey, $providerKey); + $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 = $this->createPreAuthenticatedToken($credentials, $uniqueAuthenticatorKey, $this->providerKey); + + 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->authenticationManager->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->guardHandler->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->guardHandler->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->guardHandler->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)); } - protected function getGuardKey(string $key): string + protected function createPreAuthenticatedToken($credentials, string $uniqueAuthenticatorKey, string $providerKey): PreAuthenticationGuardToken { - // Guard authenticators in the GuardManagerListener are already indexed - // by an unique key - return $key; + return new PreAuthenticationGuardToken($credentials, $uniqueAuthenticatorKey, $providerKey); } } diff --git a/src/Symfony/Component/Security/Http/Firewall/GuardManagerListenerTrait.php b/src/Symfony/Component/Security/Http/Firewall/GuardManagerListenerTrait.php index 794d1dd133..a1cf6880ad 100644 --- a/src/Symfony/Component/Security/Http/Firewall/GuardManagerListenerTrait.php +++ b/src/Symfony/Component/Security/Http/Firewall/GuardManagerListenerTrait.php @@ -46,134 +46,5 @@ trait GuardManagerListenerTrait return $guardAuthenticators; } - /** - * @param (CoreAuthenticatorInterface|AuthenticatorInterface)[] $guardAuthenticators - */ - protected function executeGuardAuthenticators(array $guardAuthenticators, RequestEvent $event): void - { - foreach ($guardAuthenticators as $key => $guardAuthenticator) { - $uniqueGuardKey = $this->getGuardKey($key); - - $this->executeGuardAuthenticator($uniqueGuardKey, $guardAuthenticator, $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($guardAuthenticator)]); - } - - break; - } - } - } - - /** - * @param CoreAuthenticatorInterface|AuthenticatorInterface $guardAuthenticator - */ - private function executeGuardAuthenticator(string $uniqueGuardKey, $guardAuthenticator, RequestEvent $event) - { - if (!$guardAuthenticator instanceof AuthenticatorInterface && !$guardAuthenticator instanceof CoreAuthenticatorInterface) { - throw new \UnexpectedValueException('Invalid guard authenticator passed to '.__METHOD__.'. Expected AuthenticatorInterface of either Security Core or Security Guard.'); - } - - $request = $event->getRequest(); - try { - if (null !== $this->logger) { - $this->logger->debug('Calling getCredentials() on guard authenticator.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)]); - } - - // allow the authenticator to fetch authentication info from the request - $credentials = $guardAuthenticator->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_debug_type($guardAuthenticator))); - } - - // create a token with the unique key, so that the provider knows which authenticator to use - $token = $this->createPreAuthenticatedToken($credentials, $uniqueGuardKey, $this->providerKey); - - if (null !== $this->logger) { - $this->logger->debug('Passing guard token information to the GuardAuthenticationProvider', ['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!', ['token' => $token, 'authenticator' => \get_class($guardAuthenticator)]); - } - - // sets the token on the token storage, etc - $this->guardHandler->authenticateWithToken($token, $request, $this->providerKey); - } catch (AuthenticationException $e) { - // oh no! Authentication failed! - - if (null !== $this->logger) { - $this->logger->info('Guard authentication failed.', ['exception' => $e, 'authenticator' => \get_class($guardAuthenticator)]); - } - - $response = $this->guardHandler->handleAuthenticationFailure($e, $request, $guardAuthenticator, $this->providerKey); - - if ($response instanceof Response) { - $event->setResponse($response); - } - - return; - } - - // success! - $response = $this->guardHandler->handleAuthenticationSuccess($token, $request, $guardAuthenticator, $this->providerKey); - if ($response instanceof Response) { - if (null !== $this->logger) { - $this->logger->debug('Guard authenticator set success response.', ['response' => $response, 'authenticator' => \get_class($guardAuthenticator)]); - } - - $event->setResponse($response); - } else { - if (null !== $this->logger) { - $this->logger->debug('Guard authenticator set no success response: request continues.', ['authenticator' => \get_class($guardAuthenticator)]); - } - } - - // attempt to trigger the remember me functionality - $this->triggerRememberMe($guardAuthenticator, $request, $token, $response); - } - - /** - * Checks to see if remember me is supported in the authenticator and - * on the firewall. If it is, the RememberMeServicesInterface is notified. - * - * @param CoreAuthenticatorInterface|AuthenticatorInterface $guardAuthenticator - */ - private function triggerRememberMe($guardAuthenticator, Request $request, TokenInterface $token, Response $response = null) - { - if (!$guardAuthenticator instanceof AuthenticatorInterface && !$guardAuthenticator instanceof CoreAuthenticatorInterface) { - throw new \UnexpectedValueException('Invalid guard authenticator passed to '.__METHOD__.'. Expected AuthenticatorInterface of either Security Core or Security Guard.'); - } - - if (null === $this->rememberMeServices) { - if (null !== $this->logger) { - $this->logger->debug('Remember me skipped: it is not configured for the firewall.', ['authenticator' => \get_class($guardAuthenticator)]); - } - - return; - } - - if (!$guardAuthenticator->supportsRememberMe()) { - if (null !== $this->logger) { - $this->logger->debug('Remember me skipped: your authenticator does not support it.', ['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_debug_type($guardAuthenticator))); - } - - $this->rememberMeServices->loginSuccess($request, $response, $token); - } - - abstract protected function getGuardKey(string $key): string; - abstract protected function createPreAuthenticatedToken($credentials, string $uniqueGuardKey, string $providerKey): PreAuthenticationGuardToken; } From 7859977324852dcb2b193106bb1066e6061fe010 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Thu, 6 Feb 2020 15:41:40 +0100 Subject: [PATCH 14/30] Removed all mentions of 'guard' in the new system This to remove confusion between the new system and Guard. When using the new system, guard should not be installed. Guard did however influence the idea behind the new system. Thus keeping the mentions of "guard" makes it confusing to use the new system. --- .../DependencyInjection/MainConfiguration.php | 2 +- .../Security/Factory/AnonymousFactory.php | 4 +- ....php => AuthenticatorFactoryInterface.php} | 8 +-- .../Security/Factory/FormLoginFactory.php | 4 +- .../Security/Factory/HttpBasicFactory.php | 4 +- .../DependencyInjection/SecurityExtension.php | 37 +++++----- ...p => LazyAuthenticatorManagerListener.php} | 12 ++-- .../Resources/config/authenticators.xml | 27 ++++--- .../SecurityBundle/Resources/config/guard.xml | 6 +- .../Resources/config/security.xml | 4 +- .../HttpBasicAuthenticatorTest.php | 18 ++--- .../Firewall/GuardAuthenticationListener.php | 27 +++---- ...henticatorHandler.php => GuardHandler.php} | 4 +- .../Provider/GuardAuthenticationProvider.php | 20 +++--- .../GuardAuthenticationListenerTest.php | 8 ++- .../Tests/GuardAuthenticatorHandlerTest.php | 16 ++--- .../GuardAuthenticationProviderTest.php | 12 ++-- ...rdToken.php => PreAuthenticationToken.php} | 8 ++- ...orHandler.php => AuthenticatorHandler.php} | 34 ++++----- ...onManager.php => AuthenticatorManager.php} | 36 +++++----- .../AuthenticatorManagerTrait.php | 46 ++++++++++++ .../GuardAuthenticationManagerTrait.php | 55 -------------- .../Authenticator/AbstractAuthenticator.php | 10 +-- .../AbstractLoginFormAuthenticator.php} | 4 +- .../Authenticator/AnonymousAuthenticator.php | 2 +- .../Authenticator/AuthenticatorInterface.php | 2 +- .../CustomAuthenticatedInterface.php | 11 ++- .../Authenticator/FormLoginAuthenticator.php | 8 +-- .../Authenticator/HttpBasicAuthenticator.php | 3 +- .../PasswordAuthenticatedInterface.php | 31 ++++++++ .../Token/PostAuthenticationToken.php | 71 +++++++++++++++++++ .../Token/PreAuthenticationToken.php} | 28 ++++---- .../TokenAuthenticatedInterface.php | 11 ++- .../Security/Http/Event/LoginFailureEvent.php | 2 +- .../Security/Http/Event/LoginSuccessEvent.php | 2 +- .../VerifyAuthenticatorCredentialsEvent.php | 2 +- .../EventListener/AuthenticatingListener.php | 6 +- .../PasswordMigratingListener.php | 8 +-- .../Http/EventListener/RememberMeListener.php | 2 +- ...r.php => AuthenticatorManagerListener.php} | 49 ++++++------- .../AuthenticatorManagerListenerTrait.php | 41 +++++++++++ .../Firewall/GuardManagerListenerTrait.php | 50 ------------- 42 files changed, 419 insertions(+), 316 deletions(-) rename src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/{GuardFactoryInterface.php => AuthenticatorFactoryInterface.php} (59%) rename src/Symfony/Bundle/SecurityBundle/EventListener/{LazyGuardManagerListener.php => LazyAuthenticatorManagerListener.php} (79%) rename src/Symfony/Component/Security/Guard/{GuardAuthenticatorHandler.php => GuardHandler.php} (76%) rename src/Symfony/Component/Security/Guard/Token/{PreAuthenticationGuardToken.php => PreAuthenticationToken.php} (71%) rename src/Symfony/Component/Security/Http/Authentication/{GuardAuthenticatorHandler.php => AuthenticatorHandler.php} (74%) rename src/Symfony/Component/Security/Http/Authentication/{GuardAuthenticationManager.php => AuthenticatorManager.php} (78%) create mode 100644 src/Symfony/Component/Security/Http/Authentication/AuthenticatorManagerTrait.php delete mode 100644 src/Symfony/Component/Security/Http/Authentication/GuardAuthenticationManagerTrait.php rename src/Symfony/Component/Security/Http/{Authentication => }/Authenticator/AbstractAuthenticator.php (68%) rename src/Symfony/Component/Security/Http/{Authentication/Authenticator/AbstractFormLoginAuthenticator.php => Authenticator/AbstractLoginFormAuthenticator.php} (92%) rename src/Symfony/Component/Security/Http/{Authentication => }/Authenticator/AnonymousAuthenticator.php (96%) rename src/Symfony/Component/Security/Http/{Authentication => }/Authenticator/AuthenticatorInterface.php (98%) rename src/Symfony/Component/Security/Http/{Authentication => }/Authenticator/CustomAuthenticatedInterface.php (73%) rename src/Symfony/Component/Security/Http/{Authentication => }/Authenticator/FormLoginAuthenticator.php (94%) rename src/Symfony/Component/Security/Http/{Authentication => }/Authenticator/HttpBasicAuthenticator.php (95%) create mode 100644 src/Symfony/Component/Security/Http/Authenticator/PasswordAuthenticatedInterface.php create mode 100644 src/Symfony/Component/Security/Http/Authenticator/Token/PostAuthenticationToken.php rename src/Symfony/Component/Security/{Core/Authentication/Token/PreAuthenticationGuardToken.php => Http/Authenticator/Token/PreAuthenticationToken.php} (52%) rename src/Symfony/Component/Security/Http/{Authentication => }/Authenticator/TokenAuthenticatedInterface.php (67%) rename src/Symfony/Component/Security/Http/Firewall/{GuardManagerListener.php => AuthenticatorManagerListener.php} (72%) create mode 100644 src/Symfony/Component/Security/Http/Firewall/AuthenticatorManagerListenerTrait.php delete mode 100644 src/Symfony/Component/Security/Http/Firewall/GuardManagerListenerTrait.php diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php index b0d7e5c342..dfac1554d4 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -73,7 +73,7 @@ class MainConfiguration implements ConfigurationInterface ->booleanNode('hide_user_not_found')->defaultTrue()->end() ->booleanNode('always_authenticate_before_granting')->defaultFalse()->end() ->booleanNode('erase_credentials')->defaultTrue()->end() - ->booleanNode('guard_authentication_manager')->defaultFalse()->end() + ->booleanNode('enable_authenticator_manager')->defaultFalse()->info('Enables the new Symfony Security system based on Authenticators, all used authenticators must support this before enabling this.')->end() ->arrayNode('access_decision_manager') ->addDefaultsIfNotSet() ->children() diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php index 2479cff3ac..b7e2347a57 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php @@ -19,7 +19,7 @@ use Symfony\Component\DependencyInjection\Parameter; /** * @author Wouter de Jong */ -class AnonymousFactory implements SecurityFactoryInterface, GuardFactoryInterface +class AnonymousFactory implements SecurityFactoryInterface, AuthenticatorFactoryInterface { public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint) { @@ -42,7 +42,7 @@ class AnonymousFactory implements SecurityFactoryInterface, GuardFactoryInterfac return [$providerId, $listenerId, $defaultEntryPoint]; } - public function createGuard(ContainerBuilder $container, string $id, array $config, ?string $userProviderId): string + public function createAuthenticator(ContainerBuilder $container, string $id, array $config, ?string $userProviderId): string { if (null === $config['secret']) { $config['secret'] = new Parameter('container.build_hash'); diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardFactoryInterface.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AuthenticatorFactoryInterface.php similarity index 59% rename from src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardFactoryInterface.php rename to src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AuthenticatorFactoryInterface.php index 34314e5a43..e85ba0b495 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardFactoryInterface.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AuthenticatorFactoryInterface.php @@ -18,12 +18,12 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; * * @experimental in 5.1 */ -interface GuardFactoryInterface +interface AuthenticatorFactoryInterface { /** - * Creates the Guard service(s) for the provided configuration. + * Creates the authenticator service(s) for the provided configuration. * - * @return string|string[] The Guard service ID(s) to be used by the firewall + * @return string|string[] The authenticator service ID(s) to be used by the firewall */ - public function createGuard(ContainerBuilder $container, string $id, array $config, ?string $userProviderId); + public function createAuthenticator(ContainerBuilder $container, string $id, array $config, ?string $userProviderId); } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php index cfed004d86..368cde156e 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php @@ -22,7 +22,7 @@ use Symfony\Component\DependencyInjection\Reference; * @author Fabien Potencier * @author Johannes M. Schmitt */ -class FormLoginFactory extends AbstractFactory implements GuardFactoryInterface, EntryPointFactoryInterface +class FormLoginFactory extends AbstractFactory implements AuthenticatorFactoryInterface, EntryPointFactoryInterface { public function __construct() { @@ -97,7 +97,7 @@ class FormLoginFactory extends AbstractFactory implements GuardFactoryInterface, return $entryPointId; } - public function createGuard(ContainerBuilder $container, string $id, array $config, ?string $userProviderId): string + public function createAuthenticator(ContainerBuilder $container, string $id, array $config, ?string $userProviderId): string { $authenticatorId = 'security.authenticator.form_login.'.$id; $defaultOptions = array_merge($this->defaultSuccessHandlerOptions, $this->options); diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php index c632ebf587..dea437e94c 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php @@ -21,7 +21,7 @@ use Symfony\Component\DependencyInjection\Reference; * * @author Fabien Potencier */ -class HttpBasicFactory implements SecurityFactoryInterface, GuardFactoryInterface +class HttpBasicFactory implements SecurityFactoryInterface, AuthenticatorFactoryInterface { public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint) { @@ -46,7 +46,7 @@ class HttpBasicFactory implements SecurityFactoryInterface, GuardFactoryInterfac return [$provider, $listenerId, $entryPointId]; } - public function createGuard(ContainerBuilder $container, string $id, array $config, ?string $userProviderId): string + public function createAuthenticator(ContainerBuilder $container, string $id, array $config, ?string $userProviderId): string { $authenticatorId = 'security.authenticator.http_basic.'.$id; $container diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index d67682e883..fb402288be 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -11,8 +11,8 @@ namespace Symfony\Bundle\SecurityBundle\DependencyInjection; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AuthenticatorFactoryInterface; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\EntryPointFactoryInterface; -use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\GuardFactoryInterface; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\RememberMeFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\UserProviderFactoryInterface; @@ -54,7 +54,7 @@ class SecurityExtension extends Extension implements PrependExtensionInterface private $userProviderFactories = []; private $statelessFirewallKeys = []; - private $guardAuthenticationManagerEnabled = false; + private $authenticatorManagerEnabled = false; public function __construct() { @@ -139,7 +139,7 @@ class SecurityExtension extends Extension implements PrependExtensionInterface $container->setParameter('security.access.always_authenticate_before_granting', $config['always_authenticate_before_granting']); $container->setParameter('security.authentication.hide_user_not_found', $config['hide_user_not_found']); - if ($this->guardAuthenticationManagerEnabled = $config['guard_authentication_manager']) { + if ($this->authenticatorManagerEnabled = $config['enable_authenticator_manager']) { $loader->load('authenticators.xml'); } @@ -150,6 +150,11 @@ class SecurityExtension extends Extension implements PrependExtensionInterface $container->getDefinition('security.authentication.guard_handler') ->replaceArgument(2, $this->statelessFirewallKeys); + if ($this->authenticatorManagerEnabled) { + $container->getDefinition('security.authenticator_handler') + ->replaceArgument(2, $this->statelessFirewallKeys); + } + if ($config['encoders']) { $this->createEncoders($config['encoders'], $container); } @@ -267,8 +272,8 @@ class SecurityExtension extends Extension implements PrependExtensionInterface return new Reference($id); }, array_unique($authenticationProviders)); $authenticationManagerId = 'security.authentication.manager.provider'; - if ($this->guardAuthenticationManagerEnabled) { - $authenticationManagerId = 'security.authentication.manager.guard'; + if ($this->authenticatorManagerEnabled) { + $authenticationManagerId = 'security.authentication.manager.authenticator'; $container->setAlias('security.authentication.manager', new Alias($authenticationManagerId)); } $container @@ -418,7 +423,7 @@ class SecurityExtension extends Extension implements PrependExtensionInterface // Determine default entry point $configuredEntryPoint = isset($firewall['entry_point']) ? $firewall['entry_point'] : null; - if ($this->guardAuthenticationManagerEnabled) { + if ($this->authenticatorManagerEnabled) { // Remember me listener (must be before calling createAuthenticationListeners() to inject remember me services) $container ->setDefinition('security.listener.remember_me.'.$id, new ChildDefinition('security.listener.remember_me')) @@ -434,10 +439,10 @@ class SecurityExtension extends Extension implements PrependExtensionInterface $authenticationProviders = array_merge($authenticationProviders, $firewallAuthenticationProviders); - if ($this->guardAuthenticationManagerEnabled) { - // guard authentication manager listener + if ($this->authenticatorManagerEnabled) { + // authenticator manager listener $container - ->setDefinition('security.firewall.guard.'.$id.'.locator', new ChildDefinition('security.firewall.guard.locator')) + ->setDefinition('security.firewall.authenticator.'.$id.'.locator', new ChildDefinition('security.firewall.authenticator.locator')) ->setArguments([array_map(function ($id) { return new Reference($id); }, $firewallAuthenticationProviders)]) @@ -445,13 +450,13 @@ class SecurityExtension extends Extension implements PrependExtensionInterface ; $container - ->setDefinition('security.firewall.guard.'.$id, new ChildDefinition('security.firewall.guard')) - ->replaceArgument(2, new Reference('security.firewall.guard.'.$id.'.locator')) + ->setDefinition('security.firewall.authenticator.'.$id, new ChildDefinition('security.firewall.authenticator')) + ->replaceArgument(2, new Reference('security.firewall.authenticator.'.$id.'.locator')) ->replaceArgument(3, $id) ->addTag('kernel.event_listener', ['event' => KernelEvents::REQUEST]) ; - $listeners[] = new Reference('security.firewall.guard.'.$id); + $listeners[] = new Reference('security.firewall.authenticator.'.$id); } $config->replaceArgument(7, $configuredEntryPoint ?: $defaultEntryPoint); @@ -515,12 +520,12 @@ class SecurityExtension extends Extension implements PrependExtensionInterface if (isset($firewall[$key])) { $userProvider = $this->getUserProvider($container, $id, $firewall, $key, $defaultProvider, $providerIds, $contextListenerId); - if ($this->guardAuthenticationManagerEnabled) { - if (!$factory instanceof GuardFactoryInterface) { - throw new InvalidConfigurationException(sprintf('Cannot configure GuardAuthenticationManager as %s authentication does not support it, set security.guard_authentication_manager to `false`.', $key)); + if ($this->authenticatorManagerEnabled) { + if (!$factory instanceof AuthenticatorFactoryInterface) { + throw new InvalidConfigurationException(sprintf('Cannot configure AuthenticatorManager as "%s" authentication does not support it, set "security.enable_authenticator_manager" to `false`.', $key)); } - $authenticators = $factory->createGuard($container, $id, $firewall[$key], $userProvider); + $authenticators = $factory->createAuthenticator($container, $id, $firewall[$key], $userProvider); if (\is_array($authenticators)) { foreach ($authenticators as $i => $authenticator) { $authenticationProviders[$id.'_'.$key.$i] = $authenticator; diff --git a/src/Symfony/Bundle/SecurityBundle/EventListener/LazyGuardManagerListener.php b/src/Symfony/Bundle/SecurityBundle/EventListener/LazyAuthenticatorManagerListener.php similarity index 79% rename from src/Symfony/Bundle/SecurityBundle/EventListener/LazyGuardManagerListener.php rename to src/Symfony/Bundle/SecurityBundle/EventListener/LazyAuthenticatorManagerListener.php index 4cea805737..2a8a04e081 100644 --- a/src/Symfony/Bundle/SecurityBundle/EventListener/LazyGuardManagerListener.php +++ b/src/Symfony/Bundle/SecurityBundle/EventListener/LazyAuthenticatorManagerListener.php @@ -16,32 +16,32 @@ 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\Guard\GuardAuthenticatorHandler; -use Symfony\Component\Security\Http\Firewall\GuardManagerListener; +use Symfony\Component\Security\Http\Authentication\AuthenticatorHandler; +use Symfony\Component\Security\Http\Firewall\AuthenticatorManagerListener; /** * @author Wouter de Jong * * @experimental in 5.1 */ -class LazyGuardManagerListener extends GuardManagerListener +class LazyAuthenticatorManagerListener extends AuthenticatorManagerListener { private $guardLocator; public function __construct( AuthenticationManagerInterface $authenticationManager, - GuardAuthenticatorHandler $guardHandler, + AuthenticatorHandler $authenticatorHandler, ServiceLocator $guardLocator, string $providerKey, EventDispatcherInterface $eventDispatcher, ?LoggerInterface $logger = null ) { - parent::__construct($authenticationManager, $guardHandler, [], $providerKey, $eventDispatcher, $logger); + parent::__construct($authenticationManager, $authenticatorHandler, [], $providerKey, $eventDispatcher, $logger); $this->guardLocator = $guardLocator; } - protected function getSupportingGuardAuthenticators(Request $request): array + protected function getSupportingAuthenticators(Request $request): array { $guardAuthenticators = []; foreach ($this->guardLocator->getProvidedServices() as $key => $type) { diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml index a6b1a0a9f5..92d72ee238 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml @@ -4,17 +4,28 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd"> - + + + + + + + + + - - - + + @@ -48,7 +59,7 @@ realm name user provider @@ -57,7 +68,7 @@ @@ -66,7 +77,7 @@ secret diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/guard.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/guard.xml index 7b17aff868..4bfd1229a8 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/guard.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/guard.xml @@ -8,7 +8,7 @@ @@ -17,8 +17,8 @@ - - + + - - + + %security.authentication.manager.erase_credentials% diff --git a/src/Symfony/Component/Security/Core/Tests/Authentication/Authenticator/HttpBasicAuthenticatorTest.php b/src/Symfony/Component/Security/Core/Tests/Authentication/Authenticator/HttpBasicAuthenticatorTest.php index c0265cd55a..b713840441 100644 --- a/src/Symfony/Component/Security/Core/Tests/Authentication/Authenticator/HttpBasicAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authentication/Authenticator/HttpBasicAuthenticatorTest.php @@ -5,12 +5,12 @@ namespace Symfony\Component\Security\Core\Tests\Authentication\Authenticator; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\Security\Http\Authentication\Authenticator\HttpBasicAuthenticator; use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Http\Authenticator\HttpBasicAuthenticator; class HttpBasicAuthenticatorTest extends TestCase { @@ -39,8 +39,8 @@ class HttpBasicAuthenticatorTest extends TestCase 'PHP_AUTH_PW' => 'ThePassword', ]); - $guard = new HttpBasicAuthenticator('test', $this->userProvider, $this->encoderFactory); - $credentials = $guard->getCredentials($request); + $authenticator = new HttpBasicAuthenticator('test', $this->userProvider, $this->encoderFactory); + $credentials = $authenticator->getCredentials($request); $this->assertEquals([ 'username' => 'TheUsername', 'password' => 'ThePassword', @@ -55,7 +55,7 @@ class HttpBasicAuthenticatorTest extends TestCase ->with('TheUsername') ->willReturn($mockedUser); - $user = $guard->getUser($credentials, $this->userProvider); + $user = $authenticator->getUser($credentials, $this->userProvider); $this->assertSame($mockedUser, $user); $this->encoder @@ -64,14 +64,14 @@ class HttpBasicAuthenticatorTest extends TestCase ->with('ThePassword', 'ThePassword', null) ->willReturn(true); - $checkCredentials = $guard->checkCredentials($credentials, $user); + $checkCredentials = $authenticator->checkCredentials($credentials, $user); $this->assertTrue($checkCredentials); } /** @dataProvider provideInvalidPasswords */ public function testInvalidPassword($presentedPassword, $exceptionMessage) { - $guard = new HttpBasicAuthenticator('test', $this->userProvider, $this->encoderFactory); + $authenticator = new HttpBasicAuthenticator('test', $this->userProvider, $this->encoderFactory); $this->encoder ->expects($this->any()) @@ -81,7 +81,7 @@ class HttpBasicAuthenticatorTest extends TestCase $this->expectException(BadCredentialsException::class); $this->expectExceptionMessage($exceptionMessage); - $guard->checkCredentials([ + $authenticator->checkCredentials([ 'username' => 'TheUsername', 'password' => $presentedPassword, ], $this->getMockBuilder(UserInterface::class)->getMock()); @@ -100,8 +100,8 @@ class HttpBasicAuthenticatorTest extends TestCase { $request = new Request([], [], [], [], [], $serverParameters); - $guard = new HttpBasicAuthenticator('test', $this->userProvider, $this->encoderFactory); - $this->assertFalse($guard->supports($request)); + $authenticator = new HttpBasicAuthenticator('test', $this->userProvider, $this->encoderFactory); + $this->assertFalse($authenticator->supports($request)); } public function provideMissingHttpBasicServerParameters() diff --git a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php index 50b42990c5..4ce55930f6 100644 --- a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php +++ b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php @@ -16,14 +16,12 @@ 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\Authentication\Token\PreAuthenticationGuardToken; 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\GuardAuthenticatorHandler; -use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken as GuardPreAuthenticationGuardToken; +use Symfony\Component\Security\Guard\GuardHandler; +use Symfony\Component\Security\Guard\Token\PreAuthenticationToken as GuardPreAuthenticationGuardToken; use Symfony\Component\Security\Http\Firewall\AbstractListener; -use Symfony\Component\Security\Http\Firewall\GuardManagerListenerTrait; use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; /** @@ -36,12 +34,12 @@ use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; */ class GuardAuthenticationListener extends AbstractListener { - use GuardManagerListenerTrait; + use AuthenticatorManagerListenerTrait; private $guardHandler; private $authenticationManager; private $providerKey; - private $guardAuthenticators; + private $authenticators; private $logger; private $rememberMeServices; @@ -49,7 +47,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(GuardAuthenticatorHandler $guardHandler, AuthenticationManagerInterface $authenticationManager, string $providerKey, iterable $guardAuthenticators, LoggerInterface $logger = null) + public function __construct(GuardHandler $guardHandler, AuthenticationManagerInterface $authenticationManager, string $providerKey, iterable $guardAuthenticators, LoggerInterface $logger = null) { if (empty($providerKey)) { throw new \InvalidArgumentException('$providerKey must not be empty.'); @@ -58,7 +56,7 @@ class GuardAuthenticationListener extends AbstractListener $this->guardHandler = $guardHandler; $this->authenticationManager = $authenticationManager; $this->providerKey = $providerKey; - $this->guardAuthenticators = $guardAuthenticators; + $this->authenticators = $guardAuthenticators; $this->logger = $logger; } @@ -70,14 +68,14 @@ class GuardAuthenticationListener extends AbstractListener if (null !== $this->logger) { $context = ['firewall_key' => $this->providerKey]; - if ($this->guardAuthenticators instanceof \Countable || \is_array($this->guardAuthenticators)) { - $context['authenticators'] = \count($this->guardAuthenticators); + if ($this->authenticators instanceof \Countable || \is_array($this->authenticators)) { + $context['authenticators'] = \count($this->authenticators); } $this->logger->debug('Checking for guard authentication credentials.', $context); } - $guardAuthenticators = $this->getSupportingGuardAuthenticators($request); + $guardAuthenticators = $this->getSupportingAuthenticators($request); if (!$guardAuthenticators) { return false; } @@ -143,7 +141,7 @@ class GuardAuthenticationListener extends AbstractListener } // create a token with the unique key, so that the provider knows which authenticator to use - $token = $this->createPreAuthenticatedToken($credentials, $uniqueGuardKey, $this->providerKey); + $token = new GuardPreAuthenticationGuardToken($credentials, $uniqueGuardKey, $this->providerKey); if (null !== $this->logger) { $this->logger->debug('Passing guard token information to the GuardAuthenticationProvider', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)]); @@ -220,9 +218,4 @@ class GuardAuthenticationListener extends AbstractListener $this->rememberMeServices->loginSuccess($request, $response, $token); } - - protected function createPreAuthenticatedToken($credentials, string $uniqueGuardKey, string $providerKey): PreAuthenticationGuardToken - { - return new GuardPreAuthenticationGuardToken($credentials, $uniqueGuardKey, $providerKey); - } } diff --git a/src/Symfony/Component/Security/Guard/GuardAuthenticatorHandler.php b/src/Symfony/Component/Security/Guard/GuardHandler.php similarity index 76% rename from src/Symfony/Component/Security/Guard/GuardAuthenticatorHandler.php rename to src/Symfony/Component/Security/Guard/GuardHandler.php index 2f16dfa140..73e5a6e882 100644 --- a/src/Symfony/Component/Security/Guard/GuardAuthenticatorHandler.php +++ b/src/Symfony/Component/Security/Guard/GuardHandler.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Security\Guard; -use Symfony\Component\Security\Http\Authentication\GuardAuthenticatorHandler as CoreAuthenticatorHandlerAlias; +use Symfony\Component\Security\Http\Authentication\AuthenticatorHandler; /** * A utility class that does much of the *work* during the guard authentication process. @@ -23,6 +23,6 @@ use Symfony\Component\Security\Http\Authentication\GuardAuthenticatorHandler as * * @final */ -class GuardAuthenticatorHandler extends CoreAuthenticatorHandlerAlias +class GuardHandler extends AuthenticatorHandler { } diff --git a/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php b/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php index 9733584119..246d5173f1 100644 --- a/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php +++ b/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php @@ -19,7 +19,6 @@ 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\Http\Authentication\GuardAuthenticationManagerTrait; use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; @@ -29,7 +28,8 @@ use Symfony\Component\Security\Core\User\UserCheckerInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Guard\AuthenticatorInterface; use Symfony\Component\Security\Guard\Token\GuardTokenInterface; -use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken; +use Symfony\Component\Security\Guard\Token\PreAuthenticationToken; +use Symfony\Component\Security\Http\Authentication\AuthenticatorManagerTrait; use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; /** @@ -40,12 +40,12 @@ use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; */ class GuardAuthenticationProvider implements AuthenticationProviderInterface { - use GuardAuthenticationManagerTrait; + use AuthenticatorManagerTrait; /** * @var AuthenticatorInterface[] */ - private $guardAuthenticators; + private $authenticators; private $userProvider; private $providerKey; private $userChecker; @@ -58,7 +58,7 @@ class GuardAuthenticationProvider implements AuthenticationProviderInterface */ public function __construct(iterable $guardAuthenticators, UserProviderInterface $userProvider, string $providerKey, UserCheckerInterface $userChecker, UserPasswordEncoderInterface $passwordEncoder = null) { - $this->guardAuthenticators = $guardAuthenticators; + $this->authenticators = $guardAuthenticators; $this->userProvider = $userProvider; $this->providerKey = $providerKey; $this->userChecker = $userChecker; @@ -78,7 +78,7 @@ class GuardAuthenticationProvider implements AuthenticationProviderInterface throw new \InvalidArgumentException('GuardAuthenticationProvider only supports GuardTokenInterface.'); } - if (!$token instanceof PreAuthenticationGuardToken) { + if (!$token instanceof PreAuthenticationToken) { /* * The listener *only* passes PreAuthenticationGuardToken instances. * This means that an authenticated token (e.g. PostAuthenticationGuardToken) @@ -101,7 +101,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->getGuardProviderKey(), $this->providerKey)); + 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)); } return $this->authenticateViaGuard($guardAuthenticator, $token, $this->providerKey); @@ -109,7 +109,7 @@ class GuardAuthenticationProvider implements AuthenticationProviderInterface public function supports(TokenInterface $token) { - if ($token instanceof PreAuthenticationGuardToken) { + if ($token instanceof PreAuthenticationToken) { return null !== $this->findOriginatingAuthenticator($token); } @@ -121,12 +121,12 @@ class GuardAuthenticationProvider implements AuthenticationProviderInterface $this->rememberMeServices = $rememberMeServices; } - protected function getGuardKey(string $key): string + protected function getAuthenticatorKey(string $key): string { return $this->providerKey.'_'.$key; } - private function authenticateViaGuard(AuthenticatorInterface $guardAuthenticator, \Symfony\Component\Security\Core\Authentication\Token\PreAuthenticationGuardToken $token, string $providerKey): TokenInterface + private function authenticateViaGuard(AuthenticatorInterface $guardAuthenticator, \Symfony\Component\Security\Http\Authenticator\Token\PreAuthenticationToken $token, string $providerKey): TokenInterface { // get the user from the GuardAuthenticator $user = $guardAuthenticator->getUser($token->getCredentials(), $this->userProvider); diff --git a/src/Symfony/Component/Security/Guard/Tests/Firewall/GuardAuthenticationListenerTest.php b/src/Symfony/Component/Security/Guard/Tests/Firewall/GuardAuthenticationListenerTest.php index c5e1c92b89..6504aa1997 100644 --- a/src/Symfony/Component/Security/Guard/Tests/Firewall/GuardAuthenticationListenerTest.php +++ b/src/Symfony/Component/Security/Guard/Tests/Firewall/GuardAuthenticationListenerTest.php @@ -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\PreAuthenticationGuardToken; +use Symfony\Component\Security\Guard\Token\PreAuthenticationToken; /** * @author Ryan Weaver @@ -53,7 +53,7 @@ class GuardAuthenticationListenerTest extends TestCase // a clone of the token that should be created internally $uniqueGuardKey = 'my_firewall_0'; - $nonAuthedToken = new PreAuthenticationGuardToken($credentials, $uniqueGuardKey); + $nonAuthedToken = new PreAuthenticationToken($credentials, $uniqueGuardKey); $this->authenticationManager ->expects($this->once()) @@ -266,7 +266,9 @@ class GuardAuthenticationListenerTest extends TestCase ->disableOriginalConstructor() ->getMock(); - $this->guardAuthenticatorHandler = $this->getMockBuilder('Symfony\Component\Security\Guard\GuardAuthenticatorHandler') + $this->guardAuthenticatorHandler = $this->getMockBuilder( + 'Symfony\Component\Security\Guard\GuardHandler' + ) ->disableOriginalConstructor() ->getMock(); diff --git a/src/Symfony/Component/Security/Guard/Tests/GuardAuthenticatorHandlerTest.php b/src/Symfony/Component/Security/Guard/Tests/GuardAuthenticatorHandlerTest.php index e078a6be12..d6dfacca10 100644 --- a/src/Symfony/Component/Security/Guard/Tests/GuardAuthenticatorHandlerTest.php +++ b/src/Symfony/Component/Security/Guard/Tests/GuardAuthenticatorHandlerTest.php @@ -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\GuardAuthenticatorHandler; +use Symfony\Component\Security\Guard\GuardHandler; 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 GuardAuthenticatorHandler($this->tokenStorage, $this->dispatcher); + $handler = new GuardHandler($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 GuardAuthenticatorHandler($this->tokenStorage, $this->dispatcher); + $handler = new GuardHandler($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 GuardAuthenticatorHandler($this->tokenStorage, $this->dispatcher); + $handler = new GuardHandler($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 GuardAuthenticatorHandler($this->tokenStorage, $this->dispatcher); + $handler = new GuardHandler($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 GuardAuthenticatorHandler($this->tokenStorage, $this->dispatcher); + $handler = new GuardHandler($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 GuardAuthenticatorHandler($this->tokenStorage, $this->dispatcher); + $handler = new GuardHandler($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 GuardAuthenticatorHandler($this->tokenStorage, $this->dispatcher, ['some_provider_key']); + $handler = new GuardHandler($this->tokenStorage, $this->dispatcher, ['some_provider_key']); $handler->setSessionAuthenticationStrategy($this->sessionStrategy); $handler->authenticateWithToken($this->token, $this->request, 'some_provider_key'); } diff --git a/src/Symfony/Component/Security/Guard/Tests/Provider/GuardAuthenticationProviderTest.php b/src/Symfony/Component/Security/Guard/Tests/Provider/GuardAuthenticationProviderTest.php index b742046af0..c1bb302f9c 100644 --- a/src/Symfony/Component/Security/Guard/Tests/Provider/GuardAuthenticationProviderTest.php +++ b/src/Symfony/Component/Security/Guard/Tests/Provider/GuardAuthenticationProviderTest.php @@ -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\PreAuthenticationGuardToken; +use Symfony\Component\Security\Guard\Token\PreAuthenticationToken; /** * @author Ryan Weaver @@ -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 PreAuthenticationGuardToken($mockedUser, 'first_firewall_1'); + $token = new PreAuthenticationToken($mockedUser, 'first_firewall_1'); $supports = $provider->supports($token); $this->assertTrue($supports); - $token = new PreAuthenticationGuardToken($mockedUser, 'second_firewall_0'); + $token = new PreAuthenticationToken($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 PreAuthenticationGuardToken($mockedUser, 'second_firewall_0'); + $token = new PreAuthenticationToken($mockedUser, 'second_firewall_0'); $provider->authenticate($token); } @@ -170,7 +170,9 @@ 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\PreAuthenticationGuardToken') + $this->preAuthenticationToken = $this->getMockBuilder( + 'Symfony\Component\Security\Guard\Token\PreAuthenticationToken' + ) ->disableOriginalConstructor() ->getMock(); } diff --git a/src/Symfony/Component/Security/Guard/Token/PreAuthenticationGuardToken.php b/src/Symfony/Component/Security/Guard/Token/PreAuthenticationToken.php similarity index 71% rename from src/Symfony/Component/Security/Guard/Token/PreAuthenticationGuardToken.php rename to src/Symfony/Component/Security/Guard/Token/PreAuthenticationToken.php index 69013599f3..1ae9be445e 100644 --- a/src/Symfony/Component/Security/Guard/Token/PreAuthenticationGuardToken.php +++ b/src/Symfony/Component/Security/Guard/Token/PreAuthenticationToken.php @@ -11,8 +11,6 @@ namespace Symfony\Component\Security\Guard\Token; -use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticationGuardToken as CorePreAuthenticationGuardToken; - /** * The token used by the guard auth system before authentication. * @@ -22,6 +20,10 @@ use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticationGuardT * * @author Ryan Weaver */ -class PreAuthenticationGuardToken extends CorePreAuthenticationGuardToken implements GuardTokenInterface +class PreAuthenticationToken extends \Symfony\Component\Security\Http\Authenticator\Token\CorePreAuthenticationGuardToken implements GuardTokenInterface { + public function getGuardKey() + { + return $this->getAuthenticatorKey(); + } } diff --git a/src/Symfony/Component/Security/Http/Authentication/GuardAuthenticatorHandler.php b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorHandler.php similarity index 74% rename from src/Symfony/Component/Security/Http/Authentication/GuardAuthenticatorHandler.php rename to src/Symfony/Component/Security/Http/Authentication/AuthenticatorHandler.php index d930df1896..7a579a9b2c 100644 --- a/src/Symfony/Component/Security/Http/Authentication/GuardAuthenticatorHandler.php +++ b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorHandler.php @@ -13,7 +13,7 @@ namespace Symfony\Component\Security\Http\Authentication; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Security\Http\Authentication\Authenticator\AuthenticatorInterface; +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; @@ -25,7 +25,7 @@ use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterfa use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** - * A utility class that does much of the *work* during the guard authentication process. + * A utility class that does much of the *work* during the 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. @@ -34,7 +34,7 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; * * @internal */ -class GuardAuthenticatorHandler +class AuthenticatorHandler { private $tokenStorage; private $dispatcher; @@ -66,24 +66,24 @@ class GuardAuthenticatorHandler } /** - * Returns the "on success" response for the given GuardAuthenticator. + * Returns the "on success" response for the given Authenticator. * - * @param AuthenticatorInterface|GuardAuthenticatorInterface $guardAuthenticator + * @param AuthenticatorInterface|GuardAuthenticatorInterface $authenticator */ - public function handleAuthenticationSuccess(TokenInterface $token, Request $request, $guardAuthenticator, string $providerKey): ?Response + public function handleAuthenticationSuccess(TokenInterface $token, Request $request, $authenticator, string $providerKey): ?Response { - if (!$guardAuthenticator instanceof AuthenticatorInterface && !$guardAuthenticator instanceof GuardAuthenticatorInterface) { - throw new \UnexpectedValueException('Invalid guard authenticator passed to '.__METHOD__.'. Expected AuthenticatorInterface of either Security Core or Security Guard.'); + 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 = $guardAuthenticator->onAuthenticationSuccess($request, $token, $providerKey); + $response = $authenticator->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), get_debug_type($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))); } /** @@ -95,7 +95,7 @@ class GuardAuthenticatorHandler public function authenticateUserAndHandleSuccess(UserInterface $user, Request $request, $authenticator, string $providerKey): ?Response { if (!$authenticator instanceof AuthenticatorInterface && !$authenticator instanceof GuardAuthenticatorInterface) { - throw new \UnexpectedValueException('Invalid guard authenticator passed to '.__METHOD__.'. Expected AuthenticatorInterface of either Security Core or Security Guard.'); + throw new \UnexpectedValueException('Invalid authenticator passed to '.__METHOD__.'. Expected AuthenticatorInterface of either Security Core or Security Guard.'); } // create an authenticated token for the User @@ -111,21 +111,21 @@ class GuardAuthenticatorHandler * Handles an authentication failure and returns the Response for the * GuardAuthenticator. * - * @param AuthenticatorInterface|GuardAuthenticatorInterface $guardAuthenticator + * @param AuthenticatorInterface|GuardAuthenticatorInterface $authenticator */ - public function handleAuthenticationFailure(AuthenticationException $authenticationException, Request $request, $guardAuthenticator, string $providerKey): ?Response + public function handleAuthenticationFailure(AuthenticationException $authenticationException, Request $request, $authenticator, string $providerKey): ?Response { - if (!$guardAuthenticator instanceof AuthenticatorInterface && !$guardAuthenticator instanceof GuardAuthenticatorInterface) { - throw new \UnexpectedValueException('Invalid guard authenticator passed to '.__METHOD__.'. Expected AuthenticatorInterface of either Security Core or Security Guard.'); + 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 = $guardAuthenticator->onAuthenticationFailure($request, $authenticationException); + $response = $authenticator->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), get_debug_type($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))); } /** diff --git a/src/Symfony/Component/Security/Http/Authentication/GuardAuthenticationManager.php b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php similarity index 78% rename from src/Symfony/Component/Security/Http/Authentication/GuardAuthenticationManager.php rename to src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php index 29bb5476ed..39208002b0 100644 --- a/src/Symfony/Component/Security/Http/Authentication/GuardAuthenticationManager.php +++ b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php @@ -15,8 +15,8 @@ use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterfac 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\Authentication\Authenticator\AuthenticatorInterface; -use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticationGuardToken; +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; @@ -33,20 +33,20 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; * * @experimental in 5.1 */ -class GuardAuthenticationManager implements AuthenticationManagerInterface +class AuthenticatorManager implements AuthenticationManagerInterface { - use GuardAuthenticationManagerTrait; + use AuthenticatorManagerTrait; - private $guardAuthenticators; + private $authenticators; private $eventDispatcher; private $eraseCredentials; /** - * @param iterable|AuthenticatorInterface[] $guardAuthenticators The authenticators, with keys that match what's passed to GuardAuthenticationListener + * @param AuthenticatorInterface[] $authenticators The authenticators, with keys that match what's passed to AuthenticatorManagerListener */ - public function __construct(iterable $guardAuthenticators, EventDispatcherInterface $eventDispatcher, bool $eraseCredentials = true) + public function __construct(iterable $authenticators, EventDispatcherInterface $eventDispatcher, bool $eraseCredentials = true) { - $this->guardAuthenticators = $guardAuthenticators; + $this->authenticators = $authenticators; $this->eventDispatcher = $eventDispatcher; $this->eraseCredentials = $eraseCredentials; } @@ -58,10 +58,10 @@ class GuardAuthenticationManager implements AuthenticationManagerInterface public function authenticate(TokenInterface $token) { - if (!$token instanceof PreAuthenticationGuardToken) { + if (!$token instanceof PreAuthenticationToken) { /* - * The listener *only* passes PreAuthenticationGuardToken instances. - * This means that an authenticated token (e.g. PostAuthenticationGuardToken) + * 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. @@ -77,13 +77,13 @@ class GuardAuthenticationManager implements AuthenticationManagerInterface throw new AuthenticationExpiredException(); } - $guard = $this->findOriginatingAuthenticator($token); - if (null === $guard) { - $this->handleFailure(new ProviderNotFoundException(sprintf('Token with provider key "%s" did not originate from any of the guard authenticators.', $token->getGuardProviderKey())), $token); + $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); } try { - $result = $this->authenticateViaGuard($guard, $token, $token->getProviderKey()); + $result = $this->authenticateViaAuthenticator($authenticator, $token, $token->getProviderKey()); } catch (AuthenticationException $exception) { $this->handleFailure($exception, $token); } @@ -101,14 +101,14 @@ class GuardAuthenticationManager implements AuthenticationManagerInterface return $result; } - protected function getGuardKey(string $key): string + protected function getAuthenticatorKey(string $key): string { - // Guard authenticators in the GuardAuthenticationManager are already indexed + // Authenticators in the AuthenticatorManager are already indexed // by an unique key return $key; } - private function authenticateViaGuard(AuthenticatorInterface $authenticator, PreAuthenticationGuardToken $token, string $providerKey): TokenInterface + private function authenticateViaAuthenticator(AuthenticatorInterface $authenticator, PreAuthenticationToken $token, string $providerKey): TokenInterface { // get the user from the Authenticator $user = $authenticator->getUser($token->getCredentials()); diff --git a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManagerTrait.php b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManagerTrait.php new file mode 100644 index 0000000000..b1df45daab --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManagerTrait.php @@ -0,0 +1,46 @@ + + * + * 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 + * + * @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; +} diff --git a/src/Symfony/Component/Security/Http/Authentication/GuardAuthenticationManagerTrait.php b/src/Symfony/Component/Security/Http/Authentication/GuardAuthenticationManagerTrait.php deleted file mode 100644 index 3808d79be1..0000000000 --- a/src/Symfony/Component/Security/Http/Authentication/GuardAuthenticationManagerTrait.php +++ /dev/null @@ -1,55 +0,0 @@ - - * - * 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\Http\Authentication\Authenticator\AuthenticatorInterface as CoreAuthenticatorInterface; -use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticationGuardToken; -use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -use Symfony\Component\Security\Core\Exception\BadCredentialsException; -use Symfony\Component\Security\Core\Exception\LogicException; -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\AuthenticatorInterface; -use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface; - -/** - * @author Ryan Weaver - * - * @internal - */ -trait GuardAuthenticationManagerTrait -{ - /** - * @return CoreAuthenticatorInterface|\Symfony\Component\Security\Guard\AuthenticatorInterface|null - */ - private function findOriginatingAuthenticator(PreAuthenticationGuardToken $token) - { - // 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->getGuardKey($key); - - if ($uniqueGuardKey === $token->getGuardProviderKey()) { - return $guardAuthenticator; - } - } - - // no matching authenticator found - but there will be multiple GuardAuthenticationProvider - // instances that will be checked if you have multiple firewalls. - - return null; - } - - abstract protected function getGuardKey(string $key): string; -} diff --git a/src/Symfony/Component/Security/Http/Authentication/Authenticator/AbstractAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/AbstractAuthenticator.php similarity index 68% rename from src/Symfony/Component/Security/Http/Authentication/Authenticator/AbstractAuthenticator.php rename to src/Symfony/Component/Security/Http/Authenticator/AbstractAuthenticator.php index ce22dce368..0301a97110 100644 --- a/src/Symfony/Component/Security/Http/Authentication/Authenticator/AbstractAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AbstractAuthenticator.php @@ -9,11 +9,11 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Http\Authentication\Authenticator; +namespace Symfony\Component\Security\Http\Authenticator; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\User\UserInterface; -use Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken; +use Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken; /** * An optional base class that creates the necessary tokens for you. @@ -25,13 +25,13 @@ use Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken; abstract class AbstractAuthenticator implements AuthenticatorInterface { /** - * Shortcut to create a PostAuthenticationGuardToken for you, if you don't really + * Shortcut to create a PostAuthenticationToken for you, if you don't really * care about which authenticated token you're using. * - * @return PostAuthenticationGuardToken + * @return PostAuthenticationToken */ public function createAuthenticatedToken(UserInterface $user, string $providerKey): TokenInterface { - return new PostAuthenticationGuardToken($user, $providerKey, $user->getRoles()); + return new PostAuthenticationToken($user, $providerKey, $user->getRoles()); } } diff --git a/src/Symfony/Component/Security/Http/Authentication/Authenticator/AbstractFormLoginAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php similarity index 92% rename from src/Symfony/Component/Security/Http/Authentication/Authenticator/AbstractFormLoginAuthenticator.php rename to src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php index 5cc2f95414..07c71b1c3b 100644 --- a/src/Symfony/Component/Security/Http/Authentication/Authenticator/AbstractFormLoginAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Http\Authentication\Authenticator; +namespace Symfony\Component\Security\Http\Authenticator; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; @@ -25,7 +25,7 @@ use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface * * @experimental in 5.1 */ -abstract class AbstractFormLoginAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface +abstract class AbstractLoginFormAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface { /** * Return the URL to the login page. diff --git a/src/Symfony/Component/Security/Http/Authentication/Authenticator/AnonymousAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php similarity index 96% rename from src/Symfony/Component/Security/Http/Authentication/Authenticator/AnonymousAuthenticator.php rename to src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php index c6b9427fce..202da3b026 100644 --- a/src/Symfony/Component/Security/Http/Authentication/Authenticator/AnonymousAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Http\Authentication\Authenticator; +namespace Symfony\Component\Security\Http\Authenticator; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; diff --git a/src/Symfony/Component/Security/Http/Authentication/Authenticator/AuthenticatorInterface.php b/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php similarity index 98% rename from src/Symfony/Component/Security/Http/Authentication/Authenticator/AuthenticatorInterface.php rename to src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php index e2ca2e2e0c..5530eb32dd 100644 --- a/src/Symfony/Component/Security/Http/Authentication/Authenticator/AuthenticatorInterface.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Http\Authentication\Authenticator; +namespace Symfony\Component\Security\Http\Authenticator; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; diff --git a/src/Symfony/Component/Security/Http/Authentication/Authenticator/CustomAuthenticatedInterface.php b/src/Symfony/Component/Security/Http/Authenticator/CustomAuthenticatedInterface.php similarity index 73% rename from src/Symfony/Component/Security/Http/Authentication/Authenticator/CustomAuthenticatedInterface.php rename to src/Symfony/Component/Security/Http/Authenticator/CustomAuthenticatedInterface.php index 69ec6da097..79b995e55f 100644 --- a/src/Symfony/Component/Security/Http/Authentication/Authenticator/CustomAuthenticatedInterface.php +++ b/src/Symfony/Component/Security/Http/Authenticator/CustomAuthenticatedInterface.php @@ -1,6 +1,15 @@ + * + * 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; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\User\UserInterface; diff --git a/src/Symfony/Component/Security/Http/Authentication/Authenticator/FormLoginAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php similarity index 94% rename from src/Symfony/Component/Security/Http/Authentication/Authenticator/FormLoginAuthenticator.php rename to src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php index acdb5e257a..75bac9bd90 100644 --- a/src/Symfony/Component/Security/Http/Authentication/Authenticator/FormLoginAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php @@ -9,22 +9,18 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Http\Authentication\Authenticator; +namespace Symfony\Component\Security\Http\Authenticator; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; -use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; use Symfony\Component\Security\Core\Exception\BadCredentialsException; -use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException; use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; -use Symfony\Component\Security\Csrf\CsrfToken; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; -use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface; use Symfony\Component\Security\Http\HttpUtils; use Symfony\Component\Security\Http\ParameterBagUtils; use Symfony\Component\Security\Http\Util\TargetPathTrait; @@ -36,7 +32,7 @@ use Symfony\Component\Security\Http\Util\TargetPathTrait; * @final * @experimental in 5.1 */ -class FormLoginAuthenticator extends AbstractFormLoginAuthenticator implements PasswordAuthenticatedInterface +class FormLoginAuthenticator extends AbstractLoginFormAuthenticator implements PasswordAuthenticatedInterface { use TargetPathTrait; diff --git a/src/Symfony/Component/Security/Http/Authentication/Authenticator/HttpBasicAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php similarity index 95% rename from src/Symfony/Component/Security/Http/Authentication/Authenticator/HttpBasicAuthenticator.php rename to src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php index c3ff43f01c..51ad3339b7 100644 --- a/src/Symfony/Component/Security/Http/Authentication/Authenticator/HttpBasicAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Http\Authentication\Authenticator; +namespace Symfony\Component\Security\Http\Authenticator; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\Request; @@ -20,7 +20,6 @@ use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; -use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface; use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface; /** diff --git a/src/Symfony/Component/Security/Http/Authenticator/PasswordAuthenticatedInterface.php b/src/Symfony/Component/Security/Http/Authenticator/PasswordAuthenticatedInterface.php new file mode 100644 index 0000000000..7386fc3373 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/PasswordAuthenticatedInterface.php @@ -0,0 +1,31 @@ + + * + * 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; + +/** + * This interface should be implemented when the authenticator + * uses a password to authenticate. + * + * The EncoderFactory will be used to automatically validate + * the password. + * + * @author Wouter de Jong + */ +interface PasswordAuthenticatedInterface +{ + /** + * Returns the clear-text password contained in credentials if any. + * + * @param mixed $credentials The user credentials + */ + public function getPassword($credentials): ?string; +} diff --git a/src/Symfony/Component/Security/Http/Authenticator/Token/PostAuthenticationToken.php b/src/Symfony/Component/Security/Http/Authenticator/Token/PostAuthenticationToken.php new file mode 100644 index 0000000000..3525fa4765 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/Token/PostAuthenticationToken.php @@ -0,0 +1,71 @@ +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 + $this->setAuthenticated(true); + } + + /** + * This is meant to be only an authenticated token, where credentials + * have already been used and are thus cleared. + * + * {@inheritdoc} + */ + public function getCredentials() + { + return []; + } + + /** + * Returns the provider (firewall) key. + * + * @return string + */ + public function getProviderKey() + { + return $this->providerKey; + } + + /** + * {@inheritdoc} + */ + public function __serialize(): array + { + return [$this->providerKey, parent::__serialize()]; + } + + /** + * {@inheritdoc} + */ + public function __unserialize(array $data): void + { + [$this->providerKey, $parentData] = $data; + parent::__unserialize($parentData); + } +} diff --git a/src/Symfony/Component/Security/Core/Authentication/Token/PreAuthenticationGuardToken.php b/src/Symfony/Component/Security/Http/Authenticator/Token/PreAuthenticationToken.php similarity index 52% rename from src/Symfony/Component/Security/Core/Authentication/Token/PreAuthenticationGuardToken.php rename to src/Symfony/Component/Security/Http/Authenticator/Token/PreAuthenticationToken.php index b19b82e066..27daf7f8ba 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Token/PreAuthenticationGuardToken.php +++ b/src/Symfony/Component/Security/Http/Authenticator/Token/PreAuthenticationToken.php @@ -9,32 +9,34 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Core\Authentication\Token; +namespace Symfony\Component\Security\Http\Authenticator\Token; + +use Symfony\Component\Security\Core\Authentication\Token\AbstractToken; /** - * The token used by the guard auth system before authentication. + * The token used by the authenticator system before authentication. * - * The GuardAuthenticationListener creates this, which is then consumed - * immediately by the GuardAuthenticationProvider. If authentication is + * 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 */ -class PreAuthenticationGuardToken extends AbstractToken +class PreAuthenticationToken extends AbstractToken { private $credentials; - private $guardProviderKey; + private $authenticatorProviderKey; private $providerKey; /** * @param mixed $credentials - * @param string $guardProviderKey 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) + * @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 $guardProviderKey, ?string $providerKey = null) + public function __construct($credentials, string $authenticatorProviderKey, ?string $providerKey = null) { $this->credentials = $credentials; - $this->guardProviderKey = $guardProviderKey; + $this->authenticatorProviderKey = $authenticatorProviderKey; $this->providerKey = $providerKey; parent::__construct([]); @@ -48,9 +50,9 @@ class PreAuthenticationGuardToken extends AbstractToken return $this->providerKey; } - public function getGuardProviderKey() + public function getAuthenticatorKey() { - return $this->guardProviderKey; + return $this->authenticatorProviderKey; } /** @@ -66,6 +68,6 @@ class PreAuthenticationGuardToken extends AbstractToken public function setAuthenticated(bool $authenticated) { - throw new \LogicException('The PreAuthenticationGuardToken is *never* authenticated.'); + throw new \LogicException('The PreAuthenticationToken is *never* authenticated.'); } } diff --git a/src/Symfony/Component/Security/Http/Authentication/Authenticator/TokenAuthenticatedInterface.php b/src/Symfony/Component/Security/Http/Authenticator/TokenAuthenticatedInterface.php similarity index 67% rename from src/Symfony/Component/Security/Http/Authentication/Authenticator/TokenAuthenticatedInterface.php rename to src/Symfony/Component/Security/Http/Authenticator/TokenAuthenticatedInterface.php index 4630c57ae9..88d0d7f965 100644 --- a/src/Symfony/Component/Security/Http/Authentication/Authenticator/TokenAuthenticatedInterface.php +++ b/src/Symfony/Component/Security/Http/Authenticator/TokenAuthenticatedInterface.php @@ -1,6 +1,15 @@ + * + * 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; /** * This interface should be implemented when the authenticator diff --git a/src/Symfony/Component/Security/Http/Event/LoginFailureEvent.php b/src/Symfony/Component/Security/Http/Event/LoginFailureEvent.php index 6a5cf03e01..bc4e551e91 100644 --- a/src/Symfony/Component/Security/Http/Event/LoginFailureEvent.php +++ b/src/Symfony/Component/Security/Http/Event/LoginFailureEvent.php @@ -5,7 +5,7 @@ namespace Symfony\Component\Security\Http\Event; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Exception\AuthenticationException; -use Symfony\Component\Security\Http\Authentication\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; use Symfony\Contracts\EventDispatcher\Event; /** diff --git a/src/Symfony/Component/Security/Http/Event/LoginSuccessEvent.php b/src/Symfony/Component/Security/Http/Event/LoginSuccessEvent.php index de93b3a78c..22e11a8c87 100644 --- a/src/Symfony/Component/Security/Http/Event/LoginSuccessEvent.php +++ b/src/Symfony/Component/Security/Http/Event/LoginSuccessEvent.php @@ -5,7 +5,7 @@ namespace Symfony\Component\Security\Http\Event; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -use Symfony\Component\Security\Http\Authentication\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; use Symfony\Contracts\EventDispatcher\Event; /** diff --git a/src/Symfony/Component/Security/Http/Event/VerifyAuthenticatorCredentialsEvent.php b/src/Symfony/Component/Security/Http/Event/VerifyAuthenticatorCredentialsEvent.php index 173f448048..87bcb56a8b 100644 --- a/src/Symfony/Component/Security/Http/Event/VerifyAuthenticatorCredentialsEvent.php +++ b/src/Symfony/Component/Security/Http/Event/VerifyAuthenticatorCredentialsEvent.php @@ -4,7 +4,7 @@ namespace Symfony\Component\Security\Http\Event; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\User\UserInterface; -use Symfony\Component\Security\Http\Authentication\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; use Symfony\Contracts\EventDispatcher\Event; /** diff --git a/src/Symfony/Component/Security/Http/EventListener/AuthenticatingListener.php b/src/Symfony/Component/Security/Http/EventListener/AuthenticatingListener.php index 738142bc05..086eb92431 100644 --- a/src/Symfony/Component/Security/Http/EventListener/AuthenticatingListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/AuthenticatingListener.php @@ -5,9 +5,9 @@ namespace Symfony\Component\Security\Http\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; use Symfony\Component\Security\Core\Exception\LogicException; -use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface; -use Symfony\Component\Security\Http\Authentication\Authenticator\CustomAuthenticatedInterface; -use Symfony\Component\Security\Http\Authentication\Authenticator\TokenAuthenticatedInterface; +use Symfony\Component\Security\Http\Authenticator\CustomAuthenticatedInterface; +use Symfony\Component\Security\Http\Authenticator\PasswordAuthenticatedInterface; +use Symfony\Component\Security\Http\Authenticator\TokenAuthenticatedInterface; use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; /** diff --git a/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php b/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php index f981c983fe..b57605e551 100644 --- a/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php @@ -6,7 +6,7 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; use Symfony\Component\Security\Core\User\UserInterface; -use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface; +use Symfony\Component\Security\Http\Authenticator\PasswordAuthenticatedInterface; use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; /** @@ -32,7 +32,7 @@ class PasswordMigratingListener implements EventSubscriberInterface } $authenticator = $event->getAuthenticator(); - if (!$authenticator instanceof PasswordAuthenticatedInterface) { + if (!$authenticator instanceof PasswordAuthenticatedInterface || !$authenticator instanceof PasswordUpgraderInterface) { return; } @@ -51,10 +51,6 @@ class PasswordMigratingListener implements EventSubscriberInterface return; } - if (!$authenticator instanceof PasswordUpgraderInterface) { - return; - } - $authenticator->upgradePassword($user, $passwordEncoder->encodePassword($user, $password)); } diff --git a/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php b/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php index 9e612d7778..882258b1a6 100644 --- a/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php @@ -4,7 +4,7 @@ namespace Symfony\Component\Security\Http\EventListener; use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\Security\Http\Authentication\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; use Symfony\Component\Security\Http\Event\LoginFailureEvent; use Symfony\Component\Security\Http\Event\LoginSuccessEvent; use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; diff --git a/src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php b/src/Symfony/Component/Security/Http/Firewall/AuthenticatorManagerListener.php similarity index 72% rename from src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php rename to src/Symfony/Component/Security/Http/Firewall/AuthenticatorManagerListener.php index 71a448384d..6c7cf10ff9 100644 --- a/src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/AuthenticatorManagerListener.php @@ -13,15 +13,13 @@ 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\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; -use Symfony\Component\Security\Http\Authentication\Authenticator\AuthenticatorInterface; -use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticationGuardToken; -use Symfony\Component\Security\Guard\GuardAuthenticatorHandler; +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; @@ -32,25 +30,25 @@ use Symfony\Component\Security\Http\Event\LoginSuccessEvent; * * @experimental in 5.1 */ -class GuardManagerListener +class AuthenticatorManagerListener { - use GuardManagerListenerTrait; + use AuthenticatorManagerListenerTrait; private $authenticationManager; - private $guardHandler; - private $guardAuthenticators; + private $authenticatorHandler; + private $authenticators; protected $providerKey; private $eventDispatcher; protected $logger; /** - * @param AuthenticatorInterface[] $guardAuthenticators + * @param AuthenticatorInterface[] $authenticators */ - public function __construct(AuthenticationManagerInterface $authenticationManager, GuardAuthenticatorHandler $guardHandler, iterable $guardAuthenticators, string $providerKey, EventDispatcherInterface $eventDispatcher, ?LoggerInterface $logger = null) + public function __construct(AuthenticationManagerInterface $authenticationManager, AuthenticatorHandler $authenticatorHandler, iterable $authenticators, string $providerKey, EventDispatcherInterface $eventDispatcher, ?LoggerInterface $logger = null) { $this->authenticationManager = $authenticationManager; - $this->guardHandler = $guardHandler; - $this->guardAuthenticators = $guardAuthenticators; + $this->authenticatorHandler = $authenticatorHandler; + $this->authenticators = $authenticators; $this->providerKey = $providerKey; $this->logger = $logger; $this->eventDispatcher = $eventDispatcher; @@ -59,12 +57,12 @@ class GuardManagerListener public function __invoke(RequestEvent $requestEvent) { $request = $requestEvent->getRequest(); - $guardAuthenticators = $this->getSupportingGuardAuthenticators($request); - if (!$guardAuthenticators) { + $authenticators = $this->getSupportingAuthenticators($request); + if (!$authenticators) { return; } - $this->executeAuthenticators($guardAuthenticators, $requestEvent); + $this->executeAuthenticators($authenticators, $requestEvent); } /** @@ -72,12 +70,12 @@ class GuardManagerListener */ protected function executeAuthenticators(array $authenticators, RequestEvent $event): void { - foreach ($authenticators as $key => $guardAuthenticator) { - $this->executeAuthenticator($key, $guardAuthenticator, $event); + foreach ($authenticators as $key => $authenticator) { + $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($guardAuthenticator)]); + $this->logger->debug('The "{authenticator}" authenticator set the response. Any later authenticator will not be called', ['authenticator' => \get_class($authenticator)]); } break; @@ -101,7 +99,7 @@ class GuardManagerListener } // create a token with the unique key, so that the provider knows which authenticator to use - $token = $this->createPreAuthenticatedToken($credentials, $uniqueAuthenticatorKey, $this->providerKey); + $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)]); @@ -115,7 +113,7 @@ class GuardManagerListener } // sets the token on the token storage, etc - $this->guardHandler->authenticateWithToken($token, $request, $this->providerKey); + $this->authenticatorHandler->authenticateWithToken($token, $request, $this->providerKey); } catch (AuthenticationException $e) { // oh no! Authentication failed! @@ -123,7 +121,7 @@ class GuardManagerListener $this->logger->info('Authenticator failed.', ['exception' => $e, 'authenticator' => \get_class($authenticator)]); } - $response = $this->guardHandler->handleAuthenticationFailure($e, $request, $authenticator, $this->providerKey); + $response = $this->authenticatorHandler->handleAuthenticationFailure($e, $request, $authenticator, $this->providerKey); if ($response instanceof Response) { $event->setResponse($response); @@ -135,7 +133,7 @@ class GuardManagerListener } // success! - $response = $this->guardHandler->handleAuthenticationSuccess($token, $request, $authenticator, $this->providerKey); + $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)]); @@ -150,9 +148,4 @@ class GuardManagerListener $this->eventDispatcher->dispatch(new LoginSuccessEvent($authenticator, $token, $request, $response, $this->providerKey)); } - - protected function createPreAuthenticatedToken($credentials, string $uniqueAuthenticatorKey, string $providerKey): PreAuthenticationGuardToken - { - return new PreAuthenticationGuardToken($credentials, $uniqueAuthenticatorKey, $providerKey); - } } diff --git a/src/Symfony/Component/Security/Http/Firewall/AuthenticatorManagerListenerTrait.php b/src/Symfony/Component/Security/Http/Firewall/AuthenticatorManagerListenerTrait.php new file mode 100644 index 0000000000..046c5ef493 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Firewall/AuthenticatorManagerListenerTrait.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Firewall; + +use Symfony\Component\HttpFoundation\Request; + +/** + * @author Ryan Weaver + * @author Amaury Leroux de Lens + * + * @internal + */ +trait AuthenticatorManagerListenerTrait +{ + protected function getSupportingAuthenticators(Request $request): array + { + $authenticators = []; + 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 ($authenticator->supports($request)) { + $authenticators[$key] = $authenticator; + } elseif (null !== $this->logger) { + $this->logger->debug('Authenticator does not support the request.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($authenticator)]); + } + } + + return $authenticators; + } +} diff --git a/src/Symfony/Component/Security/Http/Firewall/GuardManagerListenerTrait.php b/src/Symfony/Component/Security/Http/Firewall/GuardManagerListenerTrait.php deleted file mode 100644 index a1cf6880ad..0000000000 --- a/src/Symfony/Component/Security/Http/Firewall/GuardManagerListenerTrait.php +++ /dev/null @@ -1,50 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Security\Http\Firewall; - -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\Event\RequestEvent; -use Symfony\Component\Security\Http\Authentication\Authenticator\AuthenticatorInterface as CoreAuthenticatorInterface; -use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticationGuardToken; -use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -use Symfony\Component\Security\Core\Exception\AuthenticationException; -use Symfony\Component\Security\Guard\AuthenticatorInterface; - -/** - * @author Ryan Weaver - * @author Amaury Leroux de Lens - * - * @internal - */ -trait GuardManagerListenerTrait -{ - protected function getSupportingGuardAuthenticators(Request $request): array - { - $guardAuthenticators = []; - foreach ($this->guardAuthenticators as $key => $guardAuthenticator) { - if (null !== $this->logger) { - $this->logger->debug('Checking support on guard authenticator.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)]); - } - - if ($guardAuthenticator->supports($request)) { - $guardAuthenticators[$key] = $guardAuthenticator; - } elseif (null !== $this->logger) { - $this->logger->debug('Guard authenticator does not support the request.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)]); - } - } - - return $guardAuthenticators; - } - - abstract protected function createPreAuthenticatedToken($credentials, string $uniqueGuardKey, string $providerKey): PreAuthenticationGuardToken; -} From 1c810d5d2a62cf7c5da0109969011bc415df5561 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sun, 9 Feb 2020 20:58:49 +0100 Subject: [PATCH 15/30] Added support for lazy firewalls --- .../DependencyInjection/SecurityExtension.php | 1 - .../LazyAuthenticatorManagerListener.php | 24 +++---- .../Resources/config/authenticators.xml | 2 +- .../Firewall/GuardAuthenticationListener.php | 16 ++++- .../Authenticator/AnonymousAuthenticator.php | 3 +- .../Firewall/AuthenticatorManagerListener.php | 65 +++++++++++++++++-- .../AuthenticatorManagerListenerTrait.php | 41 ------------ 7 files changed, 87 insertions(+), 65 deletions(-) delete mode 100644 src/Symfony/Component/Security/Http/Firewall/AuthenticatorManagerListenerTrait.php diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index fb402288be..0e857e53d1 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -453,7 +453,6 @@ class SecurityExtension extends Extension implements PrependExtensionInterface ->setDefinition('security.firewall.authenticator.'.$id, new ChildDefinition('security.firewall.authenticator')) ->replaceArgument(2, new Reference('security.firewall.authenticator.'.$id.'.locator')) ->replaceArgument(3, $id) - ->addTag('kernel.event_listener', ['event' => KernelEvents::REQUEST]) ; $listeners[] = new Reference('security.firewall.authenticator.'.$id); diff --git a/src/Symfony/Bundle/SecurityBundle/EventListener/LazyAuthenticatorManagerListener.php b/src/Symfony/Bundle/SecurityBundle/EventListener/LazyAuthenticatorManagerListener.php index 2a8a04e081..e4299bcc0c 100644 --- a/src/Symfony/Bundle/SecurityBundle/EventListener/LazyAuthenticatorManagerListener.php +++ b/src/Symfony/Bundle/SecurityBundle/EventListener/LazyAuthenticatorManagerListener.php @@ -26,37 +26,39 @@ use Symfony\Component\Security\Http\Firewall\AuthenticatorManagerListener; */ class LazyAuthenticatorManagerListener extends AuthenticatorManagerListener { - private $guardLocator; + private $authenticatorLocator; public function __construct( AuthenticationManagerInterface $authenticationManager, AuthenticatorHandler $authenticatorHandler, - ServiceLocator $guardLocator, + ServiceLocator $authenticatorLocator, string $providerKey, EventDispatcherInterface $eventDispatcher, ?LoggerInterface $logger = null ) { parent::__construct($authenticationManager, $authenticatorHandler, [], $providerKey, $eventDispatcher, $logger); - $this->guardLocator = $guardLocator; + $this->authenticatorLocator = $authenticatorLocator; } protected function getSupportingAuthenticators(Request $request): array { - $guardAuthenticators = []; - foreach ($this->guardLocator->getProvidedServices() as $key => $type) { - $guardAuthenticator = $this->guardLocator->get($key); + $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($guardAuthenticator)]); + $this->logger->debug('Checking support on guard authenticator.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($authenticator)]); } - if ($guardAuthenticator->supports($request)) { - $guardAuthenticators[$key] = $guardAuthenticator; + 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($guardAuthenticator)]); + $this->logger->debug('Guard authenticator does not support the request.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($authenticator)]); } } - return $guardAuthenticators; + return [$authenticators, $lazy]; } } diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml index 92d72ee238..b42cf0fab0 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml @@ -80,7 +80,7 @@ class="Symfony\Component\Security\Http\Authenticator\AnonymousAuthenticator" abstract="true"> secret - + diff --git a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php index 4ce55930f6..37665d4fa8 100644 --- a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php +++ b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php @@ -34,8 +34,6 @@ use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; */ class GuardAuthenticationListener extends AbstractListener { - use AuthenticatorManagerListenerTrait; - private $guardHandler; private $authenticationManager; private $providerKey; @@ -75,7 +73,19 @@ class GuardAuthenticationListener extends AbstractListener $this->logger->debug('Checking for guard authentication credentials.', $context); } - $guardAuthenticators = $this->getSupportingAuthenticators($request); + $guardAuthenticators = []; + 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 ($authenticator->supports($request)) { + $guardAuthenticators[$key] = $authenticator; + } elseif (null !== $this->logger) { + $this->logger->debug('Authenticator does not support the request.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($authenticator)]); + } + } + if (!$guardAuthenticators) { return false; } diff --git a/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php index 202da3b026..7e56b71579 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php @@ -41,7 +41,8 @@ class AnonymousAuthenticator implements AuthenticatorInterface, CustomAuthentica public function supports(Request $request): ?bool { // do not overwrite already stored tokens (i.e. from the session) - return null === $this->tokenStorage->getToken(); + // the `null` return value indicates that this authenticator supports lazy firewalls + return null === $this->tokenStorage->getToken() ? null : false; } public function getCredentials(Request $request) diff --git a/src/Symfony/Component/Security/Http/Firewall/AuthenticatorManagerListener.php b/src/Symfony/Component/Security/Http/Firewall/AuthenticatorManagerListener.php index 6c7cf10ff9..b5327bd958 100644 --- a/src/Symfony/Component/Security/Http/Firewall/AuthenticatorManagerListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/AuthenticatorManagerListener.php @@ -13,6 +13,7 @@ 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; @@ -30,10 +31,8 @@ use Symfony\Component\Security\Http\Event\LoginSuccessEvent; * * @experimental in 5.1 */ -class AuthenticatorManagerListener +class AuthenticatorManagerListener extends AbstractListener { - use AuthenticatorManagerListenerTrait; - private $authenticationManager; private $authenticatorHandler; private $authenticators; @@ -54,15 +53,58 @@ class AuthenticatorManagerListener $this->eventDispatcher = $eventDispatcher; } - public function __invoke(RequestEvent $requestEvent) + public function supports(Request $request): ?bool { - $request = $requestEvent->getRequest(); - $authenticators = $this->getSupportingAuthenticators($request); + 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; + } + + public function authenticate(RequestEvent $event) + { + $request = $event->getRequest(); + $authenticators = $request->attributes->get('_guard_authenticators'); + $request->attributes->remove('_guard_authenticators'); if (!$authenticators) { return; } - $this->executeAuthenticators($authenticators, $requestEvent); + $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]; } /** @@ -71,6 +113,15 @@ class AuthenticatorManagerListener 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()) { diff --git a/src/Symfony/Component/Security/Http/Firewall/AuthenticatorManagerListenerTrait.php b/src/Symfony/Component/Security/Http/Firewall/AuthenticatorManagerListenerTrait.php deleted file mode 100644 index 046c5ef493..0000000000 --- a/src/Symfony/Component/Security/Http/Firewall/AuthenticatorManagerListenerTrait.php +++ /dev/null @@ -1,41 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Security\Http\Firewall; - -use Symfony\Component\HttpFoundation\Request; - -/** - * @author Ryan Weaver - * @author Amaury Leroux de Lens - * - * @internal - */ -trait AuthenticatorManagerListenerTrait -{ - protected function getSupportingAuthenticators(Request $request): array - { - $authenticators = []; - 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 ($authenticator->supports($request)) { - $authenticators[$key] = $authenticator; - } elseif (null !== $this->logger) { - $this->logger->debug('Authenticator does not support the request.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($authenticator)]); - } - } - - return $authenticators; - } -} From ddf430fc1ef75724bba87670310b3cb79f2daffe Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Wed, 12 Feb 2020 23:56:17 +0100 Subject: [PATCH 16/30] Added remember me functionality --- .../Security/Factory/AnonymousFactory.php | 2 +- .../Factory/AuthenticatorFactoryInterface.php | 2 +- .../Security/Factory/FormLoginFactory.php | 2 +- .../Security/Factory/HttpBasicFactory.php | 2 +- .../Security/Factory/RememberMeFactory.php | 133 +++++++++++++----- .../DependencyInjection/SecurityExtension.php | 36 +++-- .../Resources/config/authenticators.xml | 13 +- .../AbstractLoginFormAuthenticator.php | 12 +- .../Authenticator/AnonymousAuthenticator.php | 5 - .../Authenticator/AuthenticatorInterface.php | 14 -- .../Authenticator/HttpBasicAuthenticator.php | 5 - .../Authenticator/RememberMeAuthenticator.php | 110 +++++++++++++++ .../RememberMeAuthenticatorInterface.php | 31 ++++ .../Http/EventListener/RememberMeListener.php | 29 ++-- .../RememberMe/AbstractRememberMeServices.php | 5 + 15 files changed, 296 insertions(+), 105 deletions(-) create mode 100644 src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php create mode 100644 src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticatorInterface.php diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php index b7e2347a57..cf77d99fdf 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php @@ -42,7 +42,7 @@ class AnonymousFactory implements SecurityFactoryInterface, AuthenticatorFactory return [$providerId, $listenerId, $defaultEntryPoint]; } - public function createAuthenticator(ContainerBuilder $container, string $id, array $config, ?string $userProviderId): string + public function createAuthenticator(ContainerBuilder $container, string $id, array $config, string $userProviderId): string { if (null === $config['secret']) { $config['secret'] = new Parameter('container.build_hash'); diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AuthenticatorFactoryInterface.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AuthenticatorFactoryInterface.php index e85ba0b495..acd1fce318 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AuthenticatorFactoryInterface.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AuthenticatorFactoryInterface.php @@ -25,5 +25,5 @@ interface AuthenticatorFactoryInterface * * @return string|string[] The authenticator service ID(s) to be used by the firewall */ - public function createAuthenticator(ContainerBuilder $container, string $id, array $config, ?string $userProviderId); + public function createAuthenticator(ContainerBuilder $container, string $id, array $config, string $userProviderId); } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php index 368cde156e..555cac383e 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php @@ -97,7 +97,7 @@ class FormLoginFactory extends AbstractFactory implements AuthenticatorFactoryIn return $entryPointId; } - public function createAuthenticator(ContainerBuilder $container, string $id, array $config, ?string $userProviderId): string + public function createAuthenticator(ContainerBuilder $container, string $id, array $config, string $userProviderId): string { $authenticatorId = 'security.authenticator.form_login.'.$id; $defaultOptions = array_merge($this->defaultSuccessHandlerOptions, $this->options); diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php index dea437e94c..9d121b17fe 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php @@ -46,7 +46,7 @@ class HttpBasicFactory implements SecurityFactoryInterface, AuthenticatorFactory return [$provider, $listenerId, $entryPointId]; } - public function createAuthenticator(ContainerBuilder $container, string $id, array $config, ?string $userProviderId): string + public function createAuthenticator(ContainerBuilder $container, string $id, array $config, string $userProviderId): string { $authenticatorId = 'security.authenticator.http_basic.'.$id; $container diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php index 06ad4134bd..979acc79dc 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php @@ -20,7 +20,7 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\Security\Http\EventListener\RememberMeLogoutListener; -class RememberMeFactory implements SecurityFactoryInterface +class RememberMeFactory implements SecurityFactoryInterface, AuthenticatorFactoryInterface { protected $options = [ 'name' => 'REMEMBERME', @@ -46,29 +46,8 @@ class RememberMeFactory implements SecurityFactoryInterface ; // remember me services - if (isset($config['service'])) { - $templateId = $config['service']; - $rememberMeServicesId = $templateId.'.'.$id; - } elseif (isset($config['token_provider'])) { - $templateId = 'security.authentication.rememberme.services.persistent'; - $rememberMeServicesId = $templateId.'.'.$id; - } else { - $templateId = 'security.authentication.rememberme.services.simplehash'; - $rememberMeServicesId = $templateId.'.'.$id; - } - - $rememberMeServices = $container->setDefinition($rememberMeServicesId, new ChildDefinition($templateId)); - $rememberMeServices->replaceArgument(1, $config['secret']); - $rememberMeServices->replaceArgument(2, $id); - - if (isset($config['token_provider'])) { - $rememberMeServices->addMethodCall('setTokenProvider', [ - new Reference($config['token_provider']), - ]); - } - - // remember-me options - $rememberMeServices->replaceArgument(3, array_intersect_key($config, $this->options)); + $templateId = $this->generateRememberMeServicesTemplateId($config, $id); + $rememberMeServicesId = $templateId.'.'.$id; // attach to remember-me aware listeners $userProviders = []; @@ -93,17 +72,8 @@ class RememberMeFactory implements SecurityFactoryInterface ; } } - if ($config['user_providers']) { - $userProviders = []; - foreach ($config['user_providers'] as $providerName) { - $userProviders[] = new Reference('security.user.provider.concrete.'.$providerName); - } - } - if (0 === \count($userProviders)) { - throw new \RuntimeException('You must configure at least one remember-me aware listener (such as form-login) for each firewall that has remember-me enabled.'); - } - $rememberMeServices->replaceArgument(0, new IteratorArgument(array_unique($userProviders))); + $this->createRememberMeServices($container, $id, $templateId, $userProviders, $config); // remember-me listener $listenerId = 'security.authentication.listener.rememberme.'.$id; @@ -119,6 +89,42 @@ class RememberMeFactory implements SecurityFactoryInterface return [$authProviderId, $listenerId, $defaultEntryPoint]; } + public function createAuthenticator(ContainerBuilder $container, string $id, array $config, string $userProviderId): string + { + $templateId = $this->generateRememberMeServicesTemplateId($config, $id); + $rememberMeServicesId = $templateId.'.'.$id; + + // create remember me services (which manage the remember me cookies) + $this->createRememberMeServices($container, $id, $templateId, [new Reference($userProviderId)], $config); + + // create remember me listener (which executes the remember me services for other authenticators and logout) + $this->createRememberMeListener($container, $id, $rememberMeServicesId); + + // create remember me authenticator (which re-authenticates the user based on the remember me cookie) + $authenticatorId = 'security.authenticator.remember_me.'.$id; + $container + ->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.remember_me')) + ->replaceArgument(0, new Reference($rememberMeServicesId)) + ->replaceArgument(3, array_intersect_key($config, $this->options)) + ; + + foreach ($container->findTaggedServiceIds('security.remember_me_aware') as $serviceId => $attributes) { + // register ContextListener + if ('security.context_listener' === substr($serviceId, 0, 25)) { + $container + ->getDefinition($serviceId) + ->addMethodCall('setRememberMeServices', [new Reference($rememberMeServicesId)]) + ; + + continue; + } + + throw new \LogicException(sprintf('Symfony Authenticator Security dropped support for the "security.remember_me_aware" tag, service "%s" will no longer work as expected.', $serviceId)); + } + + return $authenticatorId; + } + public function getPosition() { return 'remember_me'; @@ -163,4 +169,63 @@ class RememberMeFactory implements SecurityFactoryInterface } } } + + private function generateRememberMeServicesTemplateId(array $config, string $id): string + { + if (isset($config['service'])) { + return $config['service']; + } + + if (isset($config['token_provider'])) { + return 'security.authentication.rememberme.services.persistent'; + } + + return 'security.authentication.rememberme.services.simplehash'; + } + + private function createRememberMeServices(ContainerBuilder $container, string $id, string $templateId, array $userProviders, array $config): void + { + $rememberMeServicesId = $templateId.'.'.$id; + + $rememberMeServices = $container->setDefinition($rememberMeServicesId, new ChildDefinition($templateId)); + $rememberMeServices->replaceArgument(1, $config['secret']); + $rememberMeServices->replaceArgument(2, $id); + + if (isset($config['token_provider'])) { + $rememberMeServices->addMethodCall('setTokenProvider', [ + new Reference($config['token_provider']), + ]); + } + + // remember-me options + $rememberMeServices->replaceArgument(3, array_intersect_key($config, $this->options)); + + if ($config['user_providers']) { + $userProviders = []; + foreach ($config['user_providers'] as $providerName) { + $userProviders[] = new Reference('security.user.provider.concrete.'.$providerName); + } + } + + if (0 === \count($userProviders)) { + throw new \RuntimeException('You must configure at least one remember-me aware listener (such as form-login) for each firewall that has remember-me enabled.'); + } + + $rememberMeServices->replaceArgument(0, new IteratorArgument(array_unique($userProviders))); + } + + private function createRememberMeListener(ContainerBuilder $container, string $id, string $rememberMeServicesId): void + { + $container + ->setDefinition('security.listener.remember_me.'.$id, new ChildDefinition('security.listener.remember_me')) + ->addTag('kernel.event_subscriber') + ->replaceArgument(0, new Reference($rememberMeServicesId)) + ->replaceArgument(1, $id) + ; + + $container + ->setDefinition('security.logout.listener.remember_me.'.$id, new Definition(RememberMeLogoutListener::class)) + ->addTag('kernel.event_subscriber', ['dispatcher' => 'security.event_dispatcher.'.$id]) + ->addArgument(new Reference($rememberMeServicesId)); + } } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 0e857e53d1..97ede2281f 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -26,6 +26,7 @@ use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\DependencyInjection\Reference; @@ -34,6 +35,7 @@ use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder; use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder; +use Symfony\Component\Security\Core\User\ChainUserProvider; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Controller\UserValueResolver; use Twig\Extension\AbstractExtension; @@ -230,9 +232,16 @@ class SecurityExtension extends Extension implements PrependExtensionInterface foreach ($providerIds as $userProviderId) { $userProviders[] = new Reference($userProviderId); } - $arguments[1] = new IteratorArgument($userProviders); + $arguments[1] = $userProviderIteratorsArgument = new IteratorArgument($userProviders); $contextListenerDefinition->setArguments($arguments); + if (\count($userProviders) > 1) { + $container->setDefinition('security.user_providers', new Definition(ChainUserProvider::class, [$userProviderIteratorsArgument])) + ->setPublic(false); + } else { + $container->setAlias('security.user_providers', new Alias(current($providerIds)))->setPublic(false); + } + if (1 === \count($providerIds)) { $container->setAlias(UserProviderInterface::class, current($providerIds)); } @@ -423,16 +432,6 @@ class SecurityExtension extends Extension implements PrependExtensionInterface // Determine default entry point $configuredEntryPoint = isset($firewall['entry_point']) ? $firewall['entry_point'] : null; - if ($this->authenticatorManagerEnabled) { - // Remember me listener (must be before calling createAuthenticationListeners() to inject remember me services) - $container - ->setDefinition('security.listener.remember_me.'.$id, new ChildDefinition('security.listener.remember_me')) - ->replaceArgument(0, $id) - ->addTag('kernel.event_subscriber') - ->addTag('security.remember_me_aware', ['id' => $id, 'provider' => 'none']) - ; - } - // Authentication listeners $firewallAuthenticationProviders = []; list($authListeners, $defaultEntryPoint) = $this->createAuthenticationListeners($container, $id, $firewall, $firewallAuthenticationProviders, $defaultProvider, $providerIds, $configuredEntryPoint, $contextListenerId); @@ -554,7 +553,7 @@ class SecurityExtension extends Extension implements PrependExtensionInterface return [$listeners, $defaultEntryPoint]; } - private function getUserProvider(ContainerBuilder $container, string $id, array $firewall, string $factoryKey, ?string $defaultProvider, array $providerIds, ?string $contextListenerId): ?string + private function getUserProvider(ContainerBuilder $container, string $id, array $firewall, string $factoryKey, ?string $defaultProvider, array $providerIds, ?string $contextListenerId): string { if (isset($firewall[$factoryKey]['provider'])) { if (!isset($providerIds[$normalizedName = str_replace('-', '_', $firewall[$factoryKey]['provider'])])) { @@ -564,13 +563,8 @@ class SecurityExtension extends Extension implements PrependExtensionInterface return $providerIds[$normalizedName]; } - if ('remember_me' === $factoryKey || 'anonymous' === $factoryKey) { - if ('remember_me' === $factoryKey && $contextListenerId) { - $container->getDefinition($contextListenerId)->addTag('security.remember_me_aware', ['id' => $id, 'provider' => 'none']); - } - - // RememberMeFactory will use the firewall secret when created - return null; + if ('remember_me' === $factoryKey && $contextListenerId) { + $container->getDefinition($contextListenerId)->addTag('security.remember_me_aware', ['id' => $id, 'provider' => 'none']); } if ($defaultProvider) { @@ -587,6 +581,10 @@ class SecurityExtension extends Extension implements PrependExtensionInterface return $userProvider; } + if ('remember_me' === $factoryKey || 'anonymous' === $factoryKey) { + return 'security.user_providers'; + } + throw new InvalidConfigurationException(sprintf('Not configuring explicitly the provider for the "%s" listener on "%s" firewall is ambiguous as there is more than one registered provider.', $factoryKey, $id)); } diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml index b42cf0fab0..9ec5f17e0a 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml @@ -52,7 +52,8 @@ class="Symfony\Component\Security\Http\EventListener\RememberMeListener" abstract="true"> - + remember me services + provider key @@ -82,5 +83,15 @@ secret + + + remember me services + %kernel.secret% + + options + + diff --git a/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php index 07c71b1c3b..3469e8c509 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php @@ -25,7 +25,7 @@ use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface * * @experimental in 5.1 */ -abstract class AbstractLoginFormAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface +abstract class AbstractLoginFormAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface, RememberMeAuthenticatorInterface { /** * Return the URL to the login page. @@ -46,11 +46,6 @@ abstract class AbstractLoginFormAuthenticator extends AbstractAuthenticator impl return new RedirectResponse($url); } - public function supportsRememberMe(): bool - { - return true; - } - /** * Override to control what happens when the user hits a secure page * but isn't logged in yet. @@ -61,4 +56,9 @@ abstract class AbstractLoginFormAuthenticator extends AbstractAuthenticator impl return new RedirectResponse($url); } + + public function supportsRememberMe(): bool + { + return true; + } } diff --git a/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php index 7e56b71579..93d6931218 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php @@ -75,9 +75,4 @@ class AnonymousAuthenticator implements AuthenticatorInterface, CustomAuthentica { return null; } - - public function supportsRememberMe(): bool - { - return false; - } } diff --git a/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php b/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php index 5530eb32dd..6a85062e6c 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php @@ -102,18 +102,4 @@ interface AuthenticatorInterface * will be authenticated. This makes sense, for example, with an API. */ public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response; - - /** - * 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 - * D) The onAuthenticationSuccess method returns a Response object - */ - public function supportsRememberMe(): bool; } diff --git a/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php index 51ad3339b7..f896d924a8 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php @@ -94,9 +94,4 @@ class HttpBasicAuthenticator implements AuthenticatorInterface, AuthenticationEn return $this->start($request, $exception); } - - public function supportsRememberMe(): bool - { - return false; - } } diff --git a/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php new file mode 100644 index 0000000000..893bd099de --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php @@ -0,0 +1,110 @@ + + * + * 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\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\Authentication\Token\RememberMeToken; +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\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\RememberMe\AbstractRememberMeServices; +use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy; + +/** + * The RememberMe *Authenticator* performs remember me authentication. + * + * This authenticator is executed whenever a user's session + * expired and a remember me cookie was found. This authenticator + * then "re-authenticates" the user using the information in the + * cookie. + * + * @author Johannes M. Schmitt + * @author Wouter de Jong + * + * @final + */ +class RememberMeAuthenticator implements AuthenticatorInterface +{ + private $rememberMeServices; + private $secret; + private $tokenStorage; + private $options; + private $sessionStrategy; + + public function __construct(AbstractRememberMeServices $rememberMeServices, string $secret, TokenStorageInterface $tokenStorage, array $options, ?SessionAuthenticationStrategy $sessionStrategy = null) + { + $this->rememberMeServices = $rememberMeServices; + $this->secret = $secret; + $this->tokenStorage = $tokenStorage; + $this->options = $options; + $this->sessionStrategy = $sessionStrategy; + } + + public function supports(Request $request): ?bool + { + // do not overwrite already stored tokens (i.e. from the session) + if (null !== $this->tokenStorage->getToken()) { + return false; + } + + if (($cookie = $request->attributes->get(AbstractRememberMeServices::COOKIE_ATTR_NAME)) && null === $cookie->getValue()) { + return false; + } + + if (!$request->cookies->has($this->options['name'])) { + return false; + } + + // the `null` return value indicates that this authenticator supports lazy firewalls + return null; + } + + public function getCredentials(Request $request) + { + return [ + 'cookie_parts' => explode(AbstractRememberMeServices::COOKIE_DELIMITER, base64_decode($request->cookies->get($this->options['name']))), + 'request' => $request, + ]; + } + + /** + * @param array $credentials + */ + public function getUser($credentials): ?UserInterface + { + return $this->rememberMeServices->performLogin($credentials['cookie_parts'], $credentials['request']); + } + + public function createAuthenticatedToken(UserInterface $user, string $providerKey): TokenInterface + { + return new RememberMeToken($user, $providerKey, $this->secret); + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response + { + $this->rememberMeServices->loginFail($request, $exception); + + return null; + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response + { + if ($request->hasSession() && $request->getSession()->isStarted()) { + $this->sessionStrategy->onAuthentication($request, $token); + } + + return null; + } +} diff --git a/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticatorInterface.php b/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticatorInterface.php new file mode 100644 index 0000000000..d9eb6fa70b --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticatorInterface.php @@ -0,0 +1,31 @@ + + * + * 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; + +/** + * This interface must be extended if the authenticator supports remember me functionality. + * + * Remember me cookie will be set if *all* of the following are met: + * A) SupportsRememberMe() returns true in the successful authenticator + * 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 + * D) The onAuthenticationSuccess method returns a Response object + * + * @author Wouter de Jong + */ +interface RememberMeAuthenticatorInterface +{ + public function supportsRememberMe(): bool; +} diff --git a/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php b/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php index 882258b1a6..522f5090d6 100644 --- a/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php @@ -5,11 +5,19 @@ namespace Symfony\Component\Security\Http\EventListener; use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\RememberMeAuthenticatorInterface; use Symfony\Component\Security\Http\Event\LoginFailureEvent; use Symfony\Component\Security\Http\Event\LoginSuccessEvent; use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; /** + * The RememberMe *listener* creates and deletes remember me cookies. + * + * Upon login success or failure and support for remember me + * in the firewall and authenticator, this listener will create + * a remember me cookie. + * Upon login failure, all remember me cookies are removed. + * * @author Wouter de Jong * * @final @@ -17,23 +25,18 @@ use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; */ class RememberMeListener implements EventSubscriberInterface { + private $rememberMeServices; private $providerKey; private $logger; - /** @var RememberMeServicesInterface|null */ - private $rememberMeServices; - public function __construct(string $providerKey, ?LoggerInterface $logger = null) + public function __construct(RememberMeServicesInterface $rememberMeServices, string $providerKey, ?LoggerInterface $logger = null) { + $this->rememberMeServices = $rememberMeServices; $this->providerKey = $providerKey; $this->logger = $logger; } - public function setRememberMeServices(RememberMeServicesInterface $rememberMeServices): void - { - $this->rememberMeServices = $rememberMeServices; - } - public function onSuccessfulLogin(LoginSuccessEvent $event): void { if (!$this->isRememberMeEnabled($event->getAuthenticator(), $event->getProviderKey())) { @@ -59,15 +62,7 @@ class RememberMeListener implements EventSubscriberInterface return false; } - if (null === $this->rememberMeServices) { - if (null !== $this->logger) { - $this->logger->debug('Remember me skipped: it is not configured for the firewall.', ['authenticator' => \get_class($authenticator)]); - } - - return false; - } - - if (!$authenticator->supportsRememberMe()) { + if (!$authenticator instanceof RememberMeAuthenticatorInterface || !$authenticator->supportsRememberMe()) { if (null !== $this->logger) { $this->logger->debug('Remember me skipped: your authenticator does not support it.', ['authenticator' => \get_class($authenticator)]); } diff --git a/src/Symfony/Component/Security/Http/RememberMe/AbstractRememberMeServices.php b/src/Symfony/Component/Security/Http/RememberMe/AbstractRememberMeServices.php index 22f9dde14b..e9065d7f52 100644 --- a/src/Symfony/Component/Security/Http/RememberMe/AbstractRememberMeServices.php +++ b/src/Symfony/Component/Security/Http/RememberMe/AbstractRememberMeServices.php @@ -89,6 +89,11 @@ abstract class AbstractRememberMeServices implements RememberMeServicesInterface return $this->secret; } + public function performLogin(array $cookieParts, Request $request): UserInterface + { + return $this->processAutoLoginCookie($cookieParts, $request); + } + /** * Implementation of RememberMeServicesInterface. Detects whether a remember-me * cookie was set, decodes it, and hands it to subclasses for further processing. From 09bed16d3d04e52021e492709ea219b19c65602c Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sat, 22 Feb 2020 17:24:05 +0100 Subject: [PATCH 17/30] Only load old manager if new system is disabled --- .../DependencyInjection/SecurityExtension.php | 10 ++++++---- .../Resources/config/security.xml | 18 ----------------- ...icators.xml => security_authenticator.xml} | 17 +++++++++++++--- .../Resources/config/security_legacy.xml | 20 +++++++++++++++++++ 4 files changed, 40 insertions(+), 25 deletions(-) rename src/Symfony/Bundle/SecurityBundle/Resources/config/{authenticators.xml => security_authenticator.xml} (82%) create mode 100644 src/Symfony/Bundle/SecurityBundle/Resources/config/security_legacy.xml diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 97ede2281f..dbecca12e9 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -107,6 +107,12 @@ class SecurityExtension extends Extension implements PrependExtensionInterface $loader->load('security_listeners.xml'); $loader->load('security_rememberme.xml'); + if ($this->authenticatorManagerEnabled = $config['enable_authenticator_manager']) { + $loader->load('security_authenticator.xml'); + } else { + $loader->load('security_legacy.xml'); + } + if (class_exists(AbstractExtension::class)) { $loader->load('templating_twig.xml'); } @@ -141,10 +147,6 @@ class SecurityExtension extends Extension implements PrependExtensionInterface $container->setParameter('security.access.always_authenticate_before_granting', $config['always_authenticate_before_granting']); $container->setParameter('security.authentication.hide_user_not_found', $config['hide_user_not_found']); - if ($this->authenticatorManagerEnabled = $config['enable_authenticator_manager']) { - $loader->load('authenticators.xml'); - } - $this->createFirewalls($config, $container); $this->createAuthorization($config, $container); $this->createRoleHierarchy($config, $container); diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml index b04662aaf7..26da337312 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml @@ -45,24 +45,6 @@ - - - %security.authentication.manager.erase_credentials% - - - - - - - - %security.authentication.manager.erase_credentials% - - - - - - - diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml similarity index 82% rename from src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml rename to src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml index 9ec5f17e0a..4cbc440625 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml @@ -4,6 +4,18 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd"> + + + + authenticators + + %security.authentication.manager.erase_credentials% + + + + + + @@ -38,12 +50,12 @@ - + - + @@ -53,7 +65,6 @@ abstract="true"> remember me services - provider key diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_legacy.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_legacy.xml new file mode 100644 index 0000000000..85d672a078 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_legacy.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + %security.authentication.manager.erase_credentials% + + + + + + + From 44cc76fec2c0c98336a8cdd015719f8dad912545 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sat, 29 Feb 2020 01:49:11 +0100 Subject: [PATCH 18/30] Use one AuthenticatorManager per firewall --- .../DependencyInjection/SecurityExtension.php | 39 +++++++++------ .../config/security_authenticator.xml | 19 +++++++- .../FirewallAwareAuthenticatorManager.php | 48 +++++++++++++++++++ .../Authentication/AuthenticatorManager.php | 4 +- .../Firewall/AuthenticatorManagerListener.php | 6 +-- 5 files changed, 95 insertions(+), 21 deletions(-) create mode 100644 src/Symfony/Bundle/SecurityBundle/Security/FirewallAwareAuthenticatorManager.php diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index dbecca12e9..57ecde2068 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -23,6 +23,7 @@ use Symfony\Component\Config\FileLocator; use Symfony\Component\Console\Application; use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\Argument\IteratorArgument; +use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -278,19 +279,16 @@ class SecurityExtension extends Extension implements PrependExtensionInterface $mapDef->replaceArgument(0, ServiceLocatorTagPass::register($container, $contextRefs)); $mapDef->replaceArgument(1, new IteratorArgument($map)); - // add authentication providers to authentication manager - $authenticationProviders = array_map(function ($id) { - return new Reference($id); - }, array_unique($authenticationProviders)); - $authenticationManagerId = 'security.authentication.manager.provider'; - if ($this->authenticatorManagerEnabled) { - $authenticationManagerId = 'security.authentication.manager.authenticator'; - $container->setAlias('security.authentication.manager', new Alias($authenticationManagerId)); + if (!$this->authenticatorManagerEnabled) { + // add authentication providers to authentication manager + $authenticationProviders = array_map(function ($id) { + return new Reference($id); + }, array_unique($authenticationProviders)); + + $container + ->getDefinition('security.authentication.manager') + ->replaceArgument(0, new IteratorArgument($authenticationProviders)); } - $container - ->getDefinition($authenticationManagerId) - ->replaceArgument(0, new IteratorArgument($authenticationProviders)) - ; // register an autowire alias for the UserCheckerInterface if no custom user checker service is configured if (!$customUserChecker) { @@ -441,17 +439,28 @@ class SecurityExtension extends Extension implements PrependExtensionInterface $authenticationProviders = array_merge($authenticationProviders, $firewallAuthenticationProviders); if ($this->authenticatorManagerEnabled) { + // authenticator manager + $authenticators = array_map(function ($id) { + return new Reference($id); + }, $firewallAuthenticationProviders); + $container + ->setDefinition($managerId = 'security.authenticator.manager.'.$id, new ChildDefinition('security.authentication.manager.authenticator')) + ->replaceArgument(0, $authenticators) + ; + + $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([array_map(function ($id) { - return new Reference($id); - }, $firewallAuthenticationProviders)]) + ->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) ; diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml index 4cbc440625..9b52c37ec8 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml @@ -6,7 +6,10 @@ - + authenticators %security.authentication.manager.erase_credentials% @@ -14,6 +17,18 @@ + + + + + + + + + + - + authenticator manager diff --git a/src/Symfony/Bundle/SecurityBundle/Security/FirewallAwareAuthenticatorManager.php b/src/Symfony/Bundle/SecurityBundle/Security/FirewallAwareAuthenticatorManager.php new file mode 100644 index 0000000000..a3974dd2b3 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Security/FirewallAwareAuthenticatorManager.php @@ -0,0 +1,48 @@ + + * + * 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 + */ +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); + } +} diff --git a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php index 39208002b0..6a565ad1bb 100644 --- a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php +++ b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php @@ -40,14 +40,16 @@ class AuthenticatorManager implements AuthenticationManagerInterface private $authenticators; private $eventDispatcher; private $eraseCredentials; + private $providerKey; /** * @param AuthenticatorInterface[] $authenticators The authenticators, with keys that match what's passed to AuthenticatorManagerListener */ - public function __construct(iterable $authenticators, EventDispatcherInterface $eventDispatcher, bool $eraseCredentials = true) + public function __construct(iterable $authenticators, EventDispatcherInterface $eventDispatcher, string $providerKey, bool $eraseCredentials = true) { $this->authenticators = $authenticators; $this->eventDispatcher = $eventDispatcher; + $this->providerKey = $providerKey; $this->eraseCredentials = $eraseCredentials; } diff --git a/src/Symfony/Component/Security/Http/Firewall/AuthenticatorManagerListener.php b/src/Symfony/Component/Security/Http/Firewall/AuthenticatorManagerListener.php index b5327bd958..016bb826af 100644 --- a/src/Symfony/Component/Security/Http/Firewall/AuthenticatorManagerListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/AuthenticatorManagerListener.php @@ -33,7 +33,7 @@ use Symfony\Component\Security\Http\Event\LoginSuccessEvent; */ class AuthenticatorManagerListener extends AbstractListener { - private $authenticationManager; + private $authenticatorManager; private $authenticatorHandler; private $authenticators; protected $providerKey; @@ -45,7 +45,7 @@ class AuthenticatorManagerListener extends AbstractListener */ public function __construct(AuthenticationManagerInterface $authenticationManager, AuthenticatorHandler $authenticatorHandler, iterable $authenticators, string $providerKey, EventDispatcherInterface $eventDispatcher, ?LoggerInterface $logger = null) { - $this->authenticationManager = $authenticationManager; + $this->authenticatorManager = $authenticationManager; $this->authenticatorHandler = $authenticatorHandler; $this->authenticators = $authenticators; $this->providerKey = $providerKey; @@ -157,7 +157,7 @@ class AuthenticatorManagerListener extends AbstractListener } // pass the token into the AuthenticationManager system // this indirectly calls AuthenticatorManager::authenticate() - $token = $this->authenticationManager->authenticate($token); + $token = $this->authenticatorManager->authenticate($token); if (null !== $this->logger) { $this->logger->info('Authenticator successful!', ['token' => $token, 'authenticator' => \get_class($authenticator)]); From bf1a452e94a46e00d6ad3b75fccae8b77f5625c3 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sun, 1 Mar 2020 10:21:22 +0100 Subject: [PATCH 19/30] 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. --- .../DependencyInjection/SecurityExtension.php | 16 +- .../LazyAuthenticatorManagerListener.php | 64 ----- .../SecurityBundle/Resources/config/guard.xml | 4 +- .../config/security_authenticator.xml | 45 ++-- .../FirewallAwareAuthenticatorManager.php | 48 ---- .../Security/UserAuthenticator.php | 59 +++++ .../Firewall/GuardAuthenticationListener.php | 8 +- .../GuardAuthenticatorHandler.php} | 44 +--- .../Component/Security/Guard/GuardHandler.php | 28 -- .../Provider/GuardAuthenticationProvider.php | 48 ++-- .../GuardAuthenticationListenerTest.php | 6 +- .../Tests/GuardAuthenticatorHandlerTest.php | 16 +- .../GuardAuthenticationProviderTest.php | 10 +- .../Token/PreAuthenticationGuardToken.php | 65 +++++ .../Guard/Token/PreAuthenticationToken.php | 29 -- .../Authentication/AuthenticatorManager.php | 249 ++++++++++++++---- .../AuthenticatorManagerInterface.php | 37 +++ .../AuthenticatorManagerTrait.php | 46 ---- .../NoopAuthenticationManager.php | 33 +++ .../UserAuthenticatorInterface.php | 31 +++ .../Authenticator/AbstractAuthenticator.php | 2 +- .../AbstractLoginFormAuthenticator.php | 2 +- .../Authenticator/AuthenticatorInterface.php | 4 +- .../Token/PreAuthenticationToken.php | 73 ----- .../Security/Http/Event/LoginFailureEvent.php | 5 + .../Security/Http/Event/LoginSuccessEvent.php | 15 +- .../VerifyAuthenticatorCredentialsEvent.php | 10 +- .../EventListener/AuthenticatingListener.php | 4 +- .../PasswordMigratingListener.php | 5 +- .../EventListener/SessionStrategyListener.php | 56 ++++ .../Firewall/AuthenticatorManagerListener.php | 171 +----------- 31 files changed, 594 insertions(+), 639 deletions(-) delete mode 100644 src/Symfony/Bundle/SecurityBundle/EventListener/LazyAuthenticatorManagerListener.php delete mode 100644 src/Symfony/Bundle/SecurityBundle/Security/FirewallAwareAuthenticatorManager.php create mode 100644 src/Symfony/Bundle/SecurityBundle/Security/UserAuthenticator.php rename src/Symfony/Component/Security/{Http/Authentication/AuthenticatorHandler.php => Guard/GuardAuthenticatorHandler.php} (65%) delete mode 100644 src/Symfony/Component/Security/Guard/GuardHandler.php create mode 100644 src/Symfony/Component/Security/Guard/Token/PreAuthenticationGuardToken.php delete mode 100644 src/Symfony/Component/Security/Guard/Token/PreAuthenticationToken.php create mode 100644 src/Symfony/Component/Security/Http/Authentication/AuthenticatorManagerInterface.php delete mode 100644 src/Symfony/Component/Security/Http/Authentication/AuthenticatorManagerTrait.php create mode 100644 src/Symfony/Component/Security/Http/Authentication/NoopAuthenticationManager.php create mode 100644 src/Symfony/Component/Security/Http/Authentication/UserAuthenticatorInterface.php delete mode 100644 src/Symfony/Component/Security/Http/Authenticator/Token/PreAuthenticationToken.php create mode 100644 src/Symfony/Component/Security/Http/EventListener/SessionStrategyListener.php diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 57ecde2068..e4ef468c88 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -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); diff --git a/src/Symfony/Bundle/SecurityBundle/EventListener/LazyAuthenticatorManagerListener.php b/src/Symfony/Bundle/SecurityBundle/EventListener/LazyAuthenticatorManagerListener.php deleted file mode 100644 index e4299bcc0c..0000000000 --- a/src/Symfony/Bundle/SecurityBundle/EventListener/LazyAuthenticatorManagerListener.php +++ /dev/null @@ -1,64 +0,0 @@ - - * - * 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 - * - * @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]; - } -} diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/guard.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/guard.xml index 4bfd1229a8..c9bb06d179 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/guard.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/guard.xml @@ -8,7 +8,7 @@ @@ -18,7 +18,7 @@ - + - authenticators + + provider key + %security.authentication.manager.erase_credentials% - - - - + + + + - - - - - - - - - - - - authenticator manager - - - - - @@ -75,6 +58,12 @@ + + + + stateless firewall keys + + diff --git a/src/Symfony/Bundle/SecurityBundle/Security/FirewallAwareAuthenticatorManager.php b/src/Symfony/Bundle/SecurityBundle/Security/FirewallAwareAuthenticatorManager.php deleted file mode 100644 index a3974dd2b3..0000000000 --- a/src/Symfony/Bundle/SecurityBundle/Security/FirewallAwareAuthenticatorManager.php +++ /dev/null @@ -1,48 +0,0 @@ - - * - * 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 - */ -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); - } -} diff --git a/src/Symfony/Bundle/SecurityBundle/Security/UserAuthenticator.php b/src/Symfony/Bundle/SecurityBundle/Security/UserAuthenticator.php new file mode 100644 index 0000000000..ab2dded798 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Security/UserAuthenticator.php @@ -0,0 +1,59 @@ + + * + * 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 + * + * @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()); + } +} diff --git a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php index 37665d4fa8..5ac7935f31 100644 --- a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php +++ b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php @@ -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); diff --git a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorHandler.php b/src/Symfony/Component/Security/Guard/GuardAuthenticatorHandler.php similarity index 65% rename from src/Symfony/Component/Security/Http/Authentication/AuthenticatorHandler.php rename to src/Symfony/Component/Security/Guard/GuardAuthenticatorHandler.php index 7a579a9b2c..11f207a9ab 100644 --- a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorHandler.php +++ b/src/Symfony/Component/Security/Guard/GuardAuthenticatorHandler.php @@ -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 * - * @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))); } /** diff --git a/src/Symfony/Component/Security/Guard/GuardHandler.php b/src/Symfony/Component/Security/Guard/GuardHandler.php deleted file mode 100644 index 73e5a6e882..0000000000 --- a/src/Symfony/Component/Security/Guard/GuardHandler.php +++ /dev/null @@ -1,28 +0,0 @@ - - * - * 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 - * - * @final - */ -class GuardHandler extends AuthenticatorHandler -{ -} diff --git a/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php b/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php index 246d5173f1..0f8287ccc2 100644 --- a/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php +++ b/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php @@ -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; + } } diff --git a/src/Symfony/Component/Security/Guard/Tests/Firewall/GuardAuthenticationListenerTest.php b/src/Symfony/Component/Security/Guard/Tests/Firewall/GuardAuthenticationListenerTest.php index 6504aa1997..8c32d4b24f 100644 --- a/src/Symfony/Component/Security/Guard/Tests/Firewall/GuardAuthenticationListenerTest.php +++ b/src/Symfony/Component/Security/Guard/Tests/Firewall/GuardAuthenticationListenerTest.php @@ -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 @@ -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(); diff --git a/src/Symfony/Component/Security/Guard/Tests/GuardAuthenticatorHandlerTest.php b/src/Symfony/Component/Security/Guard/Tests/GuardAuthenticatorHandlerTest.php index d6dfacca10..e078a6be12 100644 --- a/src/Symfony/Component/Security/Guard/Tests/GuardAuthenticatorHandlerTest.php +++ b/src/Symfony/Component/Security/Guard/Tests/GuardAuthenticatorHandlerTest.php @@ -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'); } diff --git a/src/Symfony/Component/Security/Guard/Tests/Provider/GuardAuthenticationProviderTest.php b/src/Symfony/Component/Security/Guard/Tests/Provider/GuardAuthenticationProviderTest.php index c1bb302f9c..477bf56622 100644 --- a/src/Symfony/Component/Security/Guard/Tests/Provider/GuardAuthenticationProviderTest.php +++ b/src/Symfony/Component/Security/Guard/Tests/Provider/GuardAuthenticationProviderTest.php @@ -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 @@ -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(); diff --git a/src/Symfony/Component/Security/Guard/Token/PreAuthenticationGuardToken.php b/src/Symfony/Component/Security/Guard/Token/PreAuthenticationGuardToken.php new file mode 100644 index 0000000000..451d96c6ee --- /dev/null +++ b/src/Symfony/Component/Security/Guard/Token/PreAuthenticationGuardToken.php @@ -0,0 +1,65 @@ + + * + * 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 + */ +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.'); + } +} diff --git a/src/Symfony/Component/Security/Guard/Token/PreAuthenticationToken.php b/src/Symfony/Component/Security/Guard/Token/PreAuthenticationToken.php deleted file mode 100644 index 1ae9be445e..0000000000 --- a/src/Symfony/Component/Security/Guard/Token/PreAuthenticationToken.php +++ /dev/null @@ -1,29 +0,0 @@ - - * - * 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 - */ -class PreAuthenticationToken extends \Symfony\Component\Security\Http\Authenticator\Token\CorePreAuthenticationGuardToken implements GuardTokenInterface -{ - public function getGuardKey() - { - return $this->getAuthenticatorKey(); - } -} diff --git a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php index 6a565ad1bb..f7dacacbc4 100644 --- a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php +++ b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php @@ -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 - * @author Ryan Weaver + * @author Ryan Weaver + * @author Amaury Leroux de Lens * * @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(); + } } diff --git a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManagerInterface.php b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManagerInterface.php new file mode 100644 index 0000000000..89bcef8b52 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManagerInterface.php @@ -0,0 +1,37 @@ + + * + * 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 + * @author Ryan Weaver + * + * @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; +} diff --git a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManagerTrait.php b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManagerTrait.php deleted file mode 100644 index b1df45daab..0000000000 --- a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManagerTrait.php +++ /dev/null @@ -1,46 +0,0 @@ - - * - * 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 - * - * @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; -} diff --git a/src/Symfony/Component/Security/Http/Authentication/NoopAuthenticationManager.php b/src/Symfony/Component/Security/Http/Authentication/NoopAuthenticationManager.php new file mode 100644 index 0000000000..1a6efeb379 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authentication/NoopAuthenticationManager.php @@ -0,0 +1,33 @@ + + * + * 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 + */ +class NoopAuthenticationManager implements AuthenticationManagerInterface +{ + public function authenticate(TokenInterface $token) + { + } +} diff --git a/src/Symfony/Component/Security/Http/Authentication/UserAuthenticatorInterface.php b/src/Symfony/Component/Security/Http/Authentication/UserAuthenticatorInterface.php new file mode 100644 index 0000000000..76cb572921 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authentication/UserAuthenticatorInterface.php @@ -0,0 +1,31 @@ + + * + * 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 + * + * @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; +} diff --git a/src/Symfony/Component/Security/Http/Authenticator/AbstractAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/AbstractAuthenticator.php index 0301a97110..3683827d12 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AbstractAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AbstractAuthenticator.php @@ -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 + * @author Ryan Weaver * * @experimental in 5.1 */ diff --git a/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php index 3469e8c509..e702144787 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php @@ -21,7 +21,7 @@ use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface /** * A base class to make form login authentication easier! * - * @author Ryan Weaver + * @author Ryan Weaver * * @experimental in 5.1 */ diff --git a/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php b/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php index 6a85062e6c..0f1053e109 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php @@ -20,7 +20,7 @@ use Symfony\Component\Security\Core\User\UserInterface; /** * The interface for all authenticators. * - * @author Ryan Weaver + * @author Ryan Weaver * @author Amaury Leroux de Lens * @author Wouter de Jong * @@ -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; diff --git a/src/Symfony/Component/Security/Http/Authenticator/Token/PreAuthenticationToken.php b/src/Symfony/Component/Security/Http/Authenticator/Token/PreAuthenticationToken.php deleted file mode 100644 index 27daf7f8ba..0000000000 --- a/src/Symfony/Component/Security/Http/Authenticator/Token/PreAuthenticationToken.php +++ /dev/null @@ -1,73 +0,0 @@ - - * - * 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 - */ -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.'); - } -} diff --git a/src/Symfony/Component/Security/Http/Event/LoginFailureEvent.php b/src/Symfony/Component/Security/Http/Event/LoginFailureEvent.php index bc4e551e91..03a1c7a78c 100644 --- a/src/Symfony/Component/Security/Http/Event/LoginFailureEvent.php +++ b/src/Symfony/Component/Security/Http/Event/LoginFailureEvent.php @@ -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; diff --git a/src/Symfony/Component/Security/Http/Event/LoginSuccessEvent.php b/src/Symfony/Component/Security/Http/Event/LoginSuccessEvent.php index 22e11a8c87..6e48e171b6 100644 --- a/src/Symfony/Component/Security/Http/Event/LoginSuccessEvent.php +++ b/src/Symfony/Component/Security/Http/Event/LoginSuccessEvent.php @@ -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; + } } diff --git a/src/Symfony/Component/Security/Http/Event/VerifyAuthenticatorCredentialsEvent.php b/src/Symfony/Component/Security/Http/Event/VerifyAuthenticatorCredentialsEvent.php index 87bcb56a8b..cc37bf33f2 100644 --- a/src/Symfony/Component/Security/Http/Event/VerifyAuthenticatorCredentialsEvent.php +++ b/src/Symfony/Component/Security/Http/Event/VerifyAuthenticatorCredentialsEvent.php @@ -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 diff --git a/src/Symfony/Component/Security/Http/EventListener/AuthenticatingListener.php b/src/Symfony/Component/Security/Http/EventListener/AuthenticatingListener.php index 086eb92431..6795100a9c 100644 --- a/src/Symfony/Component/Security/Http/EventListener/AuthenticatingListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/AuthenticatingListener.php @@ -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; } diff --git a/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php b/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php index b57605e551..c97b722ff1 100644 --- a/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php @@ -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; } diff --git a/src/Symfony/Component/Security/Http/EventListener/SessionStrategyListener.php b/src/Symfony/Component/Security/Http/EventListener/SessionStrategyListener.php new file mode 100644 index 0000000000..436d525a5a --- /dev/null +++ b/src/Symfony/Component/Security/Http/EventListener/SessionStrategyListener.php @@ -0,0 +1,56 @@ + + * + * 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 + */ +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']; + } +} diff --git a/src/Symfony/Component/Security/Http/Firewall/AuthenticatorManagerListener.php b/src/Symfony/Component/Security/Http/Firewall/AuthenticatorManagerListener.php index 016bb826af..f30d9b6004 100644 --- a/src/Symfony/Component/Security/Http/Firewall/AuthenticatorManagerListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/AuthenticatorManagerListener.php @@ -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 - * @author Ryan Weaver - * @author Amaury Leroux de Lens * * @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); } } From 60d396f2d1bf2b01974d882481b6dd0fa32df9a4 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sun, 1 Mar 2020 11:22:25 +0100 Subject: [PATCH 20/30] Added automatically CSRF protected authenticators --- .../config/security_authenticator.xml | 5 ++ .../CsrfProtectedAuthenticatorInterface.php | 34 ++++++++++++ .../Authenticator/FormLoginAuthenticator.php | 18 +++---- .../EventListener/CsrfProtectionListener.php | 52 +++++++++++++++++++ 4 files changed, 99 insertions(+), 10 deletions(-) create mode 100644 src/Symfony/Component/Security/Http/Authenticator/CsrfProtectedAuthenticatorInterface.php create mode 100644 src/Symfony/Component/Security/Http/EventListener/CsrfProtectionListener.php diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml index 861c606f5f..a09c04ea5b 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml @@ -64,6 +64,11 @@ stateless firewall keys + + + + + diff --git a/src/Symfony/Component/Security/Http/Authenticator/CsrfProtectedAuthenticatorInterface.php b/src/Symfony/Component/Security/Http/Authenticator/CsrfProtectedAuthenticatorInterface.php new file mode 100644 index 0000000000..0f93ad1e86 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/CsrfProtectedAuthenticatorInterface.php @@ -0,0 +1,34 @@ + + * + * 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; + +/** + * This interface can be implemented to automatically add CSF + * protection to the authenticator. + * + * @author Wouter de Jong + */ +interface CsrfProtectedAuthenticatorInterface +{ + /** + * An arbitrary string used to generate the value of the CSRF token. + * Using a different string for each authenticator improves its security. + */ + public function getCsrfTokenId(): string; + + /** + * Returns the CSRF token contained in credentials if any. + * + * @param mixed $credentials the credentials returned by getCredentials() + */ + public function getCsrfToken($credentials): ?string; +} diff --git a/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php index 75bac9bd90..2ec3792a7c 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php @@ -32,7 +32,7 @@ use Symfony\Component\Security\Http\Util\TargetPathTrait; * @final * @experimental in 5.1 */ -class FormLoginAuthenticator extends AbstractLoginFormAuthenticator implements PasswordAuthenticatedInterface +class FormLoginAuthenticator extends AbstractLoginFormAuthenticator implements PasswordAuthenticatedInterface, CsrfProtectedAuthenticatorInterface { use TargetPathTrait; @@ -113,17 +113,15 @@ class FormLoginAuthenticator extends AbstractLoginFormAuthenticator implements P return $this->userProvider->loadUserByUsername($credentials['username']); } - /* @todo How to do CSRF protection? - public function checkCredentials($credentials, UserInterface $user): bool + public function getCsrfTokenId(): string { - if (null !== $this->csrfTokenManager) { - if (false === $this->csrfTokenManager->isTokenValid(new CsrfToken($this->options['csrf_token_id'], $credentials['csrf_token']))) { - throw new InvalidCsrfTokenException('Invalid CSRF token.'); - } - } + return $this->options['csrf_token_id']; + } - return $this->checkPassword($credentials, $user); - }*/ + public function getCsrfToken($credentials): ?string + { + return $credentials['csrf_token']; + } public function createAuthenticatedToken(UserInterface $user, $providerKey): TokenInterface { diff --git a/src/Symfony/Component/Security/Http/EventListener/CsrfProtectionListener.php b/src/Symfony/Component/Security/Http/EventListener/CsrfProtectionListener.php new file mode 100644 index 0000000000..fcde792452 --- /dev/null +++ b/src/Symfony/Component/Security/Http/EventListener/CsrfProtectionListener.php @@ -0,0 +1,52 @@ + + * + * 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\Core\Exception\InvalidCsrfTokenException; +use Symfony\Component\Security\Csrf\CsrfToken; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; +use Symfony\Component\Security\Http\Authenticator\CsrfProtectedAuthenticatorInterface; +use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; + +class CsrfProtectionListener implements EventSubscriberInterface +{ + private $csrfTokenManager; + + public function __construct(CsrfTokenManagerInterface $csrfTokenManager) + { + $this->csrfTokenManager = $csrfTokenManager; + } + + public function verifyCredentials(VerifyAuthenticatorCredentialsEvent $event): void + { + $authenticator = $event->getAuthenticator(); + if (!$authenticator instanceof CsrfProtectedAuthenticatorInterface) { + return; + } + + $csrfTokenValue = $authenticator->getCsrfToken($event->getCredentials()); + if (null === $csrfTokenValue) { + return; + } + + $csrfToken = new CsrfToken($authenticator->getCsrfTokenId(), $csrfTokenValue); + if (false === $this->csrfTokenManager->isTokenValid($csrfToken)) { + throw new InvalidCsrfTokenException('Invalid CSRF token.'); + } + } + + public static function getSubscribedEvents(): array + { + return [VerifyAuthenticatorCredentialsEvent::class => ['verifyCredentials', 256]]; + } +} From 59f49b20cab8813b4e37f8fd514f4ec31bd6610c Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sat, 7 Mar 2020 14:04:35 +0100 Subject: [PATCH 21/30] Rename AuthenticatingListener --- .../config/security_authenticator.xml | 2 +- ...erifyAuthenticatorCredentialsListener.php} | 28 +++++++++++-------- 2 files changed, 18 insertions(+), 12 deletions(-) rename src/Symfony/Component/Security/Http/EventListener/{AuthenticatingListener.php => VerifyAuthenticatorCredentialsListener.php} (80%) diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml index a09c04ea5b..757aef78e7 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml @@ -43,7 +43,7 @@ - + diff --git a/src/Symfony/Component/Security/Http/EventListener/AuthenticatingListener.php b/src/Symfony/Component/Security/Http/EventListener/VerifyAuthenticatorCredentialsListener.php similarity index 80% rename from src/Symfony/Component/Security/Http/EventListener/AuthenticatingListener.php rename to src/Symfony/Component/Security/Http/EventListener/VerifyAuthenticatorCredentialsListener.php index 6795100a9c..c8ab235f79 100644 --- a/src/Symfony/Component/Security/Http/EventListener/AuthenticatingListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/VerifyAuthenticatorCredentialsListener.php @@ -4,6 +4,7 @@ namespace Symfony\Component\Security\Http\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; +use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Exception\LogicException; use Symfony\Component\Security\Http\Authenticator\CustomAuthenticatedInterface; use Symfony\Component\Security\Http\Authenticator\PasswordAuthenticatedInterface; @@ -19,7 +20,7 @@ use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; * @final * @experimental in 5.1 */ -class AuthenticatingListener implements EventSubscriberInterface +class VerifyAuthenticatorCredentialsListener implements EventSubscriberInterface { private $encoderFactory; @@ -28,22 +29,22 @@ class AuthenticatingListener implements EventSubscriberInterface $this->encoderFactory = $encoderFactory; } - public static function getSubscribedEvents(): array - { - return [VerifyAuthenticatorCredentialsEvent::class => ['onAuthenticating', 128]]; - } - public function onAuthenticating(VerifyAuthenticatorCredentialsEvent $event): void { $authenticator = $event->getAuthenticator(); if ($authenticator instanceof PasswordAuthenticatedInterface) { // Use the password encoder to validate the credentials $user = $event->getUser(); - $event->setCredentialsValid($this->encoderFactory->getEncoder($user)->isPasswordValid( - $user->getPassword(), - $authenticator->getPassword($event->getCredentials()), - $user->getSalt() - )); + $presentedPassword = $authenticator->getPassword($event->getCredentials()); + if ('' === $presentedPassword) { + throw new BadCredentialsException('The presented password cannot be empty.'); + } + + if (null === $user->getPassword()) { + return; + } + + $event->setCredentialsValid($this->encoderFactory->getEncoder($user)->isPasswordValid($user->getPassword(), $presentedPassword, $user->getSalt())); return; } @@ -65,4 +66,9 @@ class AuthenticatingListener implements EventSubscriberInterface throw new LogicException(sprintf('Authenticator %s does not have valid credentials. Authenticators must implement one of the authenticated interfaces (%s, %s or %s).', \get_class($authenticator), PasswordAuthenticatedInterface::class, TokenAuthenticatedInterface::class, CustomAuthenticatedInterface::class)); } + + public static function getSubscribedEvents(): array + { + return [VerifyAuthenticatorCredentialsEvent::class => ['onAuthenticating', 128]]; + } } From 6b9d78d5e0b0a0f39eac87320fe948eb7002f3e0 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sat, 7 Mar 2020 18:06:29 +0100 Subject: [PATCH 22/30] Added tests --- .../Security/Factory/FormLoginFactory.php | 5 +- .../config/security_authenticator.xml | 2 - .../Authentication/AuthenticatorManager.php | 29 +-- .../Authenticator/FormLoginAuthenticator.php | 10 +- .../Authenticator/HttpBasicAuthenticator.php | 5 +- .../Authenticator/RememberMeAuthenticator.php | 27 ++- .../PasswordMigratingListener.php | 6 +- .../Http/EventListener/RememberMeListener.php | 16 +- .../EventListener/UserCheckerListener.php | 8 + ...VerifyAuthenticatorCredentialsListener.php | 4 + .../AuthenticatorManagerTest.php | 225 ++++++++++++++++++ .../AnonymousAuthenticatorTest.php | 61 +++++ .../FormLoginAuthenticatorTest.php | 141 +++++++++++ .../HttpBasicAuthenticatorTest.php | 58 +---- .../RememberMeAuthenticatorTest.php | 92 +++++++ .../CsrfProtectionListenerTest.php | 89 +++++++ .../PasswordMigratingListenerTest.php | 101 ++++++++ .../EventListener/RememberMeListenerTest.php | 101 ++++++++ .../EventListener/SessionListenerTest.php | 75 ++++++ .../EventListener/UserCheckerListenerTest.php | 78 ++++++ ...fyAuthenticatorCredentialsListenerTest.php | 167 +++++++++++++ 21 files changed, 1193 insertions(+), 107 deletions(-) create mode 100644 src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php create mode 100644 src/Symfony/Component/Security/Http/Tests/Authenticator/AnonymousAuthenticatorTest.php create mode 100644 src/Symfony/Component/Security/Http/Tests/Authenticator/FormLoginAuthenticatorTest.php rename src/Symfony/Component/Security/{Core/Tests/Authentication => Http/Tests}/Authenticator/HttpBasicAuthenticatorTest.php (52%) create mode 100644 src/Symfony/Component/Security/Http/Tests/Authenticator/RememberMeAuthenticatorTest.php create mode 100644 src/Symfony/Component/Security/Http/Tests/EventListener/CsrfProtectionListenerTest.php create mode 100644 src/Symfony/Component/Security/Http/Tests/EventListener/PasswordMigratingListenerTest.php create mode 100644 src/Symfony/Component/Security/Http/Tests/EventListener/RememberMeListenerTest.php create mode 100644 src/Symfony/Component/Security/Http/Tests/EventListener/SessionListenerTest.php create mode 100644 src/Symfony/Component/Security/Http/Tests/EventListener/UserCheckerListenerTest.php create mode 100644 src/Symfony/Component/Security/Http/Tests/EventListener/VerifyAuthenticatorCredentialsListenerTest.php diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php index 555cac383e..0fe2d995b3 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php @@ -104,9 +104,8 @@ class FormLoginFactory extends AbstractFactory implements AuthenticatorFactoryIn $options = array_merge($defaultOptions, array_intersect_key($config, $defaultOptions)); $container ->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.form_login')) - ->replaceArgument(1, isset($config['csrf_token_generator']) ? new Reference($config['csrf_token_generator']) : null) - ->replaceArgument(2, new Reference($userProviderId)) - ->replaceArgument(3, $options); + ->replaceArgument(1, new Reference($userProviderId)) + ->replaceArgument(2, $options); return $authenticatorId; } diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml index 757aef78e7..a5b6e87782 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml @@ -84,7 +84,6 @@ abstract="true"> realm name user provider - @@ -92,7 +91,6 @@ class="Symfony\Component\Security\Http\Authenticator\FormLoginAuthenticator" abstract="true"> - user provider options diff --git a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php index f7dacacbc4..c309485293 100644 --- a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php +++ b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php @@ -23,7 +23,6 @@ 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\Http\Event\InteractiveLoginEvent; use Symfony\Component\Security\Http\Event\LoginFailureEvent; use Symfony\Component\Security\Http\Event\LoginSuccessEvent; @@ -40,8 +39,6 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; */ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthenticatorInterface { - use AuthenticatorManagerTrait; - private $authenticators; private $tokenStorage; private $eventDispatcher; @@ -131,7 +128,9 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent // 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)]); + if (null !== $this->logger) { + $this->logger->debug('Skipping the "{authenticator}" authenticator as it did not support the request.', ['authenticator' => \get_class($authenticator)]); + } continue; } @@ -215,21 +214,14 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent throw new UsernameNotFoundException(sprintf('Null returned from "%s::getUser()".', \get_class($authenticator))); } - if (!$user instanceof UserInterface) { - 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, $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))); } -// turn the UserInterface into a TokenInterface + // 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(); @@ -259,21 +251,10 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent return $loginSuccessEvent->getResponse(); } - private function handleAuthenticationFailure(AuthenticationException $exception, TokenInterface $token) - { - if (null !== $this->eventDispatcher) { - $this->eventDispatcher->dispatch(new AuthenticationFailureEvent($token, $exception), AuthenticationEvents::AUTHENTICATION_FAILURE); - } - - $exception->setToken($token); - - throw $exception; - } - /** * Handles an authentication failure and returns the Response for the authenticator. */ - private function handleAuthenticatorFailure(AuthenticationException $authenticationException, Request $request, AuthenticatorInterface $authenticator): ?Response + private function handleAuthenticationFailure(AuthenticationException $authenticationException, Request $request, AuthenticatorInterface $authenticator): ?Response { $response = $authenticator->onAuthenticationFailure($request, $authenticationException); diff --git a/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php index 2ec3792a7c..cd8c569c57 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php @@ -20,7 +20,6 @@ use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; -use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Security\Http\HttpUtils; use Symfony\Component\Security\Http\ParameterBagUtils; use Symfony\Component\Security\Http\Util\TargetPathTrait; @@ -38,13 +37,11 @@ class FormLoginAuthenticator extends AbstractLoginFormAuthenticator implements P private $options; private $httpUtils; - private $csrfTokenManager; private $userProvider; - public function __construct(HttpUtils $httpUtils, ?CsrfTokenManagerInterface $csrfTokenManager, UserProviderInterface $userProvider, array $options) + public function __construct(HttpUtils $httpUtils, UserProviderInterface $userProvider, array $options) { $this->httpUtils = $httpUtils; - $this->csrfTokenManager = $csrfTokenManager; $this->options = array_merge([ 'username_parameter' => '_username', 'password_parameter' => '_password', @@ -75,10 +72,7 @@ class FormLoginAuthenticator extends AbstractLoginFormAuthenticator implements P public function getCredentials(Request $request): array { $credentials = []; - - if (null !== $this->csrfTokenManager) { - $credentials['csrf_token'] = ParameterBagUtils::getRequestParameterValue($request, $this->options['csrf_parameter']); - } + $credentials['csrf_token'] = ParameterBagUtils::getRequestParameterValue($request, $this->options['csrf_parameter']); if ($this->options['post_only']) { $credentials['username'] = ParameterBagUtils::getParameterBagValue($request->request, $this->options['username_parameter']); diff --git a/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php index f896d924a8..77480eea45 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php @@ -16,7 +16,6 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; -use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; @@ -33,14 +32,12 @@ class HttpBasicAuthenticator implements AuthenticatorInterface, AuthenticationEn { private $realmName; private $userProvider; - private $encoderFactory; private $logger; - public function __construct(string $realmName, UserProviderInterface $userProvider, EncoderFactoryInterface $encoderFactory, ?LoggerInterface $logger = null) + public function __construct(string $realmName, UserProviderInterface $userProvider, ?LoggerInterface $logger = null) { $this->realmName = $realmName; $this->userProvider = $userProvider; - $this->encoderFactory = $encoderFactory; $this->logger = $logger; } diff --git a/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php index 893bd099de..1ffdd1b997 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Http\Authenticator\Token; +namespace Symfony\Component\Security\Http\Authenticator; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -18,9 +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\Core\User\UserInterface; -use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; use Symfony\Component\Security\Http\RememberMe\AbstractRememberMeServices; -use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy; /** * The RememberMe *Authenticator* performs remember me authentication. @@ -35,21 +33,22 @@ use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy; * * @final */ -class RememberMeAuthenticator implements AuthenticatorInterface +class RememberMeAuthenticator implements AuthenticatorInterface, CustomAuthenticatedInterface { private $rememberMeServices; private $secret; private $tokenStorage; - private $options; - private $sessionStrategy; + private $options = [ + 'secure' => false, + 'httponly' => true, + ]; - public function __construct(AbstractRememberMeServices $rememberMeServices, string $secret, TokenStorageInterface $tokenStorage, array $options, ?SessionAuthenticationStrategy $sessionStrategy = null) + public function __construct(AbstractRememberMeServices $rememberMeServices, string $secret, TokenStorageInterface $tokenStorage, array $options) { $this->rememberMeServices = $rememberMeServices; $this->secret = $secret; $this->tokenStorage = $tokenStorage; - $this->options = $options; - $this->sessionStrategy = $sessionStrategy; + $this->options = array_merge($this->options, $options); } public function supports(Request $request): ?bool @@ -87,6 +86,12 @@ class RememberMeAuthenticator implements AuthenticatorInterface return $this->rememberMeServices->performLogin($credentials['cookie_parts'], $credentials['request']); } + public function checkCredentials($credentials, UserInterface $user): bool + { + // remember me always is valid (if a user could be found) + return true; + } + public function createAuthenticatedToken(UserInterface $user, string $providerKey): TokenInterface { return new RememberMeToken($user, $providerKey, $this->secret); @@ -101,10 +106,6 @@ class RememberMeAuthenticator implements AuthenticatorInterface public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response { - if ($request->hasSession() && $request->getSession()->isStarted()) { - $this->sessionStrategy->onAuthentication($request, $token); - } - return null; } } diff --git a/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php b/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php index c97b722ff1..28800e6260 100644 --- a/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php @@ -36,7 +36,7 @@ class PasswordMigratingListener implements EventSubscriberInterface return; } - if (null !== $password = $authenticator->getPassword($event->getCredentials())) { + if (null === $password = $authenticator->getPassword($event->getCredentials())) { return; } @@ -46,11 +46,11 @@ class PasswordMigratingListener implements EventSubscriberInterface } $passwordEncoder = $this->encoderFactory->getEncoder($user); - if (!method_exists($passwordEncoder, 'needsRehash') || !$passwordEncoder->needsRehash($user)) { + if (!$passwordEncoder->needsRehash($user->getPassword())) { return; } - $authenticator->upgradePassword($user, $passwordEncoder->encodePassword($user, $password)); + $authenticator->upgradePassword($user, $passwordEncoder->encodePassword($password, $user->getSalt())); } public static function getSubscribedEvents(): array diff --git a/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php b/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php index 522f5090d6..72ce7c13f9 100644 --- a/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php @@ -39,7 +39,15 @@ class RememberMeListener implements EventSubscriberInterface public function onSuccessfulLogin(LoginSuccessEvent $event): void { - if (!$this->isRememberMeEnabled($event->getAuthenticator(), $event->getProviderKey())) { + if (!$this->isRememberMeEnabled($event->getProviderKey(), $event->getAuthenticator())) { + return; + } + + if (null === $event->getResponse()) { + if (null !== $this->logger) { + $this->logger->debug('Remember me skipped: the authenticator did not set a success response.', ['authenticator' => \get_class($event->getAuthenticator())]); + } + return; } @@ -48,21 +56,21 @@ class RememberMeListener implements EventSubscriberInterface public function onFailedLogin(LoginFailureEvent $event): void { - if (!$this->isRememberMeEnabled($event->getAuthenticator(), $event->getProviderKey())) { + if (!$this->isRememberMeEnabled($event->getProviderKey())) { return; } $this->rememberMeServices->loginFail($event->getRequest(), $event->getException()); } - private function isRememberMeEnabled(AuthenticatorInterface $authenticator, string $providerKey): bool + private function isRememberMeEnabled(string $providerKey, ?AuthenticatorInterface $authenticator = null): bool { if ($providerKey !== $this->providerKey) { // This listener is created for a different firewall. return false; } - if (!$authenticator instanceof RememberMeAuthenticatorInterface || !$authenticator->supportsRememberMe()) { + if (null !== $authenticator && (!$authenticator instanceof RememberMeAuthenticatorInterface || !$authenticator->supportsRememberMe())) { if (null !== $this->logger) { $this->logger->debug('Remember me skipped: your authenticator does not support it.', ['authenticator' => \get_class($authenticator)]); } diff --git a/src/Symfony/Component/Security/Http/EventListener/UserCheckerListener.php b/src/Symfony/Component/Security/Http/EventListener/UserCheckerListener.php index c0c6c6895d..8ebbaca709 100644 --- a/src/Symfony/Component/Security/Http/EventListener/UserCheckerListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/UserCheckerListener.php @@ -23,11 +23,19 @@ class UserCheckerListener implements EventSubscriberInterface public function preCredentialsVerification(VerifyAuthenticatorCredentialsEvent $event): void { + if (null === $event->getUser()) { + return; + } + $this->userChecker->checkPreAuth($event->getUser()); } public function postCredentialsVerification(VerifyAuthenticatorCredentialsEvent $event): void { + if (null === $event->getUser() || !$event->areCredentialsValid()) { + return; + } + $this->userChecker->checkPostAuth($event->getUser()); } diff --git a/src/Symfony/Component/Security/Http/EventListener/VerifyAuthenticatorCredentialsListener.php b/src/Symfony/Component/Security/Http/EventListener/VerifyAuthenticatorCredentialsListener.php index c8ab235f79..77bbb39ec9 100644 --- a/src/Symfony/Component/Security/Http/EventListener/VerifyAuthenticatorCredentialsListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/VerifyAuthenticatorCredentialsListener.php @@ -31,6 +31,10 @@ class VerifyAuthenticatorCredentialsListener implements EventSubscriberInterface public function onAuthenticating(VerifyAuthenticatorCredentialsEvent $event): void { + if ($event->areCredentialsValid()) { + return; + } + $authenticator = $event->getAuthenticator(); if ($authenticator instanceof PasswordAuthenticatedInterface) { // Use the password encoder to validate the credentials diff --git a/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php b/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php new file mode 100644 index 0000000000..46dc09e2f8 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php @@ -0,0 +1,225 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\Authentication; + +use PHPUnit\Framework\TestCase; +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\Event\AuthenticationSuccessEvent; +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\Authentication\AuthenticatorManager; +use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Event\LoginSuccessEvent; +use Symfony\Component\Security\Http\Event\InteractiveLoginEvent; +use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; + +class AuthenticatorManagerTest extends TestCase +{ + private $tokenStorage; + private $eventDispatcher; + private $request; + private $user; + private $token; + private $response; + + protected function setUp(): void + { + $this->tokenStorage = $this->createMock(TokenStorageInterface::class); + $this->eventDispatcher = $this->createMock(EventDispatcherInterface::class); + $this->request = new Request(); + $this->user = $this->createMock(UserInterface::class); + $this->token = $this->createMock(TokenInterface::class); + $this->response = $this->createMock(Response::class); + } + + /** + * @dataProvider provideSupportsData + */ + public function testSupports($authenticators, $result) + { + $manager = $this->createManager($authenticators); + + $this->assertEquals($result, $manager->supports($this->request)); + } + + public function provideSupportsData() + { + yield [[$this->createAuthenticator(null), $this->createAuthenticator(null)], null]; + yield [[$this->createAuthenticator(null), $this->createAuthenticator(false)], null]; + + yield [[$this->createAuthenticator(null), $this->createAuthenticator(true)], true]; + yield [[$this->createAuthenticator(true), $this->createAuthenticator(false)], true]; + + yield [[$this->createAuthenticator(false), $this->createAuthenticator(false)], false]; + yield [[], false]; + } + + public function testSupportCheckedUponRequestAuthentication() + { + // the attribute stores the supported authenticators, returning false now + // means support changed between calling supports() and authenticateRequest() + // (which is the case with lazy firewalls and e.g. the AnonymousAuthenticator) + $authenticator = $this->createAuthenticator(false); + $this->request->attributes->set('_guard_authenticators', [$authenticator]); + + $authenticator->expects($this->never())->method('getCredentials'); + + $manager = $this->createManager([$authenticator]); + $manager->authenticateRequest($this->request); + } + + /** + * @dataProvider provideMatchingAuthenticatorIndex + */ + public function testAuthenticateRequest($matchingAuthenticatorIndex) + { + $authenticators = [$this->createAuthenticator(0 === $matchingAuthenticatorIndex), $this->createAuthenticator(1 === $matchingAuthenticatorIndex)]; + $this->request->attributes->set('_guard_authenticators', $authenticators); + $matchingAuthenticator = $authenticators[$matchingAuthenticatorIndex]; + + $authenticators[($matchingAuthenticatorIndex + 1) % 2]->expects($this->never())->method('getCredentials'); + + $matchingAuthenticator->expects($this->any())->method('getCredentials')->willReturn(['password' => 'pa$$']); + $matchingAuthenticator->expects($this->any())->method('getUser')->willReturn($this->user); + $this->eventDispatcher->expects($this->exactly(4)) + ->method('dispatch') + ->with($this->callback(function ($event) use ($matchingAuthenticator) { + if ($event instanceof VerifyAuthenticatorCredentialsEvent) { + return $event->getAuthenticator() === $matchingAuthenticator + && $event->getCredentials() === ['password' => 'pa$$'] + && $event->getUser() === $this->user; + } + + return $event instanceof InteractiveLoginEvent || $event instanceof LoginSuccessEvent || $event instanceof AuthenticationSuccessEvent; + })) + ->will($this->returnCallback(function ($event) { + if ($event instanceof VerifyAuthenticatorCredentialsEvent) { + $event->setCredentialsValid(true); + } + + return $event; + })); + $matchingAuthenticator->expects($this->any())->method('createAuthenticatedToken')->willReturn($this->token); + + $this->tokenStorage->expects($this->once())->method('setToken')->with($this->token); + + $matchingAuthenticator->expects($this->any()) + ->method('onAuthenticationSuccess') + ->with($this->anything(), $this->token, 'main') + ->willReturn($this->response); + + $manager = $this->createManager($authenticators); + $this->assertSame($this->response, $manager->authenticateRequest($this->request)); + } + + public function provideMatchingAuthenticatorIndex() + { + yield [0]; + yield [1]; + } + + public function testUserNotFound() + { + $authenticator = $this->createAuthenticator(); + $this->request->attributes->set('_guard_authenticators', [$authenticator]); + + $authenticator->expects($this->any())->method('getCredentials')->willReturn(['username' => 'john']); + $authenticator->expects($this->any())->method('getUser')->with(['username' => 'john'])->willReturn(null); + + $authenticator->expects($this->once()) + ->method('onAuthenticationFailure') + ->with($this->request, $this->isInstanceOf(UsernameNotFoundException::class)); + + $manager = $this->createManager([$authenticator]); + $manager->authenticateRequest($this->request); + } + + public function testNoCredentialsValidated() + { + $authenticator = $this->createAuthenticator(); + $this->request->attributes->set('_guard_authenticators', [$authenticator]); + + $authenticator->expects($this->any())->method('getCredentials')->willReturn(['username' => 'john']); + $authenticator->expects($this->any())->method('getUser')->willReturn($this->user); + + $authenticator->expects($this->once()) + ->method('onAuthenticationFailure') + ->with($this->request, $this->isInstanceOf(BadCredentialsException::class)); + + $manager = $this->createManager([$authenticator]); + $manager->authenticateRequest($this->request); + } + + /** + * @dataProvider provideEraseCredentialsData + */ + public function testEraseCredentials($eraseCredentials) + { + $authenticator = $this->createAuthenticator(); + $this->request->attributes->set('_guard_authenticators', [$authenticator]); + + $authenticator->expects($this->any())->method('getCredentials')->willReturn(['username' => 'john']); + $authenticator->expects($this->any())->method('getUser')->willReturn($this->user); + $this->eventDispatcher->expects($this->any()) + ->method('dispatch') + ->will($this->returnCallback(function ($event) { + if ($event instanceof VerifyAuthenticatorCredentialsEvent) { + $event->setCredentialsValid(true); + } + + return $event; + })); + + $authenticator->expects($this->any())->method('createAuthenticatedToken')->willReturn($this->token); + + $this->token->expects($eraseCredentials ? $this->once() : $this->never())->method('eraseCredentials'); + + $manager = $this->createManager([$authenticator], 'main', $eraseCredentials); + $manager->authenticateRequest($this->request); + } + + public function provideEraseCredentialsData() + { + yield [true]; + yield [false]; + } + + public function testAuthenticateUser() + { + $authenticator = $this->createAuthenticator(); + $authenticator->expects($this->any())->method('createAuthenticatedToken')->willReturn($this->token); + $authenticator->expects($this->any())->method('onAuthenticationSuccess')->willReturn($this->response); + + $this->tokenStorage->expects($this->once())->method('setToken')->with($this->token); + + $manager = $this->createManager([$authenticator]); + $this->assertSame($this->response, $manager->authenticateUser($this->user, $authenticator, $this->request)); + } + + private function createAuthenticator($supports = true) + { + $authenticator = $this->createMock(AuthenticatorInterface::class); + $authenticator->expects($this->any())->method('supports')->willReturn($supports); + + return $authenticator; + } + + private function createManager($authenticators, $providerKey = 'main', $eraseCredentials = true) + { + return new AuthenticatorManager($authenticators, $this->tokenStorage, $this->eventDispatcher, $providerKey, null, $eraseCredentials); + } +} diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/AnonymousAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/AnonymousAuthenticatorTest.php new file mode 100644 index 0000000000..f5d1cfdf98 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/AnonymousAuthenticatorTest.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\Authenticator; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Http\Authenticator\AnonymousAuthenticator; + +class AnonymousAuthenticatorTest extends TestCase +{ + private $tokenStorage; + private $authenticator; + private $request; + + protected function setUp(): void + { + $this->tokenStorage = $this->createMock(TokenStorageInterface::class); + $this->authenticator = new AnonymousAuthenticator('s3cr3t', $this->tokenStorage); + $this->request = new Request(); + } + + /** + * @dataProvider provideSupportsData + */ + public function testSupports($tokenAlreadyAvailable, $result) + { + $this->tokenStorage->expects($this->any())->method('getToken')->willReturn($tokenAlreadyAvailable ? $this->createMock(TokenStorageInterface::class) : null); + + $this->assertEquals($result, $this->authenticator->supports($this->request)); + } + + public function provideSupportsData() + { + yield [true, null]; + yield [false, false]; + } + + public function testAlwaysValidCredentials() + { + $this->assertTrue($this->authenticator->checkCredentials([], $this->createMock(UserInterface::class))); + } + + public function testAuthenticatedToken() + { + $token = $this->authenticator->createAuthenticatedToken($this->authenticator->getUser([]), 'main'); + + $this->assertTrue($token->isAuthenticated()); + $this->assertEquals('anon.', $token->getUser()); + } +} diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/FormLoginAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/FormLoginAuthenticatorTest.php new file mode 100644 index 0000000000..058508f25e --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/FormLoginAuthenticatorTest.php @@ -0,0 +1,141 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\Authenticator; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\Security\Core\Exception\BadCredentialsException; +use Symfony\Component\Security\Core\Security; +use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Http\Authenticator\FormLoginAuthenticator; +use Symfony\Component\Security\Http\HttpUtils; + +class FormLoginAuthenticatorTest extends TestCase +{ + private $userProvider; + private $authenticator; + + protected function setUp(): void + { + $this->userProvider = $this->createMock(UserProviderInterface::class); + } + + /** + * @dataProvider provideUsernamesForLength + */ + public function testHandleWhenUsernameLength($username, $ok) + { + if ($ok) { + $this->expectNotToPerformAssertions(); + } else { + $this->expectException(BadCredentialsException::class); + $this->expectExceptionMessage('Invalid username.'); + } + + $request = Request::create('/login_check', 'POST', ['_username' => $username]); + $request->setSession($this->createSession()); + + $this->setUpAuthenticator(); + $this->authenticator->getCredentials($request); + } + + public function provideUsernamesForLength() + { + yield [str_repeat('x', Security::MAX_USERNAME_LENGTH + 1), false]; + yield [str_repeat('x', Security::MAX_USERNAME_LENGTH - 1), true]; + } + + /** + * @dataProvider postOnlyDataProvider + */ + public function testHandleNonStringUsernameWithArray($postOnly) + { + $this->expectException(BadRequestHttpException::class); + $this->expectExceptionMessage('The key "_username" must be a string, "array" given.'); + + $request = Request::create('/login_check', 'POST', ['_username' => []]); + $request->setSession($this->createSession()); + + $this->setUpAuthenticator(['post_only' => $postOnly]); + $this->authenticator->getCredentials($request); + } + + /** + * @dataProvider postOnlyDataProvider + */ + public function testHandleNonStringUsernameWithInt($postOnly) + { + $this->expectException(BadRequestHttpException::class); + $this->expectExceptionMessage('The key "_username" must be a string, "integer" given.'); + + $request = Request::create('/login_check', 'POST', ['_username' => 42]); + $request->setSession($this->createSession()); + + $this->setUpAuthenticator(['post_only' => $postOnly]); + $this->authenticator->getCredentials($request); + } + + /** + * @dataProvider postOnlyDataProvider + */ + public function testHandleNonStringUsernameWithObject($postOnly) + { + $this->expectException(BadRequestHttpException::class); + $this->expectExceptionMessage('The key "_username" must be a string, "object" given.'); + + $request = Request::create('/login_check', 'POST', ['_username' => new \stdClass()]); + $request->setSession($this->createSession()); + + $this->setUpAuthenticator(['post_only' => $postOnly]); + $this->authenticator->getCredentials($request); + } + + /** + * @dataProvider postOnlyDataProvider + */ + public function testHandleNonStringUsernameWith__toString($postOnly) + { + $usernameObject = $this->getMockBuilder(DummyUserClass::class)->getMock(); + $usernameObject->expects($this->once())->method('__toString')->willReturn('someUsername'); + + $request = Request::create('/login_check', 'POST', ['_username' => $usernameObject]); + $request->setSession($this->createSession()); + + $this->setUpAuthenticator(['post_only' => $postOnly]); + $this->authenticator->getCredentials($request); + } + + public function postOnlyDataProvider() + { + yield [true]; + yield [false]; + } + + private function setUpAuthenticator(array $options = []) + { + $this->authenticator = new FormLoginAuthenticator(new HttpUtils(), $this->userProvider, $options); + } + + private function createSession() + { + return $this->createMock('Symfony\Component\HttpFoundation\Session\SessionInterface'); + } +} + +class DummyUserClass +{ + public function __toString(): string + { + return ''; + } +} diff --git a/src/Symfony/Component/Security/Core/Tests/Authentication/Authenticator/HttpBasicAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/HttpBasicAuthenticatorTest.php similarity index 52% rename from src/Symfony/Component/Security/Core/Tests/Authentication/Authenticator/HttpBasicAuthenticatorTest.php rename to src/Symfony/Component/Security/Http/Tests/Authenticator/HttpBasicAuthenticatorTest.php index b713840441..e2ac0ac991 100644 --- a/src/Symfony/Component/Security/Core/Tests/Authentication/Authenticator/HttpBasicAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/HttpBasicAuthenticatorTest.php @@ -1,6 +1,6 @@ expects($this->any()) ->method('getEncoder') ->willReturn($this->encoder); + + $this->authenticator = new HttpBasicAuthenticator('test', $this->userProvider); } - public function testValidUsernameAndPasswordServerParameters() + public function testExtractCredentialsAndUserFromRequest() { $request = new Request([], [], [], [], [], [ 'PHP_AUTH_USER' => 'TheUsername', 'PHP_AUTH_PW' => 'ThePassword', ]); - $authenticator = new HttpBasicAuthenticator('test', $this->userProvider, $this->encoderFactory); - $credentials = $authenticator->getCredentials($request); + $credentials = $this->authenticator->getCredentials($request); $this->assertEquals([ 'username' => 'TheUsername', 'password' => 'ThePassword', @@ -55,53 +54,20 @@ class HttpBasicAuthenticatorTest extends TestCase ->with('TheUsername') ->willReturn($mockedUser); - $user = $authenticator->getUser($credentials, $this->userProvider); + $user = $this->authenticator->getUser($credentials); $this->assertSame($mockedUser, $user); - $this->encoder - ->expects($this->any()) - ->method('isPasswordValid') - ->with('ThePassword', 'ThePassword', null) - ->willReturn(true); - - $checkCredentials = $authenticator->checkCredentials($credentials, $user); - $this->assertTrue($checkCredentials); + $this->assertEquals('ThePassword', $this->authenticator->getPassword($credentials)); } - /** @dataProvider provideInvalidPasswords */ - public function testInvalidPassword($presentedPassword, $exceptionMessage) - { - $authenticator = new HttpBasicAuthenticator('test', $this->userProvider, $this->encoderFactory); - - $this->encoder - ->expects($this->any()) - ->method('isPasswordValid') - ->willReturn(false); - - $this->expectException(BadCredentialsException::class); - $this->expectExceptionMessage($exceptionMessage); - - $authenticator->checkCredentials([ - 'username' => 'TheUsername', - 'password' => $presentedPassword, - ], $this->getMockBuilder(UserInterface::class)->getMock()); - } - - public function provideInvalidPasswords() - { - return [ - ['InvalidPassword', 'The presented password is invalid.'], - ['', 'The presented password cannot be empty.'], - ]; - } - - /** @dataProvider provideMissingHttpBasicServerParameters */ + /** + * @dataProvider provideMissingHttpBasicServerParameters + */ public function testHttpBasicServerParametersMissing(array $serverParameters) { $request = new Request([], [], [], [], [], $serverParameters); - $authenticator = new HttpBasicAuthenticator('test', $this->userProvider, $this->encoderFactory); - $this->assertFalse($authenticator->supports($request)); + $this->assertFalse($this->authenticator->supports($request)); } public function provideMissingHttpBasicServerParameters() diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/RememberMeAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/RememberMeAuthenticatorTest.php new file mode 100644 index 0000000000..9bd11ab62d --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/RememberMeAuthenticatorTest.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\Authenticator; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Cookie; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Http\Authenticator\RememberMeAuthenticator; +use Symfony\Component\Security\Http\RememberMe\AbstractRememberMeServices; + +class RememberMeAuthenticatorTest extends TestCase +{ + private $rememberMeServices; + private $tokenStorage; + private $authenticator; + private $request; + + protected function setUp(): void + { + $this->rememberMeServices = $this->createMock(AbstractRememberMeServices::class); + $this->tokenStorage = $this->createMock(TokenStorage::class); + $this->authenticator = new RememberMeAuthenticator($this->rememberMeServices, 's3cr3t', $this->tokenStorage, [ + 'name' => '_remember_me_cookie', + ]); + $this->request = new Request(); + $this->request->cookies->set('_remember_me_cookie', $val = $this->generateCookieValue()); + $this->request->attributes->set(AbstractRememberMeServices::COOKIE_ATTR_NAME, new Cookie('_remember_me_cookie', $val)); + } + + public function testSupportsTokenStorageWithToken() + { + $this->tokenStorage->expects($this->any())->method('getToken')->willReturn(TokenInterface::class); + + $this->assertFalse($this->authenticator->supports($this->request)); + } + + public function testSupportsRequestWithoutAttribute() + { + $this->request->attributes->remove(AbstractRememberMeServices::COOKIE_ATTR_NAME); + + $this->assertNull($this->authenticator->supports($this->request)); + } + + public function testSupportsRequestWithoutCookie() + { + $this->request->cookies->remove('_remember_me_cookie'); + + $this->assertFalse($this->authenticator->supports($this->request)); + } + + public function testSupports() + { + $this->assertNull($this->authenticator->supports($this->request)); + } + + public function testAuthenticate() + { + $credentials = $this->authenticator->getCredentials($this->request); + $this->assertEquals(['part1', 'part2'], $credentials['cookie_parts']); + $this->assertSame($this->request, $credentials['request']); + + $user = $this->createMock(UserInterface::class); + $this->rememberMeServices->expects($this->any()) + ->method('performLogin') + ->with($credentials['cookie_parts'], $credentials['request']) + ->willReturn($user); + + $this->assertSame($user, $this->authenticator->getUser($credentials)); + } + + public function testCredentialsAlwaysValid() + { + $this->assertTrue($this->authenticator->checkCredentials([], $this->createMock(UserInterface::class))); + } + + private function generateCookieValue() + { + return base64_encode(implode(AbstractRememberMeServices::COOKIE_DELIMITER, ['part1', 'part2'])); + } +} diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/CsrfProtectionListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/CsrfProtectionListenerTest.php new file mode 100644 index 0000000000..0c2a15d952 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/CsrfProtectionListenerTest.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\EventListener; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException; +use Symfony\Component\Security\Csrf\CsrfToken; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; +use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\CsrfProtectedAuthenticatorInterface; +use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; +use Symfony\Component\Security\Http\EventListener\CsrfProtectionListener; + +class CsrfProtectionListenerTest extends TestCase +{ + private $csrfTokenManager; + private $listener; + + protected function setUp(): void + { + $this->csrfTokenManager = $this->createMock(CsrfTokenManagerInterface::class); + $this->listener = new CsrfProtectionListener($this->csrfTokenManager); + } + + public function testNonCsrfProtectedAuthenticator() + { + $this->csrfTokenManager->expects($this->never())->method('isTokenValid'); + + $event = $this->createEvent($this->createAuthenticator(false)); + $this->listener->verifyCredentials($event); + } + + public function testValidCsrfToken() + { + $this->csrfTokenManager->expects($this->any()) + ->method('isTokenValid') + ->with(new CsrfToken('authenticator_token_id', 'abc123')) + ->willReturn(true); + + $event = $this->createEvent($this->createAuthenticator(true), ['_csrf' => 'abc123']); + $this->listener->verifyCredentials($event); + + $this->expectNotToPerformAssertions(); + } + + public function testInvalidCsrfToken() + { + $this->expectException(InvalidCsrfTokenException::class); + $this->expectExceptionMessage('Invalid CSRF token.'); + + $this->csrfTokenManager->expects($this->any()) + ->method('isTokenValid') + ->with(new CsrfToken('authenticator_token_id', 'abc123')) + ->willReturn(false); + + $event = $this->createEvent($this->createAuthenticator(true), ['_csrf' => 'abc123']); + $this->listener->verifyCredentials($event); + } + + private function createEvent($authenticator, $credentials = null) + { + return new VerifyAuthenticatorCredentialsEvent($authenticator, $credentials, null); + } + + private function createAuthenticator($supportsCsrf) + { + if (!$supportsCsrf) { + return $this->createMock(AuthenticatorInterface::class); + } + + $authenticator = $this->createMock([AuthenticatorInterface::class, CsrfProtectedAuthenticatorInterface::class]); + $authenticator->expects($this->any())->method('getCsrfTokenId')->willReturn('authenticator_token_id'); + $authenticator->expects($this->any()) + ->method('getCsrfToken') + ->with(['_csrf' => 'abc123']) + ->willReturn('abc123'); + + return $authenticator; + } +} diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/PasswordMigratingListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/PasswordMigratingListenerTest.php new file mode 100644 index 0000000000..37d9ee23cc --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/PasswordMigratingListenerTest.php @@ -0,0 +1,101 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\EventListener; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; +use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface; +use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\PasswordAuthenticatedInterface; +use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; +use Symfony\Component\Security\Http\EventListener\PasswordMigratingListener; + +class PasswordMigratingListenerTest extends TestCase +{ + private $encoderFactory; + private $listener; + private $user; + + protected function setUp(): void + { + $this->encoderFactory = $this->createMock(EncoderFactoryInterface::class); + $this->listener = new PasswordMigratingListener($this->encoderFactory); + $this->user = $this->createMock(UserInterface::class); + } + + /** + * @dataProvider provideUnsupportedEvents + */ + public function testUnsupportedEvents($event) + { + $this->encoderFactory->expects($this->never())->method('getEncoder'); + + $this->listener->onCredentialsVerification($event); + } + + public function provideUnsupportedEvents() + { + // unsupported authenticators + yield [$this->createEvent($this->createMock(AuthenticatorInterface::class), $this->user)]; + yield [$this->createEvent($this->createMock([AuthenticatorInterface::class, PasswordAuthenticatedInterface::class]), $this->user)]; + + // null password + yield [$this->createEvent($this->createAuthenticator(null), $this->user)]; + + // no user + yield [$this->createEvent($this->createAuthenticator('pa$$word'), null)]; + + // invalid password + yield [$this->createEvent($this->createAuthenticator('pa$$word'), $this->user, false)]; + } + + public function testUpgrade() + { + $encoder = $this->createMock(PasswordEncoderInterface::class); + $encoder->expects($this->any())->method('needsRehash')->willReturn(true); + $encoder->expects($this->any())->method('encodePassword')->with('pa$$word', null)->willReturn('new-encoded-password'); + + $this->encoderFactory->expects($this->any())->method('getEncoder')->with($this->user)->willReturn($encoder); + + $this->user->expects($this->any())->method('getPassword')->willReturn('old-encoded-password'); + + $authenticator = $this->createAuthenticator('pa$$word'); + $authenticator->expects($this->once()) + ->method('upgradePassword') + ->with($this->user, 'new-encoded-password') + ; + + $event = $this->createEvent($authenticator, $this->user); + $this->listener->onCredentialsVerification($event); + } + + /** + * @return AuthenticatorInterface + */ + private function createAuthenticator($password) + { + $authenticator = $this->createMock([AuthenticatorInterface::class, PasswordAuthenticatedInterface::class, PasswordUpgraderInterface::class]); + $authenticator->expects($this->any())->method('getPassword')->willReturn($password); + + return $authenticator; + } + + private function createEvent($authenticator, $user, $credentialsValid = true) + { + $event = new VerifyAuthenticatorCredentialsEvent($authenticator, [], $user); + $event->setCredentialsValid($credentialsValid); + + return $event; + } +} diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/RememberMeListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/RememberMeListenerTest.php new file mode 100644 index 0000000000..910c67a0bd --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/RememberMeListenerTest.php @@ -0,0 +1,101 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\EventListener; + +use PHPUnit\Framework\TestCase; +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\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\RememberMeAuthenticatorInterface; +use Symfony\Component\Security\Http\Event\LoginFailureEvent; +use Symfony\Component\Security\Http\Event\LoginSuccessEvent; +use Symfony\Component\Security\Http\EventListener\RememberMeListener; +use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; + +class RememberMeListenerTest extends TestCase +{ + private $rememberMeServices; + private $listener; + private $request; + private $response; + private $token; + + protected function setUp(): void + { + $this->rememberMeServices = $this->createMock(RememberMeServicesInterface::class); + $this->listener = new RememberMeListener($this->rememberMeServices); + $this->request = $this->getMockBuilder(Request::class)->disableOriginalConstructor()->getMock(); + $this->response = $this->createMock(Response::class); + $this->token = $this->createMock(TokenInterface::class); + } + + /** + * @dataProvider provideUnsupportingAuthenticators + */ + public function testSuccessfulLoginWithoutSupportingAuthenticator($authenticator) + { + $this->rememberMeServices->expects($this->never())->method('loginSuccess'); + + $event = $this->createLoginSuccessfulEvent('main_firewall', $this->response, $authenticator); + $this->listener->onSuccessfulLogin($event); + } + + public function provideUnsupportingAuthenticators() + { + yield [$this->createMock(AuthenticatorInterface::class)]; + + $authenticator = $this->createMock([AuthenticatorInterface::class, RememberMeAuthenticatorInterface::class]); + $authenticator->expects($this->any())->method('supportsRememberMe')->willReturn(false); + yield [$authenticator]; + } + + public function testSuccessfulLoginWithoutSuccessResponse() + { + $this->rememberMeServices->expects($this->never())->method('loginSuccess'); + + $event = $this->createLoginSuccessfulEvent('main_firewall', null); + $this->listener->onSuccessfulLogin($event); + } + + public function testSuccessfulLogin() + { + $this->rememberMeServices->expects($this->once())->method('loginSuccess')->with($this->request, $this->response, $this->token); + + $event = $this->createLoginSuccessfulEvent('main_firewall', $this->response); + $this->listener->onSuccessfulLogin($event); + } + + public function testCredentialsInvalid() + { + $this->rememberMeServices->expects($this->once())->method('loginFail')->with($this->request, $this->isInstanceOf(AuthenticationException::class)); + + $event = $this->createLoginFailureEvent('main_firewall'); + $this->listener->onFailedLogin($event); + } + + private function createLoginSuccessfulEvent($providerKey, $response, $authenticator = null) + { + if (null === $authenticator) { + $authenticator = $this->createMock([AuthenticatorInterface::class, RememberMeAuthenticatorInterface::class]); + $authenticator->expects($this->any())->method('supportsRememberMe')->willReturn(true); + } + + return new LoginSuccessEvent($authenticator, $this->token, $this->request, $response, $providerKey); + } + + private function createLoginFailureEvent($providerKey) + { + return new LoginFailureEvent(new AuthenticationException(), $this->createMock(AuthenticatorInterface::class), $this->request, null, $providerKey); + } +} diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/SessionListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/SessionListenerTest.php new file mode 100644 index 0000000000..176921d1a1 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/SessionListenerTest.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\EventListener; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Event\LoginSuccessEvent; +use Symfony\Component\Security\Http\EventListener\SessionStrategyListener; +use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface; + +class SessionListenerTest extends TestCase +{ + private $sessionAuthenticationStrategy; + private $listener; + private $request; + private $token; + + protected function setUp(): void + { + $this->sessionAuthenticationStrategy = $this->createMock(SessionAuthenticationStrategyInterface::class); + $this->listener = new SessionStrategyListener($this->sessionAuthenticationStrategy); + $this->request = new Request(); + $this->token = $this->createMock(TokenInterface::class); + } + + public function testRequestWithSession() + { + $this->configurePreviousSession(); + + $this->sessionAuthenticationStrategy->expects($this->once())->method('onAuthentication')->with($this->request, $this->token); + + $this->listener->onSuccessfulLogin($this->createEvent('main_firewall')); + } + + public function testRequestWithoutPreviousSession() + { + $this->sessionAuthenticationStrategy->expects($this->never())->method('onAuthentication')->with($this->request, $this->token); + + $this->listener->onSuccessfulLogin($this->createEvent('main_firewall')); + } + + public function testStatelessFirewalls() + { + $this->sessionAuthenticationStrategy->expects($this->never())->method('onAuthentication'); + + $listener = new SessionStrategyListener($this->sessionAuthenticationStrategy, ['api_firewall']); + $listener->onSuccessfulLogin($this->createEvent('api_firewall')); + } + + private function createEvent($providerKey) + { + return new LoginSuccessEvent($this->createMock(AuthenticatorInterface::class), $this->token, $this->request, null, $providerKey); + } + + private function configurePreviousSession() + { + $session = $this->getMockBuilder('Symfony\Component\HttpFoundation\Session\SessionInterface')->getMock(); + $session->expects($this->any()) + ->method('getName') + ->willReturn('test_session_name'); + $this->request->setSession($session); + $this->request->cookies->set('test_session_name', 'session_cookie_val'); + } +} diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/UserCheckerListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/UserCheckerListenerTest.php new file mode 100644 index 0000000000..785a312963 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/UserCheckerListenerTest.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\EventListener; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Security\Core\User\UserCheckerInterface; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; +use Symfony\Component\Security\Http\EventListener\UserCheckerListener; + +class UserCheckerListenerTest extends TestCase +{ + private $userChecker; + private $listener; + private $user; + + protected function setUp(): void + { + $this->userChecker = $this->createMock(UserCheckerInterface::class); + $this->listener = new UserCheckerListener($this->userChecker); + $this->user = $this->createMock(UserInterface::class); + } + + public function testPreAuth() + { + $this->userChecker->expects($this->once())->method('checkPreAuth')->with($this->user); + + $this->listener->preCredentialsVerification($this->createEvent()); + } + + public function testPreAuthNoUser() + { + $this->userChecker->expects($this->never())->method('checkPreAuth'); + + $this->listener->preCredentialsVerification($this->createEvent(true, null)); + } + + public function testPostAuthValidCredentials() + { + $this->userChecker->expects($this->once())->method('checkPostAuth')->with($this->user); + + $this->listener->postCredentialsVerification($this->createEvent(true)); + } + + public function testPostAuthInvalidCredentials() + { + $this->userChecker->expects($this->never())->method('checkPostAuth')->with($this->user); + + $this->listener->postCredentialsVerification($this->createEvent()); + } + + public function testPostAuthNoUser() + { + $this->userChecker->expects($this->never())->method('checkPostAuth'); + + $this->listener->postCredentialsVerification($this->createEvent(true, null)); + } + + private function createEvent($credentialsValid = false, $customUser = false) + { + $event = new VerifyAuthenticatorCredentialsEvent($this->createMock(AuthenticatorInterface::class), [], false === $customUser ? $this->user : $customUser); + if ($credentialsValid) { + $event->setCredentialsValid(true); + } + + return $event; + } +} diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/VerifyAuthenticatorCredentialsListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/VerifyAuthenticatorCredentialsListenerTest.php new file mode 100644 index 0000000000..e2c2cc6605 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/VerifyAuthenticatorCredentialsListenerTest.php @@ -0,0 +1,167 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\EventListener; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; +use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface; +use Symfony\Component\Security\Core\Exception\BadCredentialsException; +use Symfony\Component\Security\Core\Exception\LogicException; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\CustomAuthenticatedInterface; +use Symfony\Component\Security\Http\Authenticator\PasswordAuthenticatedInterface; +use Symfony\Component\Security\Http\Authenticator\TokenAuthenticatedInterface; +use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; +use Symfony\Component\Security\Http\EventListener\VerifyAuthenticatorCredentialsListener; + +class VerifyAuthenticatorCredentialsListenerTest extends TestCase +{ + private $encoderFactory; + private $listener; + private $user; + + protected function setUp(): void + { + $this->encoderFactory = $this->createMock(EncoderFactoryInterface::class); + $this->listener = new VerifyAuthenticatorCredentialsListener($this->encoderFactory); + $this->user = $this->createMock(UserInterface::class); + } + + /** + * @dataProvider providePasswords + */ + public function testPasswordAuthenticated($password, $passwordValid, $result) + { + $this->user->expects($this->any())->method('getPassword')->willReturn('encoded-password'); + + $encoder = $this->createMock(PasswordEncoderInterface::class); + $encoder->expects($this->any())->method('isPasswordValid')->with('encoded-password', $password)->willReturn($passwordValid); + + $this->encoderFactory->expects($this->any())->method('getEncoder')->with($this->identicalTo($this->user))->willReturn($encoder); + + $event = new VerifyAuthenticatorCredentialsEvent($this->createAuthenticator('password', $password), ['password' => $password], $this->user); + $this->listener->onAuthenticating($event); + $this->assertEquals($result, $event->areCredentialsValid()); + } + + public function providePasswords() + { + yield ['ThePa$$word', true, true]; + yield ['Invalid', false, false]; + } + + public function testEmptyPassword() + { + $this->expectException(BadCredentialsException::class); + $this->expectExceptionMessage('The presented password cannot be empty.'); + + $this->encoderFactory->expects($this->never())->method('getEncoder'); + + $event = new VerifyAuthenticatorCredentialsEvent($this->createAuthenticator('password', ''), ['password' => ''], $this->user); + $this->listener->onAuthenticating($event); + } + + public function testTokenAuthenticated() + { + $this->encoderFactory->expects($this->never())->method('getEncoder'); + + $event = new VerifyAuthenticatorCredentialsEvent($this->createAuthenticator('token', 'some_token'), ['token' => 'abc'], $this->user); + $this->listener->onAuthenticating($event); + + $this->assertTrue($event->areCredentialsValid()); + } + + public function testTokenAuthenticatedReturningNull() + { + $this->encoderFactory->expects($this->never())->method('getEncoder'); + + $event = new VerifyAuthenticatorCredentialsEvent($this->createAuthenticator('token', null), ['token' => 'abc'], $this->user); + $this->listener->onAuthenticating($event); + + $this->assertFalse($event->areCredentialsValid()); + } + + /** + * @dataProvider provideCustomAuthenticatedResults + */ + public function testCustomAuthenticated($result) + { + $this->encoderFactory->expects($this->never())->method('getEncoder'); + + $event = new VerifyAuthenticatorCredentialsEvent($this->createAuthenticator('custom', $result), [], $this->user); + $this->listener->onAuthenticating($event); + + $this->assertEquals($result, $event->areCredentialsValid()); + } + + public function provideCustomAuthenticatedResults() + { + yield [true]; + yield [false]; + } + + public function testAlreadyAuthenticated() + { + $event = new VerifyAuthenticatorCredentialsEvent($this->createAuthenticator(), [], $this->user); + $event->setCredentialsValid(true); + $this->listener->onAuthenticating($event); + + $this->assertTrue($event->areCredentialsValid()); + } + + public function testNoAuthenticatedInterfaceImplemented() + { + $authenticator = $this->createAuthenticator(); + $this->expectException(LogicException::class); + $this->expectExceptionMessage(sprintf('Authenticator %s does not have valid credentials. Authenticators must implement one of the authenticated interfaces (%s, %s or %s).', \get_class($authenticator), PasswordAuthenticatedInterface::class, TokenAuthenticatedInterface::class, CustomAuthenticatedInterface::class)); + + $this->encoderFactory->expects($this->never())->method('getEncoder'); + + $event = new VerifyAuthenticatorCredentialsEvent($authenticator, [], $this->user); + $this->listener->onAuthenticating($event); + } + + /** + * @return AuthenticatorInterface + */ + private function createAuthenticator(?string $type = null, $result = null) + { + $interfaces = [AuthenticatorInterface::class]; + switch ($type) { + case 'password': + $interfaces[] = PasswordAuthenticatedInterface::class; + break; + case 'token': + $interfaces[] = TokenAuthenticatedInterface::class; + break; + case 'custom': + $interfaces[] = CustomAuthenticatedInterface::class; + break; + } + + $authenticator = $this->createMock(1 === \count($interfaces) ? $interfaces[0] : $interfaces); + switch ($type) { + case 'password': + $authenticator->expects($this->any())->method('getPassword')->willReturn($result); + break; + case 'token': + $authenticator->expects($this->any())->method('getToken')->willReturn($result); + break; + case 'custom': + $authenticator->expects($this->any())->method('checkCredentials')->willReturn($result); + break; + } + + return $authenticator; + } +} From ba3754a80fe10a2b75b635e365fde9c1d0fffcee Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Fri, 13 Mar 2020 15:21:38 +0100 Subject: [PATCH 23/30] Differentiate between interactive and non-interactive authenticators --- .../Authentication/AuthenticatorManager.php | 66 ++++++--------- .../AbstractLoginFormAuthenticator.php | 7 +- .../Authenticator/AnonymousAuthenticator.php | 10 +-- .../InteractiveAuthenticatorInterface.php | 35 ++++++++ .../Authenticator/RememberMeAuthenticator.php | 11 ++- .../AuthenticatorManagerTest.php | 82 ++++++++++--------- 6 files changed, 124 insertions(+), 87 deletions(-) create mode 100644 src/Symfony/Component/Security/Http/Authenticator/InteractiveAuthenticatorInterface.php diff --git a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php index c309485293..381195d833 100644 --- a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php +++ b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php @@ -23,6 +23,7 @@ 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\InteractiveAuthenticatorInterface; use Symfony\Component\Security\Http\Event\InteractiveLoginEvent; use Symfony\Component\Security\Http\Event\LoginFailureEvent; use Symfony\Component\Security\Http\Event\LoginSuccessEvent; @@ -63,10 +64,8 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent { // 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 + // authenticate this in the system return $this->handleAuthenticationSuccess($token, $request, $authenticator); } @@ -161,10 +160,6 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent 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); @@ -172,15 +167,19 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent $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)]); + // success! (sets the token on the token storage, etc) + $response = $this->handleAuthenticationSuccess($token, $request, $authenticator); + if ($response instanceof Response) { + return $response; } + if (null !== $this->logger) { + $this->logger->debug('Authenticator set no success response: request continues.', ['authenticator' => \get_class($authenticator)]); + } + + return null; + } catch (AuthenticationException $e) { + // oh no! Authentication failed! $response = $this->handleAuthenticationFailure($e, $request, $authenticator); if ($response instanceof Response) { return $response; @@ -188,22 +187,6 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent return null; } - - // 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; } private function authenticateViaAuthenticator(AuthenticatorInterface $authenticator, $credentials): TokenInterface @@ -234,19 +217,17 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent return $authenticatedToken; } - private function saveAuthenticatedToken(TokenInterface $authenticatedToken, Request $request) + private function handleAuthenticationSuccess(TokenInterface $authenticatedToken, Request $request, AuthenticatorInterface $authenticator): ?Response { $this->tokenStorage->setToken($authenticatedToken); - $loginEvent = new InteractiveLoginEvent($request, $authenticatedToken); - $this->eventDispatcher->dispatch($loginEvent, SecurityEvents::INTERACTIVE_LOGIN); - } + $response = $authenticator->onAuthenticationSuccess($request, $authenticatedToken, $this->providerKey); + if ($authenticator instanceof InteractiveAuthenticatorInterface && $authenticator->isInteractive()) { + $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)); + $this->eventDispatcher->dispatch($loginSuccessEvent = new LoginSuccessEvent($authenticator, $authenticatedToken, $request, $response, $this->providerKey)); return $loginSuccessEvent->getResponse(); } @@ -256,7 +237,14 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent */ private function handleAuthenticationFailure(AuthenticationException $authenticationException, Request $request, AuthenticatorInterface $authenticator): ?Response { + if (null !== $this->logger) { + $this->logger->info('Authenticator failed.', ['exception' => $authenticationException, 'authenticator' => \get_class($authenticator)]); + } + $response = $authenticator->onAuthenticationFailure($request, $authenticationException); + if (null !== $response && null !== $this->logger) { + $this->logger->debug('The "{authenticator}" authenticator set the failure response.', ['authenticator' => \get_class($authenticator)]); + } $this->eventDispatcher->dispatch($loginFailureEvent = new LoginFailureEvent($authenticationException, $authenticator, $request, $response, $this->providerKey)); diff --git a/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php index e702144787..5e298418cb 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php @@ -25,7 +25,7 @@ use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface * * @experimental in 5.1 */ -abstract class AbstractLoginFormAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface, RememberMeAuthenticatorInterface +abstract class AbstractLoginFormAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface, RememberMeAuthenticatorInterface, InteractiveAuthenticatorInterface { /** * Return the URL to the login page. @@ -61,4 +61,9 @@ abstract class AbstractLoginFormAuthenticator extends AbstractAuthenticator impl { return true; } + + public function isInteractive(): bool + { + return true; + } } diff --git a/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php index 93d6931218..4b6214668c 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php @@ -66,13 +66,13 @@ class AnonymousAuthenticator implements AuthenticatorInterface, CustomAuthentica return new AnonymousToken($this->secret, 'anon.', []); } + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response + { + return null; // let the original request continue + } + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response { return null; } - - public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response - { - return null; - } } diff --git a/src/Symfony/Component/Security/Http/Authenticator/InteractiveAuthenticatorInterface.php b/src/Symfony/Component/Security/Http/Authenticator/InteractiveAuthenticatorInterface.php new file mode 100644 index 0000000000..a2abf96e4a --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/InteractiveAuthenticatorInterface.php @@ -0,0 +1,35 @@ + + * + * 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; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + +/** + * This is an extension of the authenticator interface that must + * be used by interactive authenticators. + * + * Interactive login requires explicit user action (e.g. a login + * form or HTTP basic authentication). Implementing this interface + * will dispatcher the InteractiveLoginEvent upon successful login. + * + * @author Wouter de Jong + */ +interface InteractiveAuthenticatorInterface extends AuthenticatorInterface +{ + /** + * Should return true to make this authenticator perform + * an interactive login. + */ + public function isInteractive(): bool; +} diff --git a/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php index 1ffdd1b997..72c6ea5288 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php @@ -33,7 +33,7 @@ use Symfony\Component\Security\Http\RememberMe\AbstractRememberMeServices; * * @final */ -class RememberMeAuthenticator implements AuthenticatorInterface, CustomAuthenticatedInterface +class RememberMeAuthenticator implements InteractiveAuthenticatorInterface, CustomAuthenticatedInterface { private $rememberMeServices; private $secret; @@ -97,6 +97,11 @@ class RememberMeAuthenticator implements AuthenticatorInterface, CustomAuthentic return new RememberMeToken($user, $providerKey, $this->secret); } + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response + { + return null; // let the original request continue + } + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response { $this->rememberMeServices->loginFail($request, $exception); @@ -104,8 +109,8 @@ class RememberMeAuthenticator implements AuthenticatorInterface, CustomAuthentic return null; } - public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response + public function isInteractive(): bool { - return null; + return true; } } diff --git a/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php b/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php index 46dc09e2f8..7343d79788 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php @@ -12,20 +12,17 @@ namespace Symfony\Component\Security\Http\Tests\Authentication; use PHPUnit\Framework\TestCase; +use Symfony\Component\EventDispatcher\EventDispatcher; 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\Event\AuthenticationSuccessEvent; 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\Authentication\AuthenticatorManager; -use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; -use Symfony\Component\Security\Http\Event\LoginSuccessEvent; -use Symfony\Component\Security\Http\Event\InteractiveLoginEvent; +use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface; use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; -use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; class AuthenticatorManagerTest extends TestCase { @@ -39,7 +36,7 @@ class AuthenticatorManagerTest extends TestCase protected function setUp(): void { $this->tokenStorage = $this->createMock(TokenStorageInterface::class); - $this->eventDispatcher = $this->createMock(EventDispatcherInterface::class); + $this->eventDispatcher = new EventDispatcher(); $this->request = new Request(); $this->user = $this->createMock(UserInterface::class); $this->token = $this->createMock(TokenInterface::class); @@ -95,35 +92,22 @@ class AuthenticatorManagerTest extends TestCase $matchingAuthenticator->expects($this->any())->method('getCredentials')->willReturn(['password' => 'pa$$']); $matchingAuthenticator->expects($this->any())->method('getUser')->willReturn($this->user); - $this->eventDispatcher->expects($this->exactly(4)) - ->method('dispatch') - ->with($this->callback(function ($event) use ($matchingAuthenticator) { - if ($event instanceof VerifyAuthenticatorCredentialsEvent) { - return $event->getAuthenticator() === $matchingAuthenticator - && $event->getCredentials() === ['password' => 'pa$$'] - && $event->getUser() === $this->user; - } - return $event instanceof InteractiveLoginEvent || $event instanceof LoginSuccessEvent || $event instanceof AuthenticationSuccessEvent; - })) - ->will($this->returnCallback(function ($event) { - if ($event instanceof VerifyAuthenticatorCredentialsEvent) { - $event->setCredentialsValid(true); - } + $listenerCalled = false; + $this->eventDispatcher->addListener(VerifyAuthenticatorCredentialsEvent::class, function (VerifyAuthenticatorCredentialsEvent $event) use (&$listenerCalled, $matchingAuthenticator) { + if ($event->getAuthenticator() === $matchingAuthenticator && $event->getCredentials() === ['password' => 'pa$$'] && $event->getUser() === $this->user) { + $listenerCalled = true; - return $event; - })); + $event->setCredentialsValid(true); + } + }); $matchingAuthenticator->expects($this->any())->method('createAuthenticatedToken')->willReturn($this->token); $this->tokenStorage->expects($this->once())->method('setToken')->with($this->token); - $matchingAuthenticator->expects($this->any()) - ->method('onAuthenticationSuccess') - ->with($this->anything(), $this->token, 'main') - ->willReturn($this->response); - $manager = $this->createManager($authenticators); - $this->assertSame($this->response, $manager->authenticateRequest($this->request)); + $this->assertNull($manager->authenticateRequest($this->request)); + $this->assertTrue($listenerCalled, 'The VerifyAuthenticatorCredentialsEvent listener is not called'); } public function provideMatchingAuthenticatorIndex() @@ -174,15 +158,9 @@ class AuthenticatorManagerTest extends TestCase $authenticator->expects($this->any())->method('getCredentials')->willReturn(['username' => 'john']); $authenticator->expects($this->any())->method('getUser')->willReturn($this->user); - $this->eventDispatcher->expects($this->any()) - ->method('dispatch') - ->will($this->returnCallback(function ($event) { - if ($event instanceof VerifyAuthenticatorCredentialsEvent) { - $event->setCredentialsValid(true); - } - - return $event; - })); + $this->eventDispatcher->addListener(VerifyAuthenticatorCredentialsEvent::class, function (VerifyAuthenticatorCredentialsEvent $event) { + $event->setCredentialsValid(true); + }); $authenticator->expects($this->any())->method('createAuthenticatedToken')->willReturn($this->token); @@ -207,12 +185,38 @@ class AuthenticatorManagerTest extends TestCase $this->tokenStorage->expects($this->once())->method('setToken')->with($this->token); $manager = $this->createManager([$authenticator]); - $this->assertSame($this->response, $manager->authenticateUser($this->user, $authenticator, $this->request)); + $manager->authenticateUser($this->user, $authenticator, $this->request); + } + + public function testInteractiveAuthenticator() + { + $authenticator = $this->createMock(InteractiveAuthenticatorInterface::class); + $authenticator->expects($this->any())->method('isInteractive')->willReturn(true); + $this->request->attributes->set('_guard_authenticators', [$authenticator]); + + $authenticator->expects($this->any())->method('getCredentials')->willReturn(['password' => 'pa$$']); + $authenticator->expects($this->any())->method('getUser')->willReturn($this->user); + + $this->eventDispatcher->addListener(VerifyAuthenticatorCredentialsEvent::class, function (VerifyAuthenticatorCredentialsEvent $event) { + $event->setCredentialsValid(true); + }); + $authenticator->expects($this->any())->method('createAuthenticatedToken')->willReturn($this->token); + + $this->tokenStorage->expects($this->once())->method('setToken')->with($this->token); + + $authenticator->expects($this->any()) + ->method('onAuthenticationSuccess') + ->with($this->anything(), $this->token, 'main') + ->willReturn($this->response); + + $manager = $this->createManager([$authenticator]); + $response = $manager->authenticateRequest($this->request); + $this->assertSame($this->response, $response); } private function createAuthenticator($supports = true) { - $authenticator = $this->createMock(AuthenticatorInterface::class); + $authenticator = $this->createMock(InteractiveAuthenticatorInterface::class); $authenticator->expects($this->any())->method('supports')->willReturn($supports); return $authenticator; From f5e11e5f329f4d274142a539b6f2308fb2a425b8 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sat, 14 Mar 2020 12:51:02 +0100 Subject: [PATCH 24/30] Reverted changes to the Guard component --- .../Firewall/GuardAuthenticationListener.php | 57 +++++++++---------- .../Provider/GuardAuthenticationProvider.php | 55 ++++++++---------- .../GuardAuthenticationListenerTest.php | 4 +- .../GuardAuthenticationProviderTest.php | 4 +- .../Component/Security/Guard/composer.json | 2 +- 5 files changed, 54 insertions(+), 68 deletions(-) diff --git a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php index 5ac7935f31..022538731d 100644 --- a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php +++ b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php @@ -20,7 +20,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\GuardAuthenticatorHandler; -use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken as GuardPreAuthenticationGuardToken; +use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken; use Symfony\Component\Security\Http\Firewall\AbstractListener; use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; @@ -37,7 +37,7 @@ class GuardAuthenticationListener extends AbstractListener private $guardHandler; private $authenticationManager; private $providerKey; - private $authenticators; + private $guardAuthenticators; private $logger; private $rememberMeServices; @@ -54,7 +54,7 @@ class GuardAuthenticationListener extends AbstractListener $this->guardHandler = $guardHandler; $this->authenticationManager = $authenticationManager; $this->providerKey = $providerKey; - $this->authenticators = $guardAuthenticators; + $this->guardAuthenticators = $guardAuthenticators; $this->logger = $logger; } @@ -66,23 +66,24 @@ class GuardAuthenticationListener extends AbstractListener if (null !== $this->logger) { $context = ['firewall_key' => $this->providerKey]; - if ($this->authenticators instanceof \Countable || \is_array($this->authenticators)) { - $context['authenticators'] = \count($this->authenticators); + if ($this->guardAuthenticators instanceof \Countable || \is_array($this->guardAuthenticators)) { + $context['authenticators'] = \count($this->guardAuthenticators); } $this->logger->debug('Checking for guard authentication credentials.', $context); } $guardAuthenticators = []; - foreach ($this->authenticators as $key => $authenticator) { + + foreach ($this->guardAuthenticators as $key => $guardAuthenticator) { if (null !== $this->logger) { - $this->logger->debug('Checking support on authenticator.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($authenticator)]); + $this->logger->debug('Checking support on guard authenticator.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)]); } - if ($authenticator->supports($request)) { - $guardAuthenticators[$key] = $authenticator; + if ($guardAuthenticator->supports($request)) { + $guardAuthenticators[$key] = $guardAuthenticator; } elseif (null !== $this->logger) { - $this->logger->debug('Authenticator does not support the request.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($authenticator)]); + $this->logger->debug('Guard authenticator does not support the request.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)]); } } @@ -104,23 +105,9 @@ class GuardAuthenticationListener extends AbstractListener $guardAuthenticators = $request->attributes->get('_guard_authenticators'); $request->attributes->remove('_guard_authenticators'); - $this->executeGuardAuthenticators($guardAuthenticators, $event); - } - - /** - * Should be called if this listener will support remember me. - */ - public function setRememberMeServices(RememberMeServicesInterface $rememberMeServices) - { - $this->rememberMeServices = $rememberMeServices; - } - - /** - * @param AuthenticatorInterface[] $guardAuthenticators - */ - protected function executeGuardAuthenticators(array $guardAuthenticators, RequestEvent $event): void - { foreach ($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); @@ -151,7 +138,7 @@ class GuardAuthenticationListener extends AbstractListener } // create a token with the unique key, so that the provider knows which authenticator to use - $token = new GuardPreAuthenticationGuardToken($credentials, $uniqueGuardKey, $this->providerKey); + $token = new PreAuthenticationGuardToken($credentials, $uniqueGuardKey); if (null !== $this->logger) { $this->logger->debug('Passing guard token information to the GuardAuthenticationProvider', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)]); @@ -200,12 +187,20 @@ class GuardAuthenticationListener extends AbstractListener $this->triggerRememberMe($guardAuthenticator, $request, $token, $response); } - protected function triggerRememberMe($guardAuthenticator, Request $request, TokenInterface $token, Response $response = null) + /** + * Should be called if this listener will support remember me. + */ + public function setRememberMeServices(RememberMeServicesInterface $rememberMeServices) { - if (!$guardAuthenticator instanceof AuthenticatorInterface && !$guardAuthenticator instanceof CoreAuthenticatorInterface) { - throw new \UnexpectedValueException('Invalid guard authenticator passed to '.__METHOD__.'. Expected AuthenticatorInterface of either Security Core or Security Guard.'); - } + $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. + */ + private function triggerRememberMe(AuthenticatorInterface $guardAuthenticator, Request $request, TokenInterface $token, Response $response = null) + { if (null === $this->rememberMeServices) { if (null !== $this->logger) { $this->logger->debug('Remember me skipped: it is not configured for the firewall.', ['authenticator' => \get_class($guardAuthenticator)]); diff --git a/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php b/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php index 0f8287ccc2..7e9258a9c5 100644 --- a/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php +++ b/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php @@ -26,7 +26,6 @@ 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\PreAuthenticationGuardToken; -use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; /** * Responsible for accepting the PreAuthenticationGuardToken and calling @@ -39,12 +38,11 @@ class GuardAuthenticationProvider implements AuthenticationProviderInterface /** * @var AuthenticatorInterface[] */ - private $authenticators; + private $guardAuthenticators; private $userProvider; private $providerKey; private $userChecker; private $passwordEncoder; - private $rememberMeServices; /** * @param iterable|AuthenticatorInterface[] $guardAuthenticators The authenticators, with keys that match what's passed to GuardAuthenticationListener @@ -52,7 +50,7 @@ class GuardAuthenticationProvider implements AuthenticationProviderInterface */ public function __construct(iterable $guardAuthenticators, UserProviderInterface $userProvider, string $providerKey, UserCheckerInterface $userChecker, UserPasswordEncoderInterface $passwordEncoder = null) { - $this->authenticators = $guardAuthenticators; + $this->guardAuthenticators = $guardAuthenticators; $this->userProvider = $userProvider; $this->providerKey = $providerKey; $this->userChecker = $userChecker; @@ -98,27 +96,14 @@ class GuardAuthenticationProvider implements AuthenticationProviderInterface 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); + return $this->authenticateViaGuard($guardAuthenticator, $token); } - public function supports(TokenInterface $token) - { - if ($token instanceof PreAuthenticationGuardToken) { - return null !== $this->findOriginatingAuthenticator($token); - } - - return $token instanceof GuardTokenInterface; - } - - public function setRememberMeServices(RememberMeServicesInterface $rememberMeServices) - { - $this->rememberMeServices = $rememberMeServices; - } - - private function authenticateViaGuard(AuthenticatorInterface $guardAuthenticator, PreAuthenticationGuardToken $token, string $providerKey): TokenInterface + private function authenticateViaGuard(AuthenticatorInterface $guardAuthenticator, PreAuthenticationGuardToken $token): GuardTokenInterface { // get the user from the GuardAuthenticator $user = $guardAuthenticator->getUser($token->getCredentials(), $this->userProvider); + if (null === $user) { throw new UsernameNotFoundException(sprintf('Null returned from "%s::getUser()".', get_debug_type($guardAuthenticator))); } @@ -135,14 +120,13 @@ class GuardAuthenticationProvider implements AuthenticationProviderInterface throw new BadCredentialsException(sprintf('Authentication failed because "%s::checkCredentials()" did not return true.', get_debug_type($guardAuthenticator))); } - if ($this->userProvider instanceof PasswordUpgraderInterface && $guardAuthenticator instanceof PasswordAuthenticatedInterface && null !== $this->passwordEncoder && (null !== $password = $guardAuthenticator->getPassword($token->getCredentials())) && method_exists($this->passwordEncoder, 'needsRehash') && $this->passwordEncoder->needsRehash($user)) { $this->userProvider->upgradePassword($user, $this->passwordEncoder->encodePassword($user, $password)); } $this->userChecker->checkPostAuth($user); // turn the UserInterface into a TokenInterface - $authenticatedToken = $guardAuthenticator->createAuthenticatedToken($user, $providerKey); + $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_debug_type($guardAuthenticator), get_debug_type($authenticatedToken))); } @@ -152,18 +136,29 @@ class GuardAuthenticationProvider implements AuthenticationProviderInterface 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; + // 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 ($uniqueAuthenticatorKey === $token->getGuardProviderKey()) { - return $authenticator; + if ($uniqueGuardKey === $token->getGuardProviderKey()) { + return $guardAuthenticator; } } - // no matching authenticator found + // no matching authenticator found - but there will be multiple GuardAuthenticationProvider + // instances that will be checked if you have multiple firewalls. + return null; } + + public function supports(TokenInterface $token) + { + if ($token instanceof PreAuthenticationGuardToken) { + return null !== $this->findOriginatingAuthenticator($token); + } + + return $token instanceof GuardTokenInterface; + } } diff --git a/src/Symfony/Component/Security/Guard/Tests/Firewall/GuardAuthenticationListenerTest.php b/src/Symfony/Component/Security/Guard/Tests/Firewall/GuardAuthenticationListenerTest.php index 8c32d4b24f..c5e1c92b89 100644 --- a/src/Symfony/Component/Security/Guard/Tests/Firewall/GuardAuthenticationListenerTest.php +++ b/src/Symfony/Component/Security/Guard/Tests/Firewall/GuardAuthenticationListenerTest.php @@ -266,9 +266,7 @@ class GuardAuthenticationListenerTest extends TestCase ->disableOriginalConstructor() ->getMock(); - $this->guardAuthenticatorHandler = $this->getMockBuilder( - 'Symfony\Component\Security\Guard\GuardAuthenticatorHandler' - ) + $this->guardAuthenticatorHandler = $this->getMockBuilder('Symfony\Component\Security\Guard\GuardAuthenticatorHandler') ->disableOriginalConstructor() ->getMock(); diff --git a/src/Symfony/Component/Security/Guard/Tests/Provider/GuardAuthenticationProviderTest.php b/src/Symfony/Component/Security/Guard/Tests/Provider/GuardAuthenticationProviderTest.php index 477bf56622..b742046af0 100644 --- a/src/Symfony/Component/Security/Guard/Tests/Provider/GuardAuthenticationProviderTest.php +++ b/src/Symfony/Component/Security/Guard/Tests/Provider/GuardAuthenticationProviderTest.php @@ -170,9 +170,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\PreAuthenticationGuardToken' - ) + $this->preAuthenticationToken = $this->getMockBuilder('Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken') ->disableOriginalConstructor() ->getMock(); } diff --git a/src/Symfony/Component/Security/Guard/composer.json b/src/Symfony/Component/Security/Guard/composer.json index f129233640..1b2337f829 100644 --- a/src/Symfony/Component/Security/Guard/composer.json +++ b/src/Symfony/Component/Security/Guard/composer.json @@ -17,7 +17,7 @@ ], "require": { "php": "^7.2.5", - "symfony/security-core": "^5.1", + "symfony/security-core": "^5.0", "symfony/security-http": "^4.4.1|^5.0.1", "symfony/polyfill-php80": "^1.15" }, From 95edc806a1f2623f245a23cb580c46f83c7c5943 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sat, 14 Mar 2020 15:17:10 +0100 Subject: [PATCH 25/30] Added pre-authenticated authenticators (X.509 & REMOTE_USER) --- .../Security/Factory/RemoteUserFactory.php | 15 +- .../Security/Factory/X509Factory.php | 16 ++- .../config/security_authenticator.xml | 25 ++++ .../AbstractPreAuthenticatedAuthenticator.php | 136 ++++++++++++++++++ .../Authenticator/RemoteUserAuthenticator.php | 48 +++++++ .../Http/Authenticator/X509Authenticator.php | 61 ++++++++ .../EventListener/UserCheckerListener.php | 3 +- .../RemoteUserAuthenticatorTest.php | 62 ++++++++ .../Authenticator/X509AuthenticatorTest.php | 110 ++++++++++++++ 9 files changed, 473 insertions(+), 3 deletions(-) create mode 100644 src/Symfony/Component/Security/Http/Authenticator/AbstractPreAuthenticatedAuthenticator.php create mode 100644 src/Symfony/Component/Security/Http/Authenticator/RemoteUserAuthenticator.php create mode 100644 src/Symfony/Component/Security/Http/Authenticator/X509Authenticator.php create mode 100644 src/Symfony/Component/Security/Http/Tests/Authenticator/RemoteUserAuthenticatorTest.php create mode 100644 src/Symfony/Component/Security/Http/Tests/Authenticator/X509AuthenticatorTest.php diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RemoteUserFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RemoteUserFactory.php index b37229d886..0f0c44f8ab 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RemoteUserFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RemoteUserFactory.php @@ -22,7 +22,7 @@ use Symfony\Component\DependencyInjection\Reference; * @author Fabien Potencier * @author Maxime Douailin */ -class RemoteUserFactory implements SecurityFactoryInterface +class RemoteUserFactory implements SecurityFactoryInterface, AuthenticatorFactoryInterface { public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint) { @@ -43,6 +43,19 @@ class RemoteUserFactory implements SecurityFactoryInterface return [$providerId, $listenerId, $defaultEntryPoint]; } + public function createAuthenticator(ContainerBuilder $container, string $id, array $config, string $userProviderId) + { + $authenticatorId = 'security.authenticator.remote_user.'.$id; + $container + ->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.remote_user')) + ->replaceArgument(0, new Reference($userProviderId)) + ->replaceArgument(2, $firewallName) + ->replaceArgument(3, $config['user']) + ; + + return $authenticatorId; + } + public function getPosition() { return 'pre_auth'; diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/X509Factory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/X509Factory.php index e3ba596d93..604cee7e44 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/X509Factory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/X509Factory.php @@ -21,7 +21,7 @@ use Symfony\Component\DependencyInjection\Reference; * * @author Fabien Potencier */ -class X509Factory implements SecurityFactoryInterface +class X509Factory implements SecurityFactoryInterface, AuthenticatorFactoryInterface { public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint) { @@ -44,6 +44,20 @@ class X509Factory implements SecurityFactoryInterface return [$providerId, $listenerId, $defaultEntryPoint]; } + public function createAuthenticator(ContainerBuilder $container, string $id, array $config, string $userProviderId) + { + $authenticatorId = 'security.authenticator.x509.'.$id; + $container + ->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.x509')) + ->replaceArgument(0, new Reference($userProviderId)) + ->replaceArgument(2, $firewallName) + ->replaceArgument(3, $config['user']) + ->replaceArgument(4, $config['credentials']) + ; + + return $authenticatorId; + } + public function getPosition() { return 'pre_auth'; diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml index a5b6e87782..0ff79a0ebd 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml @@ -10,6 +10,7 @@ class="Symfony\Component\Security\Http\Authentication\AuthenticatorManager" abstract="true" > + authenticators @@ -82,6 +83,7 @@ + realm name user provider @@ -111,5 +113,28 @@ options + + + + user provider + + firewall name + user key + credentials key + + + + + + user provider + + firewall name + user key + + diff --git a/src/Symfony/Component/Security/Http/Authenticator/AbstractPreAuthenticatedAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/AbstractPreAuthenticatedAuthenticator.php new file mode 100644 index 0000000000..b3a02bf1bd --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/AbstractPreAuthenticatedAuthenticator.php @@ -0,0 +1,136 @@ + + * + * 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; + +use Psr\Log\LoggerInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken; +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\Exception\BadCredentialsException; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Core\User\UserProviderInterface; + +/** + * The base authenticator for authenticators to use pre-authenticated + * requests (e.g. using certificates). + * + * @author Wouter de Jong + * @author Fabien Potencier + * + * @internal + * @experimental in Symfony 5.1 + */ +abstract class AbstractPreAuthenticatedAuthenticator implements InteractiveAuthenticatorInterface, CustomAuthenticatedInterface +{ + private $userProvider; + private $tokenStorage; + private $firewallName; + private $logger; + + public function __construct(UserProviderInterface $userProvider, TokenStorageInterface $tokenStorage, string $firewallName, ?LoggerInterface $logger = null) + { + $this->userProvider = $userProvider; + $this->tokenStorage = $tokenStorage; + $this->firewallName = $firewallName; + $this->logger = $logger; + } + + /** + * Returns the username of the pre-authenticated user. + * + * This authenticator is skipped if null is returned or a custom + * BadCredentialsException is thrown. + */ + abstract protected function extractUsername(Request $request): ?string; + + public function supports(Request $request): ?bool + { + try { + $username = $this->extractUsername($request); + } catch (BadCredentialsException $e) { + $this->clearToken($e); + + if (null !== $this->logger) { + $this->logger->debug('Skipping pre-authenticated authenticator as a BadCredentialsException is thrown.', ['exception' => $e, 'authenticator' => \get_class($this)]); + } + + return false; + } + + if (null === $username) { + if (null !== $this->logger) { + $this->logger->debug('Skipping pre-authenticated authenticator no username could be extracted.', ['authenticator' => \get_class($this)]); + } + + return false; + } + + $request->attributes->set('_pre_authenticated_username', $username); + + return true; + } + + public function getCredentials(Request $request) + { + return [ + 'username' => $request->attributes->get('_pre_authenticated_username'), + ]; + } + + public function getUser($credentials): ?UserInterface + { + return $this->userProvider->loadUserByUsername($credentials['username']); + } + + public function checkCredentials($credentials, UserInterface $user): bool + { + // the user is already authenticated before it entered Symfony + return true; + } + + public function createAuthenticatedToken(UserInterface $user, string $providerKey): TokenInterface + { + return new PreAuthenticatedToken($user, null, $providerKey); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response + { + return null; // let the original request continue + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response + { + $this->clearToken($exception); + + return null; + } + + public function isInteractive(): bool + { + return true; + } + + private function clearToken(AuthenticationException $exception): void + { + $token = $this->tokenStorage->getToken(); + if ($token instanceof PreAuthenticatedToken && $this->firewallName === $token->getProviderKey()) { + $this->tokenStorage->setToken(null); + + if (null !== $this->logger) { + $this->logger->info('Cleared pre-authenticated token due to an exception.', ['exception' => $exception]); + } + } + } +} diff --git a/src/Symfony/Component/Security/Http/Authenticator/RemoteUserAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/RemoteUserAuthenticator.php new file mode 100644 index 0000000000..3a01087767 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/RemoteUserAuthenticator.php @@ -0,0 +1,48 @@ + + * + * 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; + +use Psr\Log\LoggerInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Exception\BadCredentialsException; +use Symfony\Component\Security\Core\User\UserProviderInterface; + +/** + * This authenticator authenticates a remote user. + * + * @author Wouter de Jong + * @author Fabien Potencier + * @author Maxime Douailin + * + * @internal in Symfony 5.1 + */ +class RemoteUserAuthenticator extends AbstractPreAuthenticatedAuthenticator +{ + private $userKey; + + public function __construct(UserProviderInterface $userProvider, TokenStorageInterface $tokenStorage, string $firewallName, string $userKey = 'REMOTE_USER', ?LoggerInterface $logger = null) + { + parent::__construct($userProvider, $tokenStorage, $firewallName, $logger); + + $this->userKey = $userKey; + } + + protected function extractUsername(Request $request): ?string + { + if (!$request->server->has($this->userKey)) { + throw new BadCredentialsException(sprintf('User key was not found: "%s".', $this->userKey)); + } + + return $request->server->get($this->userKey); + } +} diff --git a/src/Symfony/Component/Security/Http/Authenticator/X509Authenticator.php b/src/Symfony/Component/Security/Http/Authenticator/X509Authenticator.php new file mode 100644 index 0000000000..d482579d05 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/X509Authenticator.php @@ -0,0 +1,61 @@ + + * + * 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; + +use Psr\Log\LoggerInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Exception\BadCredentialsException; +use Symfony\Component\Security\Core\User\UserProviderInterface; + +/** + * This authenticator authenticates pre-authenticated (by the + * webserver) X.509 certificates. + * + * @author Wouter de Jong + * @author Fabien Potencier + * + * @internal + * @experimental in Symfony 5.1 + */ +class X509Authenticator extends AbstractPreAuthenticatedAuthenticator +{ + private $userKey; + private $credentialsKey; + + public function __construct(UserProviderInterface $userProvider, TokenStorageInterface $tokenStorage, string $firewallName, string $userKey = 'SSL_CLIENT_S_DN_Email', string $credentialsKey = 'SSL_CLIENT_S_DN', ?LoggerInterface $logger = null) + { + parent::__construct($userProvider, $tokenStorage, $firewallName, $logger); + + $this->userKey = $userKey; + $this->credentialsKey = $credentialsKey; + } + + protected function extractUsername(Request $request): string + { + $username = null; + if ($request->server->has($this->userKey)) { + $username = $request->server->get($this->userKey); + } elseif ( + $request->server->has($this->credentialsKey) + && preg_match('#emailAddress=([^,/@]++@[^,/]++)#', $request->server->get($this->credentialsKey), $matches) + ) { + $username = $matches[1]; + } + + if (null === $username) { + throw new BadCredentialsException(sprintf('SSL credentials not found: %s, %s', $this->userKey, $this->credentialsKey)); + } + + return $username; + } +} diff --git a/src/Symfony/Component/Security/Http/EventListener/UserCheckerListener.php b/src/Symfony/Component/Security/Http/EventListener/UserCheckerListener.php index 8ebbaca709..34fdfdf84d 100644 --- a/src/Symfony/Component/Security/Http/EventListener/UserCheckerListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/UserCheckerListener.php @@ -4,6 +4,7 @@ namespace Symfony\Component\Security\Http\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Security\Core\User\UserCheckerInterface; +use Symfony\Component\Security\Http\Authenticator\AbstractPreAuthenticatedAuthenticator; use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; /** @@ -23,7 +24,7 @@ class UserCheckerListener implements EventSubscriberInterface public function preCredentialsVerification(VerifyAuthenticatorCredentialsEvent $event): void { - if (null === $event->getUser()) { + if (null === $event->getUser() || $event->getAuthenticator() instanceof AbstractPreAuthenticatedAuthenticator) { return; } diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/RemoteUserAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/RemoteUserAuthenticatorTest.php new file mode 100644 index 0000000000..80cddd1ddb --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/RemoteUserAuthenticatorTest.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\Authenticator; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; +use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Http\Authenticator\RemoteUserAuthenticator; + +class RemoteUserAuthenticatorTest extends TestCase +{ + /** + * @dataProvider provideAuthenticators + */ + public function testSupport(RemoteUserAuthenticator $authenticator, $parameterName) + { + $request = $this->createRequest([$parameterName => 'TheUsername']); + + $this->assertTrue($authenticator->supports($request)); + } + + public function testSupportNoUser() + { + $authenticator = new RemoteUserAuthenticator($this->createMock(UserProviderInterface::class), new TokenStorage(), 'main'); + + $this->assertFalse($authenticator->supports($this->createRequest([]))); + } + + /** + * @dataProvider provideAuthenticators + */ + public function testGetCredentials(RemoteUserAuthenticator $authenticator, $parameterName) + { + $request = $this->createRequest([$parameterName => 'TheUsername']); + + $authenticator->supports($request); + $this->assertEquals(['username' => 'TheUsername'], $authenticator->getCredentials($request)); + } + + public function provideAuthenticators() + { + $userProvider = $this->createMock(UserProviderInterface::class); + + yield [new RemoteUserAuthenticator($userProvider, new TokenStorage(), 'main'), 'REMOTE_USER']; + yield [new RemoteUserAuthenticator($userProvider, new TokenStorage(), 'main', 'CUSTOM_USER_PARAMETER'), 'CUSTOM_USER_PARAMETER']; + } + + private function createRequest(array $server) + { + return new Request([], [], [], [], [], $server); + } +} diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/X509AuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/X509AuthenticatorTest.php new file mode 100644 index 0000000000..e839504285 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/X509AuthenticatorTest.php @@ -0,0 +1,110 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\Authenticator; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; +use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Http\Authenticator\X509Authenticator; + +class X509AuthenticatorTest extends TestCase +{ + private $userProvider; + private $authenticator; + + protected function setUp(): void + { + $this->userProvider = $this->createMock(UserProviderInterface::class); + $this->authenticator = new X509Authenticator($this->userProvider, new TokenStorage(), 'main'); + } + + /** + * @dataProvider provideServerVars + */ + public function testAuthentication($user, $credentials) + { + $serverVars = []; + if ('' !== $user) { + $serverVars['SSL_CLIENT_S_DN_Email'] = $user; + } + if ('' !== $credentials) { + $serverVars['SSL_CLIENT_S_DN'] = $credentials; + } + + $request = $this->createRequest($serverVars); + $this->assertTrue($this->authenticator->supports($request)); + $this->assertEquals(['username' => $user], $this->authenticator->getCredentials($request)); + } + + public static function provideServerVars() + { + yield ['TheUser', 'TheCredentials']; + yield ['TheUser', '']; + } + + /** + * @dataProvider provideServerVarsNoUser + */ + public function testAuthenticationNoUser($emailAddress, $credentials) + { + $request = $this->createRequest(['SSL_CLIENT_S_DN' => $credentials]); + + $this->assertTrue($this->authenticator->supports($request)); + $this->assertEquals(['username' => $emailAddress], $this->authenticator->getCredentials($request)); + } + + public static function provideServerVarsNoUser() + { + yield ['cert@example.com', 'CN=Sample certificate DN/emailAddress=cert@example.com']; + yield ['cert+something@example.com', 'CN=Sample certificate DN/emailAddress=cert+something@example.com']; + yield ['cert@example.com', 'CN=Sample certificate DN,emailAddress=cert@example.com']; + yield ['cert+something@example.com', 'CN=Sample certificate DN,emailAddress=cert+something@example.com']; + yield ['cert+something@example.com', 'emailAddress=cert+something@example.com,CN=Sample certificate DN']; + yield ['cert+something@example.com', 'emailAddress=cert+something@example.com']; + yield ['firstname.lastname@mycompany.co.uk', 'emailAddress=firstname.lastname@mycompany.co.uk,CN=Firstname.Lastname,OU=london,OU=company design and engineering,OU=Issuer London,OU=Roaming,OU=Interactive,OU=Users,OU=Standard,OU=Business,DC=england,DC=core,DC=company,DC=co,DC=uk']; + } + + public function testSupportNoData() + { + $request = $this->createRequest([]); + + $this->assertFalse($this->authenticator->supports($request)); + } + + public function testAuthenticationCustomUserKey() + { + $authenticator = new X509Authenticator($this->userProvider, new TokenStorage(), 'main', 'TheUserKey'); + + $request = $this->createRequest([ + 'TheUserKey' => 'TheUser', + ]); + $this->assertTrue($authenticator->supports($request)); + $this->assertEquals(['username' => 'TheUser'], $authenticator->getCredentials($request)); + } + + public function testAuthenticationCustomCredentialsKey() + { + $authenticator = new X509Authenticator($this->userProvider, new TokenStorage(), 'main', 'SSL_CLIENT_S_DN_Email', 'TheCertKey'); + + $request = $this->createRequest([ + 'TheCertKey' => 'CN=Sample certificate DN/emailAddress=cert@example.com', + ]); + $this->assertTrue($authenticator->supports($request)); + $this->assertEquals(['username' => 'cert@example.com'], $authenticator->getCredentials($request)); + } + + private function createRequest(array $server) + { + return new Request([], [], [], [], [], $server); + } +} From 7ef6a7ab039c14bda5e3d4f5218eff39d8343959 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sat, 4 Apr 2020 17:37:52 +0200 Subject: [PATCH 26/30] Use the firewall event dispatcher --- .../Security/Factory/RememberMeFactory.php | 3 +- .../DependencyInjection/SecurityExtension.php | 8 +++-- .../FirewallEventBubblingListener.php | 6 ++++ .../config/security_authenticator.xml | 5 +-- .../Http/EventListener/RememberMeListener.php | 35 +++++-------------- .../EventListener/SessionStrategyListener.php | 7 ++-- 6 files changed, 26 insertions(+), 38 deletions(-) diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php index 979acc79dc..5f530a17e2 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php @@ -218,9 +218,8 @@ class RememberMeFactory implements SecurityFactoryInterface, AuthenticatorFactor { $container ->setDefinition('security.listener.remember_me.'.$id, new ChildDefinition('security.listener.remember_me')) - ->addTag('kernel.event_subscriber') + ->addTag('kernel.event_subscriber', ['dispatcher' => 'security.event_dispatcher.'.$id]) ->replaceArgument(0, new Reference($rememberMeServicesId)) - ->replaceArgument(1, $id) ; $container diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index e4ef468c88..35bcf01557 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -156,8 +156,11 @@ class SecurityExtension extends Extension implements PrependExtensionInterface ->replaceArgument(2, $this->statelessFirewallKeys); if ($this->authenticatorManagerEnabled) { - $container->getDefinition(SessionListener::class) - ->replaceArgument(1, $this->statelessFirewallKeys); + foreach ($this->statelessFirewallKeys as $statelessFirewallId) { + $container + ->setDefinition('security.listener.session.'.$statelessFirewallId, new ChildDefinition('security.listener.session')) + ->addTag('kernel.event_subscriber', ['dispatcher' => 'security.event_dispatcher.'.$statelessFirewallId]); + } } if ($config['encoders']) { @@ -446,6 +449,7 @@ class SecurityExtension extends Extension implements PrependExtensionInterface $container ->setDefinition($managerId = 'security.authenticator.manager.'.$id, new ChildDefinition('security.authenticator.manager')) ->replaceArgument(0, $authenticators) + ->replaceArgument(2, new Reference($firewallEventDispatcherId)) ->replaceArgument(3, $id) ->addTag('monolog.logger', ['channel' => 'security']) ; diff --git a/src/Symfony/Bundle/SecurityBundle/EventListener/FirewallEventBubblingListener.php b/src/Symfony/Bundle/SecurityBundle/EventListener/FirewallEventBubblingListener.php index c3415ccc8c..38f819c44f 100644 --- a/src/Symfony/Bundle/SecurityBundle/EventListener/FirewallEventBubblingListener.php +++ b/src/Symfony/Bundle/SecurityBundle/EventListener/FirewallEventBubblingListener.php @@ -12,7 +12,10 @@ namespace Symfony\Bundle\SecurityBundle\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Security\Http\Event\LoginFailureEvent; +use Symfony\Component\Security\Http\Event\LoginSuccessEvent; use Symfony\Component\Security\Http\Event\LogoutEvent; +use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** @@ -34,6 +37,9 @@ class FirewallEventBubblingListener implements EventSubscriberInterface { return [ LogoutEvent::class => 'bubbleEvent', + LoginFailureEvent::class => 'bubbleEvent', + LoginSuccessEvent::class => 'bubbleEvent', + VerifyAuthenticatorCredentialsEvent::class => 'bubbleEvent', ]; } diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml index 0ff79a0ebd..fc21f87e6c 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml @@ -59,8 +59,9 @@ - - + stateless firewall keys diff --git a/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php b/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php index 72ce7c13f9..269d232786 100644 --- a/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php @@ -26,26 +26,29 @@ use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; class RememberMeListener implements EventSubscriberInterface { private $rememberMeServices; - private $providerKey; private $logger; - public function __construct(RememberMeServicesInterface $rememberMeServices, string $providerKey, ?LoggerInterface $logger = null) + public function __construct(RememberMeServicesInterface $rememberMeServices, ?LoggerInterface $logger = null) { $this->rememberMeServices = $rememberMeServices; - $this->providerKey = $providerKey; $this->logger = $logger; } public function onSuccessfulLogin(LoginSuccessEvent $event): void { - if (!$this->isRememberMeEnabled($event->getProviderKey(), $event->getAuthenticator())) { + $authenticator = $event->getAuthenticator(); + if (!$authenticator instanceof RememberMeAuthenticatorInterface || !$authenticator->supportsRememberMe()) { + if (null !== $this->logger) { + $this->logger->debug('Remember me skipped: your authenticator does not support it.', ['authenticator' => \get_class($authenticator)]); + } + return; } if (null === $event->getResponse()) { if (null !== $this->logger) { - $this->logger->debug('Remember me skipped: the authenticator did not set a success response.', ['authenticator' => \get_class($event->getAuthenticator())]); + $this->logger->debug('Remember me skipped: the authenticator did not set a success response.', ['authenticator' => \get_class($authenticator)]); } return; @@ -56,31 +59,9 @@ class RememberMeListener implements EventSubscriberInterface public function onFailedLogin(LoginFailureEvent $event): void { - if (!$this->isRememberMeEnabled($event->getProviderKey())) { - return; - } - $this->rememberMeServices->loginFail($event->getRequest(), $event->getException()); } - private function isRememberMeEnabled(string $providerKey, ?AuthenticatorInterface $authenticator = null): bool - { - if ($providerKey !== $this->providerKey) { - // This listener is created for a different firewall. - return false; - } - - if (null !== $authenticator && (!$authenticator instanceof RememberMeAuthenticatorInterface || !$authenticator->supportsRememberMe())) { - if (null !== $this->logger) { - $this->logger->debug('Remember me skipped: your authenticator does not support it.', ['authenticator' => \get_class($authenticator)]); - } - - return false; - } - - return true; - } - public static function getSubscribedEvents(): array { return [ diff --git a/src/Symfony/Component/Security/Http/EventListener/SessionStrategyListener.php b/src/Symfony/Component/Security/Http/EventListener/SessionStrategyListener.php index 436d525a5a..492316ec63 100644 --- a/src/Symfony/Component/Security/Http/EventListener/SessionStrategyListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/SessionStrategyListener.php @@ -28,21 +28,18 @@ use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterfa class SessionStrategyListener implements EventSubscriberInterface { private $sessionAuthenticationStrategy; - private $statelessProviderKeys; - public function __construct(SessionAuthenticationStrategyInterface $sessionAuthenticationStrategy, array $statelessProviderKeys = []) + public function __construct(SessionAuthenticationStrategyInterface $sessionAuthenticationStrategy) { $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)) { + if (!$request->hasSession() || !$request->hasPreviousSession()) { return; } From 0fe5083a3e29af82418e33f3073137c921c5f707 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sun, 5 Apr 2020 13:12:09 +0200 Subject: [PATCH 27/30] Added JSON login authenticator --- .../Security/Factory/JsonLoginFactory.php | 16 +- .../config/security_authenticator.xml | 11 ++ .../Authenticator/JsonLoginAuthenticator.php | 146 ++++++++++++++++++ .../JsonLoginAuthenticatorTest.php | 127 +++++++++++++++ 4 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 src/Symfony/Component/Security/Http/Authenticator/JsonLoginAuthenticator.php create mode 100644 src/Symfony/Component/Security/Http/Tests/Authenticator/JsonLoginAuthenticatorTest.php diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/JsonLoginFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/JsonLoginFactory.php index f4b9adee93..4e09a3d2f8 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/JsonLoginFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/JsonLoginFactory.php @@ -20,7 +20,7 @@ use Symfony\Component\DependencyInjection\Reference; * * @author Kévin Dunglas */ -class JsonLoginFactory extends AbstractFactory +class JsonLoginFactory extends AbstractFactory implements AuthenticatorFactoryInterface { public function __construct() { @@ -96,4 +96,18 @@ class JsonLoginFactory extends AbstractFactory return $listenerId; } + + public function createAuthenticator(ContainerBuilder $container, string $id, array $config, string $userProviderId) + { + $authenticatorId = 'security.authenticator.json_login.'.$id; + $options = array_intersect_key($config, $this->options); + $container + ->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.json_login')) + ->replaceArgument(1, new Reference($userProviderId)) + ->replaceArgument(2, isset($config['success_handler']) ? new Reference($this->createAuthenticationSuccessHandler($container, $id, $config)) : null) + ->replaceArgument(3, isset($config['failure_handler']) ? new Reference($this->createAuthenticationFailureHandler($container, $id, $config)) : null) + ->replaceArgument(4, $options); + + return $authenticatorId; + } } diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml index fc21f87e6c..80e9e8e2b9 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml @@ -98,6 +98,17 @@ options + + + user provider + authentication success handler + authentication failure handler + options + + + diff --git a/src/Symfony/Component/Security/Http/Authenticator/JsonLoginAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/JsonLoginAuthenticator.php new file mode 100644 index 0000000000..f10e330923 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/JsonLoginAuthenticator.php @@ -0,0 +1,146 @@ + + * + * 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; + +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\PropertyAccess\Exception\AccessException; +use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; +use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Core\Exception\BadCredentialsException; +use Symfony\Component\Security\Core\Security; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; +use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; +use Symfony\Component\Security\Http\HttpUtils; + +/** + * Provides a stateless implementation of an authentication via + * a JSON document composed of a username and a password. + * + * @author Kévin Dunglas + * @author Wouter de Jong + * + * @final + * @experimental in 5.1 + */ +class JsonLoginAuthenticator implements InteractiveAuthenticatorInterface, PasswordAuthenticatedInterface +{ + private $options; + private $httpUtils; + private $userProvider; + private $propertyAccessor; + private $successHandler; + private $failureHandler; + + public function __construct(HttpUtils $httpUtils, UserProviderInterface $userProvider, ?AuthenticationSuccessHandlerInterface $successHandler = null, ?AuthenticationFailureHandlerInterface $failureHandler = null, array $options = [], ?PropertyAccessorInterface $propertyAccessor = null) + { + $this->options = array_merge(['username_path' => 'username', 'password_path' => 'password'], $options); + $this->httpUtils = $httpUtils; + $this->successHandler = $successHandler; + $this->failureHandler = $failureHandler; + $this->userProvider = $userProvider; + $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); + } + + public function supports(Request $request): ?bool + { + if (false === strpos($request->getRequestFormat(), 'json') && false === strpos($request->getContentType(), 'json')) { + return false; + } + + if (isset($this->options['check_path']) && !$this->httpUtils->checkRequestPath($request, $this->options['check_path'])) { + return false; + } + + return true; + } + + public function getCredentials(Request $request) + { + $data = json_decode($request->getContent()); + if (!$data instanceof \stdClass) { + throw new BadRequestHttpException('Invalid JSON.'); + } + + $credentials = []; + try { + $credentials['username'] = $this->propertyAccessor->getValue($data, $this->options['username_path']); + + if (!\is_string($credentials['username'])) { + throw new BadRequestHttpException(sprintf('The key "%s" must be a string.', $this->options['username_path'])); + } + + if (\strlen($credentials['username']) > Security::MAX_USERNAME_LENGTH) { + throw new BadCredentialsException('Invalid username.'); + } + } catch (AccessException $e) { + throw new BadRequestHttpException(sprintf('The key "%s" must be provided.', $this->options['username_path']), $e); + } + + try { + $credentials['password'] = $this->propertyAccessor->getValue($data, $this->options['password_path']); + + if (!\is_string($credentials['password'])) { + throw new BadRequestHttpException(sprintf('The key "%s" must be a string.', $this->options['password_path'])); + } + } catch (AccessException $e) { + throw new BadRequestHttpException(sprintf('The key "%s" must be provided.', $this->options['password_path']), $e); + } + + return $credentials; + } + + public function getUser($credentials): ?UserInterface + { + return $this->userProvider->loadUserByUsername($credentials['username']); + } + + public function getPassword($credentials): ?string + { + return $credentials['password']; + } + + public function createAuthenticatedToken(UserInterface $user, string $providerKey): TokenInterface + { + return new UsernamePasswordToken($user, null, $providerKey, $user->getRoles()); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response + { + if (null === $this->successHandler) { + return null; // let the original request continue + } + + return $this->successHandler->onAuthenticationSuccess($request, $token); + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response + { + if (null === $this->failureHandler) { + return new JsonResponse(['error' => $exception->getMessageKey()], JsonResponse::HTTP_UNAUTHORIZED); + } + + return $this->failureHandler->onAuthenticationFailure($request, $exception); + } + + public function isInteractive(): bool + { + return true; + } +} diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/JsonLoginAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/JsonLoginAuthenticatorTest.php new file mode 100644 index 0000000000..84ff61781f --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/JsonLoginAuthenticatorTest.php @@ -0,0 +1,127 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\Authenticator; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\Security\Core\Exception\BadCredentialsException; +use Symfony\Component\Security\Core\Security; +use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Http\Authenticator\JsonLoginAuthenticator; +use Symfony\Component\Security\Http\HttpUtils; + +class JsonLoginAuthenticatorTest extends TestCase +{ + private $userProvider; + /** @var JsonLoginAuthenticator */ + private $authenticator; + + protected function setUp(): void + { + $this->userProvider = $this->createMock(UserProviderInterface::class); + } + + /** + * @dataProvider provideSupportData + */ + public function testSupport($request) + { + $this->setUpAuthenticator(); + + $this->assertTrue($this->authenticator->supports($request)); + } + + public function provideSupportData() + { + yield [new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], '{"username": "dunglas", "password": "foo"}')]; + + $request = new Request([], [], [], [], [], [], '{"username": "dunglas", "password": "foo"}'); + $request->setRequestFormat('json-ld'); + yield [$request]; + } + + /** + * @dataProvider provideSupportsWithCheckPathData + */ + public function testSupportsWithCheckPath($request, $result) + { + $this->setUpAuthenticator(['check_path' => '/api/login']); + + $this->assertSame($result, $this->authenticator->supports($request)); + } + + public function provideSupportsWithCheckPathData() + { + yield [Request::create('/api/login', 'GET', [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json']), true]; + yield [Request::create('/login', 'GET', [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json']), false]; + } + + public function testGetCredentials() + { + $this->setUpAuthenticator(); + + $request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], '{"username": "dunglas", "password": "foo"}'); + $this->assertEquals(['username' => 'dunglas', 'password' => 'foo'], $this->authenticator->getCredentials($request)); + } + + public function testGetCredentialsCustomPath() + { + $this->setUpAuthenticator([ + 'username_path' => 'authentication.username', + 'password_path' => 'authentication.password', + ]); + + $request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], '{"authentication": {"username": "dunglas", "password": "foo"}}'); + $this->assertEquals(['username' => 'dunglas', 'password' => 'foo'], $this->authenticator->getCredentials($request)); + } + + /** + * @dataProvider provideInvalidGetCredentialsData + */ + public function testGetCredentialsInvalid($request, $errorMessage, $exceptionType = BadRequestHttpException::class) + { + $this->expectException($exceptionType); + $this->expectExceptionMessage($errorMessage); + + $this->setUpAuthenticator(); + + $this->authenticator->getCredentials($request); + } + + public function provideInvalidGetCredentialsData() + { + $request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json']); + yield [$request, 'Invalid JSON.']; + + $request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], '{"usr": "dunglas", "password": "foo"}'); + yield [$request, 'The key "username" must be provided']; + + $request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], '{"username": "dunglas", "pass": "foo"}'); + yield [$request, 'The key "password" must be provided']; + + $request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], '{"username": 1, "password": "foo"}'); + yield [$request, 'The key "username" must be a string.']; + + $request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], '{"username": "dunglas", "password": 1}'); + yield [$request, 'The key "password" must be a string.']; + + $username = str_repeat('x', Security::MAX_USERNAME_LENGTH + 1); + $request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], sprintf('{"username": "%s", "password": 1}', $username)); + yield [$request, 'Invalid username.', BadCredentialsException::class]; + } + + private function setUpAuthenticator(array $options = []) + { + $this->authenticator = new JsonLoginAuthenticator(new HttpUtils(), $this->userProvider, null, null, $options); + } +} From 9ea32c4ed3e4fa7f96538f697c4f75f32b44259c Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Mon, 6 Apr 2020 14:00:37 +0200 Subject: [PATCH 28/30] Also use authentication failure/success handlers in FormLoginAuthenticator --- .../Security/Factory/AbstractFactory.php | 1 + .../Security/Factory/FormLoginFactory.php | 7 ++- .../config/security_authenticator.xml | 2 + .../AbstractLoginFormAuthenticator.php | 6 +- .../Authenticator/FormLoginAuthenticator.php | 61 ++++++------------- .../FormLoginAuthenticatorTest.php | 8 ++- 6 files changed, 36 insertions(+), 49 deletions(-) diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php index b523467f23..a5d6f7e45e 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php @@ -30,6 +30,7 @@ abstract class AbstractFactory implements SecurityFactoryInterface 'check_path' => '/login_check', 'use_forward' => false, 'require_previous_session' => false, + 'login_path' => '/login', ]; protected $defaultSuccessHandlerOptions = [ diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php index 0fe2d995b3..962c68eb2b 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php @@ -100,12 +100,13 @@ class FormLoginFactory extends AbstractFactory implements AuthenticatorFactoryIn public function createAuthenticator(ContainerBuilder $container, string $id, array $config, string $userProviderId): string { $authenticatorId = 'security.authenticator.form_login.'.$id; - $defaultOptions = array_merge($this->defaultSuccessHandlerOptions, $this->options); - $options = array_merge($defaultOptions, array_intersect_key($config, $defaultOptions)); + $options = array_intersect_key($config, $this->options); $container ->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.form_login')) ->replaceArgument(1, new Reference($userProviderId)) - ->replaceArgument(2, $options); + ->replaceArgument(2, new Reference($this->createAuthenticationSuccessHandler($container, $id, $config))) + ->replaceArgument(3, new Reference($this->createAuthenticationFailureHandler($container, $id, $config))) + ->replaceArgument(4, $options); return $authenticatorId; } diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml index 80e9e8e2b9..07ca362b03 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml @@ -95,6 +95,8 @@ abstract="true"> user provider + authentication success handler + authentication failure handler options diff --git a/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php index 5e298418cb..69ded7b062 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php @@ -30,7 +30,7 @@ abstract class AbstractLoginFormAuthenticator extends AbstractAuthenticator impl /** * Return the URL to the login page. */ - abstract protected function getLoginUrl(): string; + abstract protected function getLoginUrl(Request $request): string; /** * Override to change what happens after a bad username/password is submitted. @@ -41,7 +41,7 @@ abstract class AbstractLoginFormAuthenticator extends AbstractAuthenticator impl $request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception); } - $url = $this->getLoginUrl(); + $url = $this->getLoginUrl($request); return new RedirectResponse($url); } @@ -52,7 +52,7 @@ abstract class AbstractLoginFormAuthenticator extends AbstractAuthenticator impl */ public function start(Request $request, AuthenticationException $authException = null): Response { - $url = $this->getLoginUrl(); + $url = $this->getLoginUrl($request); return new RedirectResponse($url); } diff --git a/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php index cd8c569c57..5aaf96437f 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php @@ -16,13 +16,15 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; +use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; +use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; use Symfony\Component\Security\Http\HttpUtils; use Symfony\Component\Security\Http\ParameterBagUtils; -use Symfony\Component\Security\Http\Util\TargetPathTrait; /** * @author Wouter de Jong @@ -33,34 +35,32 @@ use Symfony\Component\Security\Http\Util\TargetPathTrait; */ class FormLoginAuthenticator extends AbstractLoginFormAuthenticator implements PasswordAuthenticatedInterface, CsrfProtectedAuthenticatorInterface { - use TargetPathTrait; - - private $options; private $httpUtils; private $userProvider; + private $successHandler; + private $failureHandler; + private $options; - public function __construct(HttpUtils $httpUtils, UserProviderInterface $userProvider, array $options) + public function __construct(HttpUtils $httpUtils, UserProviderInterface $userProvider, AuthenticationSuccessHandlerInterface $successHandler, AuthenticationFailureHandlerInterface $failureHandler, array $options) { $this->httpUtils = $httpUtils; + $this->userProvider = $userProvider; + $this->successHandler = $successHandler; + $this->failureHandler = $failureHandler; $this->options = array_merge([ 'username_parameter' => '_username', 'password_parameter' => '_password', - 'csrf_parameter' => '_csrf_token', - 'csrf_token_id' => 'authenticate', + 'check_path' => '/login_check', 'post_only' => true, - 'always_use_default_target_path' => false, - 'default_target_path' => '/', - 'login_path' => '/login', - 'target_path_parameter' => '_target_path', - 'use_referer' => false, + 'csrf_parameter' => '_csrf_token', + 'csrf_token_id' => 'authenticate', ], $options); - $this->userProvider = $userProvider; } - protected function getLoginUrl(): string + protected function getLoginUrl(Request $request): string { - return $this->options['login_path']; + return $this->httpUtils->generateUri($request, $this->options['login_path']); } public function supports(Request $request): bool @@ -122,36 +122,13 @@ class FormLoginAuthenticator extends AbstractLoginFormAuthenticator implements P return new UsernamePasswordToken($user, null, $providerKey, $user->getRoles()); } - public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): Response + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response { - return $this->httpUtils->createRedirectResponse($request, $this->determineTargetUrl($request, $providerKey)); + return $this->successHandler->onAuthenticationSuccess($request, $token); } - private function determineTargetUrl(Request $request, string $providerKey) + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response { - if ($this->options['always_use_default_target_path']) { - return $this->options['default_target_path']; - } - - if ($targetUrl = ParameterBagUtils::getRequestParameterValue($request, $this->options['target_path_parameter'])) { - return $targetUrl; - } - - if ($targetUrl = $this->getTargetPath($request->getSession(), $providerKey)) { - $this->removeTargetPath($request->getSession(), $providerKey); - - return $targetUrl; - } - - if ($this->options['use_referer'] && $targetUrl = $request->headers->get('Referer')) { - if (false !== $pos = strpos($targetUrl, '?')) { - $targetUrl = substr($targetUrl, 0, $pos); - } - if ($targetUrl && $targetUrl !== $this->httpUtils->generateUri($request, $this->options['login_path'])) { - return $targetUrl; - } - } - - return $this->options['default_target_path']; + return $this->failureHandler->onAuthenticationFailure($request, $exception); } } diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/FormLoginAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/FormLoginAuthenticatorTest.php index 058508f25e..3012da746d 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/FormLoginAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/FormLoginAuthenticatorTest.php @@ -17,17 +17,23 @@ use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; +use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; use Symfony\Component\Security\Http\Authenticator\FormLoginAuthenticator; use Symfony\Component\Security\Http\HttpUtils; class FormLoginAuthenticatorTest extends TestCase { private $userProvider; + private $successHandler; + private $failureHandler; private $authenticator; protected function setUp(): void { $this->userProvider = $this->createMock(UserProviderInterface::class); + $this->successHandler = $this->createMock(AuthenticationSuccessHandlerInterface::class); + $this->failureHandler = $this->createMock(AuthenticationFailureHandlerInterface::class); } /** @@ -123,7 +129,7 @@ class FormLoginAuthenticatorTest extends TestCase private function setUpAuthenticator(array $options = []) { - $this->authenticator = new FormLoginAuthenticator(new HttpUtils(), $this->userProvider, $options); + $this->authenticator = new FormLoginAuthenticator(new HttpUtils(), $this->userProvider, $this->successHandler, $this->failureHandler, $options); } private function createSession() From 50224aa2859541ecff713e5bcfbdfd61d27932b7 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Thu, 9 Apr 2020 14:58:06 +0200 Subject: [PATCH 29/30] Introduce Passport & Badges to extend authenticators --- .../Security/Factory/FormLoginFactory.php | 10 ++ src/Symfony/Component/Security/CHANGELOG.md | 1 + .../Authentication/AuthenticatorManager.php | 85 ++++++------- .../Authenticator/AbstractAuthenticator.php | 12 +- .../AbstractLoginFormAuthenticator.php | 7 +- .../AbstractPreAuthenticatedAuthenticator.php | 34 ++---- .../Authenticator/AnonymousAuthenticator.php | 23 +--- .../Authenticator/AuthenticatorInterface.php | 62 ++++------ .../CsrfProtectedAuthenticatorInterface.php | 34 ------ .../CustomAuthenticatedInterface.php | 36 ------ .../Authenticator/FormLoginAuthenticator.php | 91 ++++++++------ .../Authenticator/HttpBasicAuthenticator.php | 45 ++++--- .../InteractiveAuthenticatorInterface.php | 4 - .../Authenticator/JsonLoginAuthenticator.php | 92 ++++++++------ .../Passport/AnonymousPassport.php | 25 ++++ .../Passport/Badge/BadgeInterface.php | 30 +++++ .../Passport/Badge/CsrfTokenBadge.php | 65 ++++++++++ .../Passport/Badge/PasswordUpgradeBadge.php | 63 ++++++++++ .../Badge/PreAuthenticatedUserBadge.php | 34 ++++++ .../Badge/RememberMeBadge.php} | 18 ++- .../Credentials/CredentialsInterface.php | 26 ++++ .../Credentials/CustomCredentials.php | 58 +++++++++ .../Credentials/PasswordCredentials.php | 59 +++++++++ .../Http/Authenticator/Passport/Passport.php | 50 ++++++++ .../Passport/PassportInterface.php | 51 ++++++++ .../Authenticator/Passport/PassportTrait.php | 55 +++++++++ .../Passport/SelfValidatingPassport.php | 34 ++++++ .../Passport/UserPassportInterface.php | 26 ++++ .../PasswordAuthenticatedInterface.php | 31 ----- .../Authenticator/RememberMeAuthenticator.php | 44 +++---- .../Authenticator/RemoteUserAuthenticator.php | 2 + .../TokenAuthenticatedInterface.php | 33 ----- .../Http/Authenticator/X509Authenticator.php | 2 +- .../Security/Http/Event/LoginSuccessEvent.php | 22 +++- .../VerifyAuthenticatorCredentialsEvent.php | 29 +---- .../EventListener/CsrfProtectionListener.php | 22 +++- .../PasswordMigratingListener.php | 35 +++--- .../Http/EventListener/RememberMeListener.php | 12 +- .../EventListener/UserCheckerListener.php | 22 ++-- ...VerifyAuthenticatorCredentialsListener.php | 61 +++++----- .../RememberMe/AbstractRememberMeServices.php | 5 - .../AuthenticatorManagerTest.php | 51 ++------ .../AnonymousAuthenticatorTest.php | 8 +- .../FormLoginAuthenticatorTest.php | 47 ++++++-- .../HttpBasicAuthenticatorTest.php | 41 ++++--- .../JsonLoginAuthenticatorTest.php | 24 ++-- .../RememberMeAuthenticatorTest.php | 26 ++-- .../RemoteUserAuthenticatorTest.php | 19 ++- .../Authenticator/X509AuthenticatorTest.php | 33 ++++- .../CsrfProtectionListenerTest.php | 32 +++-- .../PasswordMigratingListenerTest.php | 49 +++----- .../EventListener/RememberMeListenerTest.php | 30 ++--- ...st.php => SessionStrategyListenerTest.php} | 6 +- .../EventListener/UserCheckerListenerTest.php | 50 +++++--- ...fyAuthenticatorCredentialsListenerTest.php | 114 +++++------------- 55 files changed, 1198 insertions(+), 782 deletions(-) delete mode 100644 src/Symfony/Component/Security/Http/Authenticator/CsrfProtectedAuthenticatorInterface.php delete mode 100644 src/Symfony/Component/Security/Http/Authenticator/CustomAuthenticatedInterface.php create mode 100644 src/Symfony/Component/Security/Http/Authenticator/Passport/AnonymousPassport.php create mode 100644 src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/BadgeInterface.php create mode 100644 src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/CsrfTokenBadge.php create mode 100644 src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/PasswordUpgradeBadge.php create mode 100644 src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/PreAuthenticatedUserBadge.php rename src/Symfony/Component/Security/Http/Authenticator/{RememberMeAuthenticatorInterface.php => Passport/Badge/RememberMeBadge.php} (61%) create mode 100644 src/Symfony/Component/Security/Http/Authenticator/Passport/Credentials/CredentialsInterface.php create mode 100644 src/Symfony/Component/Security/Http/Authenticator/Passport/Credentials/CustomCredentials.php create mode 100644 src/Symfony/Component/Security/Http/Authenticator/Passport/Credentials/PasswordCredentials.php create mode 100644 src/Symfony/Component/Security/Http/Authenticator/Passport/Passport.php create mode 100644 src/Symfony/Component/Security/Http/Authenticator/Passport/PassportInterface.php create mode 100644 src/Symfony/Component/Security/Http/Authenticator/Passport/PassportTrait.php create mode 100644 src/Symfony/Component/Security/Http/Authenticator/Passport/SelfValidatingPassport.php create mode 100644 src/Symfony/Component/Security/Http/Authenticator/Passport/UserPassportInterface.php delete mode 100644 src/Symfony/Component/Security/Http/Authenticator/PasswordAuthenticatedInterface.php delete mode 100644 src/Symfony/Component/Security/Http/Authenticator/TokenAuthenticatedInterface.php rename src/Symfony/Component/Security/Http/Tests/EventListener/{SessionListenerTest.php => SessionStrategyListenerTest.php} (89%) diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php index 962c68eb2b..2edfb3ff34 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory; use Symfony\Component\Config\Definition\Builder\NodeDefinition; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; @@ -30,6 +31,7 @@ class FormLoginFactory extends AbstractFactory implements AuthenticatorFactoryIn $this->addOption('password_parameter', '_password'); $this->addOption('csrf_parameter', '_csrf_token'); $this->addOption('csrf_token_id', 'authenticate'); + $this->addOption('enable_csrf', false); $this->addOption('post_only', true); } @@ -61,6 +63,10 @@ class FormLoginFactory extends AbstractFactory implements AuthenticatorFactoryIn protected function createAuthProvider(ContainerBuilder $container, string $id, array $config, string $userProviderId) { + if ($config['enable_csrf'] ?? false) { + throw new InvalidConfigurationException('The "enable_csrf" option of "form_login" is only available when "security.enable_authenticator_manager" is set to "true", use "csrf_token_generator" instead.'); + } + $provider = 'security.authentication.provider.dao.'.$id; $container ->setDefinition($provider, new ChildDefinition('security.authentication.provider.dao')) @@ -99,6 +105,10 @@ class FormLoginFactory extends AbstractFactory implements AuthenticatorFactoryIn public function createAuthenticator(ContainerBuilder $container, string $id, array $config, string $userProviderId): string { + if (isset($config['csrf_token_generator'])) { + throw new InvalidConfigurationException('The "csrf_token_generator" option of "form_login" is only available when "security.enable_authenticator_manager" is set to "false", use "enable_csrf" instead.'); + } + $authenticatorId = 'security.authenticator.form_login.'.$id; $options = array_intersect_key($config, $this->options); $container diff --git a/src/Symfony/Component/Security/CHANGELOG.md b/src/Symfony/Component/Security/CHANGELOG.md index da0d2cb8aa..adf023eac3 100644 --- a/src/Symfony/Component/Security/CHANGELOG.md +++ b/src/Symfony/Component/Security/CHANGELOG.md @@ -9,6 +9,7 @@ CHANGELOG * Hash the persistent RememberMe token value in database. * Added `LogoutEvent` to allow custom logout listeners. * Deprecated `LogoutSuccessHandlerInterface` and `LogoutHandlerInterface` in favor of listening on the `LogoutEvent`. + * Added experimental new security using `Http\Authenticator\AuthenticatorInterface`, `Http\Authentication\AuthenticatorManager` and `Http\Firewall\AuthenticatorManagerListener`. 5.0.0 ----- diff --git a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php index 381195d833..36a9916105 100644 --- a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php +++ b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php @@ -19,11 +19,13 @@ 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\InteractiveAuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\AnonymousPassport; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; use Symfony\Component\Security\Http\Event\InteractiveLoginEvent; use Symfony\Component\Security\Http\Event\LoginFailureEvent; use Symfony\Component\Security\Http\Event\LoginSuccessEvent; @@ -60,13 +62,16 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent $this->eraseCredentials = $eraseCredentials; } - public function authenticateUser(UserInterface $user, AuthenticatorInterface $authenticator, Request $request): ?Response + /** + * @param BadgeInterface[] $badges Optionally, pass some Passport badges to use for the manual login + */ + public function authenticateUser(UserInterface $user, AuthenticatorInterface $authenticator, Request $request, array $badges = []): ?Response { // create an authenticated token for the User - $token = $authenticator->createAuthenticatedToken($user, $this->providerKey); + $token = $authenticator->createAuthenticatedToken($passport = new SelfValidatingPassport($user, $badges), $this->providerKey); // authenticate this in the system - return $this->handleAuthenticationSuccess($token, $request, $authenticator); + return $this->handleAuthenticationSuccess($token, $passport, $request, $authenticator); } public function supports(Request $request): ?bool @@ -133,7 +138,7 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent continue; } - $response = $this->executeAuthenticator($key, $authenticator, $request); + $response = $this->executeAuthenticator($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)]); @@ -146,29 +151,35 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent return null; } - private function executeAuthenticator(string $uniqueAuthenticatorKey, AuthenticatorInterface $authenticator, Request $request): ?Response + private function executeAuthenticator(AuthenticatorInterface $authenticator, Request $request): ?Response { try { - if (null !== $this->logger) { - $this->logger->debug('Calling getCredentials() on authenticator.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($authenticator)]); + // get the passport from the Authenticator + $passport = $authenticator->authenticate($request); + + // check the passport (e.g. password checking) + $event = new VerifyAuthenticatorCredentialsEvent($authenticator, $passport); + $this->eventDispatcher->dispatch($event); + + // check if all badges are resolved + $passport->checkIfCompletelyResolved(); + + // create the authenticated token + $authenticatedToken = $authenticator->createAuthenticatedToken($passport, $this->providerKey); + if (true === $this->eraseCredentials) { + $authenticatedToken->eraseCredentials(); } - // 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->eventDispatcher) { + $this->eventDispatcher->dispatch(new AuthenticationSuccessEvent($authenticatedToken), AuthenticationEvents::AUTHENTICATION_SUCCESS); } - // 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)]); + $this->logger->info('Authenticator successful!', ['token' => $authenticatedToken, 'authenticator' => \get_class($authenticator)]); } // success! (sets the token on the token storage, etc) - $response = $this->handleAuthenticationSuccess($token, $request, $authenticator); + $response = $this->handleAuthenticationSuccess($authenticatedToken, $passport, $request, $authenticator); if ($response instanceof Response) { return $response; } @@ -189,35 +200,7 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent } } - private function authenticateViaAuthenticator(AuthenticatorInterface $authenticator, $credentials): TokenInterface - { - // get the user from the Authenticator - $user = $authenticator->getUser($credentials); - if (null === $user) { - throw new UsernameNotFoundException(sprintf('Null returned from "%s::getUser()".', \get_class($authenticator))); - } - - $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))); - } - - // turn the UserInterface into a TokenInterface - $authenticatedToken = $authenticator->createAuthenticatedToken($user, $this->providerKey); - - if (true === $this->eraseCredentials) { - $authenticatedToken->eraseCredentials(); - } - - if (null !== $this->eventDispatcher) { - $this->eventDispatcher->dispatch(new AuthenticationSuccessEvent($authenticatedToken), AuthenticationEvents::AUTHENTICATION_SUCCESS); - } - - return $authenticatedToken; - } - - private function handleAuthenticationSuccess(TokenInterface $authenticatedToken, Request $request, AuthenticatorInterface $authenticator): ?Response + private function handleAuthenticationSuccess(TokenInterface $authenticatedToken, PassportInterface $passport, Request $request, AuthenticatorInterface $authenticator): ?Response { $this->tokenStorage->setToken($authenticatedToken); @@ -227,7 +210,11 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent $this->eventDispatcher->dispatch($loginEvent, SecurityEvents::INTERACTIVE_LOGIN); } - $this->eventDispatcher->dispatch($loginSuccessEvent = new LoginSuccessEvent($authenticator, $authenticatedToken, $request, $response, $this->providerKey)); + if ($passport instanceof AnonymousPassport) { + return $response; + } + + $this->eventDispatcher->dispatch($loginSuccessEvent = new LoginSuccessEvent($authenticator, $passport, $authenticatedToken, $request, $response, $this->firewallName)); return $loginSuccessEvent->getResponse(); } diff --git a/src/Symfony/Component/Security/Http/Authenticator/AbstractAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/AbstractAuthenticator.php index 3683827d12..51a49a3b17 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AbstractAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AbstractAuthenticator.php @@ -12,7 +12,9 @@ namespace Symfony\Component\Security\Http\Authenticator; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Core\Exception\LogicException; +use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\UserPassportInterface; use Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken; /** @@ -30,8 +32,12 @@ abstract class AbstractAuthenticator implements AuthenticatorInterface * * @return PostAuthenticationToken */ - public function createAuthenticatedToken(UserInterface $user, string $providerKey): TokenInterface + public function createAuthenticatedToken(PassportInterface $passport, string $providerKey): TokenInterface { - return new PostAuthenticationToken($user, $providerKey, $user->getRoles()); + if (!$passport instanceof UserPassportInterface) { + throw new LogicException(sprintf('Passport does not contain a user, overwrite "createAuthenticatedToken()" in "%s" to create a custom authenticated token.', \get_class($this))); + } + + return new PostAuthenticationToken($passport->getUser(), $providerKey, $passport->getUser()->getRoles()); } } diff --git a/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php index 69ded7b062..f45fb3d074 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php @@ -25,7 +25,7 @@ use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface * * @experimental in 5.1 */ -abstract class AbstractLoginFormAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface, RememberMeAuthenticatorInterface, InteractiveAuthenticatorInterface +abstract class AbstractLoginFormAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface, InteractiveAuthenticatorInterface { /** * Return the URL to the login page. @@ -57,11 +57,6 @@ abstract class AbstractLoginFormAuthenticator extends AbstractAuthenticator impl return new RedirectResponse($url); } - public function supportsRememberMe(): bool - { - return true; - } - public function isInteractive(): bool { return true; diff --git a/src/Symfony/Component/Security/Http/Authenticator/AbstractPreAuthenticatedAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/AbstractPreAuthenticatedAuthenticator.php index b3a02bf1bd..435de68e98 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AbstractPreAuthenticatedAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AbstractPreAuthenticatedAuthenticator.php @@ -19,8 +19,10 @@ 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\Core\Exception\BadCredentialsException; -use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PreAuthenticatedUserBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; /** * The base authenticator for authenticators to use pre-authenticated @@ -32,7 +34,7 @@ use Symfony\Component\Security\Core\User\UserProviderInterface; * @internal * @experimental in Symfony 5.1 */ -abstract class AbstractPreAuthenticatedAuthenticator implements InteractiveAuthenticatorInterface, CustomAuthenticatedInterface +abstract class AbstractPreAuthenticatedAuthenticator implements InteractiveAuthenticatorInterface { private $userProvider; private $tokenStorage; @@ -63,7 +65,7 @@ abstract class AbstractPreAuthenticatedAuthenticator implements InteractiveAuthe $this->clearToken($e); if (null !== $this->logger) { - $this->logger->debug('Skipping pre-authenticated authenticator as a BadCredentialsException is thrown.', ['exception' => $e, 'authenticator' => \get_class($this)]); + $this->logger->debug('Skipping pre-authenticated authenticator as a BadCredentialsException is thrown.', ['exception' => $e, 'authenticator' => static::class]); } return false; @@ -71,7 +73,7 @@ abstract class AbstractPreAuthenticatedAuthenticator implements InteractiveAuthe if (null === $username) { if (null !== $this->logger) { - $this->logger->debug('Skipping pre-authenticated authenticator no username could be extracted.', ['authenticator' => \get_class($this)]); + $this->logger->debug('Skipping pre-authenticated authenticator no username could be extracted.', ['authenticator' => static::class]); } return false; @@ -82,27 +84,17 @@ abstract class AbstractPreAuthenticatedAuthenticator implements InteractiveAuthe return true; } - public function getCredentials(Request $request) + public function authenticate(Request $request): PassportInterface { - return [ - 'username' => $request->attributes->get('_pre_authenticated_username'), - ]; + $username = $request->attributes->get('_pre_authenticated_username'); + $user = $this->userProvider->loadUserByUsername($username); + + return new SelfValidatingPassport($user, [new PreAuthenticatedUserBadge()]); } - public function getUser($credentials): ?UserInterface + public function createAuthenticatedToken(PassportInterface $passport, string $providerKey): TokenInterface { - return $this->userProvider->loadUserByUsername($credentials['username']); - } - - public function checkCredentials($credentials, UserInterface $user): bool - { - // the user is already authenticated before it entered Symfony - return true; - } - - public function createAuthenticatedToken(UserInterface $user, string $providerKey): TokenInterface - { - return new PreAuthenticatedToken($user, null, $providerKey); + return new PreAuthenticatedToken($passport->getUser(), null, $providerKey, $passport->getUser()->getRoles()); } public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response diff --git a/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php index 4b6214668c..27a315b0f5 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php @@ -17,8 +17,8 @@ use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken; 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\User; -use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\AnonymousPassport; +use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; /** * @author Wouter de Jong @@ -27,7 +27,7 @@ use Symfony\Component\Security\Core\User\UserInterface; * @final * @experimental in 5.1 */ -class AnonymousAuthenticator implements AuthenticatorInterface, CustomAuthenticatedInterface +class AnonymousAuthenticator implements AuthenticatorInterface { private $secret; private $tokenStorage; @@ -45,23 +45,12 @@ class AnonymousAuthenticator implements AuthenticatorInterface, CustomAuthentica return null === $this->tokenStorage->getToken() ? null : false; } - public function getCredentials(Request $request) + public function authenticate(Request $request): PassportInterface { - return []; + return new AnonymousPassport(); } - public function checkCredentials($credentials, UserInterface $user): bool - { - // anonymous users do not have credentials - return true; - } - - public function getUser($credentials): ?UserInterface - { - return new User('anon.', null); - } - - public function createAuthenticatedToken(UserInterface $user, string $providerKey): TokenInterface + public function createAuthenticatedToken(PassportInterface $passport, string $providerKey): TokenInterface { return new AnonymousToken($this->secret, 'anon.', []); } diff --git a/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php b/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php index 0f1053e109..d80356e713 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php @@ -15,7 +15,7 @@ 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\Http\Authenticator\Passport\PassportInterface; /** * The interface for all authenticators. @@ -38,39 +38,19 @@ interface AuthenticatorInterface public function supports(Request $request): ?bool; /** - * Get the authentication credentials from the request and return them - * as any type (e.g. an associate array). + * Create a passport for the current request. * - * Whatever value you return here will be passed to getUser() and checkCredentials() + * The passport contains the user, credentials and any additional information + * that has to be checked by the Symfony Security system. For example, a login + * form authenticator will probably return a passport containing the user, the + * presented password and the CSRF token value. * - * For example, for a form login, you might: - * - * return [ - * 'username' => $request->request->get('_username'), - * 'password' => $request->request->get('_password'), - * ]; - * - * Or for an API token that's on a header, you might use: - * - * return ['api_key' => $request->headers->get('X-API-TOKEN')]; - * - * @return mixed Any non-null value - * - * @throws \UnexpectedValueException If null is returned - */ - public function getCredentials(Request $request); - - /** - * Return a UserInterface object based on the credentials. - * - * You may throw an AuthenticationException if you wish. If you return - * null, then a UsernameNotFoundException is thrown for you. - * - * @param mixed $credentials the value returned from getCredentials() + * You may throw any AuthenticationException in this method in case of error (e.g. + * a UsernameNotFoundException when the user cannot be found). * * @throws AuthenticationException */ - public function getUser($credentials): ?UserInterface; + public function authenticate(Request $request): PassportInterface; /** * Create an authenticated token for the given user. @@ -80,19 +60,10 @@ interface AuthenticatorInterface * the AbstractAuthenticator class from your authenticator. * * @see AbstractAuthenticator - */ - public function createAuthenticatedToken(UserInterface $user, string $providerKey): TokenInterface; - - /** - * 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 PassportInterface $passport The passport returned from authenticate() */ - public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response; + public function createAuthenticatedToken(PassportInterface $passport, string $providerKey): TokenInterface; /** * Called when authentication executed and was successful! @@ -104,4 +75,15 @@ interface AuthenticatorInterface * will be authenticated. This makes sense, for example, with an API. */ public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response; + + /** + * 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. + */ + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response; } diff --git a/src/Symfony/Component/Security/Http/Authenticator/CsrfProtectedAuthenticatorInterface.php b/src/Symfony/Component/Security/Http/Authenticator/CsrfProtectedAuthenticatorInterface.php deleted file mode 100644 index 0f93ad1e86..0000000000 --- a/src/Symfony/Component/Security/Http/Authenticator/CsrfProtectedAuthenticatorInterface.php +++ /dev/null @@ -1,34 +0,0 @@ - - * - * 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; - -/** - * This interface can be implemented to automatically add CSF - * protection to the authenticator. - * - * @author Wouter de Jong - */ -interface CsrfProtectedAuthenticatorInterface -{ - /** - * An arbitrary string used to generate the value of the CSRF token. - * Using a different string for each authenticator improves its security. - */ - public function getCsrfTokenId(): string; - - /** - * Returns the CSRF token contained in credentials if any. - * - * @param mixed $credentials the credentials returned by getCredentials() - */ - public function getCsrfToken($credentials): ?string; -} diff --git a/src/Symfony/Component/Security/Http/Authenticator/CustomAuthenticatedInterface.php b/src/Symfony/Component/Security/Http/Authenticator/CustomAuthenticatedInterface.php deleted file mode 100644 index 79b995e55f..0000000000 --- a/src/Symfony/Component/Security/Http/Authenticator/CustomAuthenticatedInterface.php +++ /dev/null @@ -1,36 +0,0 @@ - - * - * 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; - -use Symfony\Component\Security\Core\Exception\AuthenticationException; -use Symfony\Component\Security\Core\User\UserInterface; - -/** - * This interface should be implemented by authenticators that - * require custom (not password related) authentication. - * - * @author Wouter de Jong - */ -interface CustomAuthenticatedInterface -{ - /** - * Returns true if the credentials are valid. - * - * If false is returned, authentication will fail. You may also throw - * an AuthenticationException if you wish to cause authentication to fail. - * - * @param mixed $credentials the value returned from getCredentials() - * - * @throws AuthenticationException - */ - public function checkCredentials($credentials, UserInterface $user): bool; -} diff --git a/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php index 5aaf96437f..0bbbb6eb83 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php @@ -17,12 +17,20 @@ use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Core\Exception\AuthenticationServiceException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Security; +use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; +use Symfony\Component\Security\Http\Authenticator\Passport\Passport; +use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; use Symfony\Component\Security\Http\HttpUtils; use Symfony\Component\Security\Http\ParameterBagUtils; @@ -33,7 +41,7 @@ use Symfony\Component\Security\Http\ParameterBagUtils; * @final * @experimental in 5.1 */ -class FormLoginAuthenticator extends AbstractLoginFormAuthenticator implements PasswordAuthenticatedInterface, CsrfProtectedAuthenticatorInterface +class FormLoginAuthenticator extends AbstractLoginFormAuthenticator { private $httpUtils; private $userProvider; @@ -52,7 +60,7 @@ class FormLoginAuthenticator extends AbstractLoginFormAuthenticator implements P 'password_parameter' => '_password', 'check_path' => '/login_check', 'post_only' => true, - + 'enable_csrf' => false, 'csrf_parameter' => '_csrf_token', 'csrf_token_id' => 'authenticate', ], $options); @@ -69,17 +77,55 @@ class FormLoginAuthenticator extends AbstractLoginFormAuthenticator implements P && $this->httpUtils->checkRequestPath($request, $this->options['check_path']); } - public function getCredentials(Request $request): array + public function authenticate(Request $request): PassportInterface + { + $credentials = $this->getCredentials($request); + $user = $this->userProvider->loadUserByUsername($credentials['username']); + if (!$user instanceof UserInterface) { + throw new AuthenticationServiceException('The user provider must return a UserInterface object.'); + } + + $passport = new Passport($user, new PasswordCredentials($credentials['password']), [new RememberMeBadge()]); + if ($this->options['enable_csrf']) { + $passport->addBadge(new CsrfTokenBadge($this->options['csrf_token_id'], $credentials['csrf_token'])); + } + + if ($this->userProvider instanceof PasswordUpgraderInterface) { + $passport->addBadge(new PasswordUpgradeBadge($credentials['password'], $this->userProvider)); + } + + return $passport; + } + + /** + * @param Passport $passport + */ + public function createAuthenticatedToken(PassportInterface $passport, $providerKey): TokenInterface + { + return new UsernamePasswordToken($passport->getUser(), null, $providerKey, $passport->getUser()->getRoles()); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response + { + return $this->successHandler->onAuthenticationSuccess($request, $token); + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response + { + return $this->failureHandler->onAuthenticationFailure($request, $exception); + } + + private function getCredentials(Request $request): array { $credentials = []; $credentials['csrf_token'] = ParameterBagUtils::getRequestParameterValue($request, $this->options['csrf_parameter']); if ($this->options['post_only']) { $credentials['username'] = ParameterBagUtils::getParameterBagValue($request->request, $this->options['username_parameter']); - $credentials['password'] = ParameterBagUtils::getParameterBagValue($request->request, $this->options['password_parameter']); + $credentials['password'] = ParameterBagUtils::getParameterBagValue($request->request, $this->options['password_parameter']) ?? ''; } else { $credentials['username'] = ParameterBagUtils::getRequestParameterValue($request, $this->options['username_parameter']); - $credentials['password'] = ParameterBagUtils::getRequestParameterValue($request, $this->options['password_parameter']); + $credentials['password'] = ParameterBagUtils::getRequestParameterValue($request, $this->options['password_parameter']) ?? ''; } if (!\is_string($credentials['username']) && (!\is_object($credentials['username']) || !method_exists($credentials['username'], '__toString'))) { @@ -96,39 +142,4 @@ class FormLoginAuthenticator extends AbstractLoginFormAuthenticator implements P return $credentials; } - - public function getPassword($credentials): ?string - { - return $credentials['password']; - } - - public function getUser($credentials): ?UserInterface - { - return $this->userProvider->loadUserByUsername($credentials['username']); - } - - public function getCsrfTokenId(): string - { - return $this->options['csrf_token_id']; - } - - public function getCsrfToken($credentials): ?string - { - return $credentials['csrf_token']; - } - - public function createAuthenticatedToken(UserInterface $user, $providerKey): TokenInterface - { - return new UsernamePasswordToken($user, null, $providerKey, $user->getRoles()); - } - - public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response - { - return $this->successHandler->onAuthenticationSuccess($request, $token); - } - - public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response - { - return $this->failureHandler->onAuthenticationFailure($request, $exception); - } } diff --git a/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php index 77480eea45..46eb6aa7bc 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php @@ -17,8 +17,14 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Core\Exception\AuthenticationServiceException; +use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; +use Symfony\Component\Security\Http\Authenticator\Passport\Passport; +use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface; /** @@ -28,7 +34,7 @@ use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface * @final * @experimental in 5.1 */ -class HttpBasicAuthenticator implements AuthenticatorInterface, AuthenticationEntryPointInterface, PasswordAuthenticatedInterface +class HttpBasicAuthenticator implements AuthenticatorInterface, AuthenticationEntryPointInterface { private $realmName; private $userProvider; @@ -55,27 +61,30 @@ class HttpBasicAuthenticator implements AuthenticatorInterface, AuthenticationEn return $request->headers->has('PHP_AUTH_USER'); } - public function getCredentials(Request $request) + public function authenticate(Request $request): PassportInterface { - return [ - 'username' => $request->headers->get('PHP_AUTH_USER'), - 'password' => $request->headers->get('PHP_AUTH_PW', ''), - ]; + $username = $request->headers->get('PHP_AUTH_USER'); + $password = $request->headers->get('PHP_AUTH_PW', ''); + + $user = $this->userProvider->loadUserByUsername($username); + if (!$user instanceof UserInterface) { + throw new AuthenticationServiceException('The user provider must return a UserInterface object.'); + } + + $passport = new Passport($user, new PasswordCredentials($password)); + if ($this->userProvider instanceof PasswordUpgraderInterface) { + $passport->addBadge(new PasswordUpgradeBadge($password, $this->userProvider)); + } + + return $passport; } - public function getPassword($credentials): ?string + /** + * @param Passport $passport + */ + public function createAuthenticatedToken(PassportInterface $passport, $providerKey): TokenInterface { - return $credentials['password']; - } - - public function getUser($credentials): ?UserInterface - { - return $this->userProvider->loadUserByUsername($credentials['username']); - } - - public function createAuthenticatedToken(UserInterface $user, $providerKey): TokenInterface - { - return new UsernamePasswordToken($user, null, $providerKey, $user->getRoles()); + return new UsernamePasswordToken($passport->getUser(), null, $providerKey, $passport->getUser()->getRoles()); } public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey): ?Response diff --git a/src/Symfony/Component/Security/Http/Authenticator/InteractiveAuthenticatorInterface.php b/src/Symfony/Component/Security/Http/Authenticator/InteractiveAuthenticatorInterface.php index a2abf96e4a..7f26d82606 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/InteractiveAuthenticatorInterface.php +++ b/src/Symfony/Component/Security/Http/Authenticator/InteractiveAuthenticatorInterface.php @@ -11,10 +11,6 @@ namespace Symfony\Component\Security\Http\Authenticator; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; - /** * This is an extension of the authenticator interface that must * be used by interactive authenticators. diff --git a/src/Symfony/Component/Security/Http/Authenticator/JsonLoginAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/JsonLoginAuthenticator.php index f10e330923..924ed7fcca 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/JsonLoginAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/JsonLoginAuthenticator.php @@ -21,12 +21,18 @@ use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Core\Exception\AuthenticationServiceException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Security; +use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; +use Symfony\Component\Security\Http\Authenticator\Passport\Passport; +use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; use Symfony\Component\Security\Http\HttpUtils; /** @@ -39,7 +45,7 @@ use Symfony\Component\Security\Http\HttpUtils; * @final * @experimental in 5.1 */ -class JsonLoginAuthenticator implements InteractiveAuthenticatorInterface, PasswordAuthenticatedInterface +class JsonLoginAuthenticator implements InteractiveAuthenticatorInterface { private $options; private $httpUtils; @@ -71,7 +77,51 @@ class JsonLoginAuthenticator implements InteractiveAuthenticatorInterface, Passw return true; } - public function getCredentials(Request $request) + public function authenticate(Request $request): PassportInterface + { + $credentials = $this->getCredentials($request); + $user = $this->userProvider->loadUserByUsername($credentials['username']); + if (!$user instanceof UserInterface) { + throw new AuthenticationServiceException('The user provider must return a UserInterface object.'); + } + + $passport = new Passport($user, new PasswordCredentials($credentials['password'])); + if ($this->userProvider instanceof PasswordUpgraderInterface) { + $passport->addBadge(new PasswordUpgradeBadge($credentials['password'], $this->userProvider)); + } + + return $passport; + } + + public function createAuthenticatedToken(PassportInterface $passport, string $providerKey): TokenInterface + { + return new UsernamePasswordToken($passport->getUser(), null, $providerKey, $passport->getUser()->getRoles()); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response + { + if (null === $this->successHandler) { + return null; // let the original request continue + } + + return $this->successHandler->onAuthenticationSuccess($request, $token); + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response + { + if (null === $this->failureHandler) { + return new JsonResponse(['error' => $exception->getMessageKey()], JsonResponse::HTTP_UNAUTHORIZED); + } + + return $this->failureHandler->onAuthenticationFailure($request, $exception); + } + + public function isInteractive(): bool + { + return true; + } + + private function getCredentials(Request $request) { $data = json_decode($request->getContent()); if (!$data instanceof \stdClass) { @@ -105,42 +155,4 @@ class JsonLoginAuthenticator implements InteractiveAuthenticatorInterface, Passw return $credentials; } - - public function getUser($credentials): ?UserInterface - { - return $this->userProvider->loadUserByUsername($credentials['username']); - } - - public function getPassword($credentials): ?string - { - return $credentials['password']; - } - - public function createAuthenticatedToken(UserInterface $user, string $providerKey): TokenInterface - { - return new UsernamePasswordToken($user, null, $providerKey, $user->getRoles()); - } - - public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response - { - if (null === $this->successHandler) { - return null; // let the original request continue - } - - return $this->successHandler->onAuthenticationSuccess($request, $token); - } - - public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response - { - if (null === $this->failureHandler) { - return new JsonResponse(['error' => $exception->getMessageKey()], JsonResponse::HTTP_UNAUTHORIZED); - } - - return $this->failureHandler->onAuthenticationFailure($request, $exception); - } - - public function isInteractive(): bool - { - return true; - } } diff --git a/src/Symfony/Component/Security/Http/Authenticator/Passport/AnonymousPassport.php b/src/Symfony/Component/Security/Http/Authenticator/Passport/AnonymousPassport.php new file mode 100644 index 0000000000..7cbc93e658 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/Passport/AnonymousPassport.php @@ -0,0 +1,25 @@ + + * + * 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\Passport; + +/** + * A passport used during anonymous authentication. + * + * @author Wouter de Jong + * + * @internal + * @experimental in 5.1 + */ +class AnonymousPassport implements PassportInterface +{ + use PassportTrait; +} diff --git a/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/BadgeInterface.php b/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/BadgeInterface.php new file mode 100644 index 0000000000..bc9ba7cbb5 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/BadgeInterface.php @@ -0,0 +1,30 @@ + + * + * 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\Passport\Badge; + +/** + * Passport badges allow to add more information to a passport (e.g. a CSRF token). + * + * @author Wouter de Jong + * + * @experimental in 5.1 + */ +interface BadgeInterface +{ + /** + * Checks if this badge is resolved by the security system. + * + * After authentication, all badges must return `true` in this method in order + * for the authentication to succeed. + */ + public function isResolved(): bool; +} diff --git a/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/CsrfTokenBadge.php b/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/CsrfTokenBadge.php new file mode 100644 index 0000000000..9f0b4e5d89 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/CsrfTokenBadge.php @@ -0,0 +1,65 @@ + + * + * 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\Passport\Badge; + +use Symfony\Component\Security\Http\EventListener\CsrfProtectionListener; + +/** + * Adds automatic CSRF tokens checking capabilities to this authenticator. + * + * @see CsrfProtectionListener + * + * @author Wouter de Jong + * + * @final + * @experimental in5.1 + */ +class CsrfTokenBadge implements BadgeInterface +{ + private $resolved = false; + private $csrfTokenId; + private $csrfToken; + + /** + * @param string $csrfTokenId An arbitrary string used to generate the value of the CSRF token. + * Using a different string for each authenticator improves its security. + * @param string|null $csrfToken The CSRF token presented in the request, if any + */ + public function __construct(string $csrfTokenId, ?string $csrfToken) + { + $this->csrfTokenId = $csrfTokenId; + $this->csrfToken = $csrfToken; + } + + public function getCsrfTokenId(): string + { + return $this->csrfTokenId; + } + + public function getCsrfToken(): string + { + return $this->csrfToken; + } + + /** + * @internal + */ + public function markResolved(): void + { + $this->resolved = true; + } + + public function isResolved(): bool + { + return $this->resolved; + } +} diff --git a/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/PasswordUpgradeBadge.php b/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/PasswordUpgradeBadge.php new file mode 100644 index 0000000000..3812871da0 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/PasswordUpgradeBadge.php @@ -0,0 +1,63 @@ + + * + * 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\Passport\Badge; + +use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; + +/** + * Adds automatic password migration, if enabled and required in the password encoder. + * + * @see PasswordUpgraderInterface + * + * @author Wouter de Jong + * + * @final + * @experimental in 5.1 + */ +class PasswordUpgradeBadge implements BadgeInterface +{ + private $plaintextPassword; + private $passwordUpgrader; + + /** + * @param string $plaintextPassword The presented password, used in the rehash + * @param PasswordUpgraderInterface $passwordUpgrader The password upgrader, usually the UserProvider + */ + public function __construct(string $plaintextPassword, PasswordUpgraderInterface $passwordUpgrader) + { + $this->plaintextPassword = $plaintextPassword; + $this->passwordUpgrader = $passwordUpgrader; + } + + public function getPlaintextPassword(): string + { + return $this->plaintextPassword; + } + + public function getPasswordUpgrader(): PasswordUpgraderInterface + { + return $this->passwordUpgrader; + } + + /** + * @internal + */ + public function eraseCredentials() + { + $this->plaintextPassword = null; + } + + public function isResolved(): bool + { + return true; + } +} diff --git a/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/PreAuthenticatedUserBadge.php b/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/PreAuthenticatedUserBadge.php new file mode 100644 index 0000000000..7e0f330091 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/PreAuthenticatedUserBadge.php @@ -0,0 +1,34 @@ + + * + * 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\Passport\Badge; + +use Symfony\Component\Security\Http\Authenticator\AbstractPreAuthenticatedAuthenticator; + +/** + * Marks the authentication as being pre-authenticated. + * + * This disables pre-authentication user checkers. + * + * @see AbstractPreAuthenticatedAuthenticator + * + * @author Wouter de Jong + * + * @final + * @experimental in 5.1 + */ +class PreAuthenticatedUserBadge implements BadgeInterface +{ + public function isResolved(): bool + { + return true; + } +} diff --git a/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticatorInterface.php b/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/RememberMeBadge.php similarity index 61% rename from src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticatorInterface.php rename to src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/RememberMeBadge.php index d9eb6fa70b..dcee820442 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticatorInterface.php +++ b/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/RememberMeBadge.php @@ -9,23 +9,29 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Http\Authenticator; +namespace Symfony\Component\Security\Http\Authenticator\Passport\Badge; /** - * This interface must be extended if the authenticator supports remember me functionality. + * Adds support for remember me to this authenticator. * * Remember me cookie will be set if *all* of the following are met: - * A) SupportsRememberMe() returns true in the successful authenticator + * A) This badge is present in the Passport * 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 - * D) The onAuthenticationSuccess method returns a Response object + * D) The authentication process returns a success Response object * * @author Wouter de Jong + * + * @final + * @experimental in 5.1 */ -interface RememberMeAuthenticatorInterface +class RememberMeBadge implements BadgeInterface { - public function supportsRememberMe(): bool; + public function isResolved(): bool + { + return true; // remember me does not need to be explicitly resolved + } } diff --git a/src/Symfony/Component/Security/Http/Authenticator/Passport/Credentials/CredentialsInterface.php b/src/Symfony/Component/Security/Http/Authenticator/Passport/Credentials/CredentialsInterface.php new file mode 100644 index 0000000000..554fe7aff4 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/Passport/Credentials/CredentialsInterface.php @@ -0,0 +1,26 @@ + + * + * 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\Passport\Credentials; + +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface; + +/** + * Credentials are a special badge used to explicitly mark the + * credential check of an authenticator. + * + * @author Wouter de Jong + * + * @experimental in 5.1 + */ +interface CredentialsInterface extends BadgeInterface +{ +} diff --git a/src/Symfony/Component/Security/Http/Authenticator/Passport/Credentials/CustomCredentials.php b/src/Symfony/Component/Security/Http/Authenticator/Passport/Credentials/CustomCredentials.php new file mode 100644 index 0000000000..1a773f8afb --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/Passport/Credentials/CustomCredentials.php @@ -0,0 +1,58 @@ + + * + * 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\Passport\Credentials; + +use Symfony\Component\Security\Core\Exception\BadCredentialsException; +use Symfony\Component\Security\Core\User\UserInterface; + +/** + * Implements credentials checking using a custom checker function. + * + * @author Wouter de Jong + * + * @final + * @experimental in 5.1 + */ +class CustomCredentials implements CredentialsInterface +{ + private $customCredentialsChecker; + private $credentials; + private $resolved = false; + + /** + * @param callable $customCredentialsChecker the check function. If this function does not return `true`, a + * BadCredentialsException is thrown. You may also throw a more + * specific exception in the function. + * @param $credentials + */ + public function __construct(callable $customCredentialsChecker, $credentials) + { + $this->customCredentialsChecker = $customCredentialsChecker; + $this->credentials = $credentials; + } + + public function executeCustomChecker(UserInterface $user): void + { + $checker = $this->customCredentialsChecker; + + if (true !== $checker($this->credentials, $user)) { + throw new BadCredentialsException('Credentials check failed as the callable passed to CustomCredentials did not return "true".'); + } + + $this->resolved = true; + } + + public function isResolved(): bool + { + return $this->resolved; + } +} diff --git a/src/Symfony/Component/Security/Http/Authenticator/Passport/Credentials/PasswordCredentials.php b/src/Symfony/Component/Security/Http/Authenticator/Passport/Credentials/PasswordCredentials.php new file mode 100644 index 0000000000..7630a67bd7 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/Passport/Credentials/PasswordCredentials.php @@ -0,0 +1,59 @@ + + * + * 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\Passport\Credentials; + +use Symfony\Component\Security\Core\Exception\LogicException; + +/** + * Implements password credentials. + * + * These plaintext passwords are checked by the UserPasswordEncoder during + * authentication. + * + * @author Wouter de Jong + * + * @final + * @experimental in 5.1 + */ +class PasswordCredentials implements CredentialsInterface +{ + private $password; + private $resolved = false; + + public function __construct(string $password) + { + $this->password = $password; + } + + public function getPassword(): string + { + if (null === $this->password) { + throw new LogicException('The credentials are erased as another listener already verified these credentials.'); + } + + return $this->password; + } + + /** + * @internal + */ + public function markResolved(): void + { + $this->resolved = true; + $this->password = null; + } + + public function isResolved(): bool + { + return $this->resolved; + } +} diff --git a/src/Symfony/Component/Security/Http/Authenticator/Passport/Passport.php b/src/Symfony/Component/Security/Http/Authenticator/Passport/Passport.php new file mode 100644 index 0000000000..a4ead01d14 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/Passport/Passport.php @@ -0,0 +1,50 @@ + + * + * 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\Passport; + +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CredentialsInterface; + +/** + * The default implementation for passports. + * + * @author Wouter de Jong + * + * @experimental in 5.1 + */ +class Passport implements UserPassportInterface +{ + use PassportTrait; + + protected $user; + + /** + * @param CredentialsInterface $credentials the credentials to check for this authentication, use + * SelfValidatingPassport if no credentials should be checked. + * @param BadgeInterface[] $badges + */ + public function __construct(UserInterface $user, CredentialsInterface $credentials, array $badges = []) + { + $this->user = $user; + + $this->addBadge($credentials); + foreach ($badges as $badge) { + $this->addBadge($badge); + } + } + + public function getUser(): UserInterface + { + return $this->user; + } +} diff --git a/src/Symfony/Component/Security/Http/Authenticator/Passport/PassportInterface.php b/src/Symfony/Component/Security/Http/Authenticator/Passport/PassportInterface.php new file mode 100644 index 0000000000..ac77969127 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/Passport/PassportInterface.php @@ -0,0 +1,51 @@ + + * + * 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\Passport; + +use Symfony\Component\Security\Core\Exception\BadCredentialsException; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface; + +/** + * A Passport contains all security-related information that needs to be + * validated during authentication. + * + * A passport badge can be used to add any additional information to the + * passport. + * + * @author Wouter de Jong + * + * @experimental in 5.1 + */ +interface PassportInterface +{ + /** + * Adds a new security badge. + * + * A passport can hold only one instance of the same security badge. + * This method replaces the current badge if it is already set on this + * passport. + * + * @return $this + */ + public function addBadge(BadgeInterface $badge): self; + + public function hasBadge(string $badgeFqcn): bool; + + public function getBadge(string $badgeFqcn): ?BadgeInterface; + + /** + * Checks if all badges are marked as resolved. + * + * @throws BadCredentialsException when a badge is not marked as resolved + */ + public function checkIfCompletelyResolved(): void; +} diff --git a/src/Symfony/Component/Security/Http/Authenticator/Passport/PassportTrait.php b/src/Symfony/Component/Security/Http/Authenticator/Passport/PassportTrait.php new file mode 100644 index 0000000000..1cdd75546b --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/Passport/PassportTrait.php @@ -0,0 +1,55 @@ + + * + * 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\Passport; + +use Symfony\Component\Security\Core\Exception\BadCredentialsException; +use Symfony\Component\Security\Core\Exception\InvalidArgumentException; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface; + +/** + * @author Wouter de Jong + * + * @experimental in 5.1 + */ +trait PassportTrait +{ + /** + * @var BadgeInterface[] + */ + private $badges = []; + + public function addBadge(BadgeInterface $badge): PassportInterface + { + $this->badges[\get_class($badge)] = $badge; + + return $this; + } + + public function hasBadge(string $badgeFqcn): bool + { + return isset($this->badges[$badgeFqcn]); + } + + public function getBadge(string $badgeFqcn): ?BadgeInterface + { + return $this->badges[$badgeFqcn] ?? null; + } + + public function checkIfCompletelyResolved(): void + { + foreach ($this->badges as $badge) { + if (!$badge->isResolved()) { + throw new BadCredentialsException(sprintf('Authentication failed security badge "%s" is not resolved, did you forget to register the correct listeners?', \get_class($badge))); + } + } + } +} diff --git a/src/Symfony/Component/Security/Http/Authenticator/Passport/SelfValidatingPassport.php b/src/Symfony/Component/Security/Http/Authenticator/Passport/SelfValidatingPassport.php new file mode 100644 index 0000000000..dd3ef6f962 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/Passport/SelfValidatingPassport.php @@ -0,0 +1,34 @@ + + * + * 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\Passport; + +use Symfony\Component\Security\Core\User\UserInterface; + +/** + * An implementation used when there are no credentials to be checked (e.g. + * API token authentication). + * + * @author Wouter de Jong + * + * @experimental in 5.1 + */ +class SelfValidatingPassport extends Passport +{ + public function __construct(UserInterface $user, array $badges = []) + { + $this->user = $user; + + foreach ($badges as $badge) { + $this->addBadge($badge); + } + } +} diff --git a/src/Symfony/Component/Security/Http/Authenticator/Passport/UserPassportInterface.php b/src/Symfony/Component/Security/Http/Authenticator/Passport/UserPassportInterface.php new file mode 100644 index 0000000000..f308c13252 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/Passport/UserPassportInterface.php @@ -0,0 +1,26 @@ + + * + * 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\Passport; + +use Symfony\Component\Security\Core\User\UserInterface; + +/** + * Represents a passport for a Security User. + * + * @author Wouter de Jong + * + * @experimental in 5.1 + */ +interface UserPassportInterface extends PassportInterface +{ + public function getUser(): UserInterface; +} diff --git a/src/Symfony/Component/Security/Http/Authenticator/PasswordAuthenticatedInterface.php b/src/Symfony/Component/Security/Http/Authenticator/PasswordAuthenticatedInterface.php deleted file mode 100644 index 7386fc3373..0000000000 --- a/src/Symfony/Component/Security/Http/Authenticator/PasswordAuthenticatedInterface.php +++ /dev/null @@ -1,31 +0,0 @@ - - * - * 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; - -/** - * This interface should be implemented when the authenticator - * uses a password to authenticate. - * - * The EncoderFactory will be used to automatically validate - * the password. - * - * @author Wouter de Jong - */ -interface PasswordAuthenticatedInterface -{ - /** - * Returns the clear-text password contained in credentials if any. - * - * @param mixed $credentials The user credentials - */ - public function getPassword($credentials): ?string; -} diff --git a/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php index 72c6ea5288..12a70d42b4 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php @@ -17,8 +17,10 @@ use Symfony\Component\Security\Core\Authentication\Token\RememberMeToken; 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\Authenticator\Passport\PassportInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; use Symfony\Component\Security\Http\RememberMe\AbstractRememberMeServices; +use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; /** * The RememberMe *Authenticator* performs remember me authentication. @@ -33,22 +35,19 @@ use Symfony\Component\Security\Http\RememberMe\AbstractRememberMeServices; * * @final */ -class RememberMeAuthenticator implements InteractiveAuthenticatorInterface, CustomAuthenticatedInterface +class RememberMeAuthenticator implements InteractiveAuthenticatorInterface { private $rememberMeServices; private $secret; private $tokenStorage; - private $options = [ - 'secure' => false, - 'httponly' => true, - ]; + private $options = []; - public function __construct(AbstractRememberMeServices $rememberMeServices, string $secret, TokenStorageInterface $tokenStorage, array $options) + public function __construct(RememberMeServicesInterface $rememberMeServices, string $secret, TokenStorageInterface $tokenStorage, array $options) { $this->rememberMeServices = $rememberMeServices; $this->secret = $secret; $this->tokenStorage = $tokenStorage; - $this->options = array_merge($this->options, $options); + $this->options = $options; } public function supports(Request $request): ?bool @@ -62,7 +61,7 @@ class RememberMeAuthenticator implements InteractiveAuthenticatorInterface, Cust return false; } - if (!$request->cookies->has($this->options['name'])) { + if (isset($this->options['name']) && !$request->cookies->has($this->options['name'])) { return false; } @@ -70,31 +69,16 @@ class RememberMeAuthenticator implements InteractiveAuthenticatorInterface, Cust return null; } - public function getCredentials(Request $request) + public function authenticate(Request $request): PassportInterface { - return [ - 'cookie_parts' => explode(AbstractRememberMeServices::COOKIE_DELIMITER, base64_decode($request->cookies->get($this->options['name']))), - 'request' => $request, - ]; + $token = $this->rememberMeServices->autoLogin($request); + + return new SelfValidatingPassport($token->getUser()); } - /** - * @param array $credentials - */ - public function getUser($credentials): ?UserInterface + public function createAuthenticatedToken(PassportInterface $passport, string $providerKey): TokenInterface { - return $this->rememberMeServices->performLogin($credentials['cookie_parts'], $credentials['request']); - } - - public function checkCredentials($credentials, UserInterface $user): bool - { - // remember me always is valid (if a user could be found) - return true; - } - - public function createAuthenticatedToken(UserInterface $user, string $providerKey): TokenInterface - { - return new RememberMeToken($user, $providerKey, $this->secret); + return new RememberMeToken($passport->getUser(), $providerKey, $this->secret); } public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response diff --git a/src/Symfony/Component/Security/Http/Authenticator/RemoteUserAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/RemoteUserAuthenticator.php index 3a01087767..140b6c271e 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/RemoteUserAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/RemoteUserAuthenticator.php @@ -24,6 +24,8 @@ use Symfony\Component\Security\Core\User\UserProviderInterface; * @author Fabien Potencier * @author Maxime Douailin * + * @final + * * @internal in Symfony 5.1 */ class RemoteUserAuthenticator extends AbstractPreAuthenticatedAuthenticator diff --git a/src/Symfony/Component/Security/Http/Authenticator/TokenAuthenticatedInterface.php b/src/Symfony/Component/Security/Http/Authenticator/TokenAuthenticatedInterface.php deleted file mode 100644 index 88d0d7f965..0000000000 --- a/src/Symfony/Component/Security/Http/Authenticator/TokenAuthenticatedInterface.php +++ /dev/null @@ -1,33 +0,0 @@ - - * - * 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; - -/** - * This interface should be implemented when the authenticator - * doesn't need to check credentials (e.g. when using API tokens) - * - * @author Wouter de Jong - */ -interface TokenAuthenticatedInterface -{ - /** - * Extracts the token from the credentials. - * - * If you return null, the credentials will not be marked as - * valid and a BadCredentialsException is thrown. - * - * @param mixed $credentials The user credentials - * - * @return mixed|null the token - if any - or null otherwise - */ - public function getToken($credentials); -} diff --git a/src/Symfony/Component/Security/Http/Authenticator/X509Authenticator.php b/src/Symfony/Component/Security/Http/Authenticator/X509Authenticator.php index d482579d05..c76f3f94e5 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/X509Authenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/X509Authenticator.php @@ -24,7 +24,7 @@ use Symfony\Component\Security\Core\User\UserProviderInterface; * @author Wouter de Jong * @author Fabien Potencier * - * @internal + * @final * @experimental in Symfony 5.1 */ class X509Authenticator extends AbstractPreAuthenticatedAuthenticator diff --git a/src/Symfony/Component/Security/Http/Event/LoginSuccessEvent.php b/src/Symfony/Component/Security/Http/Event/LoginSuccessEvent.php index 6e48e171b6..80f740480b 100644 --- a/src/Symfony/Component/Security/Http/Event/LoginSuccessEvent.php +++ b/src/Symfony/Component/Security/Http/Event/LoginSuccessEvent.php @@ -5,7 +5,11 @@ namespace Symfony\Component\Security\Http\Event; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Exception\LogicException; +use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\UserPassportInterface; use Symfony\Contracts\EventDispatcher\Event; /** @@ -21,14 +25,16 @@ use Symfony\Contracts\EventDispatcher\Event; class LoginSuccessEvent extends Event { private $authenticator; + private $passport; private $authenticatedToken; private $request; private $response; private $providerKey; - public function __construct(AuthenticatorInterface $authenticator, TokenInterface $authenticatedToken, Request $request, ?Response $response, string $providerKey) + public function __construct(AuthenticatorInterface $authenticator, PassportInterface $passport, TokenInterface $authenticatedToken, Request $request, ?Response $response, string $providerKey) { $this->authenticator = $authenticator; + $this->passport = $passport; $this->authenticatedToken = $authenticatedToken; $this->request = $request; $this->response = $response; @@ -40,6 +46,20 @@ class LoginSuccessEvent extends Event return $this->authenticator; } + public function getPassport(): PassportInterface + { + return $this->passport; + } + + public function getUser(): UserInterface + { + if (!$this->passport instanceof UserPassportInterface) { + throw new LogicException(sprintf('Cannot call "%s" as the authenticator ("%s") did not set a user.', __METHOD__, \get_class($this->authenticator))); + } + + return $this->passport->getUser(); + } + public function getAuthenticatedToken(): TokenInterface { return $this->authenticatedToken; diff --git a/src/Symfony/Component/Security/Http/Event/VerifyAuthenticatorCredentialsEvent.php b/src/Symfony/Component/Security/Http/Event/VerifyAuthenticatorCredentialsEvent.php index cc37bf33f2..eac7f03741 100644 --- a/src/Symfony/Component/Security/Http/Event/VerifyAuthenticatorCredentialsEvent.php +++ b/src/Symfony/Component/Security/Http/Event/VerifyAuthenticatorCredentialsEvent.php @@ -5,6 +5,7 @@ namespace Symfony\Component\Security\Http\Event; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; use Symfony\Contracts\EventDispatcher\Event; /** @@ -19,15 +20,12 @@ use Symfony\Contracts\EventDispatcher\Event; class VerifyAuthenticatorCredentialsEvent extends Event { private $authenticator; - private $user; - private $credentials; - private $credentialsValid = false; + private $passport; - public function __construct(AuthenticatorInterface $authenticator, $credentials, ?UserInterface $user) + public function __construct(AuthenticatorInterface $authenticator, PassportInterface $passport) { $this->authenticator = $authenticator; - $this->credentials = $credentials; - $this->user = $user; + $this->passport = $passport; } public function getAuthenticator(): AuthenticatorInterface @@ -35,23 +33,8 @@ class VerifyAuthenticatorCredentialsEvent extends Event return $this->authenticator; } - public function getCredentials() + public function getPassport(): PassportInterface { - return $this->credentials; - } - - public function getUser(): ?UserInterface - { - return $this->user; - } - - public function setCredentialsValid(bool $validated = true): void - { - $this->credentialsValid = $validated; - } - - public function areCredentialsValid(): bool - { - return $this->credentialsValid; + return $this->passport; } } diff --git a/src/Symfony/Component/Security/Http/EventListener/CsrfProtectionListener.php b/src/Symfony/Component/Security/Http/EventListener/CsrfProtectionListener.php index fcde792452..65c8ffa3e3 100644 --- a/src/Symfony/Component/Security/Http/EventListener/CsrfProtectionListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/CsrfProtectionListener.php @@ -15,9 +15,15 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException; use Symfony\Component\Security\Csrf\CsrfToken; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; -use Symfony\Component\Security\Http\Authenticator\CsrfProtectedAuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge; use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; +/** + * @author Wouter de Jong + * + * @final + * @experimental in 5.1 + */ class CsrfProtectionListener implements EventSubscriberInterface { private $csrfTokenManager; @@ -29,20 +35,24 @@ class CsrfProtectionListener implements EventSubscriberInterface public function verifyCredentials(VerifyAuthenticatorCredentialsEvent $event): void { - $authenticator = $event->getAuthenticator(); - if (!$authenticator instanceof CsrfProtectedAuthenticatorInterface) { + $passport = $event->getPassport(); + if (!$passport->hasBadge(CsrfTokenBadge::class)) { return; } - $csrfTokenValue = $authenticator->getCsrfToken($event->getCredentials()); - if (null === $csrfTokenValue) { + /** @var CsrfTokenBadge $badge */ + $badge = $passport->getBadge(CsrfTokenBadge::class); + if ($badge->isResolved()) { return; } - $csrfToken = new CsrfToken($authenticator->getCsrfTokenId(), $csrfTokenValue); + $csrfToken = new CsrfToken($badge->getCsrfTokenId(), $badge->getCsrfToken()); + if (false === $this->csrfTokenManager->isTokenValid($csrfToken)) { throw new InvalidCsrfTokenException('Invalid CSRF token.'); } + + $badge->markResolved(); } public static function getSubscribedEvents(): array diff --git a/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php b/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php index 28800e6260..0d22bf22ca 100644 --- a/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php @@ -4,10 +4,9 @@ namespace Symfony\Component\Security\Http\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; -use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; -use Symfony\Component\Security\Core\User\UserInterface; -use Symfony\Component\Security\Http\Authenticator\PasswordAuthenticatedInterface; -use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\UserPassportInterface; +use Symfony\Component\Security\Http\Event\LoginSuccessEvent; /** * @author Wouter de Jong @@ -24,37 +23,33 @@ class PasswordMigratingListener implements EventSubscriberInterface $this->encoderFactory = $encoderFactory; } - public function onCredentialsVerification(VerifyAuthenticatorCredentialsEvent $event): void + public function onLoginSuccess(LoginSuccessEvent $event): void { - if (!$event->areCredentialsValid()) { - // Do not migrate password that are not validated + $passport = $event->getPassport(); + if (!$passport instanceof UserPassportInterface || !$passport->hasBadge(PasswordUpgradeBadge::class)) { return; } - $authenticator = $event->getAuthenticator(); - if (!$authenticator instanceof PasswordAuthenticatedInterface || !$authenticator instanceof PasswordUpgraderInterface) { - return; - } - - if (null === $password = $authenticator->getPassword($event->getCredentials())) { - return; - } - - $user = $event->getUser(); - if (!$user instanceof UserInterface) { + /** @var PasswordUpgradeBadge $badge */ + $badge = $passport->getBadge(PasswordUpgradeBadge::class); + $plaintextPassword = $badge->getPlaintextPassword(); + $badge->eraseCredentials(); + + if ('' === $plaintextPassword) { return; } + $user = $passport->getUser(); $passwordEncoder = $this->encoderFactory->getEncoder($user); if (!$passwordEncoder->needsRehash($user->getPassword())) { return; } - $authenticator->upgradePassword($user, $passwordEncoder->encodePassword($password, $user->getSalt())); + $badge->getPasswordUpgrader()->upgradePassword($user, $passwordEncoder->encodePassword($plaintextPassword, $user->getSalt())); } public static function getSubscribedEvents(): array { - return [VerifyAuthenticatorCredentialsEvent::class => ['onCredentialsVerification', -128]]; + return [LoginSuccessEvent::class => 'onLoginSuccess']; } } diff --git a/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php b/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php index 269d232786..da582a7cc6 100644 --- a/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php @@ -4,8 +4,7 @@ namespace Symfony\Component\Security\Http\EventListener; use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; -use Symfony\Component\Security\Http\Authenticator\RememberMeAuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge; use Symfony\Component\Security\Http\Event\LoginFailureEvent; use Symfony\Component\Security\Http\Event\LoginSuccessEvent; use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; @@ -34,13 +33,12 @@ class RememberMeListener implements EventSubscriberInterface $this->logger = $logger; } - public function onSuccessfulLogin(LoginSuccessEvent $event): void { - $authenticator = $event->getAuthenticator(); - if (!$authenticator instanceof RememberMeAuthenticatorInterface || !$authenticator->supportsRememberMe()) { + $passport = $event->getPassport(); + if (!$passport->hasBadge(RememberMeBadge::class)) { if (null !== $this->logger) { - $this->logger->debug('Remember me skipped: your authenticator does not support it.', ['authenticator' => \get_class($authenticator)]); + $this->logger->debug('Remember me skipped: your authenticator does not support it.', ['authenticator' => \get_class($event->getAuthenticator())]); } return; @@ -48,7 +46,7 @@ class RememberMeListener implements EventSubscriberInterface if (null === $event->getResponse()) { if (null !== $this->logger) { - $this->logger->debug('Remember me skipped: the authenticator did not set a success response.', ['authenticator' => \get_class($authenticator)]); + $this->logger->debug('Remember me skipped: the authenticator did not set a success response.', ['authenticator' => \get_class($event->getAuthenticator())]); } return; diff --git a/src/Symfony/Component/Security/Http/EventListener/UserCheckerListener.php b/src/Symfony/Component/Security/Http/EventListener/UserCheckerListener.php index 34fdfdf84d..fbcc0bd549 100644 --- a/src/Symfony/Component/Security/Http/EventListener/UserCheckerListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/UserCheckerListener.php @@ -4,7 +4,9 @@ namespace Symfony\Component\Security\Http\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Security\Core\User\UserCheckerInterface; -use Symfony\Component\Security\Http\Authenticator\AbstractPreAuthenticatedAuthenticator; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PreAuthenticatedUserBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\UserPassportInterface; +use Symfony\Component\Security\Http\Event\LoginSuccessEvent; use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; /** @@ -24,29 +26,29 @@ class UserCheckerListener implements EventSubscriberInterface public function preCredentialsVerification(VerifyAuthenticatorCredentialsEvent $event): void { - if (null === $event->getUser() || $event->getAuthenticator() instanceof AbstractPreAuthenticatedAuthenticator) { + $passport = $event->getPassport(); + if (!$passport instanceof UserPassportInterface || $passport->hasBadge(PreAuthenticatedUserBadge::class)) { return; } - $this->userChecker->checkPreAuth($event->getUser()); + $this->userChecker->checkPreAuth($passport->getUser()); } - public function postCredentialsVerification(VerifyAuthenticatorCredentialsEvent $event): void + public function postCredentialsVerification(LoginSuccessEvent $event): void { - if (null === $event->getUser() || !$event->areCredentialsValid()) { + $passport = $event->getPassport(); + if (!$passport instanceof UserPassportInterface || null === $passport->getUser()) { return; } - $this->userChecker->checkPostAuth($event->getUser()); + $this->userChecker->checkPostAuth($passport->getUser()); } public static function getSubscribedEvents(): array { return [ - VerifyAuthenticatorCredentialsEvent::class => [ - ['preCredentialsVerification', 256], - ['preCredentialsVerification', 32] - ], + VerifyAuthenticatorCredentialsEvent::class => [['preCredentialsVerification', 256]], + LoginSuccessEvent::class => ['postCredentialsVerification', 256], ]; } } diff --git a/src/Symfony/Component/Security/Http/EventListener/VerifyAuthenticatorCredentialsListener.php b/src/Symfony/Component/Security/Http/EventListener/VerifyAuthenticatorCredentialsListener.php index 77bbb39ec9..0287dc4f5d 100644 --- a/src/Symfony/Component/Security/Http/EventListener/VerifyAuthenticatorCredentialsListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/VerifyAuthenticatorCredentialsListener.php @@ -5,10 +5,9 @@ namespace Symfony\Component\Security\Http\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; use Symfony\Component\Security\Core\Exception\BadCredentialsException; -use Symfony\Component\Security\Core\Exception\LogicException; -use Symfony\Component\Security\Http\Authenticator\CustomAuthenticatedInterface; -use Symfony\Component\Security\Http\Authenticator\PasswordAuthenticatedInterface; -use Symfony\Component\Security\Http\Authenticator\TokenAuthenticatedInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CustomCredentials; +use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; +use Symfony\Component\Security\Http\Authenticator\Passport\UserPassportInterface; use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; /** @@ -31,44 +30,46 @@ class VerifyAuthenticatorCredentialsListener implements EventSubscriberInterface public function onAuthenticating(VerifyAuthenticatorCredentialsEvent $event): void { - if ($event->areCredentialsValid()) { - return; - } - - $authenticator = $event->getAuthenticator(); - if ($authenticator instanceof PasswordAuthenticatedInterface) { + $passport = $event->getPassport(); + if ($passport instanceof UserPassportInterface && $passport->hasBadge(PasswordCredentials::class)) { // Use the password encoder to validate the credentials - $user = $event->getUser(); - $presentedPassword = $authenticator->getPassword($event->getCredentials()); + $user = $passport->getUser(); + /** @var PasswordCredentials $badge */ + $badge = $passport->getBadge(PasswordCredentials::class); + + if ($badge->isResolved()) { + return; + } + + $presentedPassword = $badge->getPassword(); if ('' === $presentedPassword) { throw new BadCredentialsException('The presented password cannot be empty.'); } if (null === $user->getPassword()) { + throw new BadCredentialsException('The presented password is invalid.'); + } + + if (!$this->encoderFactory->getEncoder($user)->isPasswordValid($user->getPassword(), $presentedPassword, $user->getSalt())) { + throw new BadCredentialsException('The presented password is invalid.'); + } + + $badge->markResolved(); + + return; + } + + if ($passport->hasBadge(CustomCredentials::class)) { + /** @var CustomCredentials $badge */ + $badge = $passport->getBadge(CustomCredentials::class); + if ($badge->isResolved()) { return; } - $event->setCredentialsValid($this->encoderFactory->getEncoder($user)->isPasswordValid($user->getPassword(), $presentedPassword, $user->getSalt())); + $badge->executeCustomChecker($passport->getUser()); return; } - - if ($authenticator instanceof TokenAuthenticatedInterface) { - if (null !== $authenticator->getToken($event->getCredentials())) { - // Token based authenticators do not have a credential validation step - $event->setCredentialsValid(); - } - - return; - } - - if ($authenticator instanceof CustomAuthenticatedInterface) { - $event->setCredentialsValid($authenticator->checkCredentials($event->getCredentials(), $event->getUser())); - - return; - } - - throw new LogicException(sprintf('Authenticator %s does not have valid credentials. Authenticators must implement one of the authenticated interfaces (%s, %s or %s).', \get_class($authenticator), PasswordAuthenticatedInterface::class, TokenAuthenticatedInterface::class, CustomAuthenticatedInterface::class)); } public static function getSubscribedEvents(): array diff --git a/src/Symfony/Component/Security/Http/RememberMe/AbstractRememberMeServices.php b/src/Symfony/Component/Security/Http/RememberMe/AbstractRememberMeServices.php index e9065d7f52..22f9dde14b 100644 --- a/src/Symfony/Component/Security/Http/RememberMe/AbstractRememberMeServices.php +++ b/src/Symfony/Component/Security/Http/RememberMe/AbstractRememberMeServices.php @@ -89,11 +89,6 @@ abstract class AbstractRememberMeServices implements RememberMeServicesInterface return $this->secret; } - public function performLogin(array $cookieParts, Request $request): UserInterface - { - return $this->processAutoLoginCookie($cookieParts, $request); - } - /** * Implementation of RememberMeServicesInterface. Detects whether a remember-me * cookie was set, decodes it, and hands it to subclasses for further processing. diff --git a/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php b/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php index 7343d79788..2cf7994db7 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php @@ -18,10 +18,12 @@ 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\BadCredentialsException; -use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; -use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Http\Authentication\AuthenticatorManager; use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; +use Symfony\Component\Security\Http\Authenticator\Passport\Passport; +use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; class AuthenticatorManagerTest extends TestCase @@ -38,7 +40,7 @@ class AuthenticatorManagerTest extends TestCase $this->tokenStorage = $this->createMock(TokenStorageInterface::class); $this->eventDispatcher = new EventDispatcher(); $this->request = new Request(); - $this->user = $this->createMock(UserInterface::class); + $this->user = new User('wouter', null); $this->token = $this->createMock(TokenInterface::class); $this->response = $this->createMock(Response::class); } @@ -73,7 +75,7 @@ class AuthenticatorManagerTest extends TestCase $authenticator = $this->createAuthenticator(false); $this->request->attributes->set('_guard_authenticators', [$authenticator]); - $authenticator->expects($this->never())->method('getCredentials'); + $authenticator->expects($this->never())->method('authenticate'); $manager = $this->createManager([$authenticator]); $manager->authenticateRequest($this->request); @@ -88,17 +90,14 @@ class AuthenticatorManagerTest extends TestCase $this->request->attributes->set('_guard_authenticators', $authenticators); $matchingAuthenticator = $authenticators[$matchingAuthenticatorIndex]; - $authenticators[($matchingAuthenticatorIndex + 1) % 2]->expects($this->never())->method('getCredentials'); + $authenticators[($matchingAuthenticatorIndex + 1) % 2]->expects($this->never())->method('authenticate'); - $matchingAuthenticator->expects($this->any())->method('getCredentials')->willReturn(['password' => 'pa$$']); - $matchingAuthenticator->expects($this->any())->method('getUser')->willReturn($this->user); + $matchingAuthenticator->expects($this->any())->method('authenticate')->willReturn(new SelfValidatingPassport($this->user)); $listenerCalled = false; $this->eventDispatcher->addListener(VerifyAuthenticatorCredentialsEvent::class, function (VerifyAuthenticatorCredentialsEvent $event) use (&$listenerCalled, $matchingAuthenticator) { - if ($event->getAuthenticator() === $matchingAuthenticator && $event->getCredentials() === ['password' => 'pa$$'] && $event->getUser() === $this->user) { + if ($event->getAuthenticator() === $matchingAuthenticator && $event->getPassport()->getUser() === $this->user) { $listenerCalled = true; - - $event->setCredentialsValid(true); } }); $matchingAuthenticator->expects($this->any())->method('createAuthenticatedToken')->willReturn($this->token); @@ -116,29 +115,12 @@ class AuthenticatorManagerTest extends TestCase yield [1]; } - public function testUserNotFound() - { - $authenticator = $this->createAuthenticator(); - $this->request->attributes->set('_guard_authenticators', [$authenticator]); - - $authenticator->expects($this->any())->method('getCredentials')->willReturn(['username' => 'john']); - $authenticator->expects($this->any())->method('getUser')->with(['username' => 'john'])->willReturn(null); - - $authenticator->expects($this->once()) - ->method('onAuthenticationFailure') - ->with($this->request, $this->isInstanceOf(UsernameNotFoundException::class)); - - $manager = $this->createManager([$authenticator]); - $manager->authenticateRequest($this->request); - } - public function testNoCredentialsValidated() { $authenticator = $this->createAuthenticator(); $this->request->attributes->set('_guard_authenticators', [$authenticator]); - $authenticator->expects($this->any())->method('getCredentials')->willReturn(['username' => 'john']); - $authenticator->expects($this->any())->method('getUser')->willReturn($this->user); + $authenticator->expects($this->any())->method('authenticate')->willReturn(new Passport($this->user, new PasswordCredentials('pass'))); $authenticator->expects($this->once()) ->method('onAuthenticationFailure') @@ -156,11 +138,7 @@ class AuthenticatorManagerTest extends TestCase $authenticator = $this->createAuthenticator(); $this->request->attributes->set('_guard_authenticators', [$authenticator]); - $authenticator->expects($this->any())->method('getCredentials')->willReturn(['username' => 'john']); - $authenticator->expects($this->any())->method('getUser')->willReturn($this->user); - $this->eventDispatcher->addListener(VerifyAuthenticatorCredentialsEvent::class, function (VerifyAuthenticatorCredentialsEvent $event) { - $event->setCredentialsValid(true); - }); + $authenticator->expects($this->any())->method('authenticate')->willReturn(new SelfValidatingPassport($this->user)); $authenticator->expects($this->any())->method('createAuthenticatedToken')->willReturn($this->token); @@ -194,12 +172,7 @@ class AuthenticatorManagerTest extends TestCase $authenticator->expects($this->any())->method('isInteractive')->willReturn(true); $this->request->attributes->set('_guard_authenticators', [$authenticator]); - $authenticator->expects($this->any())->method('getCredentials')->willReturn(['password' => 'pa$$']); - $authenticator->expects($this->any())->method('getUser')->willReturn($this->user); - - $this->eventDispatcher->addListener(VerifyAuthenticatorCredentialsEvent::class, function (VerifyAuthenticatorCredentialsEvent $event) { - $event->setCredentialsValid(true); - }); + $authenticator->expects($this->any())->method('authenticate')->willReturn(new SelfValidatingPassport($this->user)); $authenticator->expects($this->any())->method('createAuthenticatedToken')->willReturn($this->token); $this->tokenStorage->expects($this->once())->method('setToken')->with($this->token); diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/AnonymousAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/AnonymousAuthenticatorTest.php index f5d1cfdf98..d5593bb375 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/AnonymousAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/AnonymousAuthenticatorTest.php @@ -14,7 +14,6 @@ namespace Symfony\Component\Security\Http\Tests\Authenticator; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; -use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Http\Authenticator\AnonymousAuthenticator; class AnonymousAuthenticatorTest extends TestCase @@ -46,14 +45,9 @@ class AnonymousAuthenticatorTest extends TestCase yield [false, false]; } - public function testAlwaysValidCredentials() - { - $this->assertTrue($this->authenticator->checkCredentials([], $this->createMock(UserInterface::class))); - } - public function testAuthenticatedToken() { - $token = $this->authenticator->createAuthenticatedToken($this->authenticator->getUser([]), 'main'); + $token = $this->authenticator->createAuthenticatedToken($this->authenticator->authenticate($this->request), 'main'); $this->assertTrue($token->isAuthenticated()); $this->assertEquals('anon.', $token->getUser()); diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/FormLoginAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/FormLoginAuthenticatorTest.php index 3012da746d..9ab9055455 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/FormLoginAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/FormLoginAuthenticatorTest.php @@ -16,10 +16,14 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Security; +use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; +use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; use Symfony\Component\Security\Http\Authenticator\FormLoginAuthenticator; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge; use Symfony\Component\Security\Http\HttpUtils; class FormLoginAuthenticatorTest extends TestCase @@ -27,11 +31,13 @@ class FormLoginAuthenticatorTest extends TestCase private $userProvider; private $successHandler; private $failureHandler; + /** @var FormLoginAuthenticator */ private $authenticator; protected function setUp(): void { $this->userProvider = $this->createMock(UserProviderInterface::class); + $this->userProvider->expects($this->any())->method('loadUserByUsername')->willReturn(new User('test', 's$cr$t')); $this->successHandler = $this->createMock(AuthenticationSuccessHandlerInterface::class); $this->failureHandler = $this->createMock(AuthenticationFailureHandlerInterface::class); } @@ -48,11 +54,11 @@ class FormLoginAuthenticatorTest extends TestCase $this->expectExceptionMessage('Invalid username.'); } - $request = Request::create('/login_check', 'POST', ['_username' => $username]); + $request = Request::create('/login_check', 'POST', ['_username' => $username, '_password' => 's$cr$t']); $request->setSession($this->createSession()); $this->setUpAuthenticator(); - $this->authenticator->getCredentials($request); + $this->authenticator->authenticate($request); } public function provideUsernamesForLength() @@ -73,7 +79,7 @@ class FormLoginAuthenticatorTest extends TestCase $request->setSession($this->createSession()); $this->setUpAuthenticator(['post_only' => $postOnly]); - $this->authenticator->getCredentials($request); + $this->authenticator->authenticate($request); } /** @@ -88,7 +94,7 @@ class FormLoginAuthenticatorTest extends TestCase $request->setSession($this->createSession()); $this->setUpAuthenticator(['post_only' => $postOnly]); - $this->authenticator->getCredentials($request); + $this->authenticator->authenticate($request); } /** @@ -103,22 +109,22 @@ class FormLoginAuthenticatorTest extends TestCase $request->setSession($this->createSession()); $this->setUpAuthenticator(['post_only' => $postOnly]); - $this->authenticator->getCredentials($request); + $this->authenticator->authenticate($request); } /** * @dataProvider postOnlyDataProvider */ - public function testHandleNonStringUsernameWith__toString($postOnly) + public function testHandleNonStringUsernameWithToString($postOnly) { $usernameObject = $this->getMockBuilder(DummyUserClass::class)->getMock(); $usernameObject->expects($this->once())->method('__toString')->willReturn('someUsername'); - $request = Request::create('/login_check', 'POST', ['_username' => $usernameObject]); + $request = Request::create('/login_check', 'POST', ['_username' => $usernameObject, '_password' => 's$cr$t']); $request->setSession($this->createSession()); $this->setUpAuthenticator(['post_only' => $postOnly]); - $this->authenticator->getCredentials($request); + $this->authenticator->authenticate($request); } public function postOnlyDataProvider() @@ -127,6 +133,31 @@ class FormLoginAuthenticatorTest extends TestCase yield [false]; } + public function testCsrfProtection() + { + $request = Request::create('/login_check', 'POST', ['_username' => 'wouter', '_password' => 's$cr$t']); + $request->setSession($this->createSession()); + + $this->setUpAuthenticator(['enable_csrf' => true]); + $passport = $this->authenticator->authenticate($request); + $this->assertTrue($passport->hasBadge(CsrfTokenBadge::class)); + } + + public function testUpgradePassword() + { + $request = Request::create('/login_check', 'POST', ['_username' => 'wouter', '_password' => 's$cr$t']); + $request->setSession($this->createSession()); + + $this->userProvider = $this->createMock([UserProviderInterface::class, PasswordUpgraderInterface::class]); + $this->userProvider->expects($this->any())->method('loadUserByUsername')->willReturn(new User('test', 's$cr$t')); + + $this->setUpAuthenticator(); + $passport = $this->authenticator->authenticate($request); + $this->assertTrue($passport->hasBadge(PasswordUpgradeBadge::class)); + $badge = $passport->getBadge(PasswordUpgradeBadge::class); + $this->assertEquals('s$cr$t', $badge->getPlaintextPassword()); + } + private function setUpAuthenticator(array $options = []) { $this->authenticator = new FormLoginAuthenticator(new HttpUtils(), $this->userProvider, $this->successHandler, $this->failureHandler, $options); diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/HttpBasicAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/HttpBasicAuthenticatorTest.php index e2ac0ac991..693eb320ab 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/HttpBasicAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/HttpBasicAuthenticatorTest.php @@ -2,15 +2,16 @@ namespace Symfony\Component\Security\Http\Tests\Authenticator; -use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface; -use Symfony\Component\Security\Core\Exception\BadCredentialsException; -use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; +use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Authenticator\HttpBasicAuthenticator; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; class HttpBasicAuthenticatorTest extends TestCase { @@ -39,25 +40,16 @@ class HttpBasicAuthenticatorTest extends TestCase 'PHP_AUTH_PW' => 'ThePassword', ]); - $credentials = $this->authenticator->getCredentials($request); - $this->assertEquals([ - 'username' => 'TheUsername', - 'password' => 'ThePassword', - ], $credentials); - - $mockedUser = $this->getMockBuilder(UserInterface::class)->getMock(); - $mockedUser->expects($this->any())->method('getPassword')->willReturn('ThePassword'); - $this->userProvider ->expects($this->any()) ->method('loadUserByUsername') ->with('TheUsername') - ->willReturn($mockedUser); + ->willReturn($user = new User('TheUsername', 'ThePassword')); - $user = $this->authenticator->getUser($credentials); - $this->assertSame($mockedUser, $user); + $passport = $this->authenticator->authenticate($request); + $this->assertEquals('ThePassword', $passport->getBadge(PasswordCredentials::class)->getPassword()); - $this->assertEquals('ThePassword', $this->authenticator->getPassword($credentials)); + $this->assertSame($user, $passport->getUser()); } /** @@ -77,4 +69,21 @@ class HttpBasicAuthenticatorTest extends TestCase [['PHP_AUTH_PW' => 'ThePassword']], ]; } + + public function testUpgradePassword() + { + $request = new Request([], [], [], [], [], [ + 'PHP_AUTH_USER' => 'TheUsername', + 'PHP_AUTH_PW' => 'ThePassword', + ]); + + $this->userProvider = $this->createMock([UserProviderInterface::class, PasswordUpgraderInterface::class]); + $this->userProvider->expects($this->any())->method('loadUserByUsername')->willReturn(new User('test', 's$cr$t')); + $authenticator = new HttpBasicAuthenticator('test', $this->userProvider); + + $passport = $authenticator->authenticate($request); + $this->assertTrue($passport->hasBadge(PasswordUpgradeBadge::class)); + $badge = $passport->getBadge(PasswordUpgradeBadge::class); + $this->assertEquals('ThePassword', $badge->getPlaintextPassword()); + } } diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/JsonLoginAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/JsonLoginAuthenticatorTest.php index 84ff61781f..0f1967600a 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/JsonLoginAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/JsonLoginAuthenticatorTest.php @@ -16,8 +16,10 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Security; +use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Authenticator\JsonLoginAuthenticator; +use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; use Symfony\Component\Security\Http\HttpUtils; class JsonLoginAuthenticatorTest extends TestCase @@ -66,39 +68,45 @@ class JsonLoginAuthenticatorTest extends TestCase yield [Request::create('/login', 'GET', [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json']), false]; } - public function testGetCredentials() + public function testAuthenticate() { $this->setUpAuthenticator(); + $this->userProvider->expects($this->once())->method('loadUserByUsername')->with('dunglas')->willReturn(new User('dunglas', 'pa$$')); + $request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], '{"username": "dunglas", "password": "foo"}'); - $this->assertEquals(['username' => 'dunglas', 'password' => 'foo'], $this->authenticator->getCredentials($request)); + $passport = $this->authenticator->authenticate($request); + $this->assertEquals('foo', $passport->getBadge(PasswordCredentials::class)->getPassword()); } - public function testGetCredentialsCustomPath() + public function testAuthenticateWithCustomPath() { $this->setUpAuthenticator([ 'username_path' => 'authentication.username', 'password_path' => 'authentication.password', ]); + $this->userProvider->expects($this->once())->method('loadUserByUsername')->with('dunglas')->willReturn(new User('dunglas', 'pa$$')); + $request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], '{"authentication": {"username": "dunglas", "password": "foo"}}'); - $this->assertEquals(['username' => 'dunglas', 'password' => 'foo'], $this->authenticator->getCredentials($request)); + $passport = $this->authenticator->authenticate($request); + $this->assertEquals('foo', $passport->getBadge(PasswordCredentials::class)->getPassword()); } /** - * @dataProvider provideInvalidGetCredentialsData + * @dataProvider provideInvalidAuthenticateData */ - public function testGetCredentialsInvalid($request, $errorMessage, $exceptionType = BadRequestHttpException::class) + public function testAuthenticateInvalid($request, $errorMessage, $exceptionType = BadRequestHttpException::class) { $this->expectException($exceptionType); $this->expectExceptionMessage($errorMessage); $this->setUpAuthenticator(); - $this->authenticator->getCredentials($request); + $this->authenticator->authenticate($request); } - public function provideInvalidGetCredentialsData() + public function provideInvalidAuthenticateData() { $request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json']); yield [$request, 'Invalid JSON.']; diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/RememberMeAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/RememberMeAuthenticatorTest.php index 9bd11ab62d..d95e681281 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/RememberMeAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/RememberMeAuthenticatorTest.php @@ -14,11 +14,13 @@ namespace Symfony\Component\Security\Http\Tests\Authenticator; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Core\Authentication\Token\RememberMeToken; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Http\Authenticator\RememberMeAuthenticator; use Symfony\Component\Security\Http\RememberMe\AbstractRememberMeServices; +use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; class RememberMeAuthenticatorTest extends TestCase { @@ -29,7 +31,7 @@ class RememberMeAuthenticatorTest extends TestCase protected function setUp(): void { - $this->rememberMeServices = $this->createMock(AbstractRememberMeServices::class); + $this->rememberMeServices = $this->createMock(RememberMeServicesInterface::class); $this->tokenStorage = $this->createMock(TokenStorage::class); $this->authenticator = new RememberMeAuthenticator($this->rememberMeServices, 's3cr3t', $this->tokenStorage, [ 'name' => '_remember_me_cookie', @@ -67,22 +69,14 @@ class RememberMeAuthenticatorTest extends TestCase public function testAuthenticate() { - $credentials = $this->authenticator->getCredentials($this->request); - $this->assertEquals(['part1', 'part2'], $credentials['cookie_parts']); - $this->assertSame($this->request, $credentials['request']); + $this->rememberMeServices->expects($this->once()) + ->method('autoLogin') + ->with($this->request) + ->willReturn(new RememberMeToken($user = new User('wouter', 'test'), 'main', 'secret')); - $user = $this->createMock(UserInterface::class); - $this->rememberMeServices->expects($this->any()) - ->method('performLogin') - ->with($credentials['cookie_parts'], $credentials['request']) - ->willReturn($user); + $passport = $this->authenticator->authenticate($this->request); - $this->assertSame($user, $this->authenticator->getUser($credentials)); - } - - public function testCredentialsAlwaysValid() - { - $this->assertTrue($this->authenticator->checkCredentials([], $this->createMock(UserInterface::class))); + $this->assertSame($user, $passport->getUser()); } private function generateCookieValue() diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/RemoteUserAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/RemoteUserAuthenticatorTest.php index 80cddd1ddb..f55c72abff 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/RemoteUserAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/RemoteUserAuthenticatorTest.php @@ -14,6 +14,7 @@ namespace Symfony\Component\Security\Http\Tests\Authenticator; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; +use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Authenticator\RemoteUserAuthenticator; @@ -22,7 +23,7 @@ class RemoteUserAuthenticatorTest extends TestCase /** * @dataProvider provideAuthenticators */ - public function testSupport(RemoteUserAuthenticator $authenticator, $parameterName) + public function testSupport(UserProviderInterface $userProvider, RemoteUserAuthenticator $authenticator, $parameterName) { $request = $this->createRequest([$parameterName => 'TheUsername']); @@ -39,20 +40,28 @@ class RemoteUserAuthenticatorTest extends TestCase /** * @dataProvider provideAuthenticators */ - public function testGetCredentials(RemoteUserAuthenticator $authenticator, $parameterName) + public function testAuthenticate(UserProviderInterface $userProvider, RemoteUserAuthenticator $authenticator, $parameterName) { $request = $this->createRequest([$parameterName => 'TheUsername']); $authenticator->supports($request); - $this->assertEquals(['username' => 'TheUsername'], $authenticator->getCredentials($request)); + + $userProvider->expects($this->once()) + ->method('loadUserByUsername') + ->with('TheUsername') + ->willReturn($user = new User('TheUsername', null)); + + $passport = $authenticator->authenticate($request); + $this->assertEquals($user, $passport->getUser()); } public function provideAuthenticators() { $userProvider = $this->createMock(UserProviderInterface::class); + yield [$userProvider, new RemoteUserAuthenticator($userProvider, new TokenStorage(), 'main'), 'REMOTE_USER']; - yield [new RemoteUserAuthenticator($userProvider, new TokenStorage(), 'main'), 'REMOTE_USER']; - yield [new RemoteUserAuthenticator($userProvider, new TokenStorage(), 'main', 'CUSTOM_USER_PARAMETER'), 'CUSTOM_USER_PARAMETER']; + $userProvider = $this->createMock(UserProviderInterface::class); + yield [$userProvider, new RemoteUserAuthenticator($userProvider, new TokenStorage(), 'main', 'CUSTOM_USER_PARAMETER'), 'CUSTOM_USER_PARAMETER']; } private function createRequest(array $server) diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/X509AuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/X509AuthenticatorTest.php index e839504285..2490f9d042 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/X509AuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/X509AuthenticatorTest.php @@ -14,6 +14,7 @@ namespace Symfony\Component\Security\Http\Tests\Authenticator; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; +use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Authenticator\X509Authenticator; @@ -43,7 +44,13 @@ class X509AuthenticatorTest extends TestCase $request = $this->createRequest($serverVars); $this->assertTrue($this->authenticator->supports($request)); - $this->assertEquals(['username' => $user], $this->authenticator->getCredentials($request)); + + $this->userProvider->expects($this->once()) + ->method('loadUserByUsername') + ->with($user) + ->willReturn(new User($user, null)); + + $this->authenticator->authenticate($request); } public static function provideServerVars() @@ -60,7 +67,13 @@ class X509AuthenticatorTest extends TestCase $request = $this->createRequest(['SSL_CLIENT_S_DN' => $credentials]); $this->assertTrue($this->authenticator->supports($request)); - $this->assertEquals(['username' => $emailAddress], $this->authenticator->getCredentials($request)); + + $this->userProvider->expects($this->once()) + ->method('loadUserByUsername') + ->with($emailAddress) + ->willReturn(new User($emailAddress, null)); + + $this->authenticator->authenticate($request); } public static function provideServerVarsNoUser() @@ -89,7 +102,13 @@ class X509AuthenticatorTest extends TestCase 'TheUserKey' => 'TheUser', ]); $this->assertTrue($authenticator->supports($request)); - $this->assertEquals(['username' => 'TheUser'], $authenticator->getCredentials($request)); + + $this->userProvider->expects($this->once()) + ->method('loadUserByUsername') + ->with('TheUser') + ->willReturn(new User('TheUser', null)); + + $authenticator->authenticate($request); } public function testAuthenticationCustomCredentialsKey() @@ -100,7 +119,13 @@ class X509AuthenticatorTest extends TestCase 'TheCertKey' => 'CN=Sample certificate DN/emailAddress=cert@example.com', ]); $this->assertTrue($authenticator->supports($request)); - $this->assertEquals(['username' => 'cert@example.com'], $authenticator->getCredentials($request)); + + $this->userProvider->expects($this->once()) + ->method('loadUserByUsername') + ->with('cert@example.com') + ->willReturn(new User('cert@example.com', null)); + + $authenticator->authenticate($request); } private function createRequest(array $server) diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/CsrfProtectionListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/CsrfProtectionListenerTest.php index 0c2a15d952..baca526bfe 100644 --- a/src/Symfony/Component/Security/Http/Tests/EventListener/CsrfProtectionListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/CsrfProtectionListenerTest.php @@ -13,10 +13,12 @@ namespace Symfony\Component\Security\Http\Tests\EventListener; use PHPUnit\Framework\TestCase; use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException; +use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Csrf\CsrfToken; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; -use Symfony\Component\Security\Http\Authenticator\CsrfProtectedAuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; use Symfony\Component\Security\Http\EventListener\CsrfProtectionListener; @@ -31,11 +33,11 @@ class CsrfProtectionListenerTest extends TestCase $this->listener = new CsrfProtectionListener($this->csrfTokenManager); } - public function testNonCsrfProtectedAuthenticator() + public function testNoCsrfTokenBadge() { $this->csrfTokenManager->expects($this->never())->method('isTokenValid'); - $event = $this->createEvent($this->createAuthenticator(false)); + $event = $this->createEvent($this->createPassport(null)); $this->listener->verifyCredentials($event); } @@ -46,7 +48,7 @@ class CsrfProtectionListenerTest extends TestCase ->with(new CsrfToken('authenticator_token_id', 'abc123')) ->willReturn(true); - $event = $this->createEvent($this->createAuthenticator(true), ['_csrf' => 'abc123']); + $event = $this->createEvent($this->createPassport(new CsrfTokenBadge('authenticator_token_id', 'abc123'))); $this->listener->verifyCredentials($event); $this->expectNotToPerformAssertions(); @@ -62,28 +64,22 @@ class CsrfProtectionListenerTest extends TestCase ->with(new CsrfToken('authenticator_token_id', 'abc123')) ->willReturn(false); - $event = $this->createEvent($this->createAuthenticator(true), ['_csrf' => 'abc123']); + $event = $this->createEvent($this->createPassport(new CsrfTokenBadge('authenticator_token_id', 'abc123'))); $this->listener->verifyCredentials($event); } - private function createEvent($authenticator, $credentials = null) + private function createEvent($passport) { - return new VerifyAuthenticatorCredentialsEvent($authenticator, $credentials, null); + return new VerifyAuthenticatorCredentialsEvent($this->createMock(AuthenticatorInterface::class), $passport); } - private function createAuthenticator($supportsCsrf) + private function createPassport(?CsrfTokenBadge $badge) { - if (!$supportsCsrf) { - return $this->createMock(AuthenticatorInterface::class); + $passport = new SelfValidatingPassport(new User('wouter', 'pass')); + if ($badge) { + $passport->addBadge($badge); } - $authenticator = $this->createMock([AuthenticatorInterface::class, CsrfProtectedAuthenticatorInterface::class]); - $authenticator->expects($this->any())->method('getCsrfTokenId')->willReturn('authenticator_token_id'); - $authenticator->expects($this->any()) - ->method('getCsrfToken') - ->with(['_csrf' => 'abc123']) - ->willReturn('abc123'); - - return $authenticator; + return $passport; } } diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/PasswordMigratingListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/PasswordMigratingListenerTest.php index 37d9ee23cc..5b08721e46 100644 --- a/src/Symfony/Component/Security/Http/Tests/EventListener/PasswordMigratingListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/PasswordMigratingListenerTest.php @@ -12,13 +12,17 @@ namespace Symfony\Component\Security\Http\Tests\EventListener; use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface; use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; -use Symfony\Component\Security\Http\Authenticator\PasswordAuthenticatedInterface; -use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; +use Symfony\Component\Security\Http\Event\LoginSuccessEvent; use Symfony\Component\Security\Http\EventListener\PasswordMigratingListener; class PasswordMigratingListenerTest extends TestCase @@ -41,23 +45,19 @@ class PasswordMigratingListenerTest extends TestCase { $this->encoderFactory->expects($this->never())->method('getEncoder'); - $this->listener->onCredentialsVerification($event); + $this->listener->onLoginSuccess($event); } public function provideUnsupportedEvents() { - // unsupported authenticators - yield [$this->createEvent($this->createMock(AuthenticatorInterface::class), $this->user)]; - yield [$this->createEvent($this->createMock([AuthenticatorInterface::class, PasswordAuthenticatedInterface::class]), $this->user)]; + // no password upgrade badge + yield [$this->createEvent(new SelfValidatingPassport($this->createMock(UserInterface::class)))]; - // null password - yield [$this->createEvent($this->createAuthenticator(null), $this->user)]; + // blank password + yield [$this->createEvent(new SelfValidatingPassport($this->createMock(UserInterface::class), [new PasswordUpgradeBadge('', $this->createPasswordUpgrader())]))]; // no user - yield [$this->createEvent($this->createAuthenticator('pa$$word'), null)]; - - // invalid password - yield [$this->createEvent($this->createAuthenticator('pa$$word'), $this->user, false)]; + yield [$this->createEvent($this->createMock(PassportInterface::class))]; } public function testUpgrade() @@ -70,32 +70,23 @@ class PasswordMigratingListenerTest extends TestCase $this->user->expects($this->any())->method('getPassword')->willReturn('old-encoded-password'); - $authenticator = $this->createAuthenticator('pa$$word'); - $authenticator->expects($this->once()) + $passwordUpgrader = $this->createPasswordUpgrader(); + $passwordUpgrader->expects($this->once()) ->method('upgradePassword') ->with($this->user, 'new-encoded-password') ; - $event = $this->createEvent($authenticator, $this->user); - $this->listener->onCredentialsVerification($event); + $event = $this->createEvent(new SelfValidatingPassport($this->user, [new PasswordUpgradeBadge('pa$$word', $passwordUpgrader)])); + $this->listener->onLoginSuccess($event); } - /** - * @return AuthenticatorInterface - */ - private function createAuthenticator($password) + private function createPasswordUpgrader() { - $authenticator = $this->createMock([AuthenticatorInterface::class, PasswordAuthenticatedInterface::class, PasswordUpgraderInterface::class]); - $authenticator->expects($this->any())->method('getPassword')->willReturn($password); - - return $authenticator; + return $this->createMock(PasswordUpgraderInterface::class); } - private function createEvent($authenticator, $user, $credentialsValid = true) + private function createEvent(PassportInterface $passport) { - $event = new VerifyAuthenticatorCredentialsEvent($authenticator, [], $user); - $event->setCredentialsValid($credentialsValid); - - return $event; + return new LoginSuccessEvent($this->createMock(AuthenticatorInterface::class), $passport, $this->createMock(TokenInterface::class), new Request(), null, 'main'); } } diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/RememberMeListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/RememberMeListenerTest.php index 910c67a0bd..9af16a6a76 100644 --- a/src/Symfony/Component/Security/Http/Tests/EventListener/RememberMeListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/RememberMeListenerTest.php @@ -16,8 +16,11 @@ 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\User; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; -use Symfony\Component\Security\Http\Authenticator\RememberMeAuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; use Symfony\Component\Security\Http\Event\LoginFailureEvent; use Symfony\Component\Security\Http\Event\LoginSuccessEvent; use Symfony\Component\Security\Http\EventListener\RememberMeListener; @@ -40,26 +43,14 @@ class RememberMeListenerTest extends TestCase $this->token = $this->createMock(TokenInterface::class); } - /** - * @dataProvider provideUnsupportingAuthenticators - */ - public function testSuccessfulLoginWithoutSupportingAuthenticator($authenticator) + public function testSuccessfulLoginWithoutSupportingAuthenticator() { $this->rememberMeServices->expects($this->never())->method('loginSuccess'); - $event = $this->createLoginSuccessfulEvent('main_firewall', $this->response, $authenticator); + $event = $this->createLoginSuccessfulEvent('main_firewall', $this->response, new SelfValidatingPassport(new User('wouter', null))); $this->listener->onSuccessfulLogin($event); } - public function provideUnsupportingAuthenticators() - { - yield [$this->createMock(AuthenticatorInterface::class)]; - - $authenticator = $this->createMock([AuthenticatorInterface::class, RememberMeAuthenticatorInterface::class]); - $authenticator->expects($this->any())->method('supportsRememberMe')->willReturn(false); - yield [$authenticator]; - } - public function testSuccessfulLoginWithoutSuccessResponse() { $this->rememberMeServices->expects($this->never())->method('loginSuccess'); @@ -84,14 +75,13 @@ class RememberMeListenerTest extends TestCase $this->listener->onFailedLogin($event); } - private function createLoginSuccessfulEvent($providerKey, $response, $authenticator = null) + private function createLoginSuccessfulEvent($providerKey, $response, PassportInterface $passport = null) { - if (null === $authenticator) { - $authenticator = $this->createMock([AuthenticatorInterface::class, RememberMeAuthenticatorInterface::class]); - $authenticator->expects($this->any())->method('supportsRememberMe')->willReturn(true); + if (null === $passport) { + $passport = new SelfValidatingPassport(new User('test', null), [new RememberMeBadge()]); } - return new LoginSuccessEvent($authenticator, $this->token, $this->request, $response, $providerKey); + return new LoginSuccessEvent($this->createMock(AuthenticatorInterface::class), $passport, $this->token, $this->request, $response, $providerKey); } private function createLoginFailureEvent($providerKey) diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/SessionListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/SessionStrategyListenerTest.php similarity index 89% rename from src/Symfony/Component/Security/Http/Tests/EventListener/SessionListenerTest.php rename to src/Symfony/Component/Security/Http/Tests/EventListener/SessionStrategyListenerTest.php index 176921d1a1..4d1dd0a5be 100644 --- a/src/Symfony/Component/Security/Http/Tests/EventListener/SessionListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/SessionStrategyListenerTest.php @@ -14,12 +14,14 @@ namespace Symfony\Component\Security\Http\Tests\EventListener; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; use Symfony\Component\Security\Http\Event\LoginSuccessEvent; use Symfony\Component\Security\Http\EventListener\SessionStrategyListener; use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface; -class SessionListenerTest extends TestCase +class SessionStrategyListenerTest extends TestCase { private $sessionAuthenticationStrategy; private $listener; @@ -60,7 +62,7 @@ class SessionListenerTest extends TestCase private function createEvent($providerKey) { - return new LoginSuccessEvent($this->createMock(AuthenticatorInterface::class), $this->token, $this->request, null, $providerKey); + return new LoginSuccessEvent($this->createMock(AuthenticatorInterface::class), new SelfValidatingPassport(new User('test', null)), $this->token, $this->request, null, $providerKey); } private function configurePreviousSession() diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/UserCheckerListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/UserCheckerListenerTest.php index 785a312963..dac1fbaf92 100644 --- a/src/Symfony/Component/Security/Http/Tests/EventListener/UserCheckerListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/UserCheckerListenerTest.php @@ -12,9 +12,15 @@ namespace Symfony\Component\Security\Http\Tests\EventListener; use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Core\User\UserCheckerInterface; -use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PreAuthenticatedUserBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; +use Symfony\Component\Security\Http\Event\LoginSuccessEvent; use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; use Symfony\Component\Security\Http\EventListener\UserCheckerListener; @@ -28,51 +34,59 @@ class UserCheckerListenerTest extends TestCase { $this->userChecker = $this->createMock(UserCheckerInterface::class); $this->listener = new UserCheckerListener($this->userChecker); - $this->user = $this->createMock(UserInterface::class); + $this->user = new User('test', null); } public function testPreAuth() { $this->userChecker->expects($this->once())->method('checkPreAuth')->with($this->user); - $this->listener->preCredentialsVerification($this->createEvent()); + $this->listener->preCredentialsVerification($this->createVerifyAuthenticatorCredentialsEvent()); } public function testPreAuthNoUser() { $this->userChecker->expects($this->never())->method('checkPreAuth'); - $this->listener->preCredentialsVerification($this->createEvent(true, null)); + $this->listener->preCredentialsVerification($this->createVerifyAuthenticatorCredentialsEvent($this->createMock(PassportInterface::class))); + } + + public function testPreAuthenticatedBadge() + { + $this->userChecker->expects($this->never())->method('checkPreAuth'); + + $this->listener->preCredentialsVerification($this->createVerifyAuthenticatorCredentialsEvent(new SelfValidatingPassport($this->user, [new PreAuthenticatedUserBadge()]))); } public function testPostAuthValidCredentials() { $this->userChecker->expects($this->once())->method('checkPostAuth')->with($this->user); - $this->listener->postCredentialsVerification($this->createEvent(true)); - } - - public function testPostAuthInvalidCredentials() - { - $this->userChecker->expects($this->never())->method('checkPostAuth')->with($this->user); - - $this->listener->postCredentialsVerification($this->createEvent()); + $this->listener->postCredentialsVerification($this->createLoginSuccessEvent()); } public function testPostAuthNoUser() { $this->userChecker->expects($this->never())->method('checkPostAuth'); - $this->listener->postCredentialsVerification($this->createEvent(true, null)); + $this->listener->postCredentialsVerification($this->createLoginSuccessEvent($this->createMock(PassportInterface::class))); } - private function createEvent($credentialsValid = false, $customUser = false) + private function createVerifyAuthenticatorCredentialsEvent($passport = null) { - $event = new VerifyAuthenticatorCredentialsEvent($this->createMock(AuthenticatorInterface::class), [], false === $customUser ? $this->user : $customUser); - if ($credentialsValid) { - $event->setCredentialsValid(true); + if (null === $passport) { + $passport = new SelfValidatingPassport($this->user); } - return $event; + return new VerifyAuthenticatorCredentialsEvent($this->createMock(AuthenticatorInterface::class), $passport); + } + + private function createLoginSuccessEvent($passport = null) + { + if (null === $passport) { + $passport = new SelfValidatingPassport($this->user); + } + + return new LoginSuccessEvent($this->createMock(AuthenticatorInterface::class), $passport, $this->createMock(TokenInterface::class), new Request(), null, 'main'); } } diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/VerifyAuthenticatorCredentialsListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/VerifyAuthenticatorCredentialsListenerTest.php index e2c2cc6605..a4850ebda7 100644 --- a/src/Symfony/Component/Security/Http/Tests/EventListener/VerifyAuthenticatorCredentialsListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/VerifyAuthenticatorCredentialsListenerTest.php @@ -15,12 +15,12 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface; use Symfony\Component\Security\Core\Exception\BadCredentialsException; -use Symfony\Component\Security\Core\Exception\LogicException; -use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; -use Symfony\Component\Security\Http\Authenticator\CustomAuthenticatedInterface; -use Symfony\Component\Security\Http\Authenticator\PasswordAuthenticatedInterface; -use Symfony\Component\Security\Http\Authenticator\TokenAuthenticatedInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CustomCredentials; +use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; +use Symfony\Component\Security\Http\Authenticator\Passport\Passport; +use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; use Symfony\Component\Security\Http\EventListener\VerifyAuthenticatorCredentialsListener; @@ -34,7 +34,7 @@ class VerifyAuthenticatorCredentialsListenerTest extends TestCase { $this->encoderFactory = $this->createMock(EncoderFactoryInterface::class); $this->listener = new VerifyAuthenticatorCredentialsListener($this->encoderFactory); - $this->user = $this->createMock(UserInterface::class); + $this->user = new User('wouter', 'encoded-password'); } /** @@ -42,16 +42,22 @@ class VerifyAuthenticatorCredentialsListenerTest extends TestCase */ public function testPasswordAuthenticated($password, $passwordValid, $result) { - $this->user->expects($this->any())->method('getPassword')->willReturn('encoded-password'); - $encoder = $this->createMock(PasswordEncoderInterface::class); $encoder->expects($this->any())->method('isPasswordValid')->with('encoded-password', $password)->willReturn($passwordValid); $this->encoderFactory->expects($this->any())->method('getEncoder')->with($this->identicalTo($this->user))->willReturn($encoder); - $event = new VerifyAuthenticatorCredentialsEvent($this->createAuthenticator('password', $password), ['password' => $password], $this->user); - $this->listener->onAuthenticating($event); - $this->assertEquals($result, $event->areCredentialsValid()); + if (false === $result) { + $this->expectException(BadCredentialsException::class); + $this->expectExceptionMessage('The presented password is invalid.'); + } + + $credentials = new PasswordCredentials($password); + $this->listener->onAuthenticating($this->createEvent(new Passport($this->user, $credentials))); + + if (true === $result) { + $this->assertTrue($credentials->isResolved()); + } } public function providePasswords() @@ -67,30 +73,10 @@ class VerifyAuthenticatorCredentialsListenerTest extends TestCase $this->encoderFactory->expects($this->never())->method('getEncoder'); - $event = new VerifyAuthenticatorCredentialsEvent($this->createAuthenticator('password', ''), ['password' => ''], $this->user); + $event = $this->createEvent(new Passport($this->user, new PasswordCredentials(''))); $this->listener->onAuthenticating($event); } - public function testTokenAuthenticated() - { - $this->encoderFactory->expects($this->never())->method('getEncoder'); - - $event = new VerifyAuthenticatorCredentialsEvent($this->createAuthenticator('token', 'some_token'), ['token' => 'abc'], $this->user); - $this->listener->onAuthenticating($event); - - $this->assertTrue($event->areCredentialsValid()); - } - - public function testTokenAuthenticatedReturningNull() - { - $this->encoderFactory->expects($this->never())->method('getEncoder'); - - $event = new VerifyAuthenticatorCredentialsEvent($this->createAuthenticator('token', null), ['token' => 'abc'], $this->user); - $this->listener->onAuthenticating($event); - - $this->assertFalse($event->areCredentialsValid()); - } - /** * @dataProvider provideCustomAuthenticatedResults */ @@ -98,10 +84,18 @@ class VerifyAuthenticatorCredentialsListenerTest extends TestCase { $this->encoderFactory->expects($this->never())->method('getEncoder'); - $event = new VerifyAuthenticatorCredentialsEvent($this->createAuthenticator('custom', $result), [], $this->user); - $this->listener->onAuthenticating($event); + if (false === $result) { + $this->expectException(BadCredentialsException::class); + } - $this->assertEquals($result, $event->areCredentialsValid()); + $credentials = new CustomCredentials(function () use ($result) { + return $result; + }, ['password' => 'foo']); + $this->listener->onAuthenticating($this->createEvent(new Passport($this->user, $credentials))); + + if (true === $result) { + $this->assertTrue($credentials->isResolved()); + } } public function provideCustomAuthenticatedResults() @@ -110,58 +104,16 @@ class VerifyAuthenticatorCredentialsListenerTest extends TestCase yield [false]; } - public function testAlreadyAuthenticated() + public function testNoCredentialsBadgeProvided() { - $event = new VerifyAuthenticatorCredentialsEvent($this->createAuthenticator(), [], $this->user); - $event->setCredentialsValid(true); - $this->listener->onAuthenticating($event); - - $this->assertTrue($event->areCredentialsValid()); - } - - public function testNoAuthenticatedInterfaceImplemented() - { - $authenticator = $this->createAuthenticator(); - $this->expectException(LogicException::class); - $this->expectExceptionMessage(sprintf('Authenticator %s does not have valid credentials. Authenticators must implement one of the authenticated interfaces (%s, %s or %s).', \get_class($authenticator), PasswordAuthenticatedInterface::class, TokenAuthenticatedInterface::class, CustomAuthenticatedInterface::class)); - $this->encoderFactory->expects($this->never())->method('getEncoder'); - $event = new VerifyAuthenticatorCredentialsEvent($authenticator, [], $this->user); + $event = $this->createEvent(new SelfValidatingPassport($this->user)); $this->listener->onAuthenticating($event); } - /** - * @return AuthenticatorInterface - */ - private function createAuthenticator(?string $type = null, $result = null) + private function createEvent($passport) { - $interfaces = [AuthenticatorInterface::class]; - switch ($type) { - case 'password': - $interfaces[] = PasswordAuthenticatedInterface::class; - break; - case 'token': - $interfaces[] = TokenAuthenticatedInterface::class; - break; - case 'custom': - $interfaces[] = CustomAuthenticatedInterface::class; - break; - } - - $authenticator = $this->createMock(1 === \count($interfaces) ? $interfaces[0] : $interfaces); - switch ($type) { - case 'password': - $authenticator->expects($this->any())->method('getPassword')->willReturn($result); - break; - case 'token': - $authenticator->expects($this->any())->method('getToken')->willReturn($result); - break; - case 'custom': - $authenticator->expects($this->any())->method('checkCredentials')->willReturn($result); - break; - } - - return $authenticator; + return new VerifyAuthenticatorCredentialsEvent($this->createMock(AuthenticatorInterface::class), $passport); } } From b1e040f311e16f4888f42564a6d5da0a489c929a Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Fri, 10 Apr 2020 23:45:43 +0200 Subject: [PATCH 30/30] Rename providerKey to firewallName for more consistent naming --- .../Security/Factory/AnonymousFactory.php | 4 +- .../Factory/AuthenticatorFactoryInterface.php | 2 +- .../Factory/CustomAuthenticatorFactory.php | 2 +- .../Security/Factory/FormLoginFactory.php | 8 ++-- .../Security/Factory/HttpBasicFactory.php | 4 +- .../Security/Factory/JsonLoginFactory.php | 8 ++-- .../Security/Factory/RememberMeFactory.php | 12 +++--- .../Security/Factory/RemoteUserFactory.php | 4 +- .../Security/Factory/X509Factory.php | 4 +- .../DependencyInjection/SecurityExtension.php | 14 +++---- .../Authentication/AuthenticatorManager.php | 39 ++++++++++--------- .../Authenticator/AbstractAuthenticator.php | 4 +- .../AbstractPreAuthenticatedAuthenticator.php | 6 +-- .../Authenticator/AnonymousAuthenticator.php | 4 +- .../Authenticator/AuthenticatorInterface.php | 4 +- .../Authenticator/FormLoginAuthenticator.php | 6 +-- .../Authenticator/HttpBasicAuthenticator.php | 6 +-- .../Authenticator/JsonLoginAuthenticator.php | 6 +-- .../Authenticator/RememberMeAuthenticator.php | 6 +-- .../Token/PostAuthenticationToken.php | 26 +++++-------- .../Security/Http/Event/LoginFailureEvent.php | 10 ++--- .../Security/Http/Event/LoginSuccessEvent.php | 6 +-- .../AuthenticatorManagerTest.php | 10 ++--- 23 files changed, 95 insertions(+), 100 deletions(-) diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php index cf77d99fdf..53a6b503a1 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php @@ -42,13 +42,13 @@ class AnonymousFactory implements SecurityFactoryInterface, AuthenticatorFactory return [$providerId, $listenerId, $defaultEntryPoint]; } - public function createAuthenticator(ContainerBuilder $container, string $id, array $config, string $userProviderId): string + public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string { if (null === $config['secret']) { $config['secret'] = new Parameter('container.build_hash'); } - $authenticatorId = 'security.authenticator.anonymous.'.$id; + $authenticatorId = 'security.authenticator.anonymous.'.$firewallName; $container ->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.anonymous')) ->replaceArgument(0, $config['secret']); diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AuthenticatorFactoryInterface.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AuthenticatorFactoryInterface.php index acd1fce318..cb65f31fe5 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AuthenticatorFactoryInterface.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AuthenticatorFactoryInterface.php @@ -25,5 +25,5 @@ interface AuthenticatorFactoryInterface * * @return string|string[] The authenticator service ID(s) to be used by the firewall */ - public function createAuthenticator(ContainerBuilder $container, string $id, array $config, string $userProviderId); + public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId); } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/CustomAuthenticatorFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/CustomAuthenticatorFactory.php index 43c236fcfa..95fa3c050f 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/CustomAuthenticatorFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/CustomAuthenticatorFactory.php @@ -49,7 +49,7 @@ class CustomAuthenticatorFactory implements AuthenticatorFactoryInterface, Secur ; } - public function createAuthenticator(ContainerBuilder $container, string $id, array $config, string $userProviderId): array + public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): array { return $config['services']; } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php index 2edfb3ff34..c5f247c307 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php @@ -103,19 +103,19 @@ class FormLoginFactory extends AbstractFactory implements AuthenticatorFactoryIn return $entryPointId; } - public function createAuthenticator(ContainerBuilder $container, string $id, array $config, string $userProviderId): string + public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string { if (isset($config['csrf_token_generator'])) { throw new InvalidConfigurationException('The "csrf_token_generator" option of "form_login" is only available when "security.enable_authenticator_manager" is set to "false", use "enable_csrf" instead.'); } - $authenticatorId = 'security.authenticator.form_login.'.$id; + $authenticatorId = 'security.authenticator.form_login.'.$firewallName; $options = array_intersect_key($config, $this->options); $container ->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.form_login')) ->replaceArgument(1, new Reference($userProviderId)) - ->replaceArgument(2, new Reference($this->createAuthenticationSuccessHandler($container, $id, $config))) - ->replaceArgument(3, new Reference($this->createAuthenticationFailureHandler($container, $id, $config))) + ->replaceArgument(2, new Reference($this->createAuthenticationSuccessHandler($container, $firewallName, $config))) + ->replaceArgument(3, new Reference($this->createAuthenticationFailureHandler($container, $firewallName, $config))) ->replaceArgument(4, $options); return $authenticatorId; diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php index 9d121b17fe..a698d2a1d1 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php @@ -46,9 +46,9 @@ class HttpBasicFactory implements SecurityFactoryInterface, AuthenticatorFactory return [$provider, $listenerId, $entryPointId]; } - public function createAuthenticator(ContainerBuilder $container, string $id, array $config, string $userProviderId): string + public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string { - $authenticatorId = 'security.authenticator.http_basic.'.$id; + $authenticatorId = 'security.authenticator.http_basic.'.$firewallName; $container ->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.http_basic')) ->replaceArgument(0, $config['realm']) diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/JsonLoginFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/JsonLoginFactory.php index 4e09a3d2f8..7aa9040579 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/JsonLoginFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/JsonLoginFactory.php @@ -97,15 +97,15 @@ class JsonLoginFactory extends AbstractFactory implements AuthenticatorFactoryIn return $listenerId; } - public function createAuthenticator(ContainerBuilder $container, string $id, array $config, string $userProviderId) + public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId) { - $authenticatorId = 'security.authenticator.json_login.'.$id; + $authenticatorId = 'security.authenticator.json_login.'.$firewallName; $options = array_intersect_key($config, $this->options); $container ->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.json_login')) ->replaceArgument(1, new Reference($userProviderId)) - ->replaceArgument(2, isset($config['success_handler']) ? new Reference($this->createAuthenticationSuccessHandler($container, $id, $config)) : null) - ->replaceArgument(3, isset($config['failure_handler']) ? new Reference($this->createAuthenticationFailureHandler($container, $id, $config)) : null) + ->replaceArgument(2, isset($config['success_handler']) ? new Reference($this->createAuthenticationSuccessHandler($container, $firewallName, $config)) : null) + ->replaceArgument(3, isset($config['failure_handler']) ? new Reference($this->createAuthenticationFailureHandler($container, $firewallName, $config)) : null) ->replaceArgument(4, $options); return $authenticatorId; diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php index 5f530a17e2..4b29db1a03 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php @@ -89,19 +89,19 @@ class RememberMeFactory implements SecurityFactoryInterface, AuthenticatorFactor return [$authProviderId, $listenerId, $defaultEntryPoint]; } - public function createAuthenticator(ContainerBuilder $container, string $id, array $config, string $userProviderId): string + public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string { - $templateId = $this->generateRememberMeServicesTemplateId($config, $id); - $rememberMeServicesId = $templateId.'.'.$id; + $templateId = $this->generateRememberMeServicesTemplateId($config, $firewallName); + $rememberMeServicesId = $templateId.'.'.$firewallName; // create remember me services (which manage the remember me cookies) - $this->createRememberMeServices($container, $id, $templateId, [new Reference($userProviderId)], $config); + $this->createRememberMeServices($container, $firewallName, $templateId, [new Reference($userProviderId)], $config); // create remember me listener (which executes the remember me services for other authenticators and logout) - $this->createRememberMeListener($container, $id, $rememberMeServicesId); + $this->createRememberMeListener($container, $firewallName, $rememberMeServicesId); // create remember me authenticator (which re-authenticates the user based on the remember me cookie) - $authenticatorId = 'security.authenticator.remember_me.'.$id; + $authenticatorId = 'security.authenticator.remember_me.'.$firewallName; $container ->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.remember_me')) ->replaceArgument(0, new Reference($rememberMeServicesId)) diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RemoteUserFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RemoteUserFactory.php index 0f0c44f8ab..e25c3c7d07 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RemoteUserFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RemoteUserFactory.php @@ -43,9 +43,9 @@ class RemoteUserFactory implements SecurityFactoryInterface, AuthenticatorFactor return [$providerId, $listenerId, $defaultEntryPoint]; } - public function createAuthenticator(ContainerBuilder $container, string $id, array $config, string $userProviderId) + public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId) { - $authenticatorId = 'security.authenticator.remote_user.'.$id; + $authenticatorId = 'security.authenticator.remote_user.'.$firewallName; $container ->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.remote_user')) ->replaceArgument(0, new Reference($userProviderId)) diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/X509Factory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/X509Factory.php index 604cee7e44..f966302a1d 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/X509Factory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/X509Factory.php @@ -44,9 +44,9 @@ class X509Factory implements SecurityFactoryInterface, AuthenticatorFactoryInter return [$providerId, $listenerId, $defaultEntryPoint]; } - public function createAuthenticator(ContainerBuilder $container, string $id, array $config, string $userProviderId) + public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId) { - $authenticatorId = 'security.authenticator.x509.'.$id; + $authenticatorId = 'security.authenticator.x509.'.$firewallName; $container ->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.x509')) ->replaceArgument(0, new Reference($userProviderId)) diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 35bcf01557..ac089d1eb2 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -286,7 +286,7 @@ class SecurityExtension extends Extension implements PrependExtensionInterface // add authentication providers to authentication manager $authenticationProviders = array_map(function ($id) { return new Reference($id); - }, array_unique($authenticationProviders)); + }, array_values(array_unique($authenticationProviders))); $container ->getDefinition('security.authentication.manager') @@ -439,9 +439,9 @@ class SecurityExtension extends Extension implements PrependExtensionInterface $firewallAuthenticationProviders = []; list($authListeners, $defaultEntryPoint) = $this->createAuthenticationListeners($container, $id, $firewall, $firewallAuthenticationProviders, $defaultProvider, $providerIds, $configuredEntryPoint, $contextListenerId); - $authenticationProviders = array_merge($authenticationProviders, $firewallAuthenticationProviders); - - if ($this->authenticatorManagerEnabled) { + if (!$this->authenticatorManagerEnabled) { + $authenticationProviders = array_merge($authenticationProviders, $firewallAuthenticationProviders); + } else { // authenticator manager $authenticators = array_map(function ($id) { return new Reference($id); @@ -535,10 +535,10 @@ class SecurityExtension extends Extension implements PrependExtensionInterface $authenticators = $factory->createAuthenticator($container, $id, $firewall[$key], $userProvider); if (\is_array($authenticators)) { foreach ($authenticators as $i => $authenticator) { - $authenticationProviders[$id.'_'.$key.$i] = $authenticator; + $authenticationProviders[] = $authenticator; } } else { - $authenticationProviders[$id.'_'.$key] = $authenticators; + $authenticationProviders[] = $authenticators; } if ($factory instanceof EntryPointFactoryInterface) { @@ -548,7 +548,7 @@ class SecurityExtension extends Extension implements PrependExtensionInterface list($provider, $listenerId, $defaultEntryPoint) = $factory->create($container, $id, $firewall[$key], $userProvider, $defaultEntryPoint); $listeners[] = new Reference($listenerId); - $authenticationProviders[$id.'_'.$key] = $provider; + $authenticationProviders[] = $provider; } $hasListeners = true; } diff --git a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php index 36a9916105..1d6e1ff2ac 100644 --- a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php +++ b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php @@ -47,17 +47,17 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent private $eventDispatcher; private $eraseCredentials; private $logger; - private $providerKey; + private $firewallName; /** - * @param AuthenticatorInterface[] $authenticators The authenticators, with their unique providerKey as key + * @param AuthenticatorInterface[] $authenticators */ - public function __construct(iterable $authenticators, TokenStorageInterface $tokenStorage, EventDispatcherInterface $eventDispatcher, string $providerKey, ?LoggerInterface $logger = null, bool $eraseCredentials = true) + public function __construct(iterable $authenticators, TokenStorageInterface $tokenStorage, EventDispatcherInterface $eventDispatcher, string $firewallName, ?LoggerInterface $logger = null, bool $eraseCredentials = true) { $this->authenticators = $authenticators; $this->tokenStorage = $tokenStorage; $this->eventDispatcher = $eventDispatcher; - $this->providerKey = $providerKey; + $this->firewallName = $firewallName; $this->logger = $logger; $this->eraseCredentials = $eraseCredentials; } @@ -68,7 +68,7 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent public function authenticateUser(UserInterface $user, AuthenticatorInterface $authenticator, Request $request, array $badges = []): ?Response { // create an authenticated token for the User - $token = $authenticator->createAuthenticatedToken($passport = new SelfValidatingPassport($user, $badges), $this->providerKey); + $token = $authenticator->createAuthenticatedToken($passport = new SelfValidatingPassport($user, $badges), $this->firewallName); // authenticate this in the system return $this->handleAuthenticationSuccess($token, $passport, $request, $authenticator); @@ -77,27 +77,27 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent public function supports(Request $request): ?bool { if (null !== $this->logger) { - $context = ['firewall_key' => $this->providerKey]; + $context = ['firewall_key' => $this->firewallName]; if ($this->authenticators instanceof \Countable || \is_array($this->authenticators)) { $context['authenticators'] = \count($this->authenticators); } - $this->logger->debug('Checking for guard authentication credentials.', $context); + $this->logger->debug('Checking for authenticator support.', $context); } $authenticators = []; $lazy = true; - foreach ($this->authenticators as $key => $authenticator) { + foreach ($this->authenticators as $authenticator) { if (null !== $this->logger) { - $this->logger->debug('Checking support on authenticator.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($authenticator)]); + $this->logger->debug('Checking support on authenticator.', ['firewall_key' => $this->firewallName, 'authenticator' => \get_class($authenticator)]); } if (false !== $supports = $authenticator->supports($request)) { - $authenticators[$key] = $authenticator; + $authenticators[] = $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)]); + $this->logger->debug('Authenticator does not support the request.', ['firewall_key' => $this->firewallName, 'authenticator' => \get_class($authenticator)]); } } @@ -105,15 +105,15 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent return false; } - $request->attributes->set('_guard_authenticators', $authenticators); + $request->attributes->set('_security_authenticators', $authenticators); return $lazy ? null : true; } public function authenticateRequest(Request $request): ?Response { - $authenticators = $request->attributes->get('_guard_authenticators'); - $request->attributes->remove('_guard_authenticators'); + $authenticators = $request->attributes->get('_security_authenticators'); + $request->attributes->remove('_security_authenticators'); if (!$authenticators) { return null; } @@ -126,8 +126,8 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent */ private function executeAuthenticators(array $authenticators, Request $request): ?Response { - foreach ($authenticators as $key => $authenticator) { - // recheck if the authenticator still supports the listener. support() is called + foreach ($authenticators as $authenticator) { + // recheck if the authenticator still supports the listener. supports() 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. @@ -135,6 +135,7 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent if (null !== $this->logger) { $this->logger->debug('Skipping the "{authenticator}" authenticator as it did not support the request.', ['authenticator' => \get_class($authenticator)]); } + continue; } @@ -165,7 +166,7 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent $passport->checkIfCompletelyResolved(); // create the authenticated token - $authenticatedToken = $authenticator->createAuthenticatedToken($passport, $this->providerKey); + $authenticatedToken = $authenticator->createAuthenticatedToken($passport, $this->firewallName); if (true === $this->eraseCredentials) { $authenticatedToken->eraseCredentials(); } @@ -204,7 +205,7 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent { $this->tokenStorage->setToken($authenticatedToken); - $response = $authenticator->onAuthenticationSuccess($request, $authenticatedToken, $this->providerKey); + $response = $authenticator->onAuthenticationSuccess($request, $authenticatedToken, $this->firewallName); if ($authenticator instanceof InteractiveAuthenticatorInterface && $authenticator->isInteractive()) { $loginEvent = new InteractiveLoginEvent($request, $authenticatedToken); $this->eventDispatcher->dispatch($loginEvent, SecurityEvents::INTERACTIVE_LOGIN); @@ -233,7 +234,7 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent $this->logger->debug('The "{authenticator}" authenticator set the failure response.', ['authenticator' => \get_class($authenticator)]); } - $this->eventDispatcher->dispatch($loginFailureEvent = new LoginFailureEvent($authenticationException, $authenticator, $request, $response, $this->providerKey)); + $this->eventDispatcher->dispatch($loginFailureEvent = new LoginFailureEvent($authenticationException, $authenticator, $request, $response, $this->firewallName)); // returning null is ok, it means they want the request to continue return $loginFailureEvent->getResponse(); diff --git a/src/Symfony/Component/Security/Http/Authenticator/AbstractAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/AbstractAuthenticator.php index 51a49a3b17..6a5ec2f150 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AbstractAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AbstractAuthenticator.php @@ -32,12 +32,12 @@ abstract class AbstractAuthenticator implements AuthenticatorInterface * * @return PostAuthenticationToken */ - public function createAuthenticatedToken(PassportInterface $passport, string $providerKey): TokenInterface + public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface { if (!$passport instanceof UserPassportInterface) { throw new LogicException(sprintf('Passport does not contain a user, overwrite "createAuthenticatedToken()" in "%s" to create a custom authenticated token.', \get_class($this))); } - return new PostAuthenticationToken($passport->getUser(), $providerKey, $passport->getUser()->getRoles()); + return new PostAuthenticationToken($passport->getUser(), $firewallName, $passport->getUser()->getRoles()); } } diff --git a/src/Symfony/Component/Security/Http/Authenticator/AbstractPreAuthenticatedAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/AbstractPreAuthenticatedAuthenticator.php index 435de68e98..85a578d8c6 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AbstractPreAuthenticatedAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AbstractPreAuthenticatedAuthenticator.php @@ -92,12 +92,12 @@ abstract class AbstractPreAuthenticatedAuthenticator implements InteractiveAuthe return new SelfValidatingPassport($user, [new PreAuthenticatedUserBadge()]); } - public function createAuthenticatedToken(PassportInterface $passport, string $providerKey): TokenInterface + public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface { - return new PreAuthenticatedToken($passport->getUser(), null, $providerKey, $passport->getUser()->getRoles()); + return new PreAuthenticatedToken($passport->getUser(), null, $firewallName, $passport->getUser()->getRoles()); } - public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response { return null; // let the original request continue } diff --git a/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php index 27a315b0f5..c0420b5d4c 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php @@ -50,12 +50,12 @@ class AnonymousAuthenticator implements AuthenticatorInterface return new AnonymousPassport(); } - public function createAuthenticatedToken(PassportInterface $passport, string $providerKey): TokenInterface + public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface { return new AnonymousToken($this->secret, 'anon.', []); } - public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response { return null; // let the original request continue } diff --git a/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php b/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php index d80356e713..e1f2b21f70 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php @@ -63,7 +63,7 @@ interface AuthenticatorInterface * * @param PassportInterface $passport The passport returned from authenticate() */ - public function createAuthenticatedToken(PassportInterface $passport, string $providerKey): TokenInterface; + public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface; /** * Called when authentication executed and was successful! @@ -74,7 +74,7 @@ interface AuthenticatorInterface * If you return null, the current request will continue, and the user * will be authenticated. This makes sense, for example, with an API. */ - public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response; + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response; /** * Called when authentication executed, but failed (e.g. wrong username password). diff --git a/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php index 0bbbb6eb83..31cab7afcd 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php @@ -100,12 +100,12 @@ class FormLoginAuthenticator extends AbstractLoginFormAuthenticator /** * @param Passport $passport */ - public function createAuthenticatedToken(PassportInterface $passport, $providerKey): TokenInterface + public function createAuthenticatedToken(PassportInterface $passport, $firewallName): TokenInterface { - return new UsernamePasswordToken($passport->getUser(), null, $providerKey, $passport->getUser()->getRoles()); + return new UsernamePasswordToken($passport->getUser(), null, $firewallName, $passport->getUser()->getRoles()); } - public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response { return $this->successHandler->onAuthenticationSuccess($request, $token); } diff --git a/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php index 46eb6aa7bc..e4c7af251e 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php @@ -82,12 +82,12 @@ class HttpBasicAuthenticator implements AuthenticatorInterface, AuthenticationEn /** * @param Passport $passport */ - public function createAuthenticatedToken(PassportInterface $passport, $providerKey): TokenInterface + public function createAuthenticatedToken(PassportInterface $passport, $firewallName): TokenInterface { - return new UsernamePasswordToken($passport->getUser(), null, $providerKey, $passport->getUser()->getRoles()); + return new UsernamePasswordToken($passport->getUser(), null, $firewallName, $passport->getUser()->getRoles()); } - public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey): ?Response + public function onAuthenticationSuccess(Request $request, TokenInterface $token, $firewallName): ?Response { return null; } diff --git a/src/Symfony/Component/Security/Http/Authenticator/JsonLoginAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/JsonLoginAuthenticator.php index 924ed7fcca..d165fbceb1 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/JsonLoginAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/JsonLoginAuthenticator.php @@ -93,12 +93,12 @@ class JsonLoginAuthenticator implements InteractiveAuthenticatorInterface return $passport; } - public function createAuthenticatedToken(PassportInterface $passport, string $providerKey): TokenInterface + public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface { - return new UsernamePasswordToken($passport->getUser(), null, $providerKey, $passport->getUser()->getRoles()); + return new UsernamePasswordToken($passport->getUser(), null, $firewallName, $passport->getUser()->getRoles()); } - public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response { if (null === $this->successHandler) { return null; // let the original request continue diff --git a/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php index 12a70d42b4..f5aa016ad1 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php @@ -76,12 +76,12 @@ class RememberMeAuthenticator implements InteractiveAuthenticatorInterface return new SelfValidatingPassport($token->getUser()); } - public function createAuthenticatedToken(PassportInterface $passport, string $providerKey): TokenInterface + public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface { - return new RememberMeToken($passport->getUser(), $providerKey, $this->secret); + return new RememberMeToken($passport->getUser(), $firewallName, $this->secret); } - public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response { return null; // let the original request continue } diff --git a/src/Symfony/Component/Security/Http/Authenticator/Token/PostAuthenticationToken.php b/src/Symfony/Component/Security/Http/Authenticator/Token/PostAuthenticationToken.php index 3525fa4765..774ba60a86 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/Token/PostAuthenticationToken.php +++ b/src/Symfony/Component/Security/Http/Authenticator/Token/PostAuthenticationToken.php @@ -7,24 +7,23 @@ use Symfony\Component\Security\Core\User\UserInterface; class PostAuthenticationToken extends AbstractToken { - private $providerKey; + private $firewallName; /** - * @param string $providerKey The provider (firewall) key - * @param string[] $roles An array of roles + * @param string[] $roles An array of roles * * @throws \InvalidArgumentException */ - public function __construct(UserInterface $user, string $providerKey, array $roles) + public function __construct(UserInterface $user, string $firewallName, array $roles) { parent::__construct($roles); - if (empty($providerKey)) { - throw new \InvalidArgumentException('$providerKey (i.e. firewall key) must not be empty.'); + if (empty($firewallName)) { + throw new \InvalidArgumentException('$firewallName must not be empty.'); } $this->setUser($user); - $this->providerKey = $providerKey; + $this->firewallName = $firewallName; // 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 @@ -42,14 +41,9 @@ class PostAuthenticationToken extends AbstractToken return []; } - /** - * Returns the provider (firewall) key. - * - * @return string - */ - public function getProviderKey() + public function getFirewallName(): string { - return $this->providerKey; + return $this->firewallName; } /** @@ -57,7 +51,7 @@ class PostAuthenticationToken extends AbstractToken */ public function __serialize(): array { - return [$this->providerKey, parent::__serialize()]; + return [$this->firewallName, parent::__serialize()]; } /** @@ -65,7 +59,7 @@ class PostAuthenticationToken extends AbstractToken */ public function __unserialize(array $data): void { - [$this->providerKey, $parentData] = $data; + [$this->firewallName, $parentData] = $data; parent::__unserialize($parentData); } } diff --git a/src/Symfony/Component/Security/Http/Event/LoginFailureEvent.php b/src/Symfony/Component/Security/Http/Event/LoginFailureEvent.php index 03a1c7a78c..96da4e35ff 100644 --- a/src/Symfony/Component/Security/Http/Event/LoginFailureEvent.php +++ b/src/Symfony/Component/Security/Http/Event/LoginFailureEvent.php @@ -22,15 +22,15 @@ class LoginFailureEvent extends Event private $authenticator; private $request; private $response; - private $providerKey; + private $firewallName; - public function __construct(AuthenticationException $exception, AuthenticatorInterface $authenticator, Request $request, ?Response $response, string $providerKey) + public function __construct(AuthenticationException $exception, AuthenticatorInterface $authenticator, Request $request, ?Response $response, string $firewallName) { $this->exception = $exception; $this->authenticator = $authenticator; $this->request = $request; $this->response = $response; - $this->providerKey = $providerKey; + $this->firewallName = $firewallName; } public function getException(): AuthenticationException @@ -43,9 +43,9 @@ class LoginFailureEvent extends Event return $this->authenticator; } - public function getProviderKey(): string + public function getFirewallName(): string { - return $this->providerKey; + return $this->firewallName; } public function getRequest(): Request diff --git a/src/Symfony/Component/Security/Http/Event/LoginSuccessEvent.php b/src/Symfony/Component/Security/Http/Event/LoginSuccessEvent.php index 80f740480b..c7eee3a66e 100644 --- a/src/Symfony/Component/Security/Http/Event/LoginSuccessEvent.php +++ b/src/Symfony/Component/Security/Http/Event/LoginSuccessEvent.php @@ -31,14 +31,14 @@ class LoginSuccessEvent extends Event private $response; private $providerKey; - public function __construct(AuthenticatorInterface $authenticator, PassportInterface $passport, TokenInterface $authenticatedToken, Request $request, ?Response $response, string $providerKey) + public function __construct(AuthenticatorInterface $authenticator, PassportInterface $passport, TokenInterface $authenticatedToken, Request $request, ?Response $response, string $firewallName) { $this->authenticator = $authenticator; $this->passport = $passport; $this->authenticatedToken = $authenticatedToken; $this->request = $request; $this->response = $response; - $this->providerKey = $providerKey; + $this->providerKey = $firewallName; } public function getAuthenticator(): AuthenticatorInterface @@ -70,7 +70,7 @@ class LoginSuccessEvent extends Event return $this->request; } - public function getProviderKey(): string + public function getFirewallName(): string { return $this->providerKey; } diff --git a/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php b/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php index 2cf7994db7..2b21b380d3 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php @@ -73,7 +73,7 @@ class AuthenticatorManagerTest extends TestCase // means support changed between calling supports() and authenticateRequest() // (which is the case with lazy firewalls and e.g. the AnonymousAuthenticator) $authenticator = $this->createAuthenticator(false); - $this->request->attributes->set('_guard_authenticators', [$authenticator]); + $this->request->attributes->set('_security_authenticators', [$authenticator]); $authenticator->expects($this->never())->method('authenticate'); @@ -87,7 +87,7 @@ class AuthenticatorManagerTest extends TestCase public function testAuthenticateRequest($matchingAuthenticatorIndex) { $authenticators = [$this->createAuthenticator(0 === $matchingAuthenticatorIndex), $this->createAuthenticator(1 === $matchingAuthenticatorIndex)]; - $this->request->attributes->set('_guard_authenticators', $authenticators); + $this->request->attributes->set('_security_authenticators', $authenticators); $matchingAuthenticator = $authenticators[$matchingAuthenticatorIndex]; $authenticators[($matchingAuthenticatorIndex + 1) % 2]->expects($this->never())->method('authenticate'); @@ -118,7 +118,7 @@ class AuthenticatorManagerTest extends TestCase public function testNoCredentialsValidated() { $authenticator = $this->createAuthenticator(); - $this->request->attributes->set('_guard_authenticators', [$authenticator]); + $this->request->attributes->set('_security_authenticators', [$authenticator]); $authenticator->expects($this->any())->method('authenticate')->willReturn(new Passport($this->user, new PasswordCredentials('pass'))); @@ -136,7 +136,7 @@ class AuthenticatorManagerTest extends TestCase public function testEraseCredentials($eraseCredentials) { $authenticator = $this->createAuthenticator(); - $this->request->attributes->set('_guard_authenticators', [$authenticator]); + $this->request->attributes->set('_security_authenticators', [$authenticator]); $authenticator->expects($this->any())->method('authenticate')->willReturn(new SelfValidatingPassport($this->user)); @@ -170,7 +170,7 @@ class AuthenticatorManagerTest extends TestCase { $authenticator = $this->createMock(InteractiveAuthenticatorInterface::class); $authenticator->expects($this->any())->method('isInteractive')->willReturn(true); - $this->request->attributes->set('_guard_authenticators', [$authenticator]); + $this->request->attributes->set('_security_authenticators', [$authenticator]); $authenticator->expects($this->any())->method('authenticate')->willReturn(new SelfValidatingPassport($this->user)); $authenticator->expects($this->any())->method('createAuthenticatedToken')->willReturn($this->token);