Skip to content

Commit

Permalink
IBX-1755: Implemented constant time authentication
Browse files Browse the repository at this point in the history
Co-authored-by: Paweł Niedzielski <pawel.tadeusz.niedzielski@gmail.com>
  • Loading branch information
glye and Steveb-p committed May 31, 2022
1 parent 34a9687 commit 09d3ee4
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
*/
class SecurityPass implements CompilerPassInterface
{
public const CONSTANT_AUTH_TIME_SETTING = 'ibexa.security.authentication.constant_auth_time';

public const CONSTANT_AUTH_TIME_DEFAULT = 1.0;

public function process(ContainerBuilder $container)
{
if (!($container->hasDefinition('security.authentication.provider.dao') &&
Expand All @@ -34,6 +38,7 @@ public function process(ContainerBuilder $container)
$configResolverRef = new Reference('ezpublish.config.resolver');
$permissionResolverRef = new Reference(PermissionResolver::class);
$userServiceRef = new Reference(UserService::class);
$loggerRef = new Reference('logger');

// Override and inject the Repository in the authentication provider.
// We need it for checking user credentials
Expand All @@ -47,6 +52,18 @@ public function process(ContainerBuilder $container)
'setUserService',
[$userServiceRef]
);
$daoAuthenticationProviderDef->addMethodCall(
'setConstantAuthTime',
[
$container->hasParameter(self::CONSTANT_AUTH_TIME_SETTING) ?
(float)$container->getParameter(self::CONSTANT_AUTH_TIME_SETTING) :
self::CONSTANT_AUTH_TIME_DEFAULT,
]
);
$daoAuthenticationProviderDef->addMethodCall(
'setLogger',
[$loggerRef]
);

$rememberMeAuthenticationProviderDef = $container->findDefinition('security.authentication.provider.rememberme');
$rememberMeAuthenticationProviderDef->setClass(RememberMeRepositoryAuthenticationProvider::class);
Expand Down
6 changes: 6 additions & 0 deletions eZ/Bundle/EzPublishCoreBundle/Resources/config/security.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
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 eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Compiler\SecurityPass::CONSTANT_AUTH_TIME_DEFAULT

services:
ezpublish.security.user_provider.username:
class: eZ\Publish\Core\MVC\Symfony\Security\User\UsernameProvider
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,38 @@
*/
namespace eZ\Publish\Core\MVC\Symfony\Security\Authentication;

use eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Compiler\SecurityPass;
use eZ\Publish\API\Repository\Exceptions\PasswordInUnsupportedFormatException;
use eZ\Publish\API\Repository\PermissionResolver;
use eZ\Publish\API\Repository\UserService;
use eZ\Publish\Core\MVC\Symfony\Security\UserInterface as EzUserInterface;
use eZ\Publish\Core\Repository\User\Exception\UnsupportedPasswordHashType;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Symfony\Component\Security\Core\Authentication\Provider\DaoAuthenticationProvider;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\User\UserInterface;

class RepositoryAuthenticationProvider extends DaoAuthenticationProvider
class RepositoryAuthenticationProvider extends DaoAuthenticationProvider implements LoggerAwareInterface
{
use LoggerAwareTrait;

/** @var float|null */
private $constantAuthTime;

/** @var \eZ\Publish\API\Repository\PermissionResolver */
private $permissionResolver;

/** @var \eZ\Publish\API\Repository\UserService */
private $userService;

public function setConstantAuthTime(float $constantAuthTime)
{
$this->constantAuthTime = $constantAuthTime;
}

public function setPermissionResolver(PermissionResolver $permissionResolver)
{
$this->permissionResolver = $permissionResolver;
Expand Down Expand Up @@ -71,10 +84,45 @@ protected function checkAuthentication(UserInterface $user, UsernamePasswordToke
*/
public function authenticate(TokenInterface $token)
{
$startTime = $this->startConstantTimer();

try {
return parent::authenticate($token);
$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) {
usleep($remainingTime * 1000000);
} 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]
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/
namespace eZ\Publish\Core\MVC\Symfony\Security\Tests\Authentication;

use eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Compiler\SecurityPass;
use eZ\Publish\API\Repository\Exceptions\PasswordInUnsupportedFormatException;
use eZ\Publish\API\Repository\PermissionResolver;
use eZ\Publish\API\Repository\UserService;
Expand All @@ -14,11 +15,14 @@
use eZ\Publish\Core\MVC\Symfony\Security\User;
use eZ\Publish\Core\Repository\User\Exception\UnsupportedPasswordHashType;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
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\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Stopwatch\Stopwatch;

class RepositoryAuthenticationProviderTest extends TestCase
{
Expand All @@ -41,6 +45,9 @@ class RepositoryAuthenticationProviderTest extends TestCase
/** @var \eZ\Publish\API\Repository\UserService|\PHPUnit\Framework\MockObject\MockObject */
private $userService;

/** @var \Psr\Log\LoggerInterface|\PHPUnit\Framework\MockObject\MockObject */
private $logger;

protected function setUp(): void
{
parent::setUp();
Expand All @@ -56,6 +63,9 @@ protected function setUp(): void
$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()
Expand Down Expand Up @@ -209,4 +219,52 @@ public function testCheckAuthenticationFailedWhenPasswordInUnsupportedFormat()

$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);
}
}
3 changes: 0 additions & 3 deletions eZ/Publish/Core/Repository/User/PasswordHashService.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,6 @@ public function isValidPassword(string $plainPassword, string $passwordHash, ?in
return password_verify($plainPassword, $passwordHash);
}

// Randomize login time to protect against timing attacks
usleep(random_int(0, 30000));

return $passwordHash === $this->createPasswordHash($plainPassword, $hashType);
}
}

0 comments on commit 09d3ee4

Please sign in to comment.