From 6b9d78d5e0b0a0f39eac87320fe948eb7002f3e0 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sat, 7 Mar 2020 18:06:29 +0100 Subject: [PATCH] Added tests --- .../Security/Factory/FormLoginFactory.php | 5 +- .../config/security_authenticator.xml | 2 - .../Authentication/AuthenticatorManager.php | 29 +-- .../Authenticator/FormLoginAuthenticator.php | 10 +- .../Authenticator/HttpBasicAuthenticator.php | 5 +- .../Authenticator/RememberMeAuthenticator.php | 27 ++- .../PasswordMigratingListener.php | 6 +- .../Http/EventListener/RememberMeListener.php | 16 +- .../EventListener/UserCheckerListener.php | 8 + ...VerifyAuthenticatorCredentialsListener.php | 4 + .../AuthenticatorManagerTest.php | 225 ++++++++++++++++++ .../AnonymousAuthenticatorTest.php | 61 +++++ .../FormLoginAuthenticatorTest.php | 141 +++++++++++ .../HttpBasicAuthenticatorTest.php | 58 +---- .../RememberMeAuthenticatorTest.php | 92 +++++++ .../CsrfProtectionListenerTest.php | 89 +++++++ .../PasswordMigratingListenerTest.php | 101 ++++++++ .../EventListener/RememberMeListenerTest.php | 101 ++++++++ .../EventListener/SessionListenerTest.php | 75 ++++++ .../EventListener/UserCheckerListenerTest.php | 78 ++++++ ...fyAuthenticatorCredentialsListenerTest.php | 167 +++++++++++++ 21 files changed, 1193 insertions(+), 107 deletions(-) create mode 100644 src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php create mode 100644 src/Symfony/Component/Security/Http/Tests/Authenticator/AnonymousAuthenticatorTest.php create mode 100644 src/Symfony/Component/Security/Http/Tests/Authenticator/FormLoginAuthenticatorTest.php rename src/Symfony/Component/Security/{Core/Tests/Authentication => Http/Tests}/Authenticator/HttpBasicAuthenticatorTest.php (52%) create mode 100644 src/Symfony/Component/Security/Http/Tests/Authenticator/RememberMeAuthenticatorTest.php create mode 100644 src/Symfony/Component/Security/Http/Tests/EventListener/CsrfProtectionListenerTest.php create mode 100644 src/Symfony/Component/Security/Http/Tests/EventListener/PasswordMigratingListenerTest.php create mode 100644 src/Symfony/Component/Security/Http/Tests/EventListener/RememberMeListenerTest.php create mode 100644 src/Symfony/Component/Security/Http/Tests/EventListener/SessionListenerTest.php create mode 100644 src/Symfony/Component/Security/Http/Tests/EventListener/UserCheckerListenerTest.php create mode 100644 src/Symfony/Component/Security/Http/Tests/EventListener/VerifyAuthenticatorCredentialsListenerTest.php diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php index 555cac383e..0fe2d995b3 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php @@ -104,9 +104,8 @@ class FormLoginFactory extends AbstractFactory implements AuthenticatorFactoryIn $options = array_merge($defaultOptions, array_intersect_key($config, $defaultOptions)); $container ->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.form_login')) - ->replaceArgument(1, isset($config['csrf_token_generator']) ? new Reference($config['csrf_token_generator']) : null) - ->replaceArgument(2, new Reference($userProviderId)) - ->replaceArgument(3, $options); + ->replaceArgument(1, new Reference($userProviderId)) + ->replaceArgument(2, $options); return $authenticatorId; } diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml index 757aef78e7..a5b6e87782 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml @@ -84,7 +84,6 @@ abstract="true"> realm name user provider - @@ -92,7 +91,6 @@ class="Symfony\Component\Security\Http\Authenticator\FormLoginAuthenticator" abstract="true"> - user provider options diff --git a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php index f7dacacbc4..c309485293 100644 --- a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php +++ b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php @@ -23,7 +23,6 @@ use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; -use Symfony\Component\Security\Http\Authenticator\Token\PreAuthenticationToken; use Symfony\Component\Security\Http\Event\InteractiveLoginEvent; use Symfony\Component\Security\Http\Event\LoginFailureEvent; use Symfony\Component\Security\Http\Event\LoginSuccessEvent; @@ -40,8 +39,6 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; */ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthenticatorInterface { - use AuthenticatorManagerTrait; - private $authenticators; private $tokenStorage; private $eventDispatcher; @@ -131,7 +128,9 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent // lazily (after initialization). This is important for e.g. the AnonymousAuthenticator // as its support is relying on the (initialized) token in the TokenStorage. if (false === $authenticator->supports($request)) { - $this->logger->debug('Skipping the "{authenticator}" authenticator as it did not support the request.', ['authenticator' => \get_class($authenticator)]); + if (null !== $this->logger) { + $this->logger->debug('Skipping the "{authenticator}" authenticator as it did not support the request.', ['authenticator' => \get_class($authenticator)]); + } continue; } @@ -215,21 +214,14 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent throw new UsernameNotFoundException(sprintf('Null returned from "%s::getUser()".', \get_class($authenticator))); } - if (!$user instanceof UserInterface) { - throw new \UnexpectedValueException(sprintf('The %s::getUser() method must return a UserInterface. You returned %s.', \get_class($authenticator), \is_object($user) ? \get_class($user) : \gettype($user))); - } - $event = new VerifyAuthenticatorCredentialsEvent($authenticator, $credentials, $user); $this->eventDispatcher->dispatch($event); if (true !== $event->areCredentialsValid()) { throw new BadCredentialsException(sprintf('Authentication failed because "%s" did not approve the credentials.', \get_class($authenticator))); } -// turn the UserInterface into a TokenInterface + // turn the UserInterface into a TokenInterface $authenticatedToken = $authenticator->createAuthenticatedToken($user, $this->providerKey); - if (!$authenticatedToken instanceof TokenInterface) { - throw new \UnexpectedValueException(sprintf('The %s::createAuthenticatedToken() method must return a TokenInterface. You returned %s.', \get_class($authenticator), \is_object($authenticatedToken) ? \get_class($authenticatedToken) : \gettype($authenticatedToken))); - } if (true === $this->eraseCredentials) { $authenticatedToken->eraseCredentials(); @@ -259,21 +251,10 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent return $loginSuccessEvent->getResponse(); } - private function handleAuthenticationFailure(AuthenticationException $exception, TokenInterface $token) - { - if (null !== $this->eventDispatcher) { - $this->eventDispatcher->dispatch(new AuthenticationFailureEvent($token, $exception), AuthenticationEvents::AUTHENTICATION_FAILURE); - } - - $exception->setToken($token); - - throw $exception; - } - /** * Handles an authentication failure and returns the Response for the authenticator. */ - private function handleAuthenticatorFailure(AuthenticationException $authenticationException, Request $request, AuthenticatorInterface $authenticator): ?Response + private function handleAuthenticationFailure(AuthenticationException $authenticationException, Request $request, AuthenticatorInterface $authenticator): ?Response { $response = $authenticator->onAuthenticationFailure($request, $authenticationException); diff --git a/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php index 2ec3792a7c..cd8c569c57 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php @@ -20,7 +20,6 @@ use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; -use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Security\Http\HttpUtils; use Symfony\Component\Security\Http\ParameterBagUtils; use Symfony\Component\Security\Http\Util\TargetPathTrait; @@ -38,13 +37,11 @@ class FormLoginAuthenticator extends AbstractLoginFormAuthenticator implements P private $options; private $httpUtils; - private $csrfTokenManager; private $userProvider; - public function __construct(HttpUtils $httpUtils, ?CsrfTokenManagerInterface $csrfTokenManager, UserProviderInterface $userProvider, array $options) + public function __construct(HttpUtils $httpUtils, UserProviderInterface $userProvider, array $options) { $this->httpUtils = $httpUtils; - $this->csrfTokenManager = $csrfTokenManager; $this->options = array_merge([ 'username_parameter' => '_username', 'password_parameter' => '_password', @@ -75,10 +72,7 @@ class FormLoginAuthenticator extends AbstractLoginFormAuthenticator implements P public function getCredentials(Request $request): array { $credentials = []; - - if (null !== $this->csrfTokenManager) { - $credentials['csrf_token'] = ParameterBagUtils::getRequestParameterValue($request, $this->options['csrf_parameter']); - } + $credentials['csrf_token'] = ParameterBagUtils::getRequestParameterValue($request, $this->options['csrf_parameter']); if ($this->options['post_only']) { $credentials['username'] = ParameterBagUtils::getParameterBagValue($request->request, $this->options['username_parameter']); diff --git a/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php index f896d924a8..77480eea45 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php @@ -16,7 +16,6 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; -use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; @@ -33,14 +32,12 @@ class HttpBasicAuthenticator implements AuthenticatorInterface, AuthenticationEn { private $realmName; private $userProvider; - private $encoderFactory; private $logger; - public function __construct(string $realmName, UserProviderInterface $userProvider, EncoderFactoryInterface $encoderFactory, ?LoggerInterface $logger = null) + public function __construct(string $realmName, UserProviderInterface $userProvider, ?LoggerInterface $logger = null) { $this->realmName = $realmName; $this->userProvider = $userProvider; - $this->encoderFactory = $encoderFactory; $this->logger = $logger; } diff --git a/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php index 893bd099de..1ffdd1b997 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Http\Authenticator\Token; +namespace Symfony\Component\Security\Http\Authenticator; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -18,9 +18,7 @@ use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInt use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\User\UserInterface; -use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; use Symfony\Component\Security\Http\RememberMe\AbstractRememberMeServices; -use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy; /** * The RememberMe *Authenticator* performs remember me authentication. @@ -35,21 +33,22 @@ use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy; * * @final */ -class RememberMeAuthenticator implements AuthenticatorInterface +class RememberMeAuthenticator implements AuthenticatorInterface, CustomAuthenticatedInterface { private $rememberMeServices; private $secret; private $tokenStorage; - private $options; - private $sessionStrategy; + private $options = [ + 'secure' => false, + 'httponly' => true, + ]; - public function __construct(AbstractRememberMeServices $rememberMeServices, string $secret, TokenStorageInterface $tokenStorage, array $options, ?SessionAuthenticationStrategy $sessionStrategy = null) + public function __construct(AbstractRememberMeServices $rememberMeServices, string $secret, TokenStorageInterface $tokenStorage, array $options) { $this->rememberMeServices = $rememberMeServices; $this->secret = $secret; $this->tokenStorage = $tokenStorage; - $this->options = $options; - $this->sessionStrategy = $sessionStrategy; + $this->options = array_merge($this->options, $options); } public function supports(Request $request): ?bool @@ -87,6 +86,12 @@ class RememberMeAuthenticator implements AuthenticatorInterface return $this->rememberMeServices->performLogin($credentials['cookie_parts'], $credentials['request']); } + public function checkCredentials($credentials, UserInterface $user): bool + { + // remember me always is valid (if a user could be found) + return true; + } + public function createAuthenticatedToken(UserInterface $user, string $providerKey): TokenInterface { return new RememberMeToken($user, $providerKey, $this->secret); @@ -101,10 +106,6 @@ class RememberMeAuthenticator implements AuthenticatorInterface public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response { - if ($request->hasSession() && $request->getSession()->isStarted()) { - $this->sessionStrategy->onAuthentication($request, $token); - } - return null; } } diff --git a/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php b/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php index c97b722ff1..28800e6260 100644 --- a/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php @@ -36,7 +36,7 @@ class PasswordMigratingListener implements EventSubscriberInterface return; } - if (null !== $password = $authenticator->getPassword($event->getCredentials())) { + if (null === $password = $authenticator->getPassword($event->getCredentials())) { return; } @@ -46,11 +46,11 @@ class PasswordMigratingListener implements EventSubscriberInterface } $passwordEncoder = $this->encoderFactory->getEncoder($user); - if (!method_exists($passwordEncoder, 'needsRehash') || !$passwordEncoder->needsRehash($user)) { + if (!$passwordEncoder->needsRehash($user->getPassword())) { return; } - $authenticator->upgradePassword($user, $passwordEncoder->encodePassword($user, $password)); + $authenticator->upgradePassword($user, $passwordEncoder->encodePassword($password, $user->getSalt())); } public static function getSubscribedEvents(): array diff --git a/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php b/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php index 522f5090d6..72ce7c13f9 100644 --- a/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php @@ -39,7 +39,15 @@ class RememberMeListener implements EventSubscriberInterface public function onSuccessfulLogin(LoginSuccessEvent $event): void { - if (!$this->isRememberMeEnabled($event->getAuthenticator(), $event->getProviderKey())) { + if (!$this->isRememberMeEnabled($event->getProviderKey(), $event->getAuthenticator())) { + return; + } + + if (null === $event->getResponse()) { + if (null !== $this->logger) { + $this->logger->debug('Remember me skipped: the authenticator did not set a success response.', ['authenticator' => \get_class($event->getAuthenticator())]); + } + return; } @@ -48,21 +56,21 @@ class RememberMeListener implements EventSubscriberInterface public function onFailedLogin(LoginFailureEvent $event): void { - if (!$this->isRememberMeEnabled($event->getAuthenticator(), $event->getProviderKey())) { + if (!$this->isRememberMeEnabled($event->getProviderKey())) { return; } $this->rememberMeServices->loginFail($event->getRequest(), $event->getException()); } - private function isRememberMeEnabled(AuthenticatorInterface $authenticator, string $providerKey): bool + private function isRememberMeEnabled(string $providerKey, ?AuthenticatorInterface $authenticator = null): bool { if ($providerKey !== $this->providerKey) { // This listener is created for a different firewall. return false; } - if (!$authenticator instanceof RememberMeAuthenticatorInterface || !$authenticator->supportsRememberMe()) { + if (null !== $authenticator && (!$authenticator instanceof RememberMeAuthenticatorInterface || !$authenticator->supportsRememberMe())) { if (null !== $this->logger) { $this->logger->debug('Remember me skipped: your authenticator does not support it.', ['authenticator' => \get_class($authenticator)]); } diff --git a/src/Symfony/Component/Security/Http/EventListener/UserCheckerListener.php b/src/Symfony/Component/Security/Http/EventListener/UserCheckerListener.php index c0c6c6895d..8ebbaca709 100644 --- a/src/Symfony/Component/Security/Http/EventListener/UserCheckerListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/UserCheckerListener.php @@ -23,11 +23,19 @@ class UserCheckerListener implements EventSubscriberInterface public function preCredentialsVerification(VerifyAuthenticatorCredentialsEvent $event): void { + if (null === $event->getUser()) { + return; + } + $this->userChecker->checkPreAuth($event->getUser()); } public function postCredentialsVerification(VerifyAuthenticatorCredentialsEvent $event): void { + if (null === $event->getUser() || !$event->areCredentialsValid()) { + return; + } + $this->userChecker->checkPostAuth($event->getUser()); } diff --git a/src/Symfony/Component/Security/Http/EventListener/VerifyAuthenticatorCredentialsListener.php b/src/Symfony/Component/Security/Http/EventListener/VerifyAuthenticatorCredentialsListener.php index c8ab235f79..77bbb39ec9 100644 --- a/src/Symfony/Component/Security/Http/EventListener/VerifyAuthenticatorCredentialsListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/VerifyAuthenticatorCredentialsListener.php @@ -31,6 +31,10 @@ class VerifyAuthenticatorCredentialsListener implements EventSubscriberInterface public function onAuthenticating(VerifyAuthenticatorCredentialsEvent $event): void { + if ($event->areCredentialsValid()) { + return; + } + $authenticator = $event->getAuthenticator(); if ($authenticator instanceof PasswordAuthenticatedInterface) { // Use the password encoder to validate the credentials diff --git a/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php b/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php new file mode 100644 index 0000000000..46dc09e2f8 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php @@ -0,0 +1,225 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\Authentication; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent; +use Symfony\Component\Security\Core\Exception\BadCredentialsException; +use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Http\Authentication\AuthenticatorManager; +use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Event\LoginSuccessEvent; +use Symfony\Component\Security\Http\Event\InteractiveLoginEvent; +use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; + +class AuthenticatorManagerTest extends TestCase +{ + private $tokenStorage; + private $eventDispatcher; + private $request; + private $user; + private $token; + private $response; + + protected function setUp(): void + { + $this->tokenStorage = $this->createMock(TokenStorageInterface::class); + $this->eventDispatcher = $this->createMock(EventDispatcherInterface::class); + $this->request = new Request(); + $this->user = $this->createMock(UserInterface::class); + $this->token = $this->createMock(TokenInterface::class); + $this->response = $this->createMock(Response::class); + } + + /** + * @dataProvider provideSupportsData + */ + public function testSupports($authenticators, $result) + { + $manager = $this->createManager($authenticators); + + $this->assertEquals($result, $manager->supports($this->request)); + } + + public function provideSupportsData() + { + yield [[$this->createAuthenticator(null), $this->createAuthenticator(null)], null]; + yield [[$this->createAuthenticator(null), $this->createAuthenticator(false)], null]; + + yield [[$this->createAuthenticator(null), $this->createAuthenticator(true)], true]; + yield [[$this->createAuthenticator(true), $this->createAuthenticator(false)], true]; + + yield [[$this->createAuthenticator(false), $this->createAuthenticator(false)], false]; + yield [[], false]; + } + + public function testSupportCheckedUponRequestAuthentication() + { + // the attribute stores the supported authenticators, returning false now + // means support changed between calling supports() and authenticateRequest() + // (which is the case with lazy firewalls and e.g. the AnonymousAuthenticator) + $authenticator = $this->createAuthenticator(false); + $this->request->attributes->set('_guard_authenticators', [$authenticator]); + + $authenticator->expects($this->never())->method('getCredentials'); + + $manager = $this->createManager([$authenticator]); + $manager->authenticateRequest($this->request); + } + + /** + * @dataProvider provideMatchingAuthenticatorIndex + */ + public function testAuthenticateRequest($matchingAuthenticatorIndex) + { + $authenticators = [$this->createAuthenticator(0 === $matchingAuthenticatorIndex), $this->createAuthenticator(1 === $matchingAuthenticatorIndex)]; + $this->request->attributes->set('_guard_authenticators', $authenticators); + $matchingAuthenticator = $authenticators[$matchingAuthenticatorIndex]; + + $authenticators[($matchingAuthenticatorIndex + 1) % 2]->expects($this->never())->method('getCredentials'); + + $matchingAuthenticator->expects($this->any())->method('getCredentials')->willReturn(['password' => 'pa$$']); + $matchingAuthenticator->expects($this->any())->method('getUser')->willReturn($this->user); + $this->eventDispatcher->expects($this->exactly(4)) + ->method('dispatch') + ->with($this->callback(function ($event) use ($matchingAuthenticator) { + if ($event instanceof VerifyAuthenticatorCredentialsEvent) { + return $event->getAuthenticator() === $matchingAuthenticator + && $event->getCredentials() === ['password' => 'pa$$'] + && $event->getUser() === $this->user; + } + + return $event instanceof InteractiveLoginEvent || $event instanceof LoginSuccessEvent || $event instanceof AuthenticationSuccessEvent; + })) + ->will($this->returnCallback(function ($event) { + if ($event instanceof VerifyAuthenticatorCredentialsEvent) { + $event->setCredentialsValid(true); + } + + return $event; + })); + $matchingAuthenticator->expects($this->any())->method('createAuthenticatedToken')->willReturn($this->token); + + $this->tokenStorage->expects($this->once())->method('setToken')->with($this->token); + + $matchingAuthenticator->expects($this->any()) + ->method('onAuthenticationSuccess') + ->with($this->anything(), $this->token, 'main') + ->willReturn($this->response); + + $manager = $this->createManager($authenticators); + $this->assertSame($this->response, $manager->authenticateRequest($this->request)); + } + + public function provideMatchingAuthenticatorIndex() + { + yield [0]; + yield [1]; + } + + public function testUserNotFound() + { + $authenticator = $this->createAuthenticator(); + $this->request->attributes->set('_guard_authenticators', [$authenticator]); + + $authenticator->expects($this->any())->method('getCredentials')->willReturn(['username' => 'john']); + $authenticator->expects($this->any())->method('getUser')->with(['username' => 'john'])->willReturn(null); + + $authenticator->expects($this->once()) + ->method('onAuthenticationFailure') + ->with($this->request, $this->isInstanceOf(UsernameNotFoundException::class)); + + $manager = $this->createManager([$authenticator]); + $manager->authenticateRequest($this->request); + } + + public function testNoCredentialsValidated() + { + $authenticator = $this->createAuthenticator(); + $this->request->attributes->set('_guard_authenticators', [$authenticator]); + + $authenticator->expects($this->any())->method('getCredentials')->willReturn(['username' => 'john']); + $authenticator->expects($this->any())->method('getUser')->willReturn($this->user); + + $authenticator->expects($this->once()) + ->method('onAuthenticationFailure') + ->with($this->request, $this->isInstanceOf(BadCredentialsException::class)); + + $manager = $this->createManager([$authenticator]); + $manager->authenticateRequest($this->request); + } + + /** + * @dataProvider provideEraseCredentialsData + */ + public function testEraseCredentials($eraseCredentials) + { + $authenticator = $this->createAuthenticator(); + $this->request->attributes->set('_guard_authenticators', [$authenticator]); + + $authenticator->expects($this->any())->method('getCredentials')->willReturn(['username' => 'john']); + $authenticator->expects($this->any())->method('getUser')->willReturn($this->user); + $this->eventDispatcher->expects($this->any()) + ->method('dispatch') + ->will($this->returnCallback(function ($event) { + if ($event instanceof VerifyAuthenticatorCredentialsEvent) { + $event->setCredentialsValid(true); + } + + return $event; + })); + + $authenticator->expects($this->any())->method('createAuthenticatedToken')->willReturn($this->token); + + $this->token->expects($eraseCredentials ? $this->once() : $this->never())->method('eraseCredentials'); + + $manager = $this->createManager([$authenticator], 'main', $eraseCredentials); + $manager->authenticateRequest($this->request); + } + + public function provideEraseCredentialsData() + { + yield [true]; + yield [false]; + } + + public function testAuthenticateUser() + { + $authenticator = $this->createAuthenticator(); + $authenticator->expects($this->any())->method('createAuthenticatedToken')->willReturn($this->token); + $authenticator->expects($this->any())->method('onAuthenticationSuccess')->willReturn($this->response); + + $this->tokenStorage->expects($this->once())->method('setToken')->with($this->token); + + $manager = $this->createManager([$authenticator]); + $this->assertSame($this->response, $manager->authenticateUser($this->user, $authenticator, $this->request)); + } + + private function createAuthenticator($supports = true) + { + $authenticator = $this->createMock(AuthenticatorInterface::class); + $authenticator->expects($this->any())->method('supports')->willReturn($supports); + + return $authenticator; + } + + private function createManager($authenticators, $providerKey = 'main', $eraseCredentials = true) + { + return new AuthenticatorManager($authenticators, $this->tokenStorage, $this->eventDispatcher, $providerKey, null, $eraseCredentials); + } +} diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/AnonymousAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/AnonymousAuthenticatorTest.php new file mode 100644 index 0000000000..f5d1cfdf98 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/AnonymousAuthenticatorTest.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\Authenticator; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Http\Authenticator\AnonymousAuthenticator; + +class AnonymousAuthenticatorTest extends TestCase +{ + private $tokenStorage; + private $authenticator; + private $request; + + protected function setUp(): void + { + $this->tokenStorage = $this->createMock(TokenStorageInterface::class); + $this->authenticator = new AnonymousAuthenticator('s3cr3t', $this->tokenStorage); + $this->request = new Request(); + } + + /** + * @dataProvider provideSupportsData + */ + public function testSupports($tokenAlreadyAvailable, $result) + { + $this->tokenStorage->expects($this->any())->method('getToken')->willReturn($tokenAlreadyAvailable ? $this->createMock(TokenStorageInterface::class) : null); + + $this->assertEquals($result, $this->authenticator->supports($this->request)); + } + + public function provideSupportsData() + { + yield [true, null]; + yield [false, false]; + } + + public function testAlwaysValidCredentials() + { + $this->assertTrue($this->authenticator->checkCredentials([], $this->createMock(UserInterface::class))); + } + + public function testAuthenticatedToken() + { + $token = $this->authenticator->createAuthenticatedToken($this->authenticator->getUser([]), 'main'); + + $this->assertTrue($token->isAuthenticated()); + $this->assertEquals('anon.', $token->getUser()); + } +} diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/FormLoginAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/FormLoginAuthenticatorTest.php new file mode 100644 index 0000000000..058508f25e --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/FormLoginAuthenticatorTest.php @@ -0,0 +1,141 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\Authenticator; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\Security\Core\Exception\BadCredentialsException; +use Symfony\Component\Security\Core\Security; +use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Http\Authenticator\FormLoginAuthenticator; +use Symfony\Component\Security\Http\HttpUtils; + +class FormLoginAuthenticatorTest extends TestCase +{ + private $userProvider; + private $authenticator; + + protected function setUp(): void + { + $this->userProvider = $this->createMock(UserProviderInterface::class); + } + + /** + * @dataProvider provideUsernamesForLength + */ + public function testHandleWhenUsernameLength($username, $ok) + { + if ($ok) { + $this->expectNotToPerformAssertions(); + } else { + $this->expectException(BadCredentialsException::class); + $this->expectExceptionMessage('Invalid username.'); + } + + $request = Request::create('/login_check', 'POST', ['_username' => $username]); + $request->setSession($this->createSession()); + + $this->setUpAuthenticator(); + $this->authenticator->getCredentials($request); + } + + public function provideUsernamesForLength() + { + yield [str_repeat('x', Security::MAX_USERNAME_LENGTH + 1), false]; + yield [str_repeat('x', Security::MAX_USERNAME_LENGTH - 1), true]; + } + + /** + * @dataProvider postOnlyDataProvider + */ + public function testHandleNonStringUsernameWithArray($postOnly) + { + $this->expectException(BadRequestHttpException::class); + $this->expectExceptionMessage('The key "_username" must be a string, "array" given.'); + + $request = Request::create('/login_check', 'POST', ['_username' => []]); + $request->setSession($this->createSession()); + + $this->setUpAuthenticator(['post_only' => $postOnly]); + $this->authenticator->getCredentials($request); + } + + /** + * @dataProvider postOnlyDataProvider + */ + public function testHandleNonStringUsernameWithInt($postOnly) + { + $this->expectException(BadRequestHttpException::class); + $this->expectExceptionMessage('The key "_username" must be a string, "integer" given.'); + + $request = Request::create('/login_check', 'POST', ['_username' => 42]); + $request->setSession($this->createSession()); + + $this->setUpAuthenticator(['post_only' => $postOnly]); + $this->authenticator->getCredentials($request); + } + + /** + * @dataProvider postOnlyDataProvider + */ + public function testHandleNonStringUsernameWithObject($postOnly) + { + $this->expectException(BadRequestHttpException::class); + $this->expectExceptionMessage('The key "_username" must be a string, "object" given.'); + + $request = Request::create('/login_check', 'POST', ['_username' => new \stdClass()]); + $request->setSession($this->createSession()); + + $this->setUpAuthenticator(['post_only' => $postOnly]); + $this->authenticator->getCredentials($request); + } + + /** + * @dataProvider postOnlyDataProvider + */ + public function testHandleNonStringUsernameWith__toString($postOnly) + { + $usernameObject = $this->getMockBuilder(DummyUserClass::class)->getMock(); + $usernameObject->expects($this->once())->method('__toString')->willReturn('someUsername'); + + $request = Request::create('/login_check', 'POST', ['_username' => $usernameObject]); + $request->setSession($this->createSession()); + + $this->setUpAuthenticator(['post_only' => $postOnly]); + $this->authenticator->getCredentials($request); + } + + public function postOnlyDataProvider() + { + yield [true]; + yield [false]; + } + + private function setUpAuthenticator(array $options = []) + { + $this->authenticator = new FormLoginAuthenticator(new HttpUtils(), $this->userProvider, $options); + } + + private function createSession() + { + return $this->createMock('Symfony\Component\HttpFoundation\Session\SessionInterface'); + } +} + +class DummyUserClass +{ + public function __toString(): string + { + return ''; + } +} diff --git a/src/Symfony/Component/Security/Core/Tests/Authentication/Authenticator/HttpBasicAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/HttpBasicAuthenticatorTest.php similarity index 52% rename from src/Symfony/Component/Security/Core/Tests/Authentication/Authenticator/HttpBasicAuthenticatorTest.php rename to src/Symfony/Component/Security/Http/Tests/Authenticator/HttpBasicAuthenticatorTest.php index b713840441..e2ac0ac991 100644 --- a/src/Symfony/Component/Security/Core/Tests/Authentication/Authenticator/HttpBasicAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/HttpBasicAuthenticatorTest.php @@ -1,6 +1,6 @@ expects($this->any()) ->method('getEncoder') ->willReturn($this->encoder); + + $this->authenticator = new HttpBasicAuthenticator('test', $this->userProvider); } - public function testValidUsernameAndPasswordServerParameters() + public function testExtractCredentialsAndUserFromRequest() { $request = new Request([], [], [], [], [], [ 'PHP_AUTH_USER' => 'TheUsername', 'PHP_AUTH_PW' => 'ThePassword', ]); - $authenticator = new HttpBasicAuthenticator('test', $this->userProvider, $this->encoderFactory); - $credentials = $authenticator->getCredentials($request); + $credentials = $this->authenticator->getCredentials($request); $this->assertEquals([ 'username' => 'TheUsername', 'password' => 'ThePassword', @@ -55,53 +54,20 @@ class HttpBasicAuthenticatorTest extends TestCase ->with('TheUsername') ->willReturn($mockedUser); - $user = $authenticator->getUser($credentials, $this->userProvider); + $user = $this->authenticator->getUser($credentials); $this->assertSame($mockedUser, $user); - $this->encoder - ->expects($this->any()) - ->method('isPasswordValid') - ->with('ThePassword', 'ThePassword', null) - ->willReturn(true); - - $checkCredentials = $authenticator->checkCredentials($credentials, $user); - $this->assertTrue($checkCredentials); + $this->assertEquals('ThePassword', $this->authenticator->getPassword($credentials)); } - /** @dataProvider provideInvalidPasswords */ - public function testInvalidPassword($presentedPassword, $exceptionMessage) - { - $authenticator = new HttpBasicAuthenticator('test', $this->userProvider, $this->encoderFactory); - - $this->encoder - ->expects($this->any()) - ->method('isPasswordValid') - ->willReturn(false); - - $this->expectException(BadCredentialsException::class); - $this->expectExceptionMessage($exceptionMessage); - - $authenticator->checkCredentials([ - 'username' => 'TheUsername', - 'password' => $presentedPassword, - ], $this->getMockBuilder(UserInterface::class)->getMock()); - } - - public function provideInvalidPasswords() - { - return [ - ['InvalidPassword', 'The presented password is invalid.'], - ['', 'The presented password cannot be empty.'], - ]; - } - - /** @dataProvider provideMissingHttpBasicServerParameters */ + /** + * @dataProvider provideMissingHttpBasicServerParameters + */ public function testHttpBasicServerParametersMissing(array $serverParameters) { $request = new Request([], [], [], [], [], $serverParameters); - $authenticator = new HttpBasicAuthenticator('test', $this->userProvider, $this->encoderFactory); - $this->assertFalse($authenticator->supports($request)); + $this->assertFalse($this->authenticator->supports($request)); } public function provideMissingHttpBasicServerParameters() diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/RememberMeAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/RememberMeAuthenticatorTest.php new file mode 100644 index 0000000000..9bd11ab62d --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/RememberMeAuthenticatorTest.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\Authenticator; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Cookie; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Http\Authenticator\RememberMeAuthenticator; +use Symfony\Component\Security\Http\RememberMe\AbstractRememberMeServices; + +class RememberMeAuthenticatorTest extends TestCase +{ + private $rememberMeServices; + private $tokenStorage; + private $authenticator; + private $request; + + protected function setUp(): void + { + $this->rememberMeServices = $this->createMock(AbstractRememberMeServices::class); + $this->tokenStorage = $this->createMock(TokenStorage::class); + $this->authenticator = new RememberMeAuthenticator($this->rememberMeServices, 's3cr3t', $this->tokenStorage, [ + 'name' => '_remember_me_cookie', + ]); + $this->request = new Request(); + $this->request->cookies->set('_remember_me_cookie', $val = $this->generateCookieValue()); + $this->request->attributes->set(AbstractRememberMeServices::COOKIE_ATTR_NAME, new Cookie('_remember_me_cookie', $val)); + } + + public function testSupportsTokenStorageWithToken() + { + $this->tokenStorage->expects($this->any())->method('getToken')->willReturn(TokenInterface::class); + + $this->assertFalse($this->authenticator->supports($this->request)); + } + + public function testSupportsRequestWithoutAttribute() + { + $this->request->attributes->remove(AbstractRememberMeServices::COOKIE_ATTR_NAME); + + $this->assertNull($this->authenticator->supports($this->request)); + } + + public function testSupportsRequestWithoutCookie() + { + $this->request->cookies->remove('_remember_me_cookie'); + + $this->assertFalse($this->authenticator->supports($this->request)); + } + + public function testSupports() + { + $this->assertNull($this->authenticator->supports($this->request)); + } + + public function testAuthenticate() + { + $credentials = $this->authenticator->getCredentials($this->request); + $this->assertEquals(['part1', 'part2'], $credentials['cookie_parts']); + $this->assertSame($this->request, $credentials['request']); + + $user = $this->createMock(UserInterface::class); + $this->rememberMeServices->expects($this->any()) + ->method('performLogin') + ->with($credentials['cookie_parts'], $credentials['request']) + ->willReturn($user); + + $this->assertSame($user, $this->authenticator->getUser($credentials)); + } + + public function testCredentialsAlwaysValid() + { + $this->assertTrue($this->authenticator->checkCredentials([], $this->createMock(UserInterface::class))); + } + + private function generateCookieValue() + { + return base64_encode(implode(AbstractRememberMeServices::COOKIE_DELIMITER, ['part1', 'part2'])); + } +} diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/CsrfProtectionListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/CsrfProtectionListenerTest.php new file mode 100644 index 0000000000..0c2a15d952 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/CsrfProtectionListenerTest.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\EventListener; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException; +use Symfony\Component\Security\Csrf\CsrfToken; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; +use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\CsrfProtectedAuthenticatorInterface; +use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; +use Symfony\Component\Security\Http\EventListener\CsrfProtectionListener; + +class CsrfProtectionListenerTest extends TestCase +{ + private $csrfTokenManager; + private $listener; + + protected function setUp(): void + { + $this->csrfTokenManager = $this->createMock(CsrfTokenManagerInterface::class); + $this->listener = new CsrfProtectionListener($this->csrfTokenManager); + } + + public function testNonCsrfProtectedAuthenticator() + { + $this->csrfTokenManager->expects($this->never())->method('isTokenValid'); + + $event = $this->createEvent($this->createAuthenticator(false)); + $this->listener->verifyCredentials($event); + } + + public function testValidCsrfToken() + { + $this->csrfTokenManager->expects($this->any()) + ->method('isTokenValid') + ->with(new CsrfToken('authenticator_token_id', 'abc123')) + ->willReturn(true); + + $event = $this->createEvent($this->createAuthenticator(true), ['_csrf' => 'abc123']); + $this->listener->verifyCredentials($event); + + $this->expectNotToPerformAssertions(); + } + + public function testInvalidCsrfToken() + { + $this->expectException(InvalidCsrfTokenException::class); + $this->expectExceptionMessage('Invalid CSRF token.'); + + $this->csrfTokenManager->expects($this->any()) + ->method('isTokenValid') + ->with(new CsrfToken('authenticator_token_id', 'abc123')) + ->willReturn(false); + + $event = $this->createEvent($this->createAuthenticator(true), ['_csrf' => 'abc123']); + $this->listener->verifyCredentials($event); + } + + private function createEvent($authenticator, $credentials = null) + { + return new VerifyAuthenticatorCredentialsEvent($authenticator, $credentials, null); + } + + private function createAuthenticator($supportsCsrf) + { + if (!$supportsCsrf) { + return $this->createMock(AuthenticatorInterface::class); + } + + $authenticator = $this->createMock([AuthenticatorInterface::class, CsrfProtectedAuthenticatorInterface::class]); + $authenticator->expects($this->any())->method('getCsrfTokenId')->willReturn('authenticator_token_id'); + $authenticator->expects($this->any()) + ->method('getCsrfToken') + ->with(['_csrf' => 'abc123']) + ->willReturn('abc123'); + + return $authenticator; + } +} diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/PasswordMigratingListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/PasswordMigratingListenerTest.php new file mode 100644 index 0000000000..37d9ee23cc --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/PasswordMigratingListenerTest.php @@ -0,0 +1,101 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\EventListener; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; +use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface; +use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\PasswordAuthenticatedInterface; +use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; +use Symfony\Component\Security\Http\EventListener\PasswordMigratingListener; + +class PasswordMigratingListenerTest extends TestCase +{ + private $encoderFactory; + private $listener; + private $user; + + protected function setUp(): void + { + $this->encoderFactory = $this->createMock(EncoderFactoryInterface::class); + $this->listener = new PasswordMigratingListener($this->encoderFactory); + $this->user = $this->createMock(UserInterface::class); + } + + /** + * @dataProvider provideUnsupportedEvents + */ + public function testUnsupportedEvents($event) + { + $this->encoderFactory->expects($this->never())->method('getEncoder'); + + $this->listener->onCredentialsVerification($event); + } + + public function provideUnsupportedEvents() + { + // unsupported authenticators + yield [$this->createEvent($this->createMock(AuthenticatorInterface::class), $this->user)]; + yield [$this->createEvent($this->createMock([AuthenticatorInterface::class, PasswordAuthenticatedInterface::class]), $this->user)]; + + // null password + yield [$this->createEvent($this->createAuthenticator(null), $this->user)]; + + // no user + yield [$this->createEvent($this->createAuthenticator('pa$$word'), null)]; + + // invalid password + yield [$this->createEvent($this->createAuthenticator('pa$$word'), $this->user, false)]; + } + + public function testUpgrade() + { + $encoder = $this->createMock(PasswordEncoderInterface::class); + $encoder->expects($this->any())->method('needsRehash')->willReturn(true); + $encoder->expects($this->any())->method('encodePassword')->with('pa$$word', null)->willReturn('new-encoded-password'); + + $this->encoderFactory->expects($this->any())->method('getEncoder')->with($this->user)->willReturn($encoder); + + $this->user->expects($this->any())->method('getPassword')->willReturn('old-encoded-password'); + + $authenticator = $this->createAuthenticator('pa$$word'); + $authenticator->expects($this->once()) + ->method('upgradePassword') + ->with($this->user, 'new-encoded-password') + ; + + $event = $this->createEvent($authenticator, $this->user); + $this->listener->onCredentialsVerification($event); + } + + /** + * @return AuthenticatorInterface + */ + private function createAuthenticator($password) + { + $authenticator = $this->createMock([AuthenticatorInterface::class, PasswordAuthenticatedInterface::class, PasswordUpgraderInterface::class]); + $authenticator->expects($this->any())->method('getPassword')->willReturn($password); + + return $authenticator; + } + + private function createEvent($authenticator, $user, $credentialsValid = true) + { + $event = new VerifyAuthenticatorCredentialsEvent($authenticator, [], $user); + $event->setCredentialsValid($credentialsValid); + + return $event; + } +} diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/RememberMeListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/RememberMeListenerTest.php new file mode 100644 index 0000000000..910c67a0bd --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/RememberMeListenerTest.php @@ -0,0 +1,101 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\EventListener; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\RememberMeAuthenticatorInterface; +use Symfony\Component\Security\Http\Event\LoginFailureEvent; +use Symfony\Component\Security\Http\Event\LoginSuccessEvent; +use Symfony\Component\Security\Http\EventListener\RememberMeListener; +use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; + +class RememberMeListenerTest extends TestCase +{ + private $rememberMeServices; + private $listener; + private $request; + private $response; + private $token; + + protected function setUp(): void + { + $this->rememberMeServices = $this->createMock(RememberMeServicesInterface::class); + $this->listener = new RememberMeListener($this->rememberMeServices); + $this->request = $this->getMockBuilder(Request::class)->disableOriginalConstructor()->getMock(); + $this->response = $this->createMock(Response::class); + $this->token = $this->createMock(TokenInterface::class); + } + + /** + * @dataProvider provideUnsupportingAuthenticators + */ + public function testSuccessfulLoginWithoutSupportingAuthenticator($authenticator) + { + $this->rememberMeServices->expects($this->never())->method('loginSuccess'); + + $event = $this->createLoginSuccessfulEvent('main_firewall', $this->response, $authenticator); + $this->listener->onSuccessfulLogin($event); + } + + public function provideUnsupportingAuthenticators() + { + yield [$this->createMock(AuthenticatorInterface::class)]; + + $authenticator = $this->createMock([AuthenticatorInterface::class, RememberMeAuthenticatorInterface::class]); + $authenticator->expects($this->any())->method('supportsRememberMe')->willReturn(false); + yield [$authenticator]; + } + + public function testSuccessfulLoginWithoutSuccessResponse() + { + $this->rememberMeServices->expects($this->never())->method('loginSuccess'); + + $event = $this->createLoginSuccessfulEvent('main_firewall', null); + $this->listener->onSuccessfulLogin($event); + } + + public function testSuccessfulLogin() + { + $this->rememberMeServices->expects($this->once())->method('loginSuccess')->with($this->request, $this->response, $this->token); + + $event = $this->createLoginSuccessfulEvent('main_firewall', $this->response); + $this->listener->onSuccessfulLogin($event); + } + + public function testCredentialsInvalid() + { + $this->rememberMeServices->expects($this->once())->method('loginFail')->with($this->request, $this->isInstanceOf(AuthenticationException::class)); + + $event = $this->createLoginFailureEvent('main_firewall'); + $this->listener->onFailedLogin($event); + } + + private function createLoginSuccessfulEvent($providerKey, $response, $authenticator = null) + { + if (null === $authenticator) { + $authenticator = $this->createMock([AuthenticatorInterface::class, RememberMeAuthenticatorInterface::class]); + $authenticator->expects($this->any())->method('supportsRememberMe')->willReturn(true); + } + + return new LoginSuccessEvent($authenticator, $this->token, $this->request, $response, $providerKey); + } + + private function createLoginFailureEvent($providerKey) + { + return new LoginFailureEvent(new AuthenticationException(), $this->createMock(AuthenticatorInterface::class), $this->request, null, $providerKey); + } +} diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/SessionListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/SessionListenerTest.php new file mode 100644 index 0000000000..176921d1a1 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/SessionListenerTest.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\EventListener; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Event\LoginSuccessEvent; +use Symfony\Component\Security\Http\EventListener\SessionStrategyListener; +use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface; + +class SessionListenerTest extends TestCase +{ + private $sessionAuthenticationStrategy; + private $listener; + private $request; + private $token; + + protected function setUp(): void + { + $this->sessionAuthenticationStrategy = $this->createMock(SessionAuthenticationStrategyInterface::class); + $this->listener = new SessionStrategyListener($this->sessionAuthenticationStrategy); + $this->request = new Request(); + $this->token = $this->createMock(TokenInterface::class); + } + + public function testRequestWithSession() + { + $this->configurePreviousSession(); + + $this->sessionAuthenticationStrategy->expects($this->once())->method('onAuthentication')->with($this->request, $this->token); + + $this->listener->onSuccessfulLogin($this->createEvent('main_firewall')); + } + + public function testRequestWithoutPreviousSession() + { + $this->sessionAuthenticationStrategy->expects($this->never())->method('onAuthentication')->with($this->request, $this->token); + + $this->listener->onSuccessfulLogin($this->createEvent('main_firewall')); + } + + public function testStatelessFirewalls() + { + $this->sessionAuthenticationStrategy->expects($this->never())->method('onAuthentication'); + + $listener = new SessionStrategyListener($this->sessionAuthenticationStrategy, ['api_firewall']); + $listener->onSuccessfulLogin($this->createEvent('api_firewall')); + } + + private function createEvent($providerKey) + { + return new LoginSuccessEvent($this->createMock(AuthenticatorInterface::class), $this->token, $this->request, null, $providerKey); + } + + private function configurePreviousSession() + { + $session = $this->getMockBuilder('Symfony\Component\HttpFoundation\Session\SessionInterface')->getMock(); + $session->expects($this->any()) + ->method('getName') + ->willReturn('test_session_name'); + $this->request->setSession($session); + $this->request->cookies->set('test_session_name', 'session_cookie_val'); + } +} diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/UserCheckerListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/UserCheckerListenerTest.php new file mode 100644 index 0000000000..785a312963 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/UserCheckerListenerTest.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\EventListener; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Security\Core\User\UserCheckerInterface; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; +use Symfony\Component\Security\Http\EventListener\UserCheckerListener; + +class UserCheckerListenerTest extends TestCase +{ + private $userChecker; + private $listener; + private $user; + + protected function setUp(): void + { + $this->userChecker = $this->createMock(UserCheckerInterface::class); + $this->listener = new UserCheckerListener($this->userChecker); + $this->user = $this->createMock(UserInterface::class); + } + + public function testPreAuth() + { + $this->userChecker->expects($this->once())->method('checkPreAuth')->with($this->user); + + $this->listener->preCredentialsVerification($this->createEvent()); + } + + public function testPreAuthNoUser() + { + $this->userChecker->expects($this->never())->method('checkPreAuth'); + + $this->listener->preCredentialsVerification($this->createEvent(true, null)); + } + + public function testPostAuthValidCredentials() + { + $this->userChecker->expects($this->once())->method('checkPostAuth')->with($this->user); + + $this->listener->postCredentialsVerification($this->createEvent(true)); + } + + public function testPostAuthInvalidCredentials() + { + $this->userChecker->expects($this->never())->method('checkPostAuth')->with($this->user); + + $this->listener->postCredentialsVerification($this->createEvent()); + } + + public function testPostAuthNoUser() + { + $this->userChecker->expects($this->never())->method('checkPostAuth'); + + $this->listener->postCredentialsVerification($this->createEvent(true, null)); + } + + private function createEvent($credentialsValid = false, $customUser = false) + { + $event = new VerifyAuthenticatorCredentialsEvent($this->createMock(AuthenticatorInterface::class), [], false === $customUser ? $this->user : $customUser); + if ($credentialsValid) { + $event->setCredentialsValid(true); + } + + return $event; + } +} diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/VerifyAuthenticatorCredentialsListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/VerifyAuthenticatorCredentialsListenerTest.php new file mode 100644 index 0000000000..e2c2cc6605 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/VerifyAuthenticatorCredentialsListenerTest.php @@ -0,0 +1,167 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\EventListener; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; +use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface; +use Symfony\Component\Security\Core\Exception\BadCredentialsException; +use Symfony\Component\Security\Core\Exception\LogicException; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\CustomAuthenticatedInterface; +use Symfony\Component\Security\Http\Authenticator\PasswordAuthenticatedInterface; +use Symfony\Component\Security\Http\Authenticator\TokenAuthenticatedInterface; +use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; +use Symfony\Component\Security\Http\EventListener\VerifyAuthenticatorCredentialsListener; + +class VerifyAuthenticatorCredentialsListenerTest extends TestCase +{ + private $encoderFactory; + private $listener; + private $user; + + protected function setUp(): void + { + $this->encoderFactory = $this->createMock(EncoderFactoryInterface::class); + $this->listener = new VerifyAuthenticatorCredentialsListener($this->encoderFactory); + $this->user = $this->createMock(UserInterface::class); + } + + /** + * @dataProvider providePasswords + */ + public function testPasswordAuthenticated($password, $passwordValid, $result) + { + $this->user->expects($this->any())->method('getPassword')->willReturn('encoded-password'); + + $encoder = $this->createMock(PasswordEncoderInterface::class); + $encoder->expects($this->any())->method('isPasswordValid')->with('encoded-password', $password)->willReturn($passwordValid); + + $this->encoderFactory->expects($this->any())->method('getEncoder')->with($this->identicalTo($this->user))->willReturn($encoder); + + $event = new VerifyAuthenticatorCredentialsEvent($this->createAuthenticator('password', $password), ['password' => $password], $this->user); + $this->listener->onAuthenticating($event); + $this->assertEquals($result, $event->areCredentialsValid()); + } + + public function providePasswords() + { + yield ['ThePa$$word', true, true]; + yield ['Invalid', false, false]; + } + + public function testEmptyPassword() + { + $this->expectException(BadCredentialsException::class); + $this->expectExceptionMessage('The presented password cannot be empty.'); + + $this->encoderFactory->expects($this->never())->method('getEncoder'); + + $event = new VerifyAuthenticatorCredentialsEvent($this->createAuthenticator('password', ''), ['password' => ''], $this->user); + $this->listener->onAuthenticating($event); + } + + public function testTokenAuthenticated() + { + $this->encoderFactory->expects($this->never())->method('getEncoder'); + + $event = new VerifyAuthenticatorCredentialsEvent($this->createAuthenticator('token', 'some_token'), ['token' => 'abc'], $this->user); + $this->listener->onAuthenticating($event); + + $this->assertTrue($event->areCredentialsValid()); + } + + public function testTokenAuthenticatedReturningNull() + { + $this->encoderFactory->expects($this->never())->method('getEncoder'); + + $event = new VerifyAuthenticatorCredentialsEvent($this->createAuthenticator('token', null), ['token' => 'abc'], $this->user); + $this->listener->onAuthenticating($event); + + $this->assertFalse($event->areCredentialsValid()); + } + + /** + * @dataProvider provideCustomAuthenticatedResults + */ + public function testCustomAuthenticated($result) + { + $this->encoderFactory->expects($this->never())->method('getEncoder'); + + $event = new VerifyAuthenticatorCredentialsEvent($this->createAuthenticator('custom', $result), [], $this->user); + $this->listener->onAuthenticating($event); + + $this->assertEquals($result, $event->areCredentialsValid()); + } + + public function provideCustomAuthenticatedResults() + { + yield [true]; + yield [false]; + } + + public function testAlreadyAuthenticated() + { + $event = new VerifyAuthenticatorCredentialsEvent($this->createAuthenticator(), [], $this->user); + $event->setCredentialsValid(true); + $this->listener->onAuthenticating($event); + + $this->assertTrue($event->areCredentialsValid()); + } + + public function testNoAuthenticatedInterfaceImplemented() + { + $authenticator = $this->createAuthenticator(); + $this->expectException(LogicException::class); + $this->expectExceptionMessage(sprintf('Authenticator %s does not have valid credentials. Authenticators must implement one of the authenticated interfaces (%s, %s or %s).', \get_class($authenticator), PasswordAuthenticatedInterface::class, TokenAuthenticatedInterface::class, CustomAuthenticatedInterface::class)); + + $this->encoderFactory->expects($this->never())->method('getEncoder'); + + $event = new VerifyAuthenticatorCredentialsEvent($authenticator, [], $this->user); + $this->listener->onAuthenticating($event); + } + + /** + * @return AuthenticatorInterface + */ + private function createAuthenticator(?string $type = null, $result = null) + { + $interfaces = [AuthenticatorInterface::class]; + switch ($type) { + case 'password': + $interfaces[] = PasswordAuthenticatedInterface::class; + break; + case 'token': + $interfaces[] = TokenAuthenticatedInterface::class; + break; + case 'custom': + $interfaces[] = CustomAuthenticatedInterface::class; + break; + } + + $authenticator = $this->createMock(1 === \count($interfaces) ? $interfaces[0] : $interfaces); + switch ($type) { + case 'password': + $authenticator->expects($this->any())->method('getPassword')->willReturn($result); + break; + case 'token': + $authenticator->expects($this->any())->method('getToken')->willReturn($result); + break; + case 'custom': + $authenticator->expects($this->any())->method('checkCredentials')->willReturn($result); + break; + } + + return $authenticator; + } +}