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
e44627b
IBX-10124: Added basic support for Argon2I(D) password hashes
glye Jun 5, 2025
5066a14
Disable setConfigResolver for CI test
glye Jul 30, 2025
07b297b
Revert "Disable setConfigResolver for CI test"
glye Jul 30, 2025
3ef4b6b
Set config resolver in constructor
glye Jul 30, 2025
688559c
Add settings to CI
glye Jul 31, 2025
ef4cafc
Fix password fail logic when not upgrading hash type
glye Jul 31, 2025
2993321
Remove unneeded docblock
glye Aug 1, 2025
77b1de0
CS?
glye Aug 1, 2025
132e4ab
Remove unneeded docblock
glye Aug 1, 2025
7cddb7b
Simplify by breaking constructor BC
glye Aug 1, 2025
b0b111a
Use actual const in yaml config example
glye Aug 1, 2025
b9074bf
Review feedback
glye Aug 1, 2025
b375fe1
Review feedback
glye Aug 1, 2025
ebb1e73
Review feedback: FQCN in config hint
glye Aug 1, 2025
ad0d728
Compile time check for Argon2 support in PHP
glye Aug 1, 2025
47b9956
Skip logger check, CS
glye Aug 1, 2025
0849c9a
phpstan logger ignore
glye Aug 1, 2025
7785736
Use LoggerAwareTrait
glye Aug 1, 2025
37b3bf6
Rename method
glye Aug 1, 2025
c50ce8d
Fix logger set in ctor
glye Aug 1, 2025
c8dc564
Regenerated phpstan baseline
glye Aug 1, 2025
69b2595
Revert "Regenerated phpstan baseline"
glye Aug 1, 2025
dccab56
Add logger to ctor params
glye Aug 1, 2025
2a44774
Updated phpstan baseline
glye Aug 1, 2025
f7c2aba
!php/const in docblock example
glye Aug 5, 2025
567d150
Improved config validation error message
glye Aug 6, 2025
23b1d0e
Made repository aware (incomplete)
glye Aug 8, 2025
fca04e0
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 @@ -3846,18 +3846,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 @@ -3912,12 +3900,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 @@ -10332,12 +10314,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 @@ -16920,12 +16896,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 @@ -17070,12 +17040,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 @@ -18264,12 +18228,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