Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5619382
IBX-10124: Added basic support for Argon2I(D) password hashes
glye Jun 5, 2025
835bbe7
Disable setConfigResolver for CI test
glye Jul 30, 2025
e99645c
Revert "Disable setConfigResolver for CI test"
glye Jul 30, 2025
2e03b94
Set config resolver in constructor
glye Jul 30, 2025
b8c684d
Add settings to CI
glye Jul 31, 2025
b7dd4c0
Fix password fail logic when not upgrading hash type
glye Jul 31, 2025
fe7651b
Remove unneeded docblock
glye Aug 1, 2025
0b19e3f
CS?
glye Aug 1, 2025
5e1c8b1
Remove unneeded docblock
glye Aug 1, 2025
d86ba0a
Simplify by breaking constructor BC
glye Aug 1, 2025
7f11814
Use actual const in yaml config example
glye Aug 1, 2025
dfbb652
Review feedback
glye Aug 1, 2025
0d4d4ac
Review feedback
glye Aug 1, 2025
eaa90ce
Review feedback: FQCN in config hint
glye Aug 1, 2025
77b393e
Compile time check for Argon2 support in PHP
glye Aug 1, 2025
e27b389
Skip logger check, CS
glye Aug 1, 2025
4976267
phpstan logger ignore
glye Aug 1, 2025
082fd2a
Use LoggerAwareTrait
glye Aug 1, 2025
a27f017
Rename method
glye Aug 1, 2025
f139500
Fix logger set in ctor
glye Aug 1, 2025
915c86b
Regenerated phpstan baseline
glye Aug 1, 2025
75c19e9
Revert "Regenerated phpstan baseline"
glye Aug 1, 2025
07a2095
Add logger to ctor params
glye Aug 1, 2025
56d9f36
Updated phpstan baseline
glye Aug 1, 2025
7c32aed
!php/const in docblock example
glye Aug 5, 2025
ac3ca36
Improved config validation error message
glye Aug 6, 2025
d30dd4e
Made repository aware (incomplete)
glye Aug 8, 2025
a258078
Pass repo settings directly to the hasher
glye Aug 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 0 additions & 42 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -3852,18 +3852,6 @@ parameters:
count: 2
path: src/bundle/Core/URLChecker/Handler/AbstractURLHandler.php

-
message: '#^Cannot call method error\(\) on Psr\\Log\\LoggerInterface\|null\.$#'
identifier: method.nonObject
count: 1
path: src/bundle/Core/URLChecker/Handler/AbstractURLHandler.php

-
message: '#^Cannot call method info\(\) on Psr\\Log\\LoggerInterface\|null\.$#'
identifier: method.nonObject
count: 1
path: src/bundle/Core/URLChecker/Handler/AbstractURLHandler.php

-
message: '#^Method Ibexa\\Bundle\\Core\\URLChecker\\Handler\\AbstractURLHandler\:\:getOptions\(\) return type has no value type specified in iterable type array\.$#'
identifier: missingType.iterableValue
Expand Down Expand Up @@ -3918,12 +3906,6 @@ parameters:
count: 1
path: src/bundle/Core/URLChecker/Handler/MailToHandler.php

-
message: '#^Cannot call method error\(\) on Psr\\Log\\LoggerInterface\|null\.$#'
identifier: method.nonObject
count: 1
path: src/bundle/Core/URLChecker/URLChecker.php

-
message: '#^Method Ibexa\\Bundle\\Core\\URLChecker\\URLChecker\:\:check\(\) has no return type specified\.$#'
identifier: missingType.return
Expand Down Expand Up @@ -10350,12 +10332,6 @@ parameters:
count: 1
path: src/lib/Persistence/Cache/Handler.php

-
message: '#^Cannot call method debug\(\) on Psr\\Log\\LoggerInterface\|null\.$#'
identifier: method.nonObject
count: 1
path: src/lib/Persistence/Cache/Identifier/CacheIdentifierGenerator.php

-
message: '#^Method Ibexa\\Core\\Persistence\\Cache\\Identifier\\CacheIdentifierGenerator\:\:__construct\(\) has parameter \$keyPatterns with no value type specified in iterable type array\.$#'
identifier: missingType.iterableValue
Expand Down Expand Up @@ -17004,12 +16980,6 @@ parameters:
count: 1
path: src/lib/Repository/ContentTypeService.php

-
message: '#^Cannot call method error\(\) on Psr\\Log\\LoggerInterface\|null\.$#'
identifier: method.nonObject
count: 1
path: src/lib/Repository/Helper/RelationProcessor.php

-
message: '#^Method Ibexa\\Core\\Repository\\Helper\\RelationProcessor\:\:appendFieldRelations\(\) has no return type specified\.$#'
identifier: missingType.return
Expand Down Expand Up @@ -17154,12 +17124,6 @@ parameters:
count: 1
path: src/lib/Repository/Mapper/ContentDomainMapper.php

