From 8934d312723de44a4da596454314603bd7e6e4ae Mon Sep 17 00:00:00 2001 From: konradoboza Date: Fri, 28 Jun 2024 16:07:05 +0200 Subject: [PATCH] IBX-8323: Reworked RepositoryAuthenticationProvider and moved its logic to a dedicated subscriber --- phpstan-baseline.neon | 80 ------ .../Compiler/SecurityPass.php | 13 - src/bundle/Core/Resources/config/security.yml | 8 +- ...RepositoryUserAuthenticationSubscriber.php | 108 +++++++ .../RepositoryAuthenticationProvider.php | 133 --------- .../Compiler/SecurityPassTest.php | 5 - ...sitoryUserAuthenticationSubscriberTest.php | 153 ++++++++++ .../RepositoryAuthenticationProviderTest.php | 272 ------------------ 8 files changed, 268 insertions(+), 504 deletions(-) create mode 100644 src/lib/MVC/Symfony/Security/Authentication/EventSubscriber/RepositoryUserAuthenticationSubscriber.php delete mode 100644 src/lib/MVC/Symfony/Security/Authentication/RepositoryAuthenticationProvider.php create mode 100644 tests/lib/MVC/Symfony/Security/Authentication/EventSubscriber/RepositoryUserAuthenticationSubscriberTest.php delete mode 100644 tests/lib/MVC/Symfony/Security/Authentication/RepositoryAuthenticationProviderTest.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index d2f8a5e796..14d3574e50 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -12275,36 +12275,6 @@ parameters: count: 1 path: src/lib/MVC/Symfony/Security/Authentication/RememberMeRepositoryAuthenticationProvider.php - - - message: "#^Dead catch \\- Ibexa\\\\Core\\\\Repository\\\\User\\\\Exception\\\\UnsupportedPasswordHashType is never thrown in the try block\\.$#" - count: 1 - path: src/lib/MVC/Symfony/Security/Authentication/RepositoryAuthenticationProvider.php - - - - message: "#^Method Ibexa\\\\Core\\\\MVC\\\\Symfony\\\\Security\\\\Authentication\\\\RepositoryAuthenticationProvider\\:\\:checkAuthentication\\(\\) has no return type specified\\.$#" - count: 1 - path: src/lib/MVC/Symfony/Security/Authentication/RepositoryAuthenticationProvider.php - - - - message: "#^Method Ibexa\\\\Core\\\\MVC\\\\Symfony\\\\Security\\\\Authentication\\\\RepositoryAuthenticationProvider\\:\\:setConstantAuthTime\\(\\) has no return type specified\\.$#" - count: 1 - path: src/lib/MVC/Symfony/Security/Authentication/RepositoryAuthenticationProvider.php - - - - message: "#^Method Ibexa\\\\Core\\\\MVC\\\\Symfony\\\\Security\\\\Authentication\\\\RepositoryAuthenticationProvider\\:\\:setPermissionResolver\\(\\) has no return type specified\\.$#" - count: 1 - path: src/lib/MVC/Symfony/Security/Authentication/RepositoryAuthenticationProvider.php - - - - message: "#^Method Ibexa\\\\Core\\\\MVC\\\\Symfony\\\\Security\\\\Authentication\\\\RepositoryAuthenticationProvider\\:\\:setUserService\\(\\) has no return type specified\\.$#" - count: 1 - path: src/lib/MVC/Symfony/Security/Authentication/RepositoryAuthenticationProvider.php - - - - message: "#^Method Ibexa\\\\Core\\\\MVC\\\\Symfony\\\\Security\\\\Authentication\\\\RepositoryAuthenticationProvider\\:\\:startConstantTimer\\(\\) has no return type specified\\.$#" - count: 1 - path: src/lib/MVC/Symfony/Security/Authentication/RepositoryAuthenticationProvider.php - - message: "#^Method Ibexa\\\\Core\\\\MVC\\\\Symfony\\\\Security\\\\Authorization\\\\Attribute\\:\\:__construct\\(\\) has parameter \\$function with no type specified\\.$#" count: 1 @@ -47095,56 +47065,6 @@ parameters: count: 1 path: tests/lib/MVC/Symfony/Security/Authentication/RememberMeRepositoryAuthenticationProviderTest.php - - - message: "#^Call to an undefined method Symfony\\\\Component\\\\Security\\\\Core\\\\User\\\\UserProviderInterface\\:\\:method\\(\\)\\.$#" - count: 1 - path: tests/lib/MVC/Symfony/Security/Authentication/RepositoryAuthenticationProviderTest.php - - - - message: "#^Method Ibexa\\\\Tests\\\\Core\\\\MVC\\\\Symfony\\\\Security\\\\Authentication\\\\RepositoryAuthenticationProviderTest\\:\\:testAuthenticationNotEzUser\\(\\) has no return type specified\\.$#" - count: 1 - path: tests/lib/MVC/Symfony/Security/Authentication/RepositoryAuthenticationProviderTest.php - - - - message: "#^Method Ibexa\\\\Tests\\\\Core\\\\MVC\\\\Symfony\\\\Security\\\\Authentication\\\\RepositoryAuthenticationProviderTest\\:\\:testCheckAuthentication\\(\\) has no return type specified\\.$#" - count: 1 - path: tests/lib/MVC/Symfony/Security/Authentication/RepositoryAuthenticationProviderTest.php - - - - message: "#^Method Ibexa\\\\Tests\\\\Core\\\\MVC\\\\Symfony\\\\Security\\\\Authentication\\\\RepositoryAuthenticationProviderTest\\:\\:testCheckAuthenticationAlreadyLoggedIn\\(\\) has no return type specified\\.$#" - count: 1 - path: tests/lib/MVC/Symfony/Security/Authentication/RepositoryAuthenticationProviderTest.php - - - - message: "#^Method Ibexa\\\\Tests\\\\Core\\\\MVC\\\\Symfony\\\\Security\\\\Authentication\\\\RepositoryAuthenticationProviderTest\\:\\:testCheckAuthenticationCredentialsChanged\\(\\) has no return type specified\\.$#" - count: 1 - path: tests/lib/MVC/Symfony/Security/Authentication/RepositoryAuthenticationProviderTest.php - - - - message: "#^Method Ibexa\\\\Tests\\\\Core\\\\MVC\\\\Symfony\\\\Security\\\\Authentication\\\\RepositoryAuthenticationProviderTest\\:\\:testCheckAuthenticationFailed\\(\\) has no return type specified\\.$#" - count: 1 - path: tests/lib/MVC/Symfony/Security/Authentication/RepositoryAuthenticationProviderTest.php - - - - message: "#^Method Ibexa\\\\Tests\\\\Core\\\\MVC\\\\Symfony\\\\Security\\\\Authentication\\\\RepositoryAuthenticationProviderTest\\:\\:testCheckAuthenticationFailedWhenPasswordInUnsupportedFormat\\(\\) has no return type specified\\.$#" - count: 1 - path: tests/lib/MVC/Symfony/Security/Authentication/RepositoryAuthenticationProviderTest.php - - - - message: "#^Parameter \\#1 \\$user of class Symfony\\\\Component\\\\Security\\\\Core\\\\Authentication\\\\Token\\\\UsernamePasswordToken constructor expects Symfony\\\\Component\\\\Security\\\\Core\\\\User\\\\UserInterface, string given\\.$#" - count: 6 - path: tests/lib/MVC/Symfony/Security/Authentication/RepositoryAuthenticationProviderTest.php - - - - message: "#^Parameter \\#3 \\$roles of class Symfony\\\\Component\\\\Security\\\\Core\\\\Authentication\\\\Token\\\\UsernamePasswordToken constructor expects array\\, string given\\.$#" - count: 9 - path: tests/lib/MVC/Symfony/Security/Authentication/RepositoryAuthenticationProviderTest.php - - - - message: "#^Parameter \\#4 \\$hasherFactory of class Ibexa\\\\Core\\\\MVC\\\\Symfony\\\\Security\\\\Authentication\\\\RepositoryAuthenticationProvider constructor expects Symfony\\\\Component\\\\PasswordHasher\\\\Hasher\\\\PasswordHasherFactoryInterface, PHPUnit\\\\Framework\\\\MockObject\\\\MockObject&Symfony\\\\Component\\\\Security\\\\Core\\\\Encoder\\\\EncoderFactoryInterface given\\.$#" - count: 1 - path: tests/lib/MVC/Symfony/Security/Authentication/RepositoryAuthenticationProviderTest.php - - message: "#^Method Ibexa\\\\Tests\\\\Core\\\\MVC\\\\Symfony\\\\Security\\\\HttpUtilsTest\\:\\:checkRequestPathProvider\\(\\) has no return type specified\\.$#" count: 1 diff --git a/src/bundle/Core/DependencyInjection/Compiler/SecurityPass.php b/src/bundle/Core/DependencyInjection/Compiler/SecurityPass.php index ffcbc8774c..5362782c4a 100644 --- a/src/bundle/Core/DependencyInjection/Compiler/SecurityPass.php +++ b/src/bundle/Core/DependencyInjection/Compiler/SecurityPass.php @@ -11,7 +11,6 @@ use Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface; use Ibexa\Core\MVC\Symfony\Security\Authentication\DefaultAuthenticationSuccessHandler; use Ibexa\Core\MVC\Symfony\Security\Authentication\GuardRepositoryAuthenticationProvider; -use Ibexa\Core\MVC\Symfony\Security\Authentication\RememberMeRepositoryAuthenticationProvider; use Ibexa\Core\MVC\Symfony\Security\HttpUtils; use Ibexa\Core\MVC\Symfony\SiteAccess; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; @@ -24,14 +23,9 @@ */ final class SecurityPass implements CompilerPassInterface { - public const string CONSTANT_AUTH_TIME_SETTING = 'ibexa.security.authentication.constant_auth_time'; - - public const float CONSTANT_AUTH_TIME_DEFAULT = 1.0; - public function process(ContainerBuilder $container): void { if ( - !$container->hasDefinition('security.authentication.provider.rememberme') || !$container->hasDefinition('security.authentication.provider.guard') ) { return; @@ -39,13 +33,6 @@ public function process(ContainerBuilder $container): void $permissionResolverRef = new Reference(PermissionResolver::class); - $rememberMeAuthenticationProviderDef = $container->findDefinition('security.authentication.provider.rememberme'); - $rememberMeAuthenticationProviderDef->setClass(RememberMeRepositoryAuthenticationProvider::class); - $rememberMeAuthenticationProviderDef->addMethodCall( - 'setPermissionResolver', - [$permissionResolverRef] - ); - $guardAuthenticationProviderDef = $container->findDefinition('security.authentication.provider.guard'); $guardAuthenticationProviderDef->setClass(GuardRepositoryAuthenticationProvider::class); $guardAuthenticationProviderDef->addMethodCall( diff --git a/src/bundle/Core/Resources/config/security.yml b/src/bundle/Core/Resources/config/security.yml index 5ca47846d9..66c129d1e2 100644 --- a/src/bundle/Core/Resources/config/security.yml +++ b/src/bundle/Core/Resources/config/security.yml @@ -2,7 +2,7 @@ parameters: # Constant authentication execution time in seconds (float). Blocks timing attacks. # Must be larger than expected real execution time, with a good margin. # If set to zero, constant time authentication is disabled. Do not do this on production environments. - ibexa.security.authentication.constant_auth_time: !php/const Ibexa\Bundle\Core\DependencyInjection\Compiler\SecurityPass::CONSTANT_AUTH_TIME_DEFAULT + ibexa.security.authentication.constant_auth_time: 1.0 services: Ibexa\Core\MVC\Symfony\Security\User\UsernameProvider: @@ -47,3 +47,9 @@ services: Ibexa\Core\MVC\Symfony\Security\Authentication\EventSubscriber\AccessDeniedSubscriber: autowire: true autoconfigure: true + + Ibexa\Core\MVC\Symfony\Security\Authentication\EventSubscriber\RepositoryUserAuthenticationSubscriber: + autowire: true + autoconfigure: true + arguments: + $constantAuthTime: '%ibexa.security.authentication.constant_auth_time%' diff --git a/src/lib/MVC/Symfony/Security/Authentication/EventSubscriber/RepositoryUserAuthenticationSubscriber.php b/src/lib/MVC/Symfony/Security/Authentication/EventSubscriber/RepositoryUserAuthenticationSubscriber.php new file mode 100644 index 0000000000..7f26f358e0 --- /dev/null +++ b/src/lib/MVC/Symfony/Security/Authentication/EventSubscriber/RepositoryUserAuthenticationSubscriber.php @@ -0,0 +1,108 @@ +logger = $logger ?? new NullLogger(); + } + + public static function getSubscribedEvents(): array + { + return [ + CheckPassportEvent::class => ['validateRepositoryUser'], + ]; + } + + public function validateRepositoryUser(CheckPassportEvent $event): void + { + $request = $this->requestStack->getCurrentRequest(); + if ($request === null) { + return; + } + + $badge = $event->getPassport()->getBadge(UserBadge::class); + if (!$badge instanceof UserBadge) { + return; + } + + $user = $badge->getUser(); + if (!$user instanceof IbexaUserInterface) { + return; + } + + $startTime = $this->startConstantTimer(); + try { + $this->userService->checkUserCredentials( + $user->getAPIUser(), + $user->getPassword() ?? '' + ); + + $event->getAuthenticator()->authenticate($request); + } catch (UnsupportedPasswordHashType $exception) { + $this->sleepUsingConstantTimer($startTime); + + throw new PasswordInUnsupportedFormatException($exception); + } catch (Exception $e) { + $this->sleepUsingConstantTimer($startTime); + + throw $e; + } + + $this->sleepUsingConstantTimer($startTime); + } + + private function startConstantTimer(): float + { + return microtime(true); + } + + private function sleepUsingConstantTimer(float $startTime): void + { + $remainingTime = $this->constantAuthTime - (microtime(true) - $startTime); + if ($remainingTime > 0) { + $microseconds = $remainingTime * self::USLEEP_MULTIPLIER; + + usleep((int)$microseconds); + } elseif ($this->logger) { + $this->logger->warning( + sprintf( + 'Authentication took longer than the configured constant time. Consider increasing the value of %s', + self::CONSTANT_AUTH_TIME_SETTING + ), + [__CLASS__] + ); + } + } +} diff --git a/src/lib/MVC/Symfony/Security/Authentication/RepositoryAuthenticationProvider.php b/src/lib/MVC/Symfony/Security/Authentication/RepositoryAuthenticationProvider.php deleted file mode 100644 index 2bbbec5a54..0000000000 --- a/src/lib/MVC/Symfony/Security/Authentication/RepositoryAuthenticationProvider.php +++ /dev/null @@ -1,133 +0,0 @@ -constantAuthTime = $constantAuthTime; - } - - public function setPermissionResolver(PermissionResolver $permissionResolver) - { - $this->permissionResolver = $permissionResolver; - } - - public function setUserService(UserService $userService) - { - $this->userService = $userService; - } - - protected function checkAuthentication(UserInterface $user, UsernamePasswordToken $token) - { - if (!$user instanceof IbexaUserInterface) { - parent::checkAuthentication($user, $token); - - return; - } - - $apiUser = $user->getAPIUser(); - - // $currentUser can either be an instance of UserInterface or just the username (e.g. during form login). - /** @var \Ibexa\Core\MVC\Symfony\Security\UserInterface|string $currentUser */ - $currentUser = $token->getUser(); - if ($currentUser instanceof UserInterface) { - if ($currentUser->getAPIUser()->passwordHash !== $apiUser->passwordHash) { - throw new BadCredentialsException('The credentials were changed in another session.'); - } - - $apiUser = $currentUser->getAPIUser(); - } else { - $credentialsValid = $this->userService->checkUserCredentials($apiUser, $token->getCredentials()); - - if (!$credentialsValid) { - throw new BadCredentialsException('Invalid credentials', 0); - } - } - - // Finally inject current user in the Repository - $this->permissionResolver->setCurrentUserReference($apiUser); - } - - /** - * @throws \Ibexa\Contracts\Core\Repository\Exceptions\PasswordInUnsupportedFormatException - */ - public function authenticate(TokenInterface $token) - { - $startTime = $this->startConstantTimer(); - - try { - $result = parent::authenticate($token); - } catch (UnsupportedPasswordHashType $exception) { - $this->sleepUsingConstantTimer($startTime); - throw new PasswordInUnsupportedFormatException($exception); - } catch (\Exception $e) { - $this->sleepUsingConstantTimer($startTime); - throw $e; - } - - $this->sleepUsingConstantTimer($startTime); - - return $result; - } - - private function startConstantTimer() - { - return microtime(true); - } - - private function sleepUsingConstantTimer(float $startTime): void - { - if ($this->constantAuthTime <= 0.0) { - return; - } - - $remainingTime = $this->constantAuthTime - (microtime(true) - $startTime); - if ($remainingTime > 0) { - $microseconds = $remainingTime * self::USLEEP_MULTIPLIER; - - usleep((int)$microseconds); - } elseif ($this->logger) { - $this->logger->warning( - sprintf( - 'Authentication took longer than the configured constant time. Consider increasing the value of %s', - SecurityPass::CONSTANT_AUTH_TIME_SETTING - ), - [static::class] - ); - } - } -} diff --git a/tests/bundle/Core/DependencyInjection/Compiler/SecurityPassTest.php b/tests/bundle/Core/DependencyInjection/Compiler/SecurityPassTest.php index d6689f86d4..1ea86e598d 100644 --- a/tests/bundle/Core/DependencyInjection/Compiler/SecurityPassTest.php +++ b/tests/bundle/Core/DependencyInjection/Compiler/SecurityPassTest.php @@ -41,11 +41,6 @@ public function testAlteredDaoAuthenticationProvider(): void { $this->compile(); - $this->assertContainerBuilderHasServiceDefinitionWithMethodCall( - 'security.authentication.provider.rememberme', - 'setPermissionResolver', - [new Reference(PermissionResolver::class)] - ); $this->assertContainerBuilderHasServiceDefinitionWithMethodCall( 'security.authentication.provider.guard', 'setPermissionResolver', diff --git a/tests/lib/MVC/Symfony/Security/Authentication/EventSubscriber/RepositoryUserAuthenticationSubscriberTest.php b/tests/lib/MVC/Symfony/Security/Authentication/EventSubscriber/RepositoryUserAuthenticationSubscriberTest.php new file mode 100644 index 0000000000..fa907c8672 --- /dev/null +++ b/tests/lib/MVC/Symfony/Security/Authentication/EventSubscriber/RepositoryUserAuthenticationSubscriberTest.php @@ -0,0 +1,153 @@ + ['validateRepositoryUser'], + ], + $this->getSubscriber()->getSubscribedEvents() + ); + } + + public function testCheckAuthenticationFailedWhenPasswordInUnsupportedFormat(): void + { + $apiUser = new APIUser(); + $user = $this->createMock(User::class); + $user + ->expects(self::once()) + ->method('getAPIUser') + ->willReturn($apiUser); + $user + ->method('getPassword') + ->willReturn('my_password'); + + $userService = $this->createMock(UserService::class); + $userService + ->expects(self::once()) + ->method('checkUserCredentials') + ->with($apiUser, 'my_password') + ->willThrowException( + new UnsupportedPasswordHashType(self::UNSUPPORTED_USER_PASSWORD_HASH_TYPE) + ); + + $this->expectException(PasswordInUnsupportedFormatException::class); + + $this->getSubscriber($userService)->validateRepositoryUser( + $this->getCheckPassportEvent($user) + ); + } + + public function testAuthenticateInConstantTime(): void + { + $constantAuthTime = 1.0; + $stopwatch = new Stopwatch(); + $stopwatch->start('authenticate_constant_time_test'); + + try { + $this->getSubscriber(null, $constantAuthTime)->validateRepositoryUser( + $this->getCheckPassportEvent() + ); + } catch (Exception) { + // We don't care, we just need test execution to continue + } + + $duration = $stopwatch->stop('authenticate_constant_time_test')->getDuration(); + + self::assertGreaterThanOrEqual($constantAuthTime * 1000, $duration); + } + + public function testAuthenticateWarningOnConstantTimeExceeded(): void + { + $logger = $this->createMock(LoggerInterface::class); + $logger + ->expects(self::once()) + ->method('warning') + ->with('Authentication took longer than the configured constant time. Consider increasing the value of 1.0'); + + $this->expectException(AuthenticationException::class); + + // constant auth time is much too short, but not zero, which would disable the check + $this->getSubscriber(null, 0.0000001)->validateRepositoryUser( + $this->getCheckPassportEvent() + ); + } + + public function testAuthenticateConstantTimeDisabled(): void + { + $logger = $this->createMock(LoggerInterface::class); + $logger->expects(self::never())->method('warning'); + + $this->expectException(AuthenticationException::class); + + $this->getSubscriber(null, 0.0); + } + + private function getSubscriber( + ?UserService $userService = null, + float $constantAuthTime = 1.0 + ): RepositoryUserAuthenticationSubscriber { + $request = $this->createMock(Request::class); + $requestStack = $this->createMock(RequestStack::class); + $requestStack + ->method('getCurrentRequest') + ->willReturn($request); + + return new RepositoryUserAuthenticationSubscriber( + $requestStack, + $userService ?? $this->createMock(UserService::class), + $constantAuthTime + ); + } + + private function getCheckPassportEvent(?UserInterface $user = null): CheckPassportEvent + { + $user = $user ?? $this->createMock(User::class); + + $userProvider = $this->createMock(User\APIUserProviderInterface::class); + $userProvider + ->expects(self::once()) + ->method('loadUserByUsername') + ->willReturn($user); + + return new CheckPassportEvent( + $this->createMock(AuthenticatorInterface::class), + new Passport( + new UserBadge($user->getUsername(), [$userProvider, 'loadUserByUsername']), + new PasswordCredentials($user->getPassword() ?? '') + ) + ); + } +} diff --git a/tests/lib/MVC/Symfony/Security/Authentication/RepositoryAuthenticationProviderTest.php b/tests/lib/MVC/Symfony/Security/Authentication/RepositoryAuthenticationProviderTest.php deleted file mode 100644 index dff44088c9..0000000000 --- a/tests/lib/MVC/Symfony/Security/Authentication/RepositoryAuthenticationProviderTest.php +++ /dev/null @@ -1,272 +0,0 @@ -encoderFactory = $this->createMock(EncoderFactoryInterface::class); - $this->userProvider = $this->createMock(UserProviderInterface::class); - $this->authProvider = new RepositoryAuthenticationProvider( - $this->userProvider, - $this->createMock(UserCheckerInterface::class), - 'foo', - $this->encoderFactory - ); - $this->permissionResolver = $this->createMock(PermissionResolver::class); - $this->userService = $this->createMock(UserService::class); - $this->authProvider->setPermissionResolver($this->permissionResolver); - $this->authProvider->setUserService($this->userService); - - $this->logger = $this->createMock(LoggerInterface::class); - $this->authProvider->setLogger($this->logger); - } - - public function testAuthenticationNotEzUser() - { - $password = 'some_encoded_password'; - $user = $this->createMock(UserInterface::class); - $user - ->expects(self::any()) - ->method('getPassword') - ->will(self::returnValue($password)); - - $tokenUser = $this->createMock(UserInterface::class); - $tokenUser - ->expects(self::any()) - ->method('getPassword') - ->will(self::returnValue($password)); - $token = new UsernamePasswordToken($tokenUser, 'foo', 'bar'); - - $method = new \ReflectionMethod($this->authProvider, 'checkAuthentication'); - $method->setAccessible(true); - $method->invoke($this->authProvider, $user, $token); - } - - public function testCheckAuthenticationCredentialsChanged() - { - $this->expectException(BadCredentialsException::class); - - $apiUser = $this->getMockBuilder(APIUser::class) - ->setConstructorArgs([['passwordHash' => 'some_encoded_password']]) - ->setMethods(['getUserId']) - ->getMockForAbstractClass(); - $apiUser - ->expects(self::once()) - ->method('getUserId') - ->will(self::returnValue(456)); - $tokenUser = new User($apiUser); - $token = new UsernamePasswordToken($tokenUser, 'foo', 'bar'); - - $renewedApiUser = $this->getMockBuilder(APIUser::class) - ->setConstructorArgs([['passwordHash' => 'renewed_encoded_password']]) - ->getMockForAbstractClass(); - - $user = $this->createMock(User::class); - $user - ->expects(self::any()) - ->method('getAPIUser') - ->will(self::returnValue($renewedApiUser)); - - $method = new \ReflectionMethod($this->authProvider, 'checkAuthentication'); - $method->setAccessible(true); - $method->invoke($this->authProvider, $user, $token); - } - - public function testCheckAuthenticationAlreadyLoggedIn() - { - $password = 'encoded_password'; - - $apiUser = $this->getMockBuilder(APIUser::class) - ->setConstructorArgs([['passwordHash' => $password]]) - ->setMethods(['getUserId']) - ->getMockForAbstractClass(); - $tokenUser = new User($apiUser); - $token = new UsernamePasswordToken($tokenUser, 'foo', 'bar'); - - $user = $this->createMock(User::class); - $user - ->expects(self::once()) - ->method('getAPIUser') - ->will(self::returnValue($apiUser)); - - $this->permissionResolver - ->expects(self::once()) - ->method('setCurrentUserReference') - ->with($apiUser); - - $method = new \ReflectionMethod($this->authProvider, 'checkAuthentication'); - $method->setAccessible(true); - $method->invoke($this->authProvider, $user, $token); - } - - public function testCheckAuthenticationFailed() - { - $this->expectException(BadCredentialsException::class); - - $apiUser = $this->createMock(APIUser::class); - $user = $this->createMock(User::class); - $user->method('getAPIUser') - ->willReturn($apiUser); - $userName = 'my_username'; - $password = 'foo'; - $token = new UsernamePasswordToken($userName, $password, 'bar'); - - $this->userService - ->expects(self::once()) - ->method('checkUserCredentials') - ->with($apiUser, $password) - ->willReturn(false); - - $method = new \ReflectionMethod($this->authProvider, 'checkAuthentication'); - $method->setAccessible(true); - $method->invoke($this->authProvider, $user, $token); - } - - public function testCheckAuthentication() - { - $userName = 'my_username'; - $password = 'foo'; - $token = new UsernamePasswordToken($userName, $password, 'bar'); - - $apiUser = $this->createMock(APIUser::class); - $user = $this->createMock(User::class); - $user->method('getAPIUser') - ->willReturn($apiUser); - - $this->userService - ->expects(self::once()) - ->method('checkUserCredentials') - ->with($apiUser, $password) - ->willReturn(true); - - $this->permissionResolver - ->expects(self::once()) - ->method('setCurrentUserReference') - ->with($apiUser); - - $method = new \ReflectionMethod($this->authProvider, 'checkAuthentication'); - $method->setAccessible(true); - $method->invoke($this->authProvider, $user, $token); - } - - public function testCheckAuthenticationFailedWhenPasswordInUnsupportedFormat() - { - $this->expectException(PasswordInUnsupportedFormatException::class); - - $apiUser = $this->createMock(APIUser::class); - $user = $this->createMock(User::class); - $user->method('getAPIUser') - ->willReturn($apiUser); - $userName = 'my_username'; - $password = 'foo'; - $token = new UsernamePasswordToken($userName, $password, 'foo'); - $this->userProvider - ->method('loadUserByUsername') - ->willReturn($user); - - $this->userService - ->expects(self::once()) - ->method('checkUserCredentials') - ->with($apiUser, $password) - ->willThrowException(new UnsupportedPasswordHashType(self::UNSUPPORTED_USER_PASSWORD_HASH_TYPE)); - - $this->authProvider->authenticate($token); - } - - public function testAuthenticateInConstantTime(): void - { - $this->authProvider->setConstantAuthTime(SecurityPass::CONSTANT_AUTH_TIME_DEFAULT); // a reasonable value - - $token = new UsernamePasswordToken('my_username', 'my_password', 'bar'); - - $stopwatch = new Stopwatch(); - $stopwatch->start('authenticate_constant_time_test'); - - try { - $this->authProvider->authenticate($token); - } catch (\Exception $e) { - // We don't care, we just need test execution to continue - } - - $duration = $stopwatch->stop('authenticate_constant_time_test')->getDuration(); - self::assertGreaterThanOrEqual(SecurityPass::CONSTANT_AUTH_TIME_DEFAULT * 1000, $duration); - } - - public function testAuthenticateWarningOnConstantTimeExceeded(): void - { - $this->authProvider->setConstantAuthTime(0.0000001); // much too short, but not zero, which would disable the check - - $token = new UsernamePasswordToken('my_username', 'my_password', 'bar'); - - $this->logger - ->expects(self::atLeastOnce()) - ->method('warning') - ->with('Authentication took longer than the configured constant time. Consider increasing the value of ' . SecurityPass::CONSTANT_AUTH_TIME_SETTING); - - $this->expectException(AuthenticationException::class); - $this->authProvider->authenticate($token); - } - - public function testAuthenticateConstantTimeDisabled(): void - { - $this->authProvider->setConstantAuthTime(0.0); // zero disables the check - - $token = new UsernamePasswordToken('my_username', 'my_password', 'bar'); - - $this->logger - ->expects(self::never()) - ->method('warning'); - - $this->expectException(AuthenticationException::class); - $this->authProvider->authenticate($token); - } -}