diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 92e4a19fdc..e2b291852a 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -12130,41 +12130,6 @@ parameters: count: 1 path: src/lib/MVC/Symfony/Routing/UrlWildcardRouter.php - - - message: "#^Method Ibexa\\\\Core\\\\MVC\\\\Symfony\\\\Security\\\\Authentication\\\\RememberMeRepositoryAuthenticationProvider\\:\\:setPermissionResolver\\(\\) has no return type specified\\.$#" - 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 @@ -46835,66 +46800,6 @@ parameters: count: 1 path: tests/lib/MVC/Symfony/Security/Authentication/GuardRepositoryAuthenticationProviderTest.php - - - message: "#^Call to an undefined method Symfony\\\\Component\\\\Security\\\\Core\\\\Authentication\\\\Token\\\\TokenInterface\\:\\:getFirewallName\\(\\)\\.$#" - count: 1 - path: tests/lib/MVC/Symfony/Security/Authentication/RememberMeRepositoryAuthenticationProviderTest.php - - - - message: "#^Call to an undefined method Symfony\\\\Component\\\\Security\\\\Core\\\\Authentication\\\\Token\\\\TokenInterface\\:\\:getSecret\\(\\)\\.$#" - 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/Resources/config/security.yml b/src/bundle/Core/Resources/config/security.yml index cc0f36177d..b93f16db4f 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: @@ -44,6 +44,12 @@ services: ibexa.security.user_provider.username: '@Ibexa\Core\MVC\Symfony\Security\User\UsernameProvider' ibexa.security.user_provider.email: '@Ibexa\Core\MVC\Symfony\Security\User\EmailProvider' + Ibexa\Core\MVC\Symfony\Security\Authentication\EventSubscriber\RepositoryUserAuthenticationSubscriber: + autowire: true + autoconfigure: true + arguments: + $constantAuthTime: '%ibexa.security.authentication.constant_auth_time%' + Ibexa\Core\MVC\Symfony\Security\Authentication\EventSubscriber\OnAuthenticationTokenCreatedRepositoryUserSubscriber: autowire: true autoconfigure: true 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..317d578841 --- /dev/null +++ b/src/lib/MVC/Symfony/Security/Authentication/EventSubscriber/RepositoryUserAuthenticationSubscriber.php @@ -0,0 +1,112 @@ +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 + { + 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', + self::CONSTANT_AUTH_TIME_SETTING + ), + [__CLASS__] + ); + } + } +} diff --git a/src/lib/MVC/Symfony/Security/Authentication/RememberMeRepositoryAuthenticationProvider.php b/src/lib/MVC/Symfony/Security/Authentication/RememberMeRepositoryAuthenticationProvider.php deleted file mode 100644 index 7e70080f49..0000000000 --- a/src/lib/MVC/Symfony/Security/Authentication/RememberMeRepositoryAuthenticationProvider.php +++ /dev/null @@ -1,44 +0,0 @@ -permissionResolver = $permissionResolver; - } - - /** - * {@inheritdoc} - */ - public function authenticate(TokenInterface $token) - { - $authenticatedToken = parent::authenticate($token); - if (empty($authenticatedToken)) { - throw new AuthenticationException('The token is not supported by this authentication provider.'); - } - - if ($authenticatedToken->getUser() instanceof UserInterface) { - $this->permissionResolver->setCurrentUserReference( - $authenticatedToken->getUser()->getAPIUser() - ); - } - - return $authenticatedToken; - } -} 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..1c53d44fcf --- /dev/null +++ b/tests/lib/MVC/Symfony/Security/Authentication/EventSubscriber/RepositoryUserAuthenticationSubscriberTest.php @@ -0,0 +1,152 @@ + ['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) { + self::fail(); + } + + $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 ' . RepositoryUserAuthenticationSubscriber::CONSTANT_AUTH_TIME_SETTING); + + // constant auth time is much too short, but not zero, which would disable the check + $this->getSubscriber(null, 0.0000001, $logger)->validateRepositoryUser( + $this->getCheckPassportEvent() + ); + } + + public function testAuthenticateConstantTimeDisabled(): void + { + $logger = $this->createMock(LoggerInterface::class); + $logger->expects(self::never())->method('warning'); + + $this->getSubscriber(null, 0.0, $logger)->validateRepositoryUser( + $this->getCheckPassportEvent() + ); + } + + private function getSubscriber( + ?UserService $userService = null, + float $constantAuthTime = 1.0, + ?LoggerInterface $logger = null + ): 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, + $logger ?? $this->createMock(LoggerInterface::class) + ); + } + + 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/RememberMeRepositoryAuthenticationProviderTest.php b/tests/lib/MVC/Symfony/Security/Authentication/RememberMeRepositoryAuthenticationProviderTest.php deleted file mode 100644 index a4bd8d69f1..0000000000 --- a/tests/lib/MVC/Symfony/Security/Authentication/RememberMeRepositoryAuthenticationProviderTest.php +++ /dev/null @@ -1,121 +0,0 @@ -permissionResolver = $this->createMock(PermissionResolver::class); - $this->authProvider = new RememberMeRepositoryAuthenticationProvider( - $this->createMock(UserCheckerInterface::class), - 'my secret', - 'my provider secret' - ); - $this->authProvider->setPermissionResolver($this->permissionResolver); - } - - public function testAuthenticateUnsupportedToken(): void - { - $this->expectException(AuthenticationException::class); - $this->expectExceptionMessage('The token is not supported by this authentication provider.'); - - $anonymousToken = $this - ->getMockBuilder(AnonymousToken::class) - ->setConstructorArgs(['secret', $this->createMock(UserInterface::class)]) - ->getMock(); - $this->authProvider->authenticate($anonymousToken); - } - - public function testAuthenticateWrongProviderKey(): void - { - $this->expectException(AuthenticationException::class); - $this->expectExceptionMessage('The token is not supported by this authentication provider.'); - - $user = $this->createMock(UserInterface::class); - $user - ->expects(self::any()) - ->method('getRoles') - ->will(self::returnValue([])); - - $rememberMeToken = $this - ->getMockBuilder(RememberMeToken::class) - ->setConstructorArgs([$user, 'wrong provider secret', 'my secret']) - ->getMock(); - $rememberMeToken - ->expects(self::any()) - ->method('getProviderKey') - ->will(self::returnValue('wrong provider secret')); - - $this->authProvider->authenticate($rememberMeToken); - } - - public function testAuthenticateWrongSecret(): void - { - $this->expectException(AuthenticationException::class); - - $user = $this->createMock(UserInterface::class); - $user - ->expects(self::any()) - ->method('getRoles') - ->will(self::returnValue([])); - - $rememberMeToken = $this - ->getMockBuilder(RememberMeToken::class) - ->setConstructorArgs([$user, 'my provider secret', 'the wrong secret']) - ->getMock(); - $rememberMeToken - ->expects(self::any()) - ->method('getProviderKey') - ->will(self::returnValue('my provider secret')); - $rememberMeToken - ->expects(self::any()) - ->method('getSecret') - ->will(self::returnValue('the wrong secret')); - - $this->authProvider->authenticate($rememberMeToken); - } - - public function testAuthenticate(): void - { - $apiUser = $this->createMock(ApiUser::class); - $apiUser - ->expects(self::any()) - ->method('getUserId') - ->will(self::returnValue(42)); - - $tokenUser = new User($apiUser); - $rememberMeToken = new RememberMeToken($tokenUser, 'my provider secret', 'my secret'); - - $authenticatedToken = $this->authProvider->authenticate($rememberMeToken); - - self::assertEquals( - [$rememberMeToken->getFirewallName(), $rememberMeToken->getSecret()], - [$authenticatedToken->getFirewallName(), $authenticatedToken->getSecret()] - ); - } -} 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); - } -}