-
message: '#^Cannot call method error\(\) on Psr\\Log\\LoggerInterface\|null\.$#'
identifier: method.nonObject
count: 1
path: src/lib/Repository/Mapper/ContentDomainMapper.php

-
message: '#^Method Ibexa\\Core\\Repository\\Mapper\\ContentDomainMapper\:\:buildContentDomainObject\(\) has parameter \$prioritizedLanguages with no value type specified in iterable type array\.$#'
identifier: missingType.iterableValue
Expand Down Expand Up @@ -18354,12 +18318,6 @@ parameters:
count: 1
path: src/lib/Repository/UserService.php

-
message: '#^Method Ibexa\\Core\\Repository\\UserService\:\:setLogger\(\) has no return type specified\.$#'
identifier: missingType.return
count: 1
path: src/lib/Repository/UserService.php

-
message: '#^PHPDoc tag @param for parameter \$id with type mixed is not subtype of native type int\.$#'
identifier: parameter.phpDocType
Expand Down
2 changes: 1 addition & 1 deletion phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ parameters:
treatPhpDocTypesAsCertain: false
ignoreErrors:
-
message: "#^Cannot call method warning\\(\\) on Psr\\\\Log\\\\LoggerInterface\\|null\\.$#"
message: "#^Cannot call method (log|debug|info|notice|warning|error|critical|alert|emergency)\\(\\) on Psr\\\\Log\\\\LoggerInterface\\|null\\.$#"
paths:
- src
- tests
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Bundle\Core\DependencyInjection\Configuration\Parser\Repository;

use Ibexa\Bundle\Core\DependencyInjection\Configuration\RepositoryConfigParserInterface;
use Ibexa\Contracts\Core\Repository\Values\User\User;
use Symfony\Component\Config\Definition\Builder\NodeBuilder;

/**
* @internal
*
* Configuration parser for password hash configuration.
*
* Example configuration:
* ```yaml
* ibexa:
* system:
* default: # configuration per siteaccess or siteaccess group
* password_hash:
* default_type: !php/const \Ibexa\Contracts\Core\Repository\Values\User\User::PASSWORD_HASH_ARGON2I
* update_type_on_change: false
* ```
*/
final class PasswordHash implements RepositoryConfigParserInterface
{
public function addSemanticConfig(NodeBuilder $nodeBuilder): void
{
$nodeBuilder
->arrayNode('password_hash')
->info('Password hash options')
->children()
->integerNode('default_type')
->info('Default password hash type, see the constants in Ibexa\Contracts\Core\Repository\Values\User\User.')
->example('!php/const:Ibexa\Contracts\Core\Repository\Values\User\User::PASSWORD_HASH_PHP_DEFAULT')
->defaultValue(User::PASSWORD_HASH_PHP_DEFAULT)
->validate()
->ifTrue(static function ($value): bool {
$hashType = (int) $value;

if ($hashType === User::PASSWORD_HASH_ARGON2I) {
return !defined('PASSWORD_ARGON2I');
} elseif ($hashType === User::PASSWORD_HASH_ARGON2ID) {
return !defined('PASSWORD_ARGON2ID');
}

return !in_array($hashType, User::SUPPORTED_PASSWORD_HASHES, true);
})
->thenInvalid('Invalid password hash type "%s". If you tried to use Argon2, make sure it\'s compiled in PHP.')
->end()
->end()
->booleanNode('update_type_on_change')
->info('Whether the password hash type should be changed when the password is changed if it differs from the default type.')
->example('false')
->defaultFalse()
->end()
->end()
->end();
}
}
5 changes: 5 additions & 0 deletions src/bundle/Core/IbexaCoreBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ public function build(ContainerBuilder $container): void
$container->addCompilerPass(new TranslationCollectorPass());
$container->addCompilerPass(new SlugConverterConfigurationPass());

/** @var \Ibexa\Bundle\Core\DependencyInjection\IbexaCoreExtension $kernel */
$kernel = $container->getExtension('ibexa');
$kernel->addRepositoryConfigParser(new RepositoryConfigParser\PasswordHash());

$container->registerForAutoconfiguration(VariableProvider::class)->addTag('ezplatform.view.variable_provider');
}

Expand Down Expand Up @@ -129,6 +133,7 @@ public function getContainerExtension(): ExtensionInterface
new RepositoryConfigParser\Search(),
new RepositoryConfigParser\FieldGroups(),
new RepositoryConfigParser\Options(),
new RepositoryConfigParser\PasswordHash(),
]
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ class PasswordInUnsupportedFormatException extends AuthenticationException
{
public function __construct(?Throwable $previous = null)
{
parent::__construct("User's password is in a format which is not supported any more.", 0, $previous);
parent::__construct("User's password is in a format which is not supported.", 0, $previous);
}
}
19 changes: 19 additions & 0 deletions src/contracts/Repository/PasswordHashService.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@

