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 000000000000..0afa2121aab9 --- /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 fc500b285f16..83b082bddedc 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 7e9258a9c5b6..ac5c4cc2d483 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\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 @@ */ class GuardAuthenticationProvider implements AuthenticationProviderInterface { + use GuardAuthenticationProviderTrait; + /** * @var AuthenticatorInterface[] */ @@ -99,60 +96,6 @@ public function authenticate(TokenInterface $token) 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 @@ public function supports(TokenInterface $token) 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 000000000000..33e82eb0229d --- /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 000000000000..9ba11d0ddb15 --- /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 000000000000..b0bad3844ee1 --- /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 000000000000..e791d5240543 --- /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 b9eaa6824607..b751bde7f1f7 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 000000000000..9e923364ea3e --- /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 | 84 ++++++++++++++----- .../Resources/config/authenticators.xml | 16 ++++ .../Resources/config/security.xml | 11 ++- .../Bundle/SecurityBundle/SecurityBundle.php | 2 + 8 files changed, 187 insertions(+), 23 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 15ff8246f787..b0d7e5c342e4 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -73,6 +73,7 @@ public function getConfigTreeBuilder() ->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 000000000000..43c236fcfaf6 --- /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 000000000000..312f73499ab0 --- /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 f731469520b4..f50698fc67b2 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 @@ * * @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 @@ public function create(ContainerBuilder $container, string $id, array $config, s 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 924013306522..73b9a55a7cf5 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 @@ public function load(array $configs, ContainerBuilder $container) $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 @@ private function createFirewalls(array $config, ContainerBuilder $container) $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 @@ private function createAuthenticationListeners(ContainerBuilder $container, stri $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 = $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)); } - $userProvider = $providerIds[$normalizedName]; - } elseif ('remember_me' === $key || 'anonymous' === $key) { - // RememberMeFactory will use the firewall secret when created, AnonymousAuthenticationListener does not load users. - $userProvider = null; - if ('remember_me' === $key && $contextListenerId) { - $container->getDefinition($contextListenerId)->addTag('security.remember_me_aware', ['id' => $id, 'provider' => 'none']); + $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); + list($provider, $listenerId, $defaultEntryPoint) = $factory->create($container, $id, $firewall[$key], $userProvider, $defaultEntryPoint); - $listeners[] = new Reference($listenerId); - $authenticationProviders[] = $provider; + $listeners[] = new Reference($listenerId); + $authenticationProviders[] = $provider; + } $hasListeners = true; } } @@ -504,6 +510,42 @@ private function createAuthenticationListeners(ContainerBuilder $container, stri 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 000000000000..4022eafd9d8f --- /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 7219210597ee..0992a92499c8 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 b3243c83d7da..d8e6590736a3 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\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 @@ public function build(ContainerBuilder $container) $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 eb3c930afe37..2479cff3ac9f 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 @@ /** * @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 @@ public function create(ContainerBuilder $container, $id, $config, $userProvider, 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 af200264061e..2a773b34adf8 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 @@ * @author Fabien Potencier * @author Johannes M. Schmitt */ -class FormLoginFactory extends AbstractFactory +class FormLoginFactory extends AbstractFactory implements GuardFactoryInterface { public function __construct() { @@ -96,4 +96,17 @@ protected function createEntryPoint(ContainerBuilder $container, string $id, arr 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 312f73499ab0..0d1dcb0fada0 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 f50698fc67b2..c632ebf587bd 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 @@ public function create(ContainerBuilder $container, string $id, array $config, s 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 73b9a55a7cf5..5a707a9f2670 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -138,7 +138,9 @@ public function load(array $configs, ContainerBuilder $container) $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 4022eafd9d8f..588f4d15676c 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 0992a92499c8..99d8550e1bc8 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 000000000000..e173792dba8b --- /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 000000000000..72e2bc5ff1f3 --- /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 5a707a9f2670..55ebd0d62f19 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -264,11 +264,24 @@ private function createFirewalls(array $config, ContainerBuilder $container) // 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 @@ private function createAuthenticationListeners(ContainerBuilder $container, stri 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 000000000000..63b201cb66db --- /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 588f4d15676c..f9268c380e07 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 022538731de8..35c4bda103aa 100644 --- a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php +++ b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php @@ -34,6 +34,8 @@ */ class GuardAuthenticationListener extends AbstractListener { + use GuardAuthenticatorListenerTrait; + private $guardHandler; private $authenticationManager; private $providerKey; @@ -73,20 +75,7 @@ public function supports(Request $request): ?bool $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 @@ public function authenticate(RequestEvent $event) $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 @@ public function setRememberMeServices(RememberMeServicesInterface $rememberMeSer $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 000000000000..935f8fa0643d --- /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 000000000000..2cfa86d4207c --- /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 55ebd0d62f19..94450d24613e 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -269,19 +269,6 @@ private function createFirewalls(array $config, ContainerBuilder $container) 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 @@ private function createFirewall(ContainerBuilder $container, string $id, array $ $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 0afa2121aab9..624b0a678c8c 100644 --- a/src/Symfony/Component/Security/Core/Authentication/GuardAuthenticationManager.php +++ b/src/Symfony/Component/Security/Core/Authentication/GuardAuthenticationManager.php @@ -81,7 +81,7 @@ public function authenticate(TokenInterface $token) } 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 935f8fa0643d..043c51c7a863 100644 --- a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticatorListenerTrait.php +++ b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticatorListenerTrait.php @@ -72,7 +72,7 @@ private function executeGuardAuthenticator(string $uniqueGuardKey, Authenticator } // 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 ac5c4cc2d483..04085aaa05ed 100644 --- a/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php +++ b/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php @@ -93,7 +93,7 @@ public function authenticate(TokenInterface $token) 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 33e82eb0229d..0112256b85cb 100644 --- a/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProviderTrait.php +++ b/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProviderTrait.php @@ -28,7 +28,7 @@ */ 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 @@ private function authenticateViaGuard(AuthenticatorInterface $guardAuthenticator $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 451d96c6eeb2..460dcf9bdab8 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 @@ public function __construct($credentials, string $guardProviderKey) 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 2cfa86d4207c..b1261bf2b1e1 100644 --- a/src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php @@ -57,7 +57,7 @@ public function __invoke(RequestEvent $requestEvent) 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 000000000000..804399ad5109 --- /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 2a773b34adf8..386ba8e462e4 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 @@ * @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 @@ protected function createListener(ContainerBuilder $container, string $id, array 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 @@ public function createGuard(ContainerBuilder $container, string $id, array $conf $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 94450d24613e..54403cfa4a97 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 @@ private function createAuthenticationListeners(ContainerBuilder $container, stri } 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 f9268c380e07..9da2d3b8a5cb 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 000000000000..8e9bee6f073e --- /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 000000000000..1f4b3352e707 --- /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 e173792dba8b..78c80800aa0f 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\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 @@ 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; @@ -40,27 +41,29 @@ public function getCredentials(Request $request) 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 000000000000..c4a996538145 --- /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 72e2bc5ff1f3..06f400242c1b 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\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\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 @@ */ 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 @@ public function __construct(HttpUtils $httpUtils, ?CsrfTokenManagerInterface $cs 'target_path_parameter' => '_target_path', 'use_referer' => false, ], $options); + $this->userProvider = $userProvider; } protected function getLoginUrl(): string @@ -91,11 +100,16 @@ public function getCredentials(Request $request): array 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 @@ public function checkCredentials($credentials, UserInterface $user): bool 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 9ba11d0ddb15..78e6d91cc228 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\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 @@ public function start(Request $request, AuthenticationException $authException = 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 @@ public function getCredentials(Request $request) ]; } + 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 b0bad3844ee1..000000000000 --- 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 e791d5240543..05f340a68fc7 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 @@ public function checkCredentials($credentials, UserInterface $user): bool 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 624b0a678c8c..68b542af97bf 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\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 b751bde7f1f7..b9eaa6824607 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 35c4bda103aa..d30a95fdd723 100644 --- a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php +++ b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php @@ -13,14 +13,10 @@ 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 043c51c7a863..245f02c9068d 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 @@ protected function getSupportingGuardAuthenticators(Request $request): array } /** - * @param AuthenticatorInterface[] $guardAuthenticators + * @param (CoreAuthenticatorInterface|AuthenticatorInterface)[] $guardAuthenticators */ protected function executeGuardAuthenticators(array $guardAuthenticators, RequestEvent $event): void { @@ -56,8 +66,15 @@ protected function executeGuardAuthenticators(array $guardAuthenticators, Reques } } - 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 @@ private function executeGuardAuthenticator(string $uniqueGuardKey, Authenticator /** * 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 11f207a9abd4..d2c0d298d222 100644 --- a/src/Symfony/Component/Security/Guard/GuardAuthenticatorHandler.php +++ b/src/Symfony/Component/Security/Guard/GuardAuthenticatorHandler.php @@ -13,6 +13,7 @@ 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 @@ public function authenticateWithToken(TokenInterface $token, Request $request, s /** * 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 @@ public function handleAuthenticationSuccess(TokenInterface $token, Request $requ /** * 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 @@ public function authenticateUserAndHandleSuccess(UserInterface $user, Request $r /** * 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 0112256b85cb..0d25f167db00 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 @@ */ 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 @@ private function authenticateViaGuard(AuthenticatorInterface $guardAuthenticator 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 b1261bf2b1e1..564f60d31bee 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 dd2eeba33dea..b6b26cbd31a7 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 0d25f167db00..667c35d05e5e 100644 --- a/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProviderTrait.php +++ b/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProviderTrait.php @@ -62,8 +62,20 @@ private function authenticateViaGuard($guardAuthenticator, PreAuthenticationGuar 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 9da2d3b8a5cb..e4fa9008ddca 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 78c80800aa0f..227981c69656 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 @@ 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 @@ 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 68b542af97bf..a836353b61ff 100644 --- a/src/Symfony/Component/Security/Core/Authentication/GuardAuthenticationManager.php +++ b/src/Symfony/Component/Security/Core/Authentication/GuardAuthenticationManager.php @@ -86,12 +86,14 @@ public function authenticate(TokenInterface $token) $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 245f02c9068d..ac1cb8200cca 100644 --- a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticatorListenerTrait.php +++ b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticatorListenerTrait.php @@ -150,6 +150,11 @@ private function triggerRememberMe($guardAuthenticator, Request $request, TokenI 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 804399ad5109..bf0e625f0ad6 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 @@ /** * @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 0d1dcb0fada0..34314e5a437d 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 @@ /** * @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 63b201cb66db..958ca5d4bbc7 100644 --- a/src/Symfony/Bundle/SecurityBundle/EventListener/LazyGuardManagerListener.php +++ b/src/Symfony/Bundle/SecurityBundle/EventListener/LazyGuardManagerListener.php @@ -20,6 +20,8 @@ /** * @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 8e9bee6f073e..1127fb67819a 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 @@ * 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 1f4b3352e707..27df412d28ca 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 @@ * 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 227981c69656..26a7d3102bfc 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 @@ /** * @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 c4a996538145..cf84ce16091f 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 @@ * @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 06f400242c1b..19c5b69029ee 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 @@ /** * @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 78e6d91cc228..6ce74c68090c 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 @@ /** * @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 05f340a68fc7..292ec370f8ef 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 @@ * @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 a836353b61ff..8b4e2e639386 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\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 564f60d31bee..e2a80c988875 100644 --- a/src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php @@ -20,6 +20,10 @@ /** * @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 54403cfa4a97..f1bf246d8d8f 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -418,6 +418,19 @@ private function createFirewall(ContainerBuilder $container, string $id, array $ // 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 @@ private function createFirewall(ContainerBuilder $container, string $id, array $ $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 @@ private function createFirewall(ContainerBuilder $container, string $id, array $ ->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 ac1cb8200cca..245f02c9068d 100644 --- a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticatorListenerTrait.php +++ b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticatorListenerTrait.php @@ -150,11 +150,6 @@ private function triggerRememberMe($guardAuthenticator, Request $request, TokenI 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 e2a80c988875..78681bd1e822 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\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 @@ public function __invoke(RequestEvent $requestEvent) $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 e4fa9008ddca..f752f923ca2a 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 99d8550e1bc8..5e31b492f082 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 000000000000..b19b82e06653 --- /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 9e923364ea3e..c0265cd55ac1 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 @@ 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 83b082bddedc..fc500b285f16 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 d30a95fdd723..7ffad324546f 100644 --- a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php +++ b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php @@ -15,9 +15,12 @@ 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 @@ */ class GuardAuthenticationListener extends AbstractListener { - use GuardAuthenticatorListenerTrait; + use GuardManagerListenerTrait; private $guardHandler; private $authenticationManager; @@ -101,6 +104,11 @@ public function setRememberMeServices(RememberMeServicesInterface $rememberMeSer $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 d2c0d298d222..2f16dfa14040 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 @@ * * @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 04085aaa05ed..01f70e9b4eb0 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 @@ */ 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 460dcf9bdab8..69013599f35b 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 @@ * * @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 1b2337f82971..f1292336409b 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 1127fb67819a..ce22dce36883 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 27df412d28ca..5cc2f9541475 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 26a7d3102bfc..bec859e7a755 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 cf84ce16091f..8bf38ac85a78 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 19c5b69029ee..2ff37f987b20 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 6ce74c68090c..92cb130ec9d7 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 292ec370f8ef..bbfbc5af024b 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 8b4e2e639386..b62516168b5b 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\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 @@ */ class GuardAuthenticationManager implements AuthenticationManagerInterface { - use GuardAuthenticationProviderTrait; + use GuardAuthenticationManagerTrait; private $guardAuthenticators; private $userChecker; @@ -58,10 +57,6 @@ public function setEventDispatcher(EventDispatcherInterface $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. 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 667c35d05e5e..7de91a75a381 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\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 000000000000..d930df1896b0 --- /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 78681bd1e822..236722365752 100644 --- a/src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php @@ -14,8 +14,8 @@ 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 @@ */ class GuardManagerListener { - use GuardAuthenticatorListenerTrait; + use GuardManagerListenerTrait; private $authenticationManager; private $guardHandler; @@ -65,6 +65,11 @@ public function setRememberMeServices(RememberMeServicesInterface $rememberMeSer $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 245f02c9068d..794d1dd133d8 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 @@ * * @internal */ -trait GuardAuthenticatorListenerTrait +trait GuardManagerListenerTrait { protected function getSupportingGuardAuthenticators(Request $request): array { @@ -89,7 +89,7 @@ private function executeGuardAuthenticator(string $uniqueGuardKey, $guardAuthent } // 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 @@ private function triggerRememberMe($guardAuthenticator, Request $request, TokenI } 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 376ee410facc..77a16c50cebe 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 | 122 ++++++++++++++++- .../Guard/PasswordAuthenticatedInterface.php | 4 - .../Provider/GuardAuthenticationProvider.php | 50 +++++++ .../Authenticator/AnonymousAuthenticator.php | 11 +- .../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, 874 insertions(+), 316 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 386ba8e462e4..cfed004d8635 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 @@ public function createGuard(ContainerBuilder $container, string $id, array $conf ->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 f1bf246d8d8f..d67682e8830d 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -419,16 +419,13 @@ private function createFirewall(ContainerBuilder $container, string $id, array $ $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 @@ private function createFirewall(ContainerBuilder $container, string $id, array $ $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 @@ private function createFirewall(ContainerBuilder $container, string $id, array $ }, $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 958ca5d4bbc7..4cea805737dc 100644 --- a/src/Symfony/Bundle/SecurityBundle/EventListener/LazyGuardManagerListener.php +++ b/src/Symfony/Bundle/SecurityBundle/EventListener/LazyGuardManagerListener.php @@ -13,6 +13,7 @@ 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 @@ public function __construct( 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 f752f923ca2a..a6b1a0a9f5a2 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 5e31b492f082..f3da0349b2d2 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 7ffad324546f..50b42990c5ce 100644 --- a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php +++ b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php @@ -13,9 +13,12 @@ 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 @@ public function setRememberMeServices(RememberMeServicesInterface $rememberMeSer $this->rememberMeServices = $rememberMeServices; } - protected function createPreAuthenticatedToken($credentials, string $uniqueGuardKey, string $providerKey): PreAuthenticationGuardToken + /** + * @param AuthenticatorInterface[] $guardAuthenticators + */ + protected function executeGuardAuthenticators(array $guardAuthenticators, RequestEvent $event): void { - return new GuardPreAuthenticationGuardToken($credentials, $uniqueGuardKey, $providerKey); + 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; + } + } } - protected function getGuardKey(string $key): string + private function executeGuardAuthenticator(string $uniqueGuardKey, AuthenticatorInterface $guardAuthenticator, RequestEvent $event) { - // get a key that's unique to *this* guard authenticator - // this MUST be the same as GuardAuthenticationProvider - return $this->providerKey.'_'.$key; + $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); } } diff --git a/src/Symfony/Component/Security/Guard/PasswordAuthenticatedInterface.php b/src/Symfony/Component/Security/Guard/PasswordAuthenticatedInterface.php index b6b26cbd31a7..dd2eeba33dea 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 01f70e9b4eb0..9733584119c0 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\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 @@ public function supports(TokenInterface $token) 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 bec859e7a755..c6b9427fceed 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 @@ * @final * @experimental in 5.1 */ -class AnonymousAuthenticator implements AuthenticatorInterface +class AnonymousAuthenticator implements AuthenticatorInterface, CustomAuthenticatedInterface { private $secret; private $tokenStorage; @@ -49,14 +49,15 @@ public function getCredentials(Request $request) return []; } - public function getUser($credentials): ?UserInterface + public function checkCredentials($credentials, UserInterface $user): bool { - return new User('anon.', null); + // anonymous users do not have credentials + return true; } - public function checkCredentials($credentials, UserInterface $user): bool + public function getUser($credentials): ?UserInterface { - return true; + return new User('anon.', null); } public function createAuthenticatedToken(UserInterface $user, string $providerKey): TokenInterface diff --git a/src/Symfony/Component/Security/Http/Authentication/Authenticator/AuthenticatorInterface.php b/src/Symfony/Component/Security/Http/Authentication/Authenticator/AuthenticatorInterface.php index 8bf38ac85a78..e2ca2e2e0ce9 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 @@ public function getCredentials(Request $request); */ 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 000000000000..69ec6da09707 --- /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 2ff37f987b20..acdb5e257ae7 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\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\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 @@ * @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 @@ public function getCredentials(Request $request): array 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 @@ public function checkCredentials($credentials, UserInterface $user): bool } 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 92cb130ec9d7..c3ff43f01c6c 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 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 @@ * @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 @@ public function getCredentials(Request $request) ]; } + 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 000000000000..4630c57ae92e --- /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 bbfbc5af024b..000000000000 --- 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 b62516168b5b..29bb5476ed98 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\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 @@ public function authenticate(TokenInterface $token) 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 @@ private function handleFailure(AuthenticationException $exception, TokenInterfac 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 7de91a75a381..3808d79be16f 100644 --- a/src/Symfony/Component/Security/Http/Authentication/GuardAuthenticationManagerTrait.php +++ b/src/Symfony/Component/Security/Http/Authentication/GuardAuthenticationManagerTrait.php @@ -29,65 +29,6 @@ */ 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 000000000000..6a5cf03e0138 --- /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 000000000000..de93b3a78c0e --- /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 000000000000..173f4480480f --- /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 000000000000..738142bc0572 --- /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 000000000000..f981c983fe0b --- /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 000000000000..9e612d777876 --- /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 000000000000..c0c6c6895de7 --- /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 236722365752..71a448384d32 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 @@ public function __invoke(RequestEvent $requestEvent) 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 794d1dd133d8..a1cf6880ad0b 100644 --- a/src/Symfony/Component/Security/Http/Firewall/GuardManagerListenerTrait.php +++ b/src/Symfony/Component/Security/Http/Firewall/GuardManagerListenerTrait.php @@ -46,134 +46,5 @@ protected function getSupportingGuardAuthenticators(Request $request): array 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 b0d7e5c342e4..dfac1554d4ba 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -73,7 +73,7 @@ public function getConfigTreeBuilder() ->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 2479cff3ac9f..b7e2347a577a 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 @@ /** * @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 @@ public function create(ContainerBuilder $container, $id, $config, $userProvider, 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 34314e5a437d..e85ba0b495f7 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 @@ * * @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 cfed004d8635..368cde156e7f 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 @@ * @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 @@ public function createEntryPoint(ContainerBuilder $container, string $id, array 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 c632ebf587bd..dea437e94c38 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 @@ * * @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 @@ public function create(ContainerBuilder $container, string $id, array $config, s 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 d67682e8830d..fb402288be68 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 @@ public function load(array $configs, ContainerBuilder $container) $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 @@ public function load(array $configs, ContainerBuilder $container) $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 @@ private function createFirewalls(array $config, ContainerBuilder $container) 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 @@ private function createFirewall(ContainerBuilder $container, string $id, array $ // 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 @@ private function createFirewall(ContainerBuilder $container, string $id, array $ $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 @@ private function createFirewall(ContainerBuilder $container, string $id, array $ ; $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 @@ private function createAuthenticationListeners(ContainerBuilder $container, stri 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 4cea805737dc..2a8a04e0812a 100644 --- a/src/Symfony/Bundle/SecurityBundle/EventListener/LazyGuardManagerListener.php +++ b/src/Symfony/Bundle/SecurityBundle/EventListener/LazyAuthenticatorManagerListener.php @@ -16,32 +16,32 @@ 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 a6b1a0a9f5a2..92d72ee238cb 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 7b17aff868c4..4bfd1229a8ed 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 c0265cd55ac1..b713840441e7 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 @@ 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 @@ public function testValidUsernameAndPasswordServerParameters() '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 @@ public function testValidUsernameAndPasswordServerParameters() ->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 @@ public function testValidUsernameAndPasswordServerParameters() ->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 @@ public function testInvalidPassword($presentedPassword, $exceptionMessage) $this->expectException(BadCredentialsException::class); $this->expectExceptionMessage($exceptionMessage); - $guard->checkCredentials([ + $authenticator->checkCredentials([ 'username' => 'TheUsername', 'password' => $presentedPassword, ], $this->getMockBuilder(UserInterface::class)->getMock()); @@ -100,8 +100,8 @@ public function testHttpBasicServerParametersMissing(array $serverParameters) { $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 50b42990c5ce..4ce55930f6cc 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\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 @@ */ 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 @@ public function __construct(GuardAuthenticatorHandler $guardHandler, Authenticat $this->guardHandler = $guardHandler; $this->authenticationManager = $authenticationManager; $this->providerKey = $providerKey; - $this->guardAuthenticators = $guardAuthenticators; + $this->authenticators = $guardAuthenticators; $this->logger = $logger; } @@ -70,14 +68,14 @@ public function supports(Request $request): ?bool 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 @@ private function executeGuardAuthenticator(string $uniqueGuardKey, Authenticator } // 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 @@ protected function triggerRememberMe($guardAuthenticator, Request $request, Toke $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 2f16dfa14040..73e5a6e8827c 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 @@ * * @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 9733584119c0..246d5173f156 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\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\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 @@ */ 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 @@ public function authenticate(TokenInterface $token) 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 @@ public function authenticate(TokenInterface $token) $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 @@ public function authenticate(TokenInterface $token) public function supports(TokenInterface $token) { - if ($token instanceof PreAuthenticationGuardToken) { + if ($token instanceof PreAuthenticationToken) { return null !== $this->findOriginatingAuthenticator($token); } @@ -121,12 +121,12 @@ public function setRememberMeServices(RememberMeServicesInterface $rememberMeSer $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 c5e1c92b89fd..6504aa1997cc 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\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 @@ public function testHandleSuccess() // 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 @@ protected function setUp(): void ->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 e078a6be123a..d6dfacca102f 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\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 @@ public function testAuthenticateWithToken() ->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 @@ public function testHandleAuthenticationSuccess() ->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 @@ public function testHandleAuthenticationFailure() ->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 @@ public function testHandleAuthenticationClearsToken($tokenProviderKey, $actualPr ->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 @@ public function testNoFailureIfSessionStrategyNotPassed() ->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 @@ public function testSessionStrategyIsCalled() ->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 @@ public function testSessionStrategyIsNotCalledWhenStateless() $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 b742046af013..c1bb302f9c80 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\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 @@ public function testSupportsChecksGuardAuthenticatorsTokenOrigin() $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 @@ public function testAuthenticateFailsOnNonOriginatingToken() $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 @@ protected function setUp(): void { $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 69013599f35b..1ae9be445ebd 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 @@ * * @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 d930df1896b0..7a579a9b2cd5 100644 --- a/src/Symfony/Component/Security/Http/Authentication/GuardAuthenticatorHandler.php +++ b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorHandler.php @@ -13,7 +13,7 @@ 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\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 @@ * * @internal */ -class GuardAuthenticatorHandler +class AuthenticatorHandler { private $tokenStorage; private $dispatcher; @@ -66,24 +66,24 @@ public function authenticateWithToken(TokenInterface $token, Request $request, s } /** - * 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 @@ public function handleAuthenticationSuccess(TokenInterface $token, Request $requ 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 @@ public function authenticateUserAndHandleSuccess(UserInterface $user, Request $r * 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 29bb5476ed98..39208002b06e 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\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 @@ * * @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 @@ public function setEventDispatcher(EventDispatcherInterface $dispatcher) 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 @@ public function authenticate(TokenInterface $token) 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 @@ public function authenticate(TokenInterface $token) 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 000000000000..b1df45daab88 --- /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 3808d79be16f..000000000000 --- 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 ce22dce36883..0301a97110e7 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 @@ 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 5cc2f9541475..07c71b1c3b41 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 @@ * * @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 c6b9427fceed..202da3b02667 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 e2ca2e2e0ce9..5530eb32dddd 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 69ec6da09707..79b995e55f83 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 acdb5e257ae7..75bac9bd90c8 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 @@ * @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 c3ff43f01c6c..51ad3339b796 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\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 000000000000..7386fc3373da --- /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 000000000000..3525fa4765b9 --- /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 b19b82e06653..27daf7f8ba94 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 @@ public function getProviderKey(): ?string return $this->providerKey; } - public function getGuardProviderKey() + public function getAuthenticatorKey() { - return $this->guardProviderKey; + return $this->authenticatorProviderKey; } /** @@ -66,6 +68,6 @@ public function getCredentials() 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 4630c57ae92e..88d0d7f9654f 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 6a5cf03e0138..bc4e551e9126 100644 --- a/src/Symfony/Component/Security/Http/Event/LoginFailureEvent.php +++ b/src/Symfony/Component/Security/Http/Event/LoginFailureEvent.php @@ -5,7 +5,7 @@ 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 de93b3a78c0e..22e11a8c8772 100644 --- a/src/Symfony/Component/Security/Http/Event/LoginSuccessEvent.php +++ b/src/Symfony/Component/Security/Http/Event/LoginSuccessEvent.php @@ -5,7 +5,7 @@ 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 173f4480480f..87bcb56a8b09 100644 --- a/src/Symfony/Component/Security/Http/Event/VerifyAuthenticatorCredentialsEvent.php +++ b/src/Symfony/Component/Security/Http/Event/VerifyAuthenticatorCredentialsEvent.php @@ -4,7 +4,7 @@ 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 738142bc0572..086eb924313b 100644 --- a/src/Symfony/Component/Security/Http/EventListener/AuthenticatingListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/AuthenticatingListener.php @@ -5,9 +5,9 @@ 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 f981c983fe0b..b57605e55141 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\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 @@ public function onCredentialsVerification(VerifyAuthenticatorCredentialsEvent $e } $authenticator = $event->getAuthenticator(); - if (!$authenticator instanceof PasswordAuthenticatedInterface) { + if (!$authenticator instanceof PasswordAuthenticatedInterface || !$authenticator instanceof PasswordUpgraderInterface) { return; } @@ -51,10 +51,6 @@ public function onCredentialsVerification(VerifyAuthenticatorCredentialsEvent $e 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 9e612d777876..882258b1a6a4 100644 --- a/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php @@ -4,7 +4,7 @@ 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 71a448384d32..6c7cf10ff933 100644 --- a/src/Symfony/Component/Security/Http/Firewall/GuardManagerListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/AuthenticatorManagerListener.php @@ -13,15 +13,13 @@ 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 @@ * * @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 @@ public function __construct(AuthenticationManagerInterface $authenticationManage 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 @@ public function __invoke(RequestEvent $requestEvent) */ 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 @@ private function executeAuthenticator(string $uniqueAuthenticatorKey, Authentica } // 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 @@ private function executeAuthenticator(string $uniqueAuthenticatorKey, Authentica } // 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 @@ private function executeAuthenticator(string $uniqueAuthenticatorKey, Authentica $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 @@ private function executeAuthenticator(string $uniqueAuthenticatorKey, Authentica } // 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 @@ private function executeAuthenticator(string $uniqueAuthenticatorKey, Authentica $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 000000000000..046c5ef4934e --- /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 a1cf6880ad0b..000000000000 --- 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 fb402288be68..0e857e53d113 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -453,7 +453,6 @@ private function createFirewall(ContainerBuilder $container, string $id, array $ ->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 2a8a04e0812a..e4299bcc0cfe 100644 --- a/src/Symfony/Bundle/SecurityBundle/EventListener/LazyAuthenticatorManagerListener.php +++ b/src/Symfony/Bundle/SecurityBundle/EventListener/LazyAuthenticatorManagerListener.php @@ -26,37 +26,39 @@ */ 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 92d72ee238cb..b42cf0fab02c 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 4ce55930f6cc..37665d4fa8cb 100644 --- a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php +++ b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php @@ -34,8 +34,6 @@ */ class GuardAuthenticationListener extends AbstractListener { - use AuthenticatorManagerListenerTrait; - private $guardHandler; private $authenticationManager; private $providerKey; @@ -75,7 +73,19 @@ public function supports(Request $request): ?bool $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 202da3b02667..7e56b715797c 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php @@ -41,7 +41,8 @@ public function __construct(string $secret, TokenStorageInterface $tokenStorage) 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 6c7cf10ff933..b5327bd958b9 100644 --- a/src/Symfony/Component/Security/Http/Firewall/AuthenticatorManagerListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/AuthenticatorManagerListener.php @@ -13,6 +13,7 @@ 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 @@ * * @experimental in 5.1 */ -class AuthenticatorManagerListener +class AuthenticatorManagerListener extends AbstractListener { - use AuthenticatorManagerListenerTrait; - private $authenticationManager; private $authenticatorHandler; private $authenticators; @@ -54,15 +53,58 @@ public function __construct(AuthenticationManagerInterface $authenticationManage $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 @@ public function __invoke(RequestEvent $requestEvent) 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 046c5ef4934e..000000000000 --- 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 b7e2347a577a..cf77d99fdf0b 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 @@ public function create(ContainerBuilder $container, $id, $config, $userProvider, 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 e85ba0b495f7..acd1fce318e9 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 368cde156e7f..555cac383ed8 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 @@ public function createEntryPoint(ContainerBuilder $container, string $id, array 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 dea437e94c38..9d121b17fec4 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 @@ public function create(ContainerBuilder $container, string $id, array $config, s 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 06ad4134bd1e..979acc79dc26 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\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 @@ public function create(ContainerBuilder $container, string $id, array $config, ? ; // 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 @@ public function create(ContainerBuilder $container, string $id, array $config, ? ; } } - 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 @@ public function create(ContainerBuilder $container, string $id, array $config, ? 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 @@ public function addConfiguration(NodeDefinition $node) } } } + + 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 0e857e53d113..97ede2281fa3 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\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\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 @@ private function createFirewalls(array $config, ContainerBuilder $container) 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 @@ private function createFirewall(ContainerBuilder $container, string $id, array $ // 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 @@ private function createAuthenticationListeners(ContainerBuilder $container, stri 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 @@ private function getUserProvider(ContainerBuilder $container, string $id, array 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 @@ private function getUserProvider(ContainerBuilder $container, string $id, array 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 b42cf0fab02c..9ec5f17e0a20 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 07c71b1c3b41..3469e8c50991 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php @@ -25,7 +25,7 @@ * * @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 @@ public function onAuthenticationFailure(Request $request, AuthenticationExceptio 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 @@ public function start(Request $request, AuthenticationException $authException = 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 7e56b715797c..93d69312182c 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php @@ -75,9 +75,4 @@ public function onAuthenticationSuccess(Request $request, TokenInterface $token, { 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 5530eb32dddd..6a85062e6c1b 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php @@ -102,18 +102,4 @@ public function onAuthenticationFailure(Request $request, AuthenticationExceptio * 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 51ad3339b796..f896d924a802 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php @@ -94,9 +94,4 @@ public function onAuthenticationFailure(Request $request, AuthenticationExceptio 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 000000000000..893bd099de70 --- /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 000000000000..d9eb6fa70bc8 --- /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 882258b1a6a4..522f5090d64c 100644 --- a/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php @@ -5,11 +5,19 @@ 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 @@ */ 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 @@ private function isRememberMeEnabled(AuthenticatorInterface $authenticator, stri 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 22f9dde14b76..e9065d7f526f 100644 --- a/src/Symfony/Component/Security/Http/RememberMe/AbstractRememberMeServices.php +++ b/src/Symfony/Component/Security/Http/RememberMe/AbstractRememberMeServices.php @@ -89,6 +89,11 @@ public function getSecret() 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 97ede2281fa3..dbecca12e956 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -107,6 +107,12 @@ public function load(array $configs, ContainerBuilder $container) $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 @@ public function load(array $configs, ContainerBuilder $container) $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 b04662aaf758..26da33731212 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 9ec5f17e0a20..4cbc44062572 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 000000000000..85d672a078da --- /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 dbecca12e956..57ecde2068d7 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -23,6 +23,7 @@ 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 @@ private function createFirewalls(array $config, ContainerBuilder $container) $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 @@ private function createFirewall(ContainerBuilder $container, string $id, array $ $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 4cbc44062572..9b52c37ec8d8 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 000000000000..a3974dd2b381 --- /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 39208002b06e..6a565ad1bb1c 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 b5327bd958b9..016bb826afc3 100644 --- a/src/Symfony/Component/Security/Http/Firewall/AuthenticatorManagerListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/AuthenticatorManagerListener.php @@ -33,7 +33,7 @@ */ 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 @@ private function executeAuthenticator(string $uniqueAuthenticatorKey, Authentica } // 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 | 243 ++++++++++++++---- .../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 | 13 +- .../VerifyAuthenticatorCredentialsEvent.php | 10 +- .../EventListener/AuthenticatingListener.php | 4 +- .../PasswordMigratingListener.php | 5 +- .../EventListener/SessionStrategyListener.php | 56 ++++ .../Firewall/AuthenticatorManagerListener.php | 171 +----------- 31 files changed, 590 insertions(+), 635 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 57ecde2068d7..e4ef468c88a8 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -156,8 +156,8 @@ public function load(array $configs, ContainerBuilder $container) ->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 @@ private function createFirewall(ContainerBuilder $container, string $id, array $ 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 e4299bcc0cfe..000000000000 --- 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 4bfd1229a8ed..c9bb06d17987 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 a3974dd2b381..000000000000 --- 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 000000000000..ab2dded7989a --- /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 37665d4fa8cb..5ac7935f3134 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\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 @@ public function setRememberMeServices(RememberMeServicesInterface $rememberMeSer 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 7a579a9b2cd5..11f207a9abd4 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 @@ public function authenticateWithToken(TokenInterface $token, Request $request, s } /** - * 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 @@ public function authenticateUserAndHandleSuccess(UserInterface $user, Request $r /** * 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 73e5a6e8827c..000000000000 --- 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 246d5173f156..0f8287ccc268 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 @@ */ class GuardAuthenticationProvider implements AuthenticationProviderInterface { - use AuthenticatorManagerTrait; - /** * @var AuthenticatorInterface[] */ @@ -78,7 +72,7 @@ public function authenticate(TokenInterface $token) 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 @@ public function authenticate(TokenInterface $token) $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 @@ public function authenticate(TokenInterface $token) public function supports(TokenInterface $token) { - if ($token instanceof PreAuthenticationToken) { + if ($token instanceof PreAuthenticationGuardToken) { return null !== $this->findOriginatingAuthenticator($token); } @@ -121,12 +115,7 @@ public function setRememberMeServices(RememberMeServicesInterface $rememberMeSer $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 @@ private function authenticateViaGuard(AuthenticatorInterface $guardAuthenticator 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 6504aa1997cc..8c32d4b24f6a 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\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 @@ public function testHandleSuccess() // 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 @@ protected function setUp(): void ->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 d6dfacca102f..e078a6be123a 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\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 @@ public function testAuthenticateWithToken() ->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 @@ public function testHandleAuthenticationSuccess() ->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 @@ public function testHandleAuthenticationFailure() ->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 @@ public function testHandleAuthenticationClearsToken($tokenProviderKey, $actualPr ->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 @@ public function testNoFailureIfSessionStrategyNotPassed() ->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 @@ public function testSessionStrategyIsCalled() ->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 @@ public function testSessionStrategyIsNotCalledWhenStateless() $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 c1bb302f9c80..477bf56622d8 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\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 @@ public function testSupportsChecksGuardAuthenticatorsTokenOrigin() $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 @@ public function testAuthenticateFailsOnNonOriginatingToken() $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 @@ protected function setUp(): void $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 000000000000..451d96c6eeb2 --- /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 1ae9be445ebd..000000000000 --- 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 6a565ad1bb1c..f7dacacbc45a 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. - */ - - // this should never happen - but technically, the token is - // authenticated... so it could just be returned - if ($token->isAuthenticated()) { - return $token; + if (null !== $this->logger) { + $context = ['firewall_key' => $this->providerKey]; + + 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)]); + } } - try { - $result = $this->authenticateViaAuthenticator($authenticator, $token, $token->getProviderKey()); - } catch (AuthenticationException $exception) { - $this->handleFailure($exception, $token); + 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; } - if (null !== $result) { - if (true === $this->eraseCredentials) { - $result->eraseCredentials(); + 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; } - if (null !== $this->eventDispatcher) { - $this->eventDispatcher->dispatch(new AuthenticationSuccessEvent($result), AuthenticationEvents::AUTHENTICATION_SUCCESS); + $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 $result; + return null; } - protected function getAuthenticatorKey(string $key): string + private function executeAuthenticator(string $uniqueAuthenticatorKey, AuthenticatorInterface $authenticator, Request $request): ?Response { - // Authenticators in the AuthenticatorManager are already indexed - // by an unique key - return $key; + 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))); + } + + 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; + } + + // 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, 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 @@ private function authenticateViaAuthenticator(AuthenticatorInterface $authentica 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 @@ private function handleFailure(AuthenticationException $exception, TokenInterfac 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 000000000000..89bcef8b528f --- /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 b1df45daab88..000000000000 --- 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 000000000000..1a6efeb37901 --- /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 000000000000..76cb57292184 --- /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 0301a97110e7..3683827d127b 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AbstractAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AbstractAuthenticator.php @@ -18,7 +18,7 @@ /** * 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 3469e8c50991..e702144787e8 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php @@ -21,7 +21,7 @@ /** * 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 6a85062e6c1b..0f1053e10933 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php @@ -20,7 +20,7 @@ /** * 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 27daf7f8ba94..000000000000 --- 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 bc4e551e9126..03a1c7a78c6a 100644 --- a/src/Symfony/Component/Security/Http/Event/LoginFailureEvent.php +++ b/src/Symfony/Component/Security/Http/Event/LoginFailureEvent.php @@ -53,6 +53,11 @@ public function getRequest(): Request 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 22e11a8c8772..6e48e171b605 100644 --- a/src/Symfony/Component/Security/Http/Event/LoginSuccessEvent.php +++ b/src/Symfony/Component/Security/Http/Event/LoginSuccessEvent.php @@ -50,13 +50,18 @@ public function getRequest(): Request return $this->request; } - public function getResponse(): ?Response + public function getProviderKey(): string { - return $this->response; + return $this->providerKey; } - public function getProviderKey(): string + public function setResponse(?Response $response): void { - return $this->providerKey; + $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 87bcb56a8b09..cc37bf33f202 100644 --- a/src/Symfony/Component/Security/Http/Event/VerifyAuthenticatorCredentialsEvent.php +++ b/src/Symfony/Component/Security/Http/Event/VerifyAuthenticatorCredentialsEvent.php @@ -19,14 +19,14 @@ 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 @@ public function getAuthenticator(): AuthenticatorInterface 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 086eb924313b..6795100a9c19 100644 --- a/src/Symfony/Component/Security/Http/EventListener/AuthenticatingListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/AuthenticatingListener.php @@ -41,7 +41,7 @@ public function onAuthenticating(VerifyAuthenticatorCredentialsEvent $event): vo $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 @@ public function onAuthenticating(VerifyAuthenticatorCredentialsEvent $event): vo } 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 b57605e55141..c97b722ff186 100644 --- a/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php @@ -36,12 +36,11 @@ public function onCredentialsVerification(VerifyAuthenticatorCredentialsEvent $e 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 000000000000..436d525a5adf --- /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 016bb826afc3..f30d9b60049c 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; - } - - 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, $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; - } - } + return $this->authenticatorManager->supports($request); } - private function executeAuthenticator(string $uniqueAuthenticatorKey, AuthenticatorInterface $authenticator, RequestEvent $event): void + public function authenticate(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)); - + $response = $this->authenticatorManager->authenticateRequest($request); + if (null === $response) { 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 861c606f5fdd..a09c04ea5b63 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 000000000000..0f93ad1e865e --- /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 75bac9bd90c8..2ec3792a7cd1 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php @@ -32,7 +32,7 @@ * @final * @experimental in 5.1 */ -class FormLoginAuthenticator extends AbstractLoginFormAuthenticator implements PasswordAuthenticatedInterface +class FormLoginAuthenticator extends AbstractLoginFormAuthenticator implements PasswordAuthenticatedInterface, CsrfProtectedAuthenticatorInterface { use TargetPathTrait; @@ -113,17 +113,15 @@ public function getUser($credentials): ?UserInterface 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 000000000000..fcde7924528f --- /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 a09c04ea5b63..757aef78e757 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 6795100a9c19..c8ab235f79f1 100644 --- a/src/Symfony/Component/Security/Http/EventListener/AuthenticatingListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/VerifyAuthenticatorCredentialsListener.php @@ -4,6 +4,7 @@ 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 @@ * @final * @experimental in 5.1 */ -class AuthenticatingListener implements EventSubscriberInterface +class VerifyAuthenticatorCredentialsListener implements EventSubscriberInterface { private $encoderFactory; @@ -28,22 +29,22 @@ 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->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 @@ public function onAuthenticating(VerifyAuthenticatorCredentialsEvent $event): vo 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 555cac383ed8..0fe2d995b369 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 @@ public function createAuthenticator(ContainerBuilder $container, string $id, arr $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 757aef78e757..a5b6e8778222 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 f7dacacbc45a..c309485293e1 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\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 @@ */ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthenticatorInterface { - use AuthenticatorManagerTrait; - private $authenticators; private $tokenStorage; private $eventDispatcher; @@ -131,7 +128,9 @@ private function executeAuthenticators(array $authenticators, Request $request): // 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 @@ private function authenticateViaAuthenticator(AuthenticatorInterface $authentica 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 @@ private function handleAuthenticationSuccess(TokenInterface $token, Request $req 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 2ec3792a7cd1..cd8c569c577f 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\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 @@ public function supports(Request $request): bool 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 f896d924a802..77480eea45bf 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\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 893bd099de70..1ffdd1b997fe 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\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 @@ * * @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 @@ public function getUser($credentials): ?UserInterface 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 @@ public function onAuthenticationFailure(Request $request, AuthenticationExceptio 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 c97b722ff186..28800e626007 100644 --- a/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php @@ -36,7 +36,7 @@ public function onCredentialsVerification(VerifyAuthenticatorCredentialsEvent $e return; } - if (null !== $password = $authenticator->getPassword($event->getCredentials())) { + if (null === $password = $authenticator->getPassword($event->getCredentials())) { return; } @@ -46,11 +46,11 @@ public function onCredentialsVerification(VerifyAuthenticatorCredentialsEvent $e } $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 522f5090d64c..72ce7c13f96c 100644 --- a/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php @@ -39,7 +39,15 @@ public function __construct(RememberMeServicesInterface $rememberMeServices, str 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 @@ public function onSuccessfulLogin(LoginSuccessEvent $event): void 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 c0c6c6895de7..8ebbaca70947 100644 --- a/src/Symfony/Component/Security/Http/EventListener/UserCheckerListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/UserCheckerListener.php @@ -23,11 +23,19 @@ public function __construct(UserCheckerInterface $userChecker) 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 c8ab235f79f1..77bbb39ec92c 100644 --- a/src/Symfony/Component/Security/Http/EventListener/VerifyAuthenticatorCredentialsListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/VerifyAuthenticatorCredentialsListener.php @@ -31,6 +31,10 @@ public function __construct(EncoderFactoryInterface $encoderFactory) 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 000000000000..46dc09e2f8d0 --- /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 000000000000..f5d1cfdf98e1 --- /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 000000000000..058508f25ee6 --- /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 b713840441e7..e2ac0ac991f1 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 @@ public function testValidUsernameAndPasswordServerParameters() ->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); - } - - /** @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.'], - ]; + $this->assertEquals('ThePassword', $this->authenticator->getPassword($credentials)); } - /** @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 000000000000..9bd11ab62d97 --- /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 000000000000..0c2a15d952e4 --- /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 000000000000..37d9ee23cc51 --- /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 000000000000..910c67a0bd65 --- /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 000000000000..176921d1a174 --- /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 000000000000..785a31296369 --- /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 000000000000..e2c2cc6605b0 --- /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 | 62 ++++++------- .../AbstractLoginFormAuthenticator.php | 7 +- .../Authenticator/AnonymousAuthenticator.php | 6 +- .../InteractiveAuthenticatorInterface.php | 35 ++++++++ .../Authenticator/RememberMeAuthenticator.php | 11 ++- .../AuthenticatorManagerTest.php | 86 ++++++++++--------- 6 files changed, 122 insertions(+), 85 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 c309485293e1..381195d833bb 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\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 @@ public function authenticateUser(UserInterface $user, AuthenticatorInterface $au { // 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 @@ private function executeAuthenticator(string $uniqueAuthenticatorKey, Authentica 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 @@ private function executeAuthenticator(string $uniqueAuthenticatorKey, Authentica $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! + // 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->info('Authenticator failed.', ['exception' => $e, 'authenticator' => \get_class($authenticator)]); + $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 @@ private function executeAuthenticator(string $uniqueAuthenticatorKey, Authentica 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 @@ private function authenticateViaAuthenticator(AuthenticatorInterface $authentica 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); - } - - private function handleAuthenticationSuccess(TokenInterface $token, Request $request, AuthenticatorInterface $authenticator): ?Response - { - $response = $authenticator->onAuthenticationSuccess($request, $token, $this->providerKey); + $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); + } - $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 @@ private function handleAuthenticationSuccess(TokenInterface $token, Request $req */ 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 e702144787e8..5e298418cbbb 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php @@ -25,7 +25,7 @@ * * @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 @@ public function supportsRememberMe(): bool { 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 93d69312182c..4b6214668ce0 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php @@ -66,12 +66,12 @@ public function createAuthenticatedToken(UserInterface $user, string $providerKe return new AnonymousToken($this->secret, 'anon.', []); } - public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response { - return null; + return null; // let the original request continue } - public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?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 000000000000..a2abf96e4a09 --- /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 1ffdd1b997fe..72c6ea528837 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php @@ -33,7 +33,7 @@ * * @final */ -class RememberMeAuthenticator implements AuthenticatorInterface, CustomAuthenticatedInterface +class RememberMeAuthenticator implements InteractiveAuthenticatorInterface, CustomAuthenticatedInterface { private $rememberMeServices; private $secret; @@ -97,6 +97,11 @@ public function createAuthenticatedToken(UserInterface $user, string $providerKe 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 @@ public function onAuthenticationFailure(Request $request, AuthenticationExceptio 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 46dc09e2f8d0..7343d79788a6 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 @@ public function testAuthenticateRequest($matchingAuthenticatorIndex) $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; - })); + + $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; + + $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 @@ public function testEraseCredentials($eraseCredentials) $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 @@ public function testAuthenticateUser() $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 | 57 +++++++++---------- .../GuardAuthenticationListenerTest.php | 4 +- .../GuardAuthenticationProviderTest.php | 4 +- .../Component/Security/Guard/composer.json | 2 +- 5 files changed, 55 insertions(+), 69 deletions(-) diff --git a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php index 5ac7935f3134..022538731de8 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\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 @@ public function __construct(GuardAuthenticatorHandler $guardHandler, Authenticat $this->guardHandler = $guardHandler; $this->authenticationManager = $authenticationManager; $this->providerKey = $providerKey; - $this->authenticators = $guardAuthenticators; + $this->guardAuthenticators = $guardAuthenticators; $this->logger = $logger; } @@ -66,23 +66,24 @@ 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); + 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 @@ public function authenticate(RequestEvent $event) $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 @@ private function executeGuardAuthenticator(string $uniqueGuardKey, Authenticator } // 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 @@ private function executeGuardAuthenticator(string $uniqueGuardKey, Authenticator $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 0f8287ccc268..7e9258a9c5b6 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\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 @@ public function authenticate(TokenInterface $token) 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 @@ private function authenticateViaGuard(AuthenticatorInterface $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); + $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 @@ private function authenticateViaGuard(AuthenticatorInterface $guardAuthenticator 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; + // 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 + // 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 8c32d4b24f6a..c5e1c92b89fd 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 @@ protected function setUp(): void ->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 477bf56622d8..b742046af013 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 @@ protected function setUp(): void { $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 f1292336409b..1b2337f82971 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 b37229d886e3..0f0c44f8abc2 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 @@ * @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 @@ public function create(ContainerBuilder $container, string $id, array $config, s 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 e3ba596d933a..604cee7e4490 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 @@ * * @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 @@ public function create(ContainerBuilder $container, string $id, array $config, s 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 a5b6e8778222..0ff79a0ebde2 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 000000000000..b3a02bf1bdb5 --- /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 000000000000..3a01087767ea --- /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 000000000000..d482579d0564 --- /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 8ebbaca70947..34fdfdf84d79 100644 --- a/src/Symfony/Component/Security/Http/EventListener/UserCheckerListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/UserCheckerListener.php @@ -4,6 +4,7 @@ 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 @@ public function __construct(UserCheckerInterface $userChecker) 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 000000000000..80cddd1ddbf3 --- /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 000000000000..e8395042855e --- /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 979acc79dc26..5f530a17e210 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 @@ private function createRememberMeListener(ContainerBuilder $container, string $i { $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 e4ef468c88a8..35bcf015575d 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -156,8 +156,11 @@ public function load(array $configs, ContainerBuilder $container) ->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 @@ private function createFirewall(ContainerBuilder $container, string $id, array $ $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 c3415ccc8c84..38f819c44f9b 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 @@ public static function getSubscribedEvents(): array { 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 0ff79a0ebde2..fc21f87e6c03 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 72ce7c13f96c..269d23278618 100644 --- a/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php @@ -26,26 +26,29 @@ 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 @@ public function onSuccessfulLogin(LoginSuccessEvent $event): void 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 436d525a5adf..492316ec63f2 100644 --- a/src/Symfony/Component/Security/Http/EventListener/SessionStrategyListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/SessionStrategyListener.php @@ -28,21 +28,18 @@ 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 f4b9adee939f..4e09a3d2f8b1 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 @@ * * @author Kévin Dunglas */ -class JsonLoginFactory extends AbstractFactory +class JsonLoginFactory extends AbstractFactory implements AuthenticatorFactoryInterface { public function __construct() { @@ -96,4 +96,18 @@ protected function createListener(ContainerBuilder $container, string $id, array 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 fc21f87e6c03..80e9e8e2b987 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 000000000000..f10e33092382 --- /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 000000000000..84ff61781fcb --- /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 b523467f230b..a5d6f7e45ea6 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 0fe2d995b369..962c68eb2b58 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 @@ public function createEntryPoint(ContainerBuilder $container, string $id, array 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 80e9e8e2b987..07ca362b0325 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 5e298418cbbb..69ded7b0629b 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 @@ public function onAuthenticationFailure(Request $request, AuthenticationExceptio $request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception); } - $url = $this->getLoginUrl(); + $url = $this->getLoginUrl($request); return new RedirectResponse($url); } @@ -52,7 +52,7 @@ public function onAuthenticationFailure(Request $request, AuthenticationExceptio */ 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 cd8c569c577f..5aaf96437f82 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\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 @@ */ 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 @@ public function createAuthenticatedToken(UserInterface $user, $providerKey): Tok 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 058508f25ee6..3012da746db3 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\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 @@ public function postOnlyDataProvider() 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 | 83 ++++++------- .../Authenticator/AbstractAuthenticator.php | 12 +- .../AbstractLoginFormAuthenticator.php | 7 +- .../AbstractPreAuthenticatedAuthenticator.php | 32 ++--- .../Authenticator/AnonymousAuthenticator.php | 23 +--- .../Authenticator/AuthenticatorInterface.php | 62 ++++------ .../CsrfProtectedAuthenticatorInterface.php | 34 ------ .../CustomAuthenticatedInterface.php | 36 ------ .../Authenticator/FormLoginAuthenticator.php | 91 ++++++++------ .../Authenticator/HttpBasicAuthenticator.php | 41 ++++--- .../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 | 42 ++----- .../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 | 33 +++-- .../Http/EventListener/RememberMeListener.php | 12 +- .../EventListener/UserCheckerListener.php | 22 ++-- ...VerifyAuthenticatorCredentialsListener.php | 51 ++++---- .../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 | 46 ++++--- ...fyAuthenticatorCredentialsListenerTest.php | 114 +++++------------- 55 files changed, 1185 insertions(+), 769 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 962c68eb2b58..2edfb3ff3479 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 @@ public function __construct() $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 @@ protected function getListenerId() 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 @@ public function createEntryPoint(ContainerBuilder $container, string $id, array 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 da0d2cb8aa28..adf023eac358 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 381195d833bb..36a991610593 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\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 @@ public function __construct(iterable $authenticators, TokenStorageInterface $tok $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 @@ private function executeAuthenticators(array $authenticators, Request $request): 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 @@ private function executeAuthenticators(array $authenticators, Request $request): 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); - // allow the authenticator to fetch authentication info from the request - $credentials = $authenticator->getCredentials($request); + // check if all badges are resolved + $passport->checkIfCompletelyResolved(); - 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 the authenticated token + $authenticatedToken = $authenticator->createAuthenticatedToken($passport, $this->providerKey); + if (true === $this->eraseCredentials) { + $authenticatedToken->eraseCredentials(); } - // authenticate the credentials (e.g. check password) - $token = $this->authenticateViaAuthenticator($authenticator, $credentials); + if (null !== $this->eventDispatcher) { + $this->eventDispatcher->dispatch(new AuthenticationSuccessEvent($authenticatedToken), AuthenticationEvents::AUTHENTICATION_SUCCESS); + } 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 @@ private function executeAuthenticator(string $uniqueAuthenticatorKey, Authentica } } - 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 @@ private function handleAuthenticationSuccess(TokenInterface $authenticatedToken, $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 3683827d127b..51a49a3b1729 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 69ded7b0629b..f45fb3d07462 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php @@ -25,7 +25,7 @@ * * @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 @@ public function start(Request $request, AuthenticationException $authException = 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 b3a02bf1bdb5..435de68e9887 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\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 @@ * @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 @@ public function supports(Request $request): ?bool $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 @@ public function supports(Request $request): ?bool 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 @@ public function supports(Request $request): ?bool return true; } - public function getCredentials(Request $request) + public function authenticate(Request $request): PassportInterface { - return [ - 'username' => $request->attributes->get('_pre_authenticated_username'), - ]; - } - - public function getUser($credentials): ?UserInterface - { - return $this->userProvider->loadUserByUsername($credentials['username']); - } + $username = $request->attributes->get('_pre_authenticated_username'); + $user = $this->userProvider->loadUserByUsername($username); - public function checkCredentials($credentials, UserInterface $user): bool - { - // the user is already authenticated before it entered Symfony - return true; + return new SelfValidatingPassport($user, [new PreAuthenticatedUserBadge()]); } - public function createAuthenticatedToken(UserInterface $user, string $providerKey): TokenInterface + public function createAuthenticatedToken(PassportInterface $passport, 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 4b6214668ce0..27a315b0f565 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\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 @@ * @final * @experimental in 5.1 */ -class AnonymousAuthenticator implements AuthenticatorInterface, CustomAuthenticatedInterface +class AnonymousAuthenticator implements AuthenticatorInterface { private $secret; private $tokenStorage; @@ -45,23 +45,12 @@ public function supports(Request $request): ?bool 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 0f1053e10933..d80356e71340 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\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 @@ public function getUser($credentials): ?UserInterface; * 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 @@ public function onAuthenticationFailure(Request $request, AuthenticationExceptio * 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 0f93ad1e865e..000000000000 --- 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 79b995e55f83..000000000000 --- 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 5aaf96437f82..0bbbb6eb8304 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\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 @@ * @final * @experimental in 5.1 */ -class FormLoginAuthenticator extends AbstractLoginFormAuthenticator implements PasswordAuthenticatedInterface, CsrfProtectedAuthenticatorInterface +class FormLoginAuthenticator extends AbstractLoginFormAuthenticator { private $httpUtils; private $userProvider; @@ -52,7 +60,7 @@ public function __construct(HttpUtils $httpUtils, UserProviderInterface $userPro '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 @@ public function supports(Request $request): bool && $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 @@ public function getCredentials(Request $request): array 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 77480eea45bf..46eb6aa7bcbf 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\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 @@ * @final * @experimental in 5.1 */ -class HttpBasicAuthenticator implements AuthenticatorInterface, AuthenticationEntryPointInterface, PasswordAuthenticatedInterface +class HttpBasicAuthenticator implements AuthenticatorInterface, AuthenticationEntryPointInterface { private $realmName; private $userProvider; @@ -55,27 +61,30 @@ public function supports(Request $request): ?bool 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', ''); - public function getPassword($credentials): ?string - { - return $credentials['password']; - } + $user = $this->userProvider->loadUserByUsername($username); + if (!$user instanceof UserInterface) { + throw new AuthenticationServiceException('The user provider must return a UserInterface object.'); + } - public function getUser($credentials): ?UserInterface - { - return $this->userProvider->loadUserByUsername($credentials['username']); + $passport = new Passport($user, new PasswordCredentials($password)); + if ($this->userProvider instanceof PasswordUpgraderInterface) { + $passport->addBadge(new PasswordUpgradeBadge($password, $this->userProvider)); + } + + return $passport; } - public function createAuthenticatedToken(UserInterface $user, $providerKey): TokenInterface + /** + * @param Passport $passport + */ + public function createAuthenticatedToken(PassportInterface $passport, $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 a2abf96e4a09..7f26d8260683 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 f10e33092382..924ed7fcca34 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\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 @@ * @final * @experimental in 5.1 */ -class JsonLoginAuthenticator implements InteractiveAuthenticatorInterface, PasswordAuthenticatedInterface +class JsonLoginAuthenticator implements InteractiveAuthenticatorInterface { private $options; private $httpUtils; @@ -71,7 +77,51 @@ public function supports(Request $request): ?bool 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 @@ public function getCredentials(Request $request) 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 000000000000..7cbc93e65875 --- /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 000000000000..bc9ba7cbb57b --- /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 000000000000..9f0b4e5d8965 --- /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 000000000000..3812871da005 --- /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 000000000000..7e0f33009180 --- /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 d9eb6fa70bc8..dcee820442ee 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 000000000000..554fe7aff497 --- /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 000000000000..1a773f8afb31 --- /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 000000000000..7630a67bd78c --- /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 000000000000..a4ead01d14cd --- /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 000000000000..ac7796912756 --- /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 000000000000..1cdd75546bb7 --- /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 000000000000..dd3ef6f96218 --- /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 000000000000..f308c13252b5 --- /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 7386fc3373da..000000000000 --- 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 72c6ea528837..12a70d42b403 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\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 @@ * * @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 @@ public function supports(Request $request): ?bool 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 @@ public function supports(Request $request): ?bool 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, - ]; - } - - /** - * @param array $credentials - */ - public function getUser($credentials): ?UserInterface - { - return $this->rememberMeServices->performLogin($credentials['cookie_parts'], $credentials['request']); - } + $token = $this->rememberMeServices->autoLogin($request); - public function checkCredentials($credentials, UserInterface $user): bool - { - // remember me always is valid (if a user could be found) - return true; + return new SelfValidatingPassport($token->getUser()); } - public function createAuthenticatedToken(UserInterface $user, string $providerKey): TokenInterface + public function createAuthenticatedToken(PassportInterface $passport, 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 3a01087767ea..140b6c271efb 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/RemoteUserAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/RemoteUserAuthenticator.php @@ -24,6 +24,8 @@ * @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 88d0d7f9654f..000000000000 --- 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 d482579d0564..c76f3f94e5f8 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/X509Authenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/X509Authenticator.php @@ -24,7 +24,7 @@ * @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 6e48e171b605..80f740480b1c 100644 --- a/src/Symfony/Component/Security/Http/Event/LoginSuccessEvent.php +++ b/src/Symfony/Component/Security/Http/Event/LoginSuccessEvent.php @@ -5,7 +5,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\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 @@ 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 @@ public function getAuthenticator(): AuthenticatorInterface 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 cc37bf33f202..eac7f0374126 100644 --- a/src/Symfony/Component/Security/Http/Event/VerifyAuthenticatorCredentialsEvent.php +++ b/src/Symfony/Component/Security/Http/Event/VerifyAuthenticatorCredentialsEvent.php @@ -5,6 +5,7 @@ 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 @@ 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 @@ public function getAuthenticator(): AuthenticatorInterface 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 fcde7924528f..65c8ffa3e397 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\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 @@ public function __construct(CsrfTokenManagerInterface $csrfTokenManager) 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 28800e626007..0d22bf22ca48 100644 --- a/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php @@ -4,10 +4,9 @@ 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 @@ public function __construct(EncoderFactoryInterface $encoderFactory) $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; - } + /** @var PasswordUpgradeBadge $badge */ + $badge = $passport->getBadge(PasswordUpgradeBadge::class); + $plaintextPassword = $badge->getPlaintextPassword(); + $badge->eraseCredentials(); - $user = $event->getUser(); - if (!$user instanceof UserInterface) { + 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 269d23278618..da582a7cc6a5 100644 --- a/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php @@ -4,8 +4,7 @@ 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 @@ public function __construct(RememberMeServicesInterface $rememberMeServices, ?Lo $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 @@ public function onSuccessfulLogin(LoginSuccessEvent $event): void 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 34fdfdf84d79..fbcc0bd549b9 100644 --- a/src/Symfony/Component/Security/Http/EventListener/UserCheckerListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/UserCheckerListener.php @@ -4,7 +4,9 @@ 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 @@ public function __construct(UserCheckerInterface $userChecker) 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 77bbb39ec92c..0287dc4f5d00 100644 --- a/src/Symfony/Component/Security/Http/EventListener/VerifyAuthenticatorCredentialsListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/VerifyAuthenticatorCredentialsListener.php @@ -5,10 +5,9 @@ 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 @@ public function __construct(EncoderFactoryInterface $encoderFactory) 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()) { - return; + throw new BadCredentialsException('The presented password is invalid.'); } - $event->setCredentialsValid($this->encoderFactory->getEncoder($user)->isPasswordValid($user->getPassword(), $presentedPassword, $user->getSalt())); + if (!$this->encoderFactory->getEncoder($user)->isPasswordValid($user->getPassword(), $presentedPassword, $user->getSalt())) { + throw new BadCredentialsException('The presented password is invalid.'); + } + + $badge->markResolved(); return; } - if ($authenticator instanceof TokenAuthenticatedInterface) { - if (null !== $authenticator->getToken($event->getCredentials())) { - // Token based authenticators do not have a credential validation step - $event->setCredentialsValid(); + if ($passport->hasBadge(CustomCredentials::class)) { + /** @var CustomCredentials $badge */ + $badge = $passport->getBadge(CustomCredentials::class); + if ($badge->isResolved()) { + return; } - return; - } - - if ($authenticator instanceof CustomAuthenticatedInterface) { - $event->setCredentialsValid($authenticator->checkCredentials($event->getCredentials(), $event->getUser())); + $badge->executeCustomChecker($passport->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 e9065d7f526f..22f9dde14b76 100644 --- a/src/Symfony/Component/Security/Http/RememberMe/AbstractRememberMeServices.php +++ b/src/Symfony/Component/Security/Http/RememberMe/AbstractRememberMeServices.php @@ -89,11 +89,6 @@ public function getSecret() 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 7343d79788a6..2cf7994db7ea 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\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 @@ protected function setUp(): void $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 @@ public function testSupportCheckedUponRequestAuthentication() $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 @@ public function testAuthenticateRequest($matchingAuthenticatorIndex) $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 @@ public function provideMatchingAuthenticatorIndex() 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 @@ 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->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 @@ public function testInteractiveAuthenticator() $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 f5d1cfdf98e1..d5593bb37509 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 @@ 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 @@ public function provideSupportsData() 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 3012da746db3..9ab9055455c9 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\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 @@ public function testHandleWhenUsernameLength($username, $ok) $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 @@ public function testHandleNonStringUsernameWithArray($postOnly) $request->setSession($this->createSession()); $this->setUpAuthenticator(['post_only' => $postOnly]); - $this->authenticator->getCredentials($request); + $this->authenticator->authenticate($request); } /** @@ -88,7 +94,7 @@ public function testHandleNonStringUsernameWithInt($postOnly) $request->setSession($this->createSession()); $this->setUpAuthenticator(['post_only' => $postOnly]); - $this->authenticator->getCredentials($request); + $this->authenticator->authenticate($request); } /** @@ -103,22 +109,22 @@ public function testHandleNonStringUsernameWithObject($postOnly) $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 @@ public function postOnlyDataProvider() 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 e2ac0ac991f1..693eb320ab2d 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 @@ public function testExtractCredentialsAndUserFromRequest() '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 @@ public function provideMissingHttpBasicServerParameters() [['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 84ff61781fcb..0f1967600aa4 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\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 @@ public function provideSupportsWithCheckPathData() 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 9bd11ab62d97..d95e68128132 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 @@ 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 @@ public function testSupports() 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 80cddd1ddbf3..f55c72abff5e 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 @@ 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 @@ public function testSupportNoUser() /** * @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 e8395042855e..2490f9d04298 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 @@ 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 @@ public function testAuthentication($user, $credentials) $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 @@ 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)); + + $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 @@ public function testAuthenticationCustomUserKey() '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 @@ public function testAuthenticationCustomCredentialsKey() '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 0c2a15d952e4..baca526bfe20 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 @@ 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 @@ protected function setUp(): void $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 @@ public function testValidCsrfToken() ->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 @@ public function testInvalidCsrfToken() ->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 37d9ee23cc51..5b08721e469c 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 @@ public function testUnsupportedEvents($event) { $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 @@ public function testUpgrade() $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 910c67a0bd65..9af16a6a767c 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\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 @@ protected function setUp(): void $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 @@ public function testCredentialsInvalid() $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 176921d1a174..4d1dd0a5be95 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 @@ 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 @@ public function testStatelessFirewalls() 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 785a31296369..dac1fbaf9268 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 @@ protected function setUp(): void { $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 testPostAuthValidCredentials() + public function testPreAuthenticatedBadge() { - $this->userChecker->expects($this->once())->method('checkPostAuth')->with($this->user); + $this->userChecker->expects($this->never())->method('checkPreAuth'); - $this->listener->postCredentialsVerification($this->createEvent(true)); + $this->listener->preCredentialsVerification($this->createVerifyAuthenticatorCredentialsEvent(new SelfValidatingPassport($this->user, [new PreAuthenticatedUserBadge()]))); } - public function testPostAuthInvalidCredentials() + public function testPostAuthValidCredentials() { - $this->userChecker->expects($this->never())->method('checkPostAuth')->with($this->user); + $this->userChecker->expects($this->once())->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 createVerifyAuthenticatorCredentialsEvent($passport = null) + { + if (null === $passport) { + $passport = new SelfValidatingPassport($this->user); + } + + return new VerifyAuthenticatorCredentialsEvent($this->createMock(AuthenticatorInterface::class), $passport); } - private function createEvent($credentialsValid = false, $customUser = false) + private function createLoginSuccessEvent($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 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 e2c2cc6605b0..a4850ebda7f3 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 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 @@ protected function setUp(): void { $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 @@ protected function setUp(): void */ 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,28 +73,8 @@ public function testEmptyPassword() $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); + $event = $this->createEvent(new Passport($this->user, new PasswordCredentials(''))); $this->listener->onAuthenticating($event); - - $this->assertFalse($event->areCredentialsValid()); } /** @@ -98,10 +84,18 @@ public function testCustomAuthenticated($result) { $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); + } + + $credentials = new CustomCredentials(function () use ($result) { + return $result; + }, ['password' => 'foo']); + $this->listener->onAuthenticating($this->createEvent(new Passport($this->user, $credentials))); - $this->assertEquals($result, $event->areCredentialsValid()); + if (true === $result) { + $this->assertTrue($credentials->isResolved()); + } } public function provideCustomAuthenticatedResults() @@ -110,58 +104,16 @@ public function provideCustomAuthenticatedResults() 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() + public function testNoCredentialsBadgeProvided() { - $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 cf77d99fdf0b..53a6b503a1e8 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 @@ public function create(ContainerBuilder $container, $id, $config, $userProvider, 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 acd1fce318e9..cb65f31fe5ef 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 43c236fcfaf6..95fa3c050fbb 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 @@ public function addConfiguration(NodeDefinition $builder) ; } - 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 2edfb3ff3479..c5f247c307be 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 @@ public function createEntryPoint(ContainerBuilder $container, string $id, array 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 9d121b17fec4..a698d2a1d1ae 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 @@ public function create(ContainerBuilder $container, string $id, array $config, s 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 4e09a3d2f8b1..7aa90405799a 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 @@ protected function createListener(ContainerBuilder $container, string $id, array 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 5f530a17e210..4b29db1a03d3 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 @@ public function create(ContainerBuilder $container, string $id, array $config, ? 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 0f0c44f8abc2..e25c3c7d07df 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 @@ public function create(ContainerBuilder $container, string $id, array $config, s 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 604cee7e4490..f966302a1da6 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 @@ public function create(ContainerBuilder $container, string $id, array $config, s 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 35bcf015575d..ac089d1eb2ea 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -286,7 +286,7 @@ private function createFirewalls(array $config, ContainerBuilder $container) // 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 @@ private function createFirewall(ContainerBuilder $container, string $id, array $ $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 @@ private function createAuthenticationListeners(ContainerBuilder $container, stri $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 @@ private function createAuthenticationListeners(ContainerBuilder $container, stri 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 36a991610593..1d6e1ff2ac5d 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 @@ public function __construct(iterable $authenticators, TokenStorageInterface $tok 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 @@ public function authenticateUser(UserInterface $user, AuthenticatorInterface $au 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 @@ public function supports(Request $request): ?bool 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 @@ public function authenticateRequest(Request $request): ?Response */ 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 @@ private function executeAuthenticators(array $authenticators, Request $request): 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 @@ private function executeAuthenticator(AuthenticatorInterface $authenticator, Req $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 @@ private function handleAuthenticationSuccess(TokenInterface $authenticatedToken, { $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 @@ private function handleAuthenticationFailure(AuthenticationException $authentica $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 51a49a3b1729..6a5ec2f1502e 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 435de68e9887..85a578d8c679 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AbstractPreAuthenticatedAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AbstractPreAuthenticatedAuthenticator.php @@ -92,12 +92,12 @@ public function authenticate(Request $request): PassportInterface 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 27a315b0f565..c0420b5d4cfe 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php @@ -50,12 +50,12 @@ public function authenticate(Request $request): PassportInterface 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 d80356e71340..e1f2b21f70a0 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php @@ -63,7 +63,7 @@ public function authenticate(Request $request): PassportInterface; * * @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 @@ public function createAuthenticatedToken(PassportInterface $passport, string $pr * 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 0bbbb6eb8304..31cab7afcd35 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php @@ -100,12 +100,12 @@ public function authenticate(Request $request): PassportInterface /** * @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 46eb6aa7bcbf..e4c7af251e8c 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php @@ -82,12 +82,12 @@ public function authenticate(Request $request): PassportInterface /** * @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 924ed7fcca34..d165fbceb191 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/JsonLoginAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/JsonLoginAuthenticator.php @@ -93,12 +93,12 @@ public function authenticate(Request $request): PassportInterface 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 12a70d42b403..f5aa016ad15d 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php @@ -76,12 +76,12 @@ public function authenticate(Request $request): PassportInterface 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 3525fa4765b9..774ba60a8603 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 @@ 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 @@ public function getCredentials() 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 @@ public function getProviderKey() */ public function __serialize(): array { - return [$this->providerKey, parent::__serialize()]; + return [$this->firewallName, parent::__serialize()]; } /** @@ -65,7 +59,7 @@ public function __serialize(): array */ 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 03a1c7a78c6a..96da4e35ff73 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 @@ public function getAuthenticator(): AuthenticatorInterface 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 80f740480b1c..c7eee3a66e74 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 @@ public function getRequest(): Request 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 2cf7994db7ea..2b21b380d376 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 @@ public function testSupportCheckedUponRequestAuthentication() // 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 @@ public function testSupportCheckedUponRequestAuthentication() 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 @@ public function provideMatchingAuthenticatorIndex() 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 @@ public function testNoCredentialsValidated() 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 @@ public function testInteractiveAuthenticator() { $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);