interface PasswordHashService
{
/**
* Sets the default password hash type.
*
* @param int $defaultHashType The default password hash type, one of Ibexa\Contracts\Core\Repository\Values\User\User::SUPPORTED_PASSWORD_HASHES.
*/
public function setDefaultHashType(int $defaultHashType): void;

/**
* Sets whether the password hash type should be updated when the password is changed.
*
* @param bool $updateTypeOnChange Whether to update the password hash type on change.
*/
public function setUpdateTypeOnChange(bool $updateTypeOnChange): void;

/**
* Returns default password hash type.
*
Expand All @@ -33,6 +47,9 @@ public function isHashTypeSupported(int $hashType): bool;
* Create hash from given plain password.
*
* If non-provided, the default password hash type will be used.
*
* @throws \Ibexa\Core\Repository\User\Exception\PasswordHashTypeNotCompiled
* @throws \Ibexa\Core\Repository\User\Exception\UnsupportedPasswordHashType
*/
public function createPasswordHash(string $plainPassword, ?int $hashType = null): string;

Expand All @@ -42,4 +59,6 @@ public function createPasswordHash(string $plainPassword, ?int $hashType = null)
* If non-provided, the default password hash type will be used.
*/
public function isValidPassword(string $plainPassword, string $passwordHash, ?int $hashType = null): bool;

public function shouldPasswordHashTypeBeUpdatedOnChange(): bool;
}
6 changes: 6 additions & 0 deletions src/contracts/Repository/Values/User/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,19 @@ abstract class User extends Content implements UserReference
public const array SUPPORTED_PASSWORD_HASHES = [
self::PASSWORD_HASH_BCRYPT,
self::PASSWORD_HASH_PHP_DEFAULT,
self::PASSWORD_HASH_ARGON2I,
self::PASSWORD_HASH_ARGON2ID,
self::PASSWORD_HASH_INVALID,
];

public const int PASSWORD_HASH_BCRYPT = 6;

public const int PASSWORD_HASH_PHP_DEFAULT = 7;

public const int PASSWORD_HASH_ARGON2I = 8;

public const int PASSWORD_HASH_ARGON2ID = 9;

public const int PASSWORD_HASH_INVALID = 256;

public const int DEFAULT_PASSWORD_HASH = self::PASSWORD_HASH_PHP_DEFAULT;
Expand Down
6 changes: 6 additions & 0 deletions src/lib/Base/Container/ApiLoader/RepositoryFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Ibexa\Contracts\Core\Repository\PermissionService;
use Ibexa\Contracts\Core\Repository\Repository;
use Ibexa\Contracts\Core\Repository\Validator\ContentValidator;
use Ibexa\Contracts\Core\Repository\Values\User\User;
use Ibexa\Contracts\Core\Search\Handler as SearchHandler;
use Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface;
use Ibexa\Core\Base\Exceptions\InvalidArgumentException;
Expand Down Expand Up @@ -98,6 +99,11 @@ public function buildRepository(
): Repository {
$config = $this->repositoryConfigurationProvider->getRepositoryConfig();

if (isset($config['password_hash'])) {
$passwordHashService->setDefaultHashType($config['password_hash']['default_type'] ?? User::PASSWORD_HASH_PHP_DEFAULT);
$passwordHashService->setUpdateTypeOnChange($config['password_hash']['update_type_on_change'] ?? false);
}

return new CoreRepository(
$persistenceHandler,
$searchHandler,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Ibexa\Contracts\Core\Repository\Exceptions\PasswordInUnsupportedFormatException;
use Ibexa\Contracts\Core\Repository\UserService;
use Ibexa\Core\MVC\Symfony\Security\UserInterface as IbexaUserInterface;
use Ibexa\Core\Repository\User\Exception\PasswordHashTypeNotCompiled;
use Ibexa\Core\Repository\User\Exception\UnsupportedPasswordHashType;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\LoggerInterface;
Expand Down Expand Up @@ -77,7 +78,7 @@ public function validateRepositoryUser(CheckPassportEvent $event): void
$user->getAPIUser(),
$user->getPassword() ?? ''
);
} catch (UnsupportedPasswordHashType $exception) {
} catch (UnsupportedPasswordHashType|PasswordHashTypeNotCompiled $exception) {
$this->sleepUsingConstantTimer($startTime);

throw new PasswordInUnsupportedFormatException($exception);
Expand Down
22 changes: 22 additions & 0 deletions src/lib/Repository/User/Exception/PasswordHashTypeNotCompiled.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Core\Repository\User\Exception;

use Ibexa\Core\Base\Exceptions\InvalidArgumentException;

final class PasswordHashTypeNotCompiled extends InvalidArgumentException
{
public function __construct(string $hashType)
{
parent::__construct(
'hashType',
"Password hash algorithm $hashType is not compiled into PHP"
);
}
}
Loading
Loading