feature #39802 [Security] Extract password hashing from security-core - with proper wording (chalasr)

This PR was merged into the 5.3-dev branch.

Discussion
----------

[Security] Extract password hashing from security-core - with proper wording

| Q             | A
| ------------- | ---
| Branch?       | 5.x
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Tickets       | Fixes #39698
| License       | MIT
| Doc PR        | todo

This PR renames password "encoders" to password _hashers_ (naming widely used, see e.g. django or laravel).
This also takes the opportunity to extract the logic related to password hashing from security-core, moving it to a new password-hasher component.
Nowadays, many modern web apps and APIs don't deal with passwords at all, that's why splitting makes sense as a step towards making security-core not tied to the password concept.

For upgrading, applications will have to use `passwords_hashers` instead of `encoders` in their security configuration,  and type-hint against `PasswordHasherInterface` (and related) instead of `PasswordEncoderInterface`.

The proposed API is not much different from the encoder one regarding behavior and signatures, and it is slightly more close to the PHP built-in password hashing API:

```php
namespace Symfony\Component\PasswordHasher;

interface PasswordHasherInterface
{
    public function hash(string $plainPassword): string;

    public function verify(string $hashedPassword, string $plainPassword): bool;

    public function needsRehash(string $hashedPassword): bool;
}
```

Commits
-------

c5c981c559 [Security] Extract password hashing from security-core - using the right naming
This commit is contained in:
Wouter de Jong 2021-02-12 16:53:00 +01:00
commit c757845643
137 changed files with 4066 additions and 492 deletions

View File

@ -69,8 +69,19 @@ PropertyInfo
Security
--------
* Deprecate all classes in the `Core\Encoder\` sub-namespace, use the `PasswordHasher` component instead
* Deprecated voters that do not return a valid decision when calling the `vote` method
SecurityBundle
--------------
* Deprecate `UserPasswordEncoderCommand` class and the corresponding `user:encode-password` command,
use `UserPasswordHashCommand` and `user:hash-password` instead
* Deprecate the `security.encoder_factory.generic` service, the `security.encoder_factory` and `Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface` aliases,
use `security.password_hasher_factory` and `Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface` instead
* Deprecate the `security.user_password_encoder.generic` service, the `security.password_encoder` and the `Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface` aliases,
use `security.user_password_hasher`, `security.password_hasher` and `Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface` instead
Serializer
----------

View File

@ -166,6 +166,7 @@ Routing
Security
--------
* Drop all classes in the `Core\Encoder\` sub-namespace, use the `PasswordHasher` component instead
* Drop support for `SessionInterface $session` as constructor argument of `SessionTokenStorage`, inject a `\Symfony\Component\HttpFoundation\RequestStack $requestStack` instead
* Drop support for `session` provided by the ServiceLocator injected in `UsageTrackingTokenStorage`, provide a `request_stack` service instead
* Make `SessionTokenStorage` throw a `SessionNotFoundException` when called outside a request context
@ -179,6 +180,16 @@ Security
* Removed the `AbstractRememberMeServices::$providerKey` property in favor of `AbstractRememberMeServices::$firewallName`
* `AccessDecisionManager` now throw an exception when a voter does not return a valid decision.
SecurityBundle
--------------
* Remove the `UserPasswordEncoderCommand` class and the corresponding `user:encode-password` command,
use `UserPasswordHashCommand` and `user:hash-password` instead
* Remove the `security.encoder_factory.generic` service, the `security.encoder_factory` and `Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface` aliases,
use `security.password_hasher_factory` and `Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface` instead
* Remove the `security.user_password_encoder.generic` service, the `security.password_encoder` and the `Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface` aliases,
use `security.user_password_hasher`, `security.password_hasher` and `Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface` instead
Serializer
----------

View File

@ -86,6 +86,7 @@
"symfony/monolog-bridge": "self.version",
"symfony/notifier": "self.version",
"symfony/options-resolver": "self.version",
"symfony/password-hasher": "self.version",
"symfony/process": "self.version",
"symfony/property-access": "self.version",
"symfony/property-info": "self.version",

View File

@ -51,7 +51,7 @@
"symfony/messenger": "^5.2",
"symfony/mime": "^4.4|^5.0",
"symfony/process": "^4.4|^5.0",
"symfony/security-bundle": "^5.2",
"symfony/security-bundle": "^5.3",
"symfony/serializer": "^5.2",
"symfony/stopwatch": "^4.4|^5.0",
"symfony/string": "^5.0",

View File

@ -1,6 +1,16 @@
CHANGELOG
=========
5.3
---
* Deprecate `UserPasswordEncoderCommand` class and the corresponding `user:encode-password` command,
use `UserPasswordHashCommand` and `user:hash-password` instead
* Deprecate the `security.encoder_factory.generic` service, the `security.encoder_factory` and `Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface` aliases,
use `security.password_hasher_factory` and `Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface` instead
* Deprecate the `security.user_password_encoder.generic` service, the `security.password_encoder` and the `Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface` aliases,
use `security.user_password_hasher`, `security.password_hasher` and `Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface` instead
5.2.0
-----

View File

@ -21,6 +21,7 @@ use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\PasswordHasher\Command\UserPasswordHashCommand;
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
use Symfony\Component\Security\Core\Encoder\SelfSaltingEncoderInterface;
@ -30,6 +31,8 @@ use Symfony\Component\Security\Core\Encoder\SelfSaltingEncoderInterface;
* @author Sarah Khalil <mkhalil.sarah@gmail.com>
*
* @final
*
* @deprecated since Symfony 5.3, use {@link UserPasswordHashCommand} instead
*/
class UserPasswordEncoderCommand extends Command
{
@ -107,6 +110,8 @@ EOF
$io = new SymfonyStyle($input, $output);
$errorIo = $output instanceof ConsoleOutputInterface ? new SymfonyStyle($input, $output->getErrorOutput()) : $io;
$errorIo->caution('The use of the "security:encode-password" command is deprecated since version 5.3 and will be removed in 6.0. Use "security:hash-password" instead.');
$input->isInteractive() ? $errorIo->title('Symfony Password Encoder Utility') : $errorIo->newLine();
$password = $input->getArgument('password');

View File

@ -65,6 +65,23 @@ class MainConfiguration implements ConfigurationInterface
return $v;
})
->end()
->beforeNormalization()
->ifTrue(function ($v) {
if ($v['encoders'] ?? false) {
trigger_deprecation('symfony/security-bundle', '5.3', 'The child node "encoders" at path "security" is deprecated, use "password_hashers" instead.');
return true;
}
return $v['password_hashers'] ?? false;
})
->then(function ($v) {
$v['password_hashers'] = array_merge($v['password_hashers'] ?? [], $v['encoders'] ?? []);
$v['encoders'] = $v['password_hashers'];
return $v;
})
->end()
->children()
->scalarNode('access_denied_url')->defaultNull()->example('/foo/error403')->end()
->enumNode('session_fixation_strategy')
@ -94,6 +111,7 @@ class MainConfiguration implements ConfigurationInterface
;
$this->addEncodersSection($rootNode);
$this->addPasswordHashersSection($rootNode);
$this->addProvidersSection($rootNode);
$this->addFirewallsSection($rootNode, $this->factories);
$this->addAccessControlSection($rootNode);
@ -401,6 +419,57 @@ class MainConfiguration implements ConfigurationInterface
;
}
private function addPasswordHashersSection(ArrayNodeDefinition $rootNode)
{
$rootNode
->fixXmlConfig('password_hasher')
->children()
->arrayNode('password_hashers')
->example([
'App\Entity\User1' => 'auto',
'App\Entity\User2' => [
'algorithm' => 'auto',
'time_cost' => 8,
'cost' => 13,
],
])
->requiresAtLeastOneElement()
->useAttributeAsKey('class')
->prototype('array')
->canBeUnset()
->performNoDeepMerging()
->beforeNormalization()->ifString()->then(function ($v) { return ['algorithm' => $v]; })->end()
->children()
->scalarNode('algorithm')
->cannotBeEmpty()
->validate()
->ifTrue(function ($v) { return !\is_string($v); })
->thenInvalid('You must provide a string value.')
->end()
->end()
->arrayNode('migrate_from')
->prototype('scalar')->end()
->beforeNormalization()->castToArray()->end()
->end()
->scalarNode('hash_algorithm')->info('Name of hashing algorithm for PBKDF2 (i.e. sha256, sha512, etc..) See hash_algos() for a list of supported algorithms.')->defaultValue('sha512')->end()
->scalarNode('key_length')->defaultValue(40)->end()
->booleanNode('ignore_case')->defaultFalse()->end()
->booleanNode('encode_as_base64')->defaultTrue()->end()
->scalarNode('iterations')->defaultValue(5000)->end()
->integerNode('cost')
->min(4)
->max(31)
->defaultNull()
->end()
->scalarNode('memory_cost')->defaultNull()->end()
->scalarNode('time_cost')->defaultNull()->end()
->scalarNode('id')->end()
->end()
->end()
->end()
->end();
}
private function getAccessDecisionStrategies()
{
$strategies = [

View File

@ -32,6 +32,10 @@ use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher;
use Symfony\Component\PasswordHasher\Hasher\Pbkdf2PasswordHasher;
use Symfony\Component\PasswordHasher\Hasher\PlaintextPasswordHasher;
use Symfony\Component\PasswordHasher\Hasher\SodiumPasswordHasher;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder;
use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder;
@ -105,6 +109,7 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
$loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/Resources/config'));
$loader->load('security.php');
$loader->load('password_hasher.php');
$loader->load('security_listeners.php');
$loader->load('security_rememberme.php');
@ -166,13 +171,22 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
$container->getDefinition('security.authentication.guard_handler')
->replaceArgument(2, $this->statelessFirewallKeys);
// @deprecated since Symfony 5.3
if ($config['encoders']) {
$this->createEncoders($config['encoders'], $container);
}
if ($config['password_hashers']) {
$this->createHashers($config['password_hashers'], $container);
}
if (class_exists(Application::class)) {
$loader->load('console.php');
// @deprecated since Symfony 5.3
$container->getDefinition('security.command.user_password_encoder')->replaceArgument(1, array_keys($config['encoders']));
$container->getDefinition('security.command.user_password_hash')->replaceArgument(1, array_keys($config['password_hashers']));
}
$container->registerForAutoconfiguration(VoterInterface::class)
@ -689,20 +703,20 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
// Argon2i encoder
if ('argon2i' === $config['algorithm']) {
if (SodiumPasswordEncoder::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) {
if (SodiumPasswordHasher::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) {
$config['algorithm'] = 'sodium';
} elseif (\defined('PASSWORD_ARGON2I')) {
$config['algorithm'] = 'native';
$config['native_algorithm'] = \PASSWORD_ARGON2I;
} else {
throw new InvalidConfigurationException(sprintf('Algorithm "argon2i" is not available. Either use "%s" or upgrade to PHP 7.2+ instead.', \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13') ? 'argon2id", "auto' : 'auto'));
throw new InvalidConfigurationException(sprintf('Algorithm "argon2i" is not available. Use "%s" instead.', \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13') ? 'argon2id", "auto' : 'auto'));
}
return $this->createEncoder($config);
}
if ('argon2id' === $config['algorithm']) {
if (($hasSodium = SodiumPasswordEncoder::isSupported()) && \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) {
if (($hasSodium = SodiumPasswordHasher::isSupported()) && \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) {
$config['algorithm'] = 'sodium';
} elseif (\defined('PASSWORD_ARGON2ID')) {
$config['algorithm'] = 'native';
@ -718,15 +732,15 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
return [
'class' => NativePasswordEncoder::class,
'arguments' => [
$config['time_cost'],
(($config['memory_cost'] ?? 0) << 10) ?: null,
$config['cost'],
] + (isset($config['native_algorithm']) ? [3 => $config['native_algorithm']] : []),
$config['time_cost'],
(($config['memory_cost'] ?? 0) << 10) ?: null,
$config['cost'],
] + (isset($config['native_algorithm']) ? [3 => $config['native_algorithm']] : []),
];
}
if ('sodium' === $config['algorithm']) {
if (!SodiumPasswordEncoder::isSupported()) {
if (!SodiumPasswordHasher::isSupported()) {
throw new InvalidConfigurationException('Libsodium is not available. Install the sodium extension or use "auto" instead.');
}
@ -743,6 +757,117 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
return $config;
}
private function createHashers(array $hashers, ContainerBuilder $container)
{
$hasherMap = [];
foreach ($hashers as $class => $hasher) {
$hasherMap[$class] = $this->createHasher($hasher);
}
$container
->getDefinition('security.password_hasher_factory')
->setArguments([$hasherMap])
;
}
private function createHasher(array $config)
{
// a custom hasher service
if (isset($config['id'])) {
return new Reference($config['id']);
}
if ($config['migrate_from'] ?? false) {
return $config;
}
// plaintext hasher
if ('plaintext' === $config['algorithm']) {
$arguments = [$config['ignore_case']];
return [
'class' => PlaintextPasswordHasher::class,
'arguments' => $arguments,
];
}
// pbkdf2 hasher
if ('pbkdf2' === $config['algorithm']) {
return [
'class' => Pbkdf2PasswordHasher::class,
'arguments' => [
$config['hash_algorithm'],
$config['encode_as_base64'],
$config['iterations'],
$config['key_length'],
],
];
}
// bcrypt hasher
if ('bcrypt' === $config['algorithm']) {
$config['algorithm'] = 'native';
$config['native_algorithm'] = \PASSWORD_BCRYPT;
return $this->createHasher($config);
}
// Argon2i hasher
if ('argon2i' === $config['algorithm']) {
if (SodiumPasswordHasher::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) {
$config['algorithm'] = 'sodium';
} elseif (\defined('PASSWORD_ARGON2I')) {
$config['algorithm'] = 'native';
$config['native_algorithm'] = \PASSWORD_ARGON2I;
} else {
throw new InvalidConfigurationException(sprintf('Algorithm "argon2i" is not available. Either use "%s" or upgrade to PHP 7.2+ instead.', \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13') ? 'argon2id", "auto' : 'auto'));
}
return $this->createHasher($config);
}
if ('argon2id' === $config['algorithm']) {
if (($hasSodium = SodiumPasswordHasher::isSupported()) && \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) {
$config['algorithm'] = 'sodium';
} elseif (\defined('PASSWORD_ARGON2ID')) {
$config['algorithm'] = 'native';
$config['native_algorithm'] = \PASSWORD_ARGON2ID;
} else {
throw new InvalidConfigurationException(sprintf('Algorithm "argon2id" is not available. Either use "%s", upgrade to PHP 7.3+ or use libsodium 1.0.15+ instead.', \defined('PASSWORD_ARGON2I') || $hasSodium ? 'argon2i", "auto' : 'auto'));
}
return $this->createHasher($config);
}
if ('native' === $config['algorithm']) {
return [
'class' => NativePasswordHasher::class,
'arguments' => [
$config['time_cost'],
(($config['memory_cost'] ?? 0) << 10) ?: null,
$config['cost'],
] + (isset($config['native_algorithm']) ? [3 => $config['native_algorithm']] : []),
];
}
if ('sodium' === $config['algorithm']) {
if (!SodiumPasswordHasher::isSupported()) {
throw new InvalidConfigurationException('Libsodium is not available. Install the sodium extension or use "auto" instead.');
}
return [
'class' => SodiumPasswordHasher::class,
'arguments' => [
$config['time_cost'],
(($config['memory_cost'] ?? 0) << 10) ?: null,
],
];
}
// run-time configured hasher
return $config;
}
// Parses user providers and returns an array of their ids
private function createUserProviders(array $config, ContainerBuilder $container): array
{

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use Symfony\Bundle\SecurityBundle\Command\UserPasswordEncoderCommand;
use Symfony\Component\PasswordHasher\Command\UserPasswordHashCommand;
return static function (ContainerConfigurator $container) {
$container->services()
@ -20,6 +21,16 @@ return static function (ContainerConfigurator $container) {
service('security.encoder_factory'),
abstract_arg('encoders user classes'),
])
->tag('console.command')
->tag('console.command', ['command' => 'security:encode-password'])
->deprecate('symfony/security-bundle', '5.3', 'The "%service_id%" service is deprecated, use "security.command.user_password_hash" instead.')
;
$container->services()
->set('security.command.user_password_hash', UserPasswordHashCommand::class)
->args([
service('security.password_hasher_factory'),
abstract_arg('list of user classes'),
])
->tag('console.command')
;
};

View File

@ -34,7 +34,7 @@ return static function (ContainerConfigurator $container) {
abstract_arg('User Provider'),
abstract_arg('Provider-shared Key'),
abstract_arg('User Checker'),
service('security.password_encoder'),
service('security.password_hasher'),
])
->set('security.authentication.listener.guard', GuardAuthenticationListener::class)

View File

@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory;
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasher;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
return static function (ContainerConfigurator $container) {
$container->services()
->set('security.password_hasher_factory', PasswordHasherFactory::class)
->args([[]])
->alias(PasswordHasherFactoryInterface::class, 'security.password_hasher_factory')
->set('security.user_password_hasher', UserPasswordHasher::class)
->args([service('security.password_hasher_factory')])
->alias('security.password_hasher', 'security.user_password_hasher')
->alias(UserPasswordHasherInterface::class, 'security.password_hasher')
;
};

View File

@ -11,6 +11,8 @@
<xsd:element name="access-decision-manager" type="access_decision_manager" minOccurs="0" maxOccurs="1" />
<xsd:element name="encoders" type="encoders" minOccurs="0" maxOccurs="1" />
<xsd:element name="encoder" type="encoder" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="password_hashers" type="password_hashers" minOccurs="0" maxOccurs="1" />
<xsd:element name="password_hasher" type="password_hasher" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="providers" type="providers" minOccurs="0" maxOccurs="1" />
<xsd:element name="provider" type="provider" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="firewalls" type="firewalls" minOccurs="0" maxOccurs="1" />
@ -31,6 +33,12 @@
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="password_hashers">
<xsd:sequence>
<xsd:element name="password_hasher" type="password_hasher" minOccurs="1" maxOccurs="unbounded" />
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="providers">
<xsd:sequence>
<xsd:element name="provider" type="provider" minOccurs="1" maxOccurs="unbounded" />
@ -84,6 +92,23 @@
<xsd:attribute name="id" type="xsd:string" />
</xsd:complexType>
<xsd:complexType name="password_hasher">
<xsd:sequence>
<xsd:element name="migrate-from" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
</xsd:sequence>
<xsd:attribute name="class" type="xsd:string" use="required" />
<xsd:attribute name="algorithm" type="xsd:string" />
<xsd:attribute name="hash-algorithm" type="xsd:string" />
<xsd:attribute name="key-length" type="xsd:string" />
<xsd:attribute name="ignore-case" type="xsd:boolean" />
<xsd:attribute name="encode-as-base64" type="xsd:boolean" />
<xsd:attribute name="iterations" type="xsd:string" />
<xsd:attribute name="cost" type="xsd:integer" />
<xsd:attribute name="memory-cost" type="xsd:string" />
<xsd:attribute name="time-cost" type="xsd:string" />
<xsd:attribute name="id" type="xsd:string" />
</xsd:complexType>
<xsd:complexType name="provider">
<xsd:choice minOccurs="0" maxOccurs="1">
<xsd:element name="chain" type="chain" />

View File

@ -18,6 +18,8 @@ use Symfony\Bundle\SecurityBundle\Security\FirewallContext;
use Symfony\Bundle\SecurityBundle\Security\FirewallMap;
use Symfony\Bundle\SecurityBundle\Security\LazyFirewallContext;
use Symfony\Component\Ldap\Security\LdapUserProvider;
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolver;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
@ -109,13 +111,20 @@ return static function (ContainerConfigurator $container) {
->args([
[],
])
->deprecate('symfony/security-bundle', '5.3', 'The "%service_id%" service is deprecated, use "security.password_hasher_factory" instead.')
->alias('security.encoder_factory', 'security.encoder_factory.generic')
->deprecate('symfony/security-bundle', '5.3', 'The "%alias_id%" service is deprecated, use "security.password_hasher_factory" instead.')
->alias(EncoderFactoryInterface::class, 'security.encoder_factory')
->deprecate('symfony/security-bundle', '5.3', 'The "%alias_id%" service is deprecated, use "'.PasswordHasherFactoryInterface::class.'" instead.')
->set('security.user_password_encoder.generic', UserPasswordEncoder::class)
->args([service('security.encoder_factory')])
->alias('security.password_encoder', 'security.user_password_encoder.generic')->public()
->deprecate('symfony/security-bundle', '5.3', 'The "%service_id%" service is deprecated, use "security.user_password_hasher" instead.')
->alias('security.password_encoder', 'security.user_password_encoder.generic')
->public()
->deprecate('symfony/security-bundle', '5.3', 'The "%alias_id%" service is deprecated, use "security.password_hasher"" instead.')
->alias(UserPasswordEncoderInterface::class, 'security.password_encoder')
->deprecate('symfony/security-bundle', '5.3', 'The "%alias_id%" service is deprecated, use "'.UserPasswordHasherInterface::class.'" instead.')
->set('security.user_checker', UserChecker::class)
@ -260,7 +269,7 @@ return static function (ContainerConfigurator $container) {
->set('security.validator.user_password', UserPasswordValidator::class)
->args([
service('security.token_storage'),
service('security.encoder_factory'),
service('security.password_hasher_factory'),
])
->tag('validator.constraint_validator', ['alias' => 'security.validator.user_password'])

View File

@ -72,7 +72,7 @@ return static function (ContainerConfigurator $container) {
// Listeners
->set('security.listener.check_authenticator_credentials', CheckCredentialsListener::class)
->args([
service('security.encoder_factory'),
service('security.password_hasher_factory'),
])
->tag('kernel.event_subscriber')
@ -90,7 +90,7 @@ return static function (ContainerConfigurator $container) {
->set('security.listener.password_migrating', PasswordMigratingListener::class)
->args([
service('security.encoder_factory'),
service('security.password_hasher_factory'),
])
->tag('kernel.event_subscriber')

View File

@ -221,7 +221,7 @@ return static function (ContainerConfigurator $container) {
abstract_arg('User Provider'),
abstract_arg('User Checker'),
abstract_arg('Provider-shared Key'),
service('security.encoder_factory'),
service('security.password_hasher_factory'),
param('security.authentication.hide_user_not_found'),
])

View File

@ -18,6 +18,10 @@ use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher;
use Symfony\Component\PasswordHasher\Hasher\Pbkdf2PasswordHasher;
use Symfony\Component\PasswordHasher\Hasher\PlaintextPasswordHasher;
use Symfony\Component\PasswordHasher\Hasher\SodiumPasswordHasher;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManager;
use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder;
use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder;
@ -275,9 +279,12 @@ abstract class CompleteConfigurationTest extends TestCase
], $container->getParameter('security.role_hierarchy.roles'));
}
/**
* @group legacy
*/
public function testEncoders()
{
$container = $this->getContainer('container1');
$container = $this->getContainer('legacy_encoders');
$this->assertEquals([[
'JMS\FooBundle\Entity\User1' => [
@ -332,6 +339,9 @@ abstract class CompleteConfigurationTest extends TestCase
]], $container->getDefinition('security.encoder_factory.generic')->getArguments());
}
/**
* @group legacy
*/
public function testEncodersWithLibsodium()
{
if (!SodiumPasswordEncoder::isSupported()) {
@ -385,6 +395,9 @@ abstract class CompleteConfigurationTest extends TestCase
]], $container->getDefinition('security.encoder_factory.generic')->getArguments());
}
/**
* @group legacy
*/
public function testEncodersWithArgon2i()
{
if (!($sodium = SodiumPasswordEncoder::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) && !\defined('PASSWORD_ARGON2I')) {
@ -438,6 +451,9 @@ abstract class CompleteConfigurationTest extends TestCase
]], $container->getDefinition('security.encoder_factory.generic')->getArguments());
}
/**
* @group legacy
*/
public function testMigratingEncoder()
{
if (!($sodium = SodiumPasswordEncoder::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) && !\defined('PASSWORD_ARGON2I')) {
@ -499,6 +515,9 @@ abstract class CompleteConfigurationTest extends TestCase
]], $container->getDefinition('security.encoder_factory.generic')->getArguments());
}
/**
* @group legacy
*/
public function testEncodersWithBCrypt()
{
$container = $this->getContainer('bcrypt_encoder');
@ -548,6 +567,279 @@ abstract class CompleteConfigurationTest extends TestCase
]], $container->getDefinition('security.encoder_factory.generic')->getArguments());
}
public function testHashers()
{
$container = $this->getContainer('container1');
$this->assertEquals([[
'JMS\FooBundle\Entity\User1' => [
'class' => PlaintextPasswordHasher::class,
'arguments' => [false],
],
'JMS\FooBundle\Entity\User2' => [
'algorithm' => 'sha1',
'encode_as_base64' => false,
'iterations' => 5,
'hash_algorithm' => 'sha512',
'key_length' => 40,
'ignore_case' => false,
'cost' => null,
'memory_cost' => null,
'time_cost' => null,
'migrate_from' => [],
],
'JMS\FooBundle\Entity\User3' => [
'algorithm' => 'md5',
'hash_algorithm' => 'sha512',
'key_length' => 40,
'ignore_case' => false,
'encode_as_base64' => true,
'iterations' => 5000,
'cost' => null,
'memory_cost' => null,
'time_cost' => null,
'migrate_from' => [],
],
'JMS\FooBundle\Entity\User4' => new Reference('security.hasher.foo'),
'JMS\FooBundle\Entity\User5' => [
'class' => Pbkdf2PasswordHasher::class,
'arguments' => ['sha1', false, 5, 30],
],
'JMS\FooBundle\Entity\User6' => [
'class' => NativePasswordHasher::class,
'arguments' => [8, 102400, 15],
],
'JMS\FooBundle\Entity\User7' => [
'algorithm' => 'auto',
'hash_algorithm' => 'sha512',
'key_length' => 40,
'ignore_case' => false,
'encode_as_base64' => true,
'iterations' => 5000,
'cost' => null,
'memory_cost' => null,
'time_cost' => null,
'migrate_from' => [],
],
]], $container->getDefinition('security.password_hasher_factory')->getArguments());
}
public function testHashersWithLibsodium()
{
if (!SodiumPasswordHasher::isSupported()) {
$this->markTestSkipped('Libsodium is not available.');
}
$container = $this->getContainer('sodium_hasher');
$this->assertEquals([[
'JMS\FooBundle\Entity\User1' => [
'class' => PlaintextPasswordHasher::class,
'arguments' => [false],
],
'JMS\FooBundle\Entity\User2' => [
'algorithm' => 'sha1',
'encode_as_base64' => false,
'iterations' => 5,
'hash_algorithm' => 'sha512',
'key_length' => 40,
'ignore_case' => false,
'cost' => null,
'memory_cost' => null,
'time_cost' => null,
'migrate_from' => [],
],
'JMS\FooBundle\Entity\User3' => [
'algorithm' => 'md5',
'hash_algorithm' => 'sha512',
'key_length' => 40,
'ignore_case' => false,
'encode_as_base64' => true,
'iterations' => 5000,
'cost' => null,
'memory_cost' => null,
'time_cost' => null,
'migrate_from' => [],
],
'JMS\FooBundle\Entity\User4' => new Reference('security.hasher.foo'),
'JMS\FooBundle\Entity\User5' => [
'class' => Pbkdf2PasswordHasher::class,
'arguments' => ['sha1', false, 5, 30],
],
'JMS\FooBundle\Entity\User6' => [
'class' => NativePasswordHasher::class,
'arguments' => [8, 102400, 15],
],
'JMS\FooBundle\Entity\User7' => [
'class' => SodiumPasswordHasher::class,
'arguments' => [8, 128 * 1024 * 1024],
],
]], $container->getDefinition('security.password_hasher_factory')->getArguments());
}
public function testHashersWithArgon2i()
{
if (!($sodium = SodiumPasswordHasher::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) && !\defined('PASSWORD_ARGON2I')) {
$this->markTestSkipped('Argon2i algorithm is not supported.');
}
$container = $this->getContainer('argon2i_hasher');
$this->assertEquals([[
'JMS\FooBundle\Entity\User1' => [
'class' => PlaintextPasswordHasher::class,
'arguments' => [false],
],
'JMS\FooBundle\Entity\User2' => [
'algorithm' => 'sha1',
'encode_as_base64' => false,
'iterations' => 5,
'hash_algorithm' => 'sha512',
'key_length' => 40,
'ignore_case' => false,
'cost' => null,
'memory_cost' => null,
'time_cost' => null,
'migrate_from' => [],
],
'JMS\FooBundle\Entity\User3' => [
'algorithm' => 'md5',
'hash_algorithm' => 'sha512',
'key_length' => 40,
'ignore_case' => false,
'encode_as_base64' => true,
'iterations' => 5000,
'cost' => null,
'memory_cost' => null,
'time_cost' => null,
'migrate_from' => [],
],
'JMS\FooBundle\Entity\User4' => new Reference('security.hasher.foo'),
'JMS\FooBundle\Entity\User5' => [
'class' => Pbkdf2PasswordHasher::class,
'arguments' => ['sha1', false, 5, 30],
],
'JMS\FooBundle\Entity\User6' => [
'class' => NativePasswordHasher::class,
'arguments' => [8, 102400, 15],
],
'JMS\FooBundle\Entity\User7' => [
'class' => $sodium ? SodiumPasswordHasher::class : NativePasswordHasher::class,
'arguments' => $sodium ? [256, 1] : [1, 262144, null, \PASSWORD_ARGON2I],
],
]], $container->getDefinition('security.password_hasher_factory')->getArguments());
}
public function testMigratingHasher()
{
if (!($sodium = SodiumPasswordHasher::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) && !\defined('PASSWORD_ARGON2I')) {
$this->markTestSkipped('Argon2i algorithm is not supported.');
}
$container = $this->getContainer('migrating_hasher');
$this->assertEquals([[
'JMS\FooBundle\Entity\User1' => [
'class' => PlaintextPasswordHasher::class,
'arguments' => [false],
],
'JMS\FooBundle\Entity\User2' => [
'algorithm' => 'sha1',
'encode_as_base64' => false,
'iterations' => 5,
'hash_algorithm' => 'sha512',
'key_length' => 40,
'ignore_case' => false,
'cost' => null,
'memory_cost' => null,
'time_cost' => null,
'migrate_from' => [],
],
'JMS\FooBundle\Entity\User3' => [
'algorithm' => 'md5',
'hash_algorithm' => 'sha512',
'key_length' => 40,
'ignore_case' => false,
'encode_as_base64' => true,
'iterations' => 5000,
'cost' => null,
'memory_cost' => null,
'time_cost' => null,
'migrate_from' => [],
],
'JMS\FooBundle\Entity\User4' => new Reference('security.hasher.foo'),
'JMS\FooBundle\Entity\User5' => [
'class' => Pbkdf2PasswordHasher::class,
'arguments' => ['sha1', false, 5, 30],
],
'JMS\FooBundle\Entity\User6' => [
'class' => NativePasswordHasher::class,
'arguments' => [8, 102400, 15],
],
'JMS\FooBundle\Entity\User7' => [
'algorithm' => 'argon2i',
'hash_algorithm' => 'sha512',
'key_length' => 40,
'ignore_case' => false,
'encode_as_base64' => true,
'iterations' => 5000,
'cost' => null,
'memory_cost' => 256,
'time_cost' => 1,
'migrate_from' => ['bcrypt'],
],
]], $container->getDefinition('security.password_hasher_factory')->getArguments());
}
public function testHashersWithBCrypt()
{
$container = $this->getContainer('bcrypt_hasher');
$this->assertEquals([[
'JMS\FooBundle\Entity\User1' => [
'class' => PlaintextPasswordHasher::class,
'arguments' => [false],
],
'JMS\FooBundle\Entity\User2' => [
'algorithm' => 'sha1',
'encode_as_base64' => false,
'iterations' => 5,
'hash_algorithm' => 'sha512',
'key_length' => 40,
'ignore_case' => false,
'cost' => null,
'memory_cost' => null,
'time_cost' => null,
'migrate_from' => [],
],
'JMS\FooBundle\Entity\User3' => [
'algorithm' => 'md5',
'hash_algorithm' => 'sha512',
'key_length' => 40,
'ignore_case' => false,
'encode_as_base64' => true,
'iterations' => 5000,
'cost' => null,
'memory_cost' => null,
'time_cost' => null,
'migrate_from' => [],
],
'JMS\FooBundle\Entity\User4' => new Reference('security.hasher.foo'),
'JMS\FooBundle\Entity\User5' => [
'class' => Pbkdf2PasswordHasher::class,
'arguments' => ['sha1', false, 5, 30],
],
'JMS\FooBundle\Entity\User6' => [
'class' => NativePasswordHasher::class,
'arguments' => [8, 102400, 15],
],
'JMS\FooBundle\Entity\User7' => [
'class' => NativePasswordHasher::class,
'arguments' => [null, null, 15, \PASSWORD_BCRYPT],
],
]], $container->getDefinition('security.password_hasher_factory')->getArguments());
}
public function testRememberMeThrowExceptionsDefault()
{
$container = $this->getContainer('container1');
@ -577,9 +869,9 @@ abstract class CompleteConfigurationTest extends TestCase
$this->assertEquals('security.user_checker', $this->getContainer('container1')->getAlias('security.user_checker.secure'));
}
public function testUserPasswordEncoderCommandIsRegistered()
public function testUserPasswordHasherCommandIsRegistered()
{
$this->assertTrue($this->getContainer('remember_me_options')->has('security.command.user_password_encoder'));
$this->assertTrue($this->getContainer('remember_me_options')->has('security.command.user_password_hash'));
}
public function testDefaultAccessDecisionManagerStrategyIsAffirmative()

View File

@ -1,6 +1,6 @@
<?php
$this->load('container1.php');
$this->load('legacy_encoders.php');
$container->loadFromExtension('security', [
'encoders' => [

View File

@ -0,0 +1,13 @@
<?php
$this->load('container1.php');
$container->loadFromExtension('security', [
'password_hashers' => [
'JMS\FooBundle\Entity\User7' => [
'algorithm' => 'argon2i',
'memory_cost' => 256,
'time_cost' => 1,
],
],
]);

View File

@ -1,6 +1,6 @@
<?php
$this->load('container1.php');
$this->load('legacy_encoders.php');
$container->loadFromExtension('security', [
'encoders' => [

View File

@ -0,0 +1,12 @@
<?php
$this->load('container1.php');
$container->loadFromExtension('security', [
'password_hashers' => [
'JMS\FooBundle\Entity\User7' => [
'algorithm' => 'bcrypt',
'cost' => 15,
],
],
]);

View File

@ -1,7 +1,7 @@
<?php
$container->loadFromExtension('security', [
'encoders' => [
'password_hashers' => [
'JMS\FooBundle\Entity\User1' => 'plaintext',
'JMS\FooBundle\Entity\User2' => [
'algorithm' => 'sha1',
@ -12,7 +12,7 @@ $container->loadFromExtension('security', [
'algorithm' => 'md5',
],
'JMS\FooBundle\Entity\User4' => [
'id' => 'security.encoder.foo',
'id' => 'security.hasher.foo',
],
'JMS\FooBundle\Entity\User5' => [
'algorithm' => 'pbkdf2',

View File

@ -0,0 +1,108 @@
<?php
$container->loadFromExtension('security', [
'encoders' => [
'JMS\FooBundle\Entity\User1' => 'plaintext',
'JMS\FooBundle\Entity\User2' => [
'algorithm' => 'sha1',
'encode_as_base64' => false,
'iterations' => 5,
],
'JMS\FooBundle\Entity\User3' => [
'algorithm' => 'md5',
],
'JMS\FooBundle\Entity\User4' => [
'id' => 'security.encoder.foo',
],
'JMS\FooBundle\Entity\User5' => [
'algorithm' => 'pbkdf2',
'hash_algorithm' => 'sha1',
'encode_as_base64' => false,
'iterations' => 5,
'key_length' => 30,
],
'JMS\FooBundle\Entity\User6' => [
'algorithm' => 'native',
'time_cost' => 8,
'memory_cost' => 100,
'cost' => 15,
],
'JMS\FooBundle\Entity\User7' => [
'algorithm' => 'auto',
],
],
'providers' => [
'default' => [
'memory' => [
'users' => [
'foo' => ['password' => 'foo', 'roles' => 'ROLE_USER'],
],
],
],
'digest' => [
'memory' => [
'users' => [
'foo' => ['password' => 'foo', 'roles' => 'ROLE_USER, ROLE_ADMIN'],
],
],
],
'basic' => [
'memory' => [
'users' => [
'foo' => ['password' => '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33', 'roles' => 'ROLE_SUPER_ADMIN'],
'bar' => ['password' => '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33', 'roles' => ['ROLE_USER', 'ROLE_ADMIN']],
],
],
],
'service' => [
'id' => 'user.manager',
],
'chain' => [
'chain' => [
'providers' => ['service', 'basic'],
],
],
],
'firewalls' => [
'simple' => ['provider' => 'default', 'pattern' => '/login', 'security' => false],
'secure' => ['stateless' => true,
'provider' => 'default',
'http_basic' => true,
'form_login' => true,
'anonymous' => true,
'switch_user' => true,
'x509' => true,
'remote_user' => true,
'logout' => true,
'remember_me' => ['secret' => 'TheSecret'],
'user_checker' => null,
],
'host' => [
'provider' => 'default',
'pattern' => '/test',
'host' => 'foo\\.example\\.org',
'methods' => ['GET', 'POST'],
'anonymous' => true,
'http_basic' => true,
],
'with_user_checker' => [
'provider' => 'default',
'user_checker' => 'app.user_checker',
'anonymous' => true,
'http_basic' => true,
],
],
'access_control' => [
['path' => '/blog/524', 'role' => 'ROLE_USER', 'requires_channel' => 'https', 'methods' => ['get', 'POST'], 'port' => 8000],
['path' => '/blog/.*', 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY'],
['path' => '/blog/524', 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY', 'allow_if' => "token.getUsername() matches '/^admin/'"],
],
'role_hierarchy' => [
'ROLE_ADMIN' => 'ROLE_USER',
'ROLE_SUPER_ADMIN' => ['ROLE_USER', 'ROLE_ADMIN', 'ROLE_ALLOWED_TO_SWITCH'],
'ROLE_REMOTE' => 'ROLE_USER,ROLE_ADMIN',
],
]);

View File

@ -1,6 +1,6 @@
<?php
$this->load('container1.php');
$this->load('legacy_encoders.php');
$container->loadFromExtension('security', [
'encoders' => [

View File

@ -0,0 +1,14 @@
<?php
$this->load('container1.php');
$container->loadFromExtension('security', [
'password_hashers' => [
'JMS\FooBundle\Entity\User7' => [
'algorithm' => 'argon2i',
'memory_cost' => 256,
'time_cost' => 1,
'migrate_from' => 'bcrypt',
],
],
]);

View File

@ -1,6 +1,6 @@
<?php
$this->load('container1.php');
$this->load('legacy_encoders.php');
$container->loadFromExtension('security', [
'encoders' => [

View File

@ -0,0 +1,13 @@
<?php
$this->load('container1.php');
$container->loadFromExtension('security', [
'password_hashers' => [
'JMS\FooBundle\Entity\User7' => [
'algorithm' => 'sodium',
'time_cost' => 8,
'memory_cost' => 128 * 1024,
],
],
]);

View File

@ -9,7 +9,7 @@
https://symfony.com/schema/dic/security/security-1.0.xsd">
<imports>
<import resource="container1.xml"/>
<import resource="legacy_encoders.xml"/>
</imports>
<sec:config>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:sec="http://symfony.com/schema/dic/security"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/security
https://symfony.com/schema/dic/security/security-1.0.xsd">
<imports>
<import resource="container1.xml"/>
</imports>
<sec:config>
<sec:password_hasher class="JMS\FooBundle\Entity\User7" algorithm="argon2i" memory-cost="256" time-cost="1" />
</sec:config>
</container>

View File

@ -9,7 +9,7 @@
https://symfony.com/schema/dic/security/security-1.0.xsd">
<imports>
<import resource="container1.xml"/>
<import resource="legacy_encoders.xml"/>
</imports>
<sec:config>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:sec="http://symfony.com/schema/dic/security"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/security
https://symfony.com/schema/dic/security/security-1.0.xsd">
<imports>
<import resource="container1.xml"/>
</imports>
<sec:config>
<sec:password_hasher class="JMS\FooBundle\Entity\User7" algorithm="bcrypt" cost="15" />
</sec:config>
</container>

View File

@ -9,19 +9,19 @@
https://symfony.com/schema/dic/security/security-1.0.xsd">
<config>
<encoder class="JMS\FooBundle\Entity\User1" algorithm="plaintext" />
<password_hasher class="JMS\FooBundle\Entity\User1" algorithm="plaintext" />
<encoder class="JMS\FooBundle\Entity\User2" algorithm="sha1" encode-as-base64="false" iterations="5" />
<password_hasher class="JMS\FooBundle\Entity\User2" algorithm="sha1" encode-as-base64="false" iterations="5" />
<encoder class="JMS\FooBundle\Entity\User3" algorithm="md5" />
<password_hasher class="JMS\FooBundle\Entity\User3" algorithm="md5" />
<encoder class="JMS\FooBundle\Entity\User4" id="security.encoder.foo" />
<password_hasher class="JMS\FooBundle\Entity\User4" id="security.hasher.foo" />
<encoder class="JMS\FooBundle\Entity\User5" algorithm="pbkdf2" hash-algorithm="sha1" encode-as-base64="false" iterations="5" key-length="30" />
<password_hasher class="JMS\FooBundle\Entity\User5" algorithm="pbkdf2" hash-algorithm="sha1" encode-as-base64="false" iterations="5" key-length="30" />
<encoder class="JMS\FooBundle\Entity\User6" algorithm="native" time-cost="8" memory-cost="100" cost="15" />
<password_hasher class="JMS\FooBundle\Entity\User6" algorithm="native" time-cost="8" memory-cost="100" cost="15" />
<encoder class="JMS\FooBundle\Entity\User7" algorithm="auto" />
<password_hasher class="JMS\FooBundle\Entity\User7" algorithm="auto" />
<provider name="default">
<memory>

View File

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8"?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:srv="http://symfony.com/schema/dic/services"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/security
https://symfony.com/schema/dic/security/security-1.0.xsd">
<config>
<encoder class="JMS\FooBundle\Entity\User1" algorithm="plaintext" />
<encoder class="JMS\FooBundle\Entity\User2" algorithm="sha1" encode-as-base64="false" iterations="5" />
<encoder class="JMS\FooBundle\Entity\User3" algorithm="md5" />
<encoder class="JMS\FooBundle\Entity\User4" id="security.encoder.foo" />
<encoder class="JMS\FooBundle\Entity\User5" algorithm="pbkdf2" hash-algorithm="sha1" encode-as-base64="false" iterations="5" key-length="30" />
<encoder class="JMS\FooBundle\Entity\User6" algorithm="native" time-cost="8" memory-cost="100" cost="15" />
<encoder class="JMS\FooBundle\Entity\User7" algorithm="auto" />
<provider name="default">
<memory>
<user name="foo" password="foo" roles="ROLE_USER" />
</memory>
</provider>
<provider name="digest">
<memory>
<user name="foo" password="foo" roles="ROLE_USER, ROLE_ADMIN" />
</memory>
</provider>
<provider name="basic">
<memory>
<user name="foo" password="0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33" roles="ROLE_SUPER_ADMIN" />
<user name="bar" password="0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33" roles="ROLE_USER, ROLE_ADMIN" />
</memory>
</provider>
<provider name="service" id="user.manager" />
<provider name="chain">
<chain providers="service, basic" />
</provider>
<firewall name="simple" pattern="/login" security="false" provider="default" />
<firewall name="secure" stateless="true" provider="default">
<http-basic />
<form-login />
<anonymous />
<switch-user />
<x509 />
<remote-user />
<logout />
<remember-me secret="TheSecret"/>
</firewall>
<firewall name="host" pattern="/test" host="foo\.example\.org" methods="GET,POST" provider="default">
<anonymous />
<http-basic />
</firewall>
<firewall name="with_user_checker" provider="default">
<anonymous />
<http-basic />
<user-checker>app.user_checker</user-checker>
</firewall>
<role id="ROLE_ADMIN">ROLE_USER</role>
<role id="ROLE_SUPER_ADMIN">ROLE_USER,ROLE_ADMIN,ROLE_ALLOWED_TO_SWITCH</role>
<role id="ROLE_REMOTE">ROLE_USER,ROLE_ADMIN</role>
<rule path="/blog/524" role="ROLE_USER" requires-channel="https" methods="get,POST" port="8000" />
<rule role='IS_AUTHENTICATED_ANONYMOUSLY' path="/blog/.*" />
<rule role='IS_AUTHENTICATED_ANONYMOUSLY' allow-if="token.getUsername() matches '/^admin/'" path="/blog/524" />
</config>
</srv:container>

View File

@ -9,7 +9,7 @@
https://symfony.com/schema/dic/security/security-1.0.xsd">
<imports>
<import resource="container1.xml"/>
<import resource="legacy_encoders.xml"/>
</imports>
<sec:config>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:sec="http://symfony.com/schema/dic/security"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/security
https://symfony.com/schema/dic/security/security-1.0.xsd">
<imports>
<import resource="container1.xml"/>
</imports>
<sec:config>
<sec:password_hasher class="JMS\FooBundle\Entity\User7" algorithm="argon2i" memory-cost="256" time-cost="1">
<sec:migrate-from>bcrypt</sec:migrate-from>
</sec:password_hasher>
</sec:config>
</container>

View File

@ -9,7 +9,7 @@
https://symfony.com/schema/dic/security/security-1.0.xsd">
<imports>
<import resource="container1.xml"/>
<import resource="legacy_encoders.xml"/>
</imports>
<sec:config>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:sec="http://symfony.com/schema/dic/security"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/security
https://symfony.com/schema/dic/security/security-1.0.xsd">
<imports>
<import resource="container1.xml"/>
</imports>
<sec:config>
<sec:password_hasher class="JMS\FooBundle\Entity\User7" algorithm="sodium" time-cost="8" memory-cost="131072" />
</sec:config>
</container>

View File

@ -1,5 +1,5 @@
imports:
- { resource: container1.yml }
- { resource: legacy_encoders.yml }
security:
encoders:

View File

@ -0,0 +1,9 @@
imports:
- { resource: container1.yml }
security:
password_hashers:
JMS\FooBundle\Entity\User7:
algorithm: argon2i
memory_cost: 256
time_cost: 1

View File

@ -1,5 +1,5 @@
imports:
- { resource: container1.yml }
- { resource: legacy_encoders.yml }
security:
encoders:

View File

@ -0,0 +1,8 @@
imports:
- { resource: container1.yml }
security:
password_hashers:
JMS\FooBundle\Entity\User7:
algorithm: bcrypt
cost: 15

View File

@ -1,5 +1,5 @@
security:
encoders:
password_hashers:
JMS\FooBundle\Entity\User1: plaintext
JMS\FooBundle\Entity\User2:
algorithm: sha1
@ -8,7 +8,7 @@ security:
JMS\FooBundle\Entity\User3:
algorithm: md5
JMS\FooBundle\Entity\User4:
id: security.encoder.foo
id: security.hasher.foo
JMS\FooBundle\Entity\User5:
algorithm: pbkdf2
hash_algorithm: sha1

View File

@ -0,0 +1,87 @@
security:
encoders:
JMS\FooBundle\Entity\User1: plaintext
JMS\FooBundle\Entity\User2:
algorithm: sha1
encode_as_base64: false
iterations: 5
JMS\FooBundle\Entity\User3:
algorithm: md5
JMS\FooBundle\Entity\User4:
id: security.encoder.foo
JMS\FooBundle\Entity\User5:
algorithm: pbkdf2
hash_algorithm: sha1
encode_as_base64: false
iterations: 5
key_length: 30
JMS\FooBundle\Entity\User6:
algorithm: native
time_cost: 8
memory_cost: 100
cost: 15
JMS\FooBundle\Entity\User7:
algorithm: auto
providers:
default:
memory:
users:
foo: { password: foo, roles: ROLE_USER }
digest:
memory:
users:
foo: { password: foo, roles: 'ROLE_USER, ROLE_ADMIN' }
basic:
memory:
users:
foo: { password: 0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33, roles: ROLE_SUPER_ADMIN }
bar: { password: 0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33, roles: [ROLE_USER, ROLE_ADMIN] }
service:
id: user.manager
chain:
chain:
providers: [service, basic]
firewalls:
simple: { pattern: /login, security: false }
secure:
provider: default
stateless: true
http_basic: true
form_login: true
anonymous: true
switch_user:
x509: true
remote_user: true
logout: true
remember_me:
secret: TheSecret
user_checker: ~
host:
provider: default
pattern: /test
host: foo\.example\.org
methods: [GET,POST]
anonymous: true
http_basic: true
with_user_checker:
provider: default
anonymous: ~
http_basic: ~
user_checker: app.user_checker
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
ROLE_REMOTE: ROLE_USER,ROLE_ADMIN
access_control:
- { path: /blog/524, role: ROLE_USER, requires_channel: https, methods: [get, POST], port: 8000}
-
path: /blog/.*
role: IS_AUTHENTICATED_ANONYMOUSLY
- { path: /blog/524, role: IS_AUTHENTICATED_ANONYMOUSLY, allow_if: "token.getUsername() matches '/^admin/'" }

View File

@ -1,5 +1,5 @@
imports:
- { resource: container1.yml }
- { resource: legacy_encoders.yml }
security:
encoders:

View File

@ -0,0 +1,10 @@
imports:
- { resource: container1.yml }
security:
password_hashers:
JMS\FooBundle\Entity\User7:
algorithm: argon2i
memory_cost: 256
time_cost: 1
migrate_from: bcrypt

View File

@ -1,5 +1,5 @@
imports:
- { resource: container1.yml }
- { resource: legacy_encoders.yml }
security:
encoders:

View File

@ -0,0 +1,9 @@
imports:
- { resource: container1.yml }
security:
password_hashers:
JMS\FooBundle\Entity\User7:
algorithm: sodium
time_cost: 8
memory_cost: 131072

View File

@ -24,6 +24,7 @@ use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder;
* Tests UserPasswordEncoderCommand.
*
* @author Sarah Khalil <mkhalil.sarah@gmail.com>
* @group legacy
*/
class UserPasswordEncoderCommandTest extends AbstractWebTestCase
{
@ -40,7 +41,7 @@ class UserPasswordEncoderCommandTest extends AbstractWebTestCase
], ['decorated' => false]);
$expected = str_replace("\n", \PHP_EOL, file_get_contents(__DIR__.'/app/PasswordEncode/emptysalt.txt'));
$this->assertEquals($expected, $this->passwordEncoderCommandTester->getDisplay());
$this->assertStringContainsString($expected, $this->passwordEncoderCommandTester->getDisplay());
}
public function testEncodeNoPasswordNoInteraction()

View File

@ -9,7 +9,7 @@ services:
security:
encoders:
password_hashers:
\Symfony\Component\Security\Core\User\UserInterface: plaintext
providers:

View File

@ -1,7 +1,7 @@
security:
enable_authenticator_manager: true
encoders:
password_hashers:
Symfony\Component\Security\Core\User\User: plaintext
providers:

View File

@ -2,7 +2,7 @@ imports:
- { resource: ./../config/framework.yml }
security:
encoders:
password_hashers:
Symfony\Component\Security\Core\User\User: plaintext
providers:

View File

@ -15,7 +15,7 @@ services:
- { name: container.service_subscriber }
security:
encoders:
password_hashers:
Symfony\Component\Security\Core\User\User: plaintext
providers:

View File

@ -28,5 +28,5 @@ security:
memory:
users:
john: { password: doe, roles: [ROLE_SECURE] }
encoders:
password_hashers:
Symfony\Component\Security\Core\User\User: plaintext

View File

@ -14,7 +14,7 @@ services:
tags: [controller.service_arguments]
security:
encoders:
password_hashers:
Symfony\Component\Security\Core\User\User: plaintext
providers:

View File

@ -5,7 +5,7 @@ framework:
serializer: ~
security:
encoders:
password_hashers:
Symfony\Component\Security\Core\User\User: plaintext
providers:

View File

@ -2,7 +2,7 @@ imports:
- { resource: ./../config/framework.yml }
security:
encoders:
password_hashers:
Symfony\Component\Security\Core\User\User: plaintext
providers:

View File

@ -2,7 +2,7 @@ imports:
- { resource: ./../config/framework.yml }
security:
encoders:
password_hashers:
Symfony\Component\Security\Core\User\User: plaintext
providers:

View File

@ -2,7 +2,7 @@ imports:
- { resource: ./../config/framework.yml }
security:
encoders:
password_hashers:
Symfony\Component\Security\Core\User\User: plaintext
providers:

View File

@ -2,7 +2,7 @@ imports:
- { resource: ./../config/framework.yml }
security:
encoders:
password_hashers:
Symfony\Component\Security\Core\User\User: plaintext
providers:

View File

@ -7,7 +7,7 @@ framework:
cookie_samesite: lax
security:
encoders:
password_hashers:
Symfony\Component\Security\Core\User\User: plaintext
providers:

View File

@ -6,7 +6,7 @@ parameters:
env(APP_IPS): '127.0.0.1, ::1'
security:
encoders:
password_hashers:
Symfony\Component\Security\Core\User\User: plaintext
providers:

View File

@ -2,7 +2,7 @@ imports:
- { resource: ./../config/default.yml }
security:
encoders:
password_hashers:
Symfony\Component\Security\Core\User\User: plaintext
providers:

View File

@ -2,7 +2,7 @@ imports:
- { resource: ./../config/default.yml }
security:
encoders:
password_hashers:
Symfony\Component\Security\Core\User\User: plaintext
providers:

View File

@ -2,7 +2,7 @@ imports:
- { resource: ./../config/default.yml }
security:
encoders:
password_hashers:
Symfony\Component\Security\Core\User\User: plaintext
providers:

View File

@ -23,11 +23,12 @@
"symfony/deprecation-contracts": "^2.1",
"symfony/event-dispatcher": "^5.1",
"symfony/http-kernel": "^5.0",
"symfony/password-hasher": "^5.3",
"symfony/polyfill-php80": "^1.15",
"symfony/security-core": "^5.3",
"symfony/security-csrf": "^4.4|^5.0",
"symfony/security-guard": "^5.2",
"symfony/security-http": "^5.2"
"symfony/security-guard": "^5.3",
"symfony/security-http": "^5.3"
},
"require-dev": {
"doctrine/doctrine-bundle": "^2.0",

View File

@ -0,0 +1,4 @@
/Tests export-ignore
/phpunit.xml.dist export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore

View File

@ -0,0 +1,3 @@
vendor/
composer.lock
phpunit.xml

View File

@ -0,0 +1,4 @@
5.3
---
* Add the component

View File

@ -0,0 +1,213 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
use Symfony\Component\PasswordHasher\LegacyPasswordHasherInterface;
/**
* Hashes a user's password.
*
* @author Sarah Khalil <mkhalil.sarah@gmail.com>
* @author Robin Chalas <robin.chalas@gmail.com>
*
* @final
*/
class UserPasswordHashCommand extends Command
{
protected static $defaultName = 'security:hash-password';
protected static $defaultDescription = "Hashes a user password";
private $hasherFactory;
private $userClasses;
public function __construct(PasswordHasherFactoryInterface $hasherFactory, array $userClasses = [])
{
$this->hasherFactory = $hasherFactory;
$this->userClasses = $userClasses;
parent::__construct();
}
/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->setDescription(self::$defaultDescription)
->addArgument('password', InputArgument::OPTIONAL, 'The plain password to hash.')
->addArgument('user-class', InputArgument::OPTIONAL, 'The User entity class path associated with the hasher used to hash the password.')
->addOption('empty-salt', null, InputOption::VALUE_NONE, 'Do not generate a salt or let the hasher generate one.')
->setHelp(<<<EOF
The <info>%command.name%</info> command hashs passwords according to your
security configuration. This command is mainly used to generate passwords for
the <comment>in_memory</comment> user provider type and for changing passwords
in the database while developing the application.
Suppose that you have the following security configuration in your application:
<comment>
# app/config/security.yml
security:
password_hashers:
Symfony\Component\Security\Core\User\User: plaintext
App\Entity\User: auto
</comment>
If you execute the command non-interactively, the first available configured
user class under the <comment>security.password_hashers</comment> key is used and a random salt is
generated to hash the password:
<info>php %command.full_name% --no-interaction [password]</info>
Pass the full user class path as the second argument to hash passwords for
your own entities:
<info>php %command.full_name% --no-interaction [password] 'App\Entity\User'</info>
Executing the command interactively allows you to generate a random salt for
hashing the password:
<info>php %command.full_name% [password] 'App\Entity\User'</info>
In case your hasher doesn't require a salt, add the <comment>empty-salt</comment> option:
<info>php %command.full_name% --empty-salt [password] 'App\Entity\User'</info>
EOF
)
;
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$errorIo = $output instanceof ConsoleOutputInterface ? new SymfonyStyle($input, $output->getErrorOutput()) : $io;
$input->isInteractive() ? $errorIo->title('Symfony Password Hash Utility') : $errorIo->newLine();
$password = $input->getArgument('password');
$userClass = $this->getUserClass($input, $io);
$emptySalt = $input->getOption('empty-salt');
$hasher = $this->hasherFactory->getPasswordHasher($userClass);
$saltlessWithoutEmptySalt = !$emptySalt && !$hasher instanceof LegacyPasswordHasherInterface;
if ($saltlessWithoutEmptySalt) {
$emptySalt = true;
}
if (!$password) {
if (!$input->isInteractive()) {
$errorIo->error('The password must not be empty.');
return 1;
}
$passwordQuestion = $this->createPasswordQuestion();
$password = $errorIo->askQuestion($passwordQuestion);
}
$salt = null;
if ($input->isInteractive() && !$emptySalt) {
$emptySalt = true;
$errorIo->note('The command will take care of generating a salt for you. Be aware that some hashers advise to let them generate their own salt. If you\'re using one of those hashers, please answer \'no\' to the question below. '.\PHP_EOL.'Provide the \'empty-salt\' option in order to let the hasher handle the generation itself.');
if ($errorIo->confirm('Confirm salt generation ?')) {
$salt = $this->generateSalt();
$emptySalt = false;
}
} elseif (!$emptySalt) {
$salt = $this->generateSalt();
}
$hashedPassword = $hasher->hash($password, $salt);
$rows = [
['Hasher used', \get_class($hasher)],
['Password hash', $hashedPassword],
];
if (!$emptySalt) {
$rows[] = ['Generated salt', $salt];
}
$io->table(['Key', 'Value'], $rows);
if (!$emptySalt) {
$errorIo->note(sprintf('Make sure that your salt storage field fits the salt length: %s chars', \strlen($salt)));
} elseif ($saltlessWithoutEmptySalt) {
$errorIo->note('Self-salting hasher used: the hasher generated its own built-in salt.');
}
$errorIo->success('Password hashing succeeded');
return 0;
}
/**
* Create the password question to ask the user for the password to be hashed.
*/
private function createPasswordQuestion(): Question
{
$passwordQuestion = new Question('Type in your password to be hashed');
return $passwordQuestion->setValidator(function ($value) {
if ('' === trim($value)) {
throw new InvalidArgumentException('The password must not be empty.');
}
return $value;
})->setHidden(true)->setMaxAttempts(20);
}
private function generateSalt(): string
{
return base64_encode(random_bytes(30));
}
private function getUserClass(InputInterface $input, SymfonyStyle $io): string
{
if (null !== $userClass = $input->getArgument('user-class')) {
return $userClass;
}
if (!$this->userClasses) {
throw new RuntimeException('There are no configured password hashers for the "security" extension.');
}
if (!$input->isInteractive() || 1 === \count($this->userClasses)) {
return reset($this->userClasses);
}
$userClasses = $this->userClasses;
natcasesort($userClasses);
$userClasses = array_values($userClasses);
return $io->choice('For which user class would you like to hash a password?', $userClasses, reset($userClasses));
}
}

View File

@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Exception;
/**
* Interface for exceptions thrown by the password-hasher component.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
interface ExceptionInterface extends \Throwable
{
}

View File

@ -0,0 +1,23 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Exception;
/**
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class InvalidPasswordException extends \RuntimeException implements ExceptionInterface
{
public function __construct(string $message = 'Invalid password.', int $code = 0, ?\Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}

View File

@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Exception;
/**
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class LogicException extends \LogicException implements ExceptionInterface
{
}

View File

@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Hasher;
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
/**
* @author Robin Chalas <robin.chalas@gmail.com>
*/
trait CheckPasswordLengthTrait
{
private function isPasswordTooLong(string $password): bool
{
return PasswordHasherInterface::MAX_PASSWORD_LENGTH < \strlen($password);
}
}

View File

@ -0,0 +1,98 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Hasher;
use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException;
use Symfony\Component\PasswordHasher\Exception\LogicException;
use Symfony\Component\PasswordHasher\LegacyPasswordHasherInterface;
/**
* MessageDigestPasswordHasher uses a message digest algorithm.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class MessageDigestPasswordHasher implements LegacyPasswordHasherInterface
{
use CheckPasswordLengthTrait;
private $algorithm;
private $encodeHashAsBase64;
private $iterations = 1;
private $hashLength = -1;
/**
* @param string $algorithm The digest algorithm to use
* @param bool $encodeHashAsBase64 Whether to base64 encode the password hash
* @param int $iterations The number of iterations to use to stretch the password hash
*/
public function __construct(string $algorithm = 'sha512', bool $encodeHashAsBase64 = true, int $iterations = 5000)
{
$this->algorithm = $algorithm;
$this->encodeHashAsBase64 = $encodeHashAsBase64;
try {
$this->hashLength = \strlen($this->hash('', 'salt'));
} catch (\LogicException $e) {
// ignore algorithm not supported
}
$this->iterations = $iterations;
}
public function hash(string $plainPassword, ?string $salt = null): string
{
if ($this->isPasswordTooLong($plainPassword)) {
throw new InvalidPasswordException();
}
if (!\in_array($this->algorithm, hash_algos(), true)) {
throw new LogicException(sprintf('The algorithm "%s" is not supported.', $this->algorithm));
}
$salted = $this->mergePasswordAndSalt($plainPassword, $salt);
$digest = hash($this->algorithm, $salted, true);
// "stretch" hash
for ($i = 1; $i < $this->iterations; ++$i) {
$digest = hash($this->algorithm, $digest.$salted, true);
}
return $this->encodeHashAsBase64 ? base64_encode($digest) : bin2hex($digest);
}
public function verify(string $hashedPassword, string $plainPassword, ?string $salt = null): bool
{
if (\strlen($hashedPassword) !== $this->hashLength || false !== strpos($hashedPassword, '$')) {
return false;
}
return !$this->isPasswordTooLong($plainPassword) && hash_equals($hashedPassword, $this->hash($plainPassword, $salt));
}
public function needsRehash(string $hashedPassword): bool
{
return false;
}
private function mergePasswordAndSalt(string $password, ?string $salt): string
{
if (!$salt) {
return $password;
}
if (false !== strrpos($salt, '{') || false !== strrpos($salt, '}')) {
throw new \InvalidArgumentException('Cannot use { or } in salt.');
}
return $password.'{'.$salt.'}';
}
}

View File

@ -0,0 +1,65 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Hasher;
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
use Symfony\Component\PasswordHasher\LegacyPasswordHasherInterface;
/**
* Hashes passwords using the best available hasher.
* Verifies them using a chain of hashers.
*
* /!\ Don't put a PlaintextPasswordHasher in the list as that'd mean a leaked hash
* could be used to authenticate successfully without knowing the cleartext password.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class MigratingPasswordHasher implements PasswordHasherInterface
{
private $bestHasher;
private $extraHashers;
public function __construct(PasswordHasherInterface $bestHasher, PasswordHasherInterface ...$extraHashers)
{
$this->bestHasher = $bestHasher;
$this->extraHashers = $extraHashers;
}
public function hash(string $plainPassword, ?string $salt = null): string
{
return $this->bestHasher->hash($plainPassword, $salt);
}
public function verify(string $hashedPassword, string $plainPassword, ?string $salt = null): bool
{
if ($this->bestHasher->verify($hashedPassword, $plainPassword, $salt)) {
return true;
}
if (!$this->bestHasher->needsRehash($hashedPassword)) {
return false;
}
foreach ($this->extraHashers as $hasher) {
if ($hasher->verify($hashedPassword, $plainPassword, $salt)) {
return true;
}
}
return false;
}
public function needsRehash(string $hashedPassword): bool
{
return $this->bestHasher->needsRehash($hashedPassword);
}
}

View File

@ -0,0 +1,112 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Hasher;
use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException;
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
/**
* Hashes passwords using password_hash().
*
* @author Elnur Abdurrakhimov <elnur@elnur.pro>
* @author Terje Bråten <terje@braten.be>
* @author Nicolas Grekas <p@tchwork.com>
*/
final class NativePasswordHasher implements PasswordHasherInterface
{
use CheckPasswordLengthTrait;
private $algo = \PASSWORD_BCRYPT;
private $options;
/**
* @param string|null $algo An algorithm supported by password_hash() or null to use the stronger available algorithm
*/
public function __construct(int $opsLimit = null, int $memLimit = null, int $cost = null, ?string $algo = null)
{
$cost = $cost ?? 13;
$opsLimit = $opsLimit ?? max(4, \defined('SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE') ? \SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE : 4);
$memLimit = $memLimit ?? max(64 * 1024 * 1024, \defined('SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE') ? \SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE : 64 * 1024 * 1024);
if (3 > $opsLimit) {
throw new \InvalidArgumentException('$opsLimit must be 3 or greater.');
}
if (10 * 1024 > $memLimit) {
throw new \InvalidArgumentException('$memLimit must be 10k or greater.');
}
if ($cost < 4 || 31 < $cost) {
throw new \InvalidArgumentException('$cost must be in the range of 4-31.');
}
$algos = [1 => \PASSWORD_BCRYPT, '2y' => \PASSWORD_BCRYPT];
if (\defined('PASSWORD_ARGON2I')) {
$this->algo = $algos[2] = $algos['argon2i'] = (string) \PASSWORD_ARGON2I;
}
if (\defined('PASSWORD_ARGON2ID')) {
$this->algo = $algos[3] = $algos['argon2id'] = (string) \PASSWORD_ARGON2ID;
}
if (null !== $algo) {
$this->algo = $algos[$algo] ?? $algo;
}
$this->options = [
'cost' => $cost,
'time_cost' => $opsLimit,
'memory_cost' => $memLimit >> 10,
'threads' => 1,
];
}
public function hash(string $plainPassword): string
{
if ($this->isPasswordTooLong($plainPassword) || ((string) \PASSWORD_BCRYPT === $this->algo && 72 < \strlen($plainPassword))) {
throw new InvalidPasswordException();
}
return password_hash($plainPassword, $this->algo, $this->options);
}
public function verify(string $hashedPassword, string $plainPassword): bool
{
if ('' === $plainPassword || $this->isPasswordTooLong($plainPassword)) {
return false;
}
if (0 !== strpos($hashedPassword, '$argon')) {
// BCrypt encodes only the first 72 chars
return (72 >= \strlen($plainPassword) || 0 !== strpos($hashedPassword, '$2')) && password_verify($plainPassword, $hashedPassword);
}
if (\extension_loaded('sodium') && version_compare(\SODIUM_LIBRARY_VERSION, '1.0.14', '>=')) {
return sodium_crypto_pwhash_str_verify($hashedPassword, $plainPassword);
}
if (\extension_loaded('libsodium') && version_compare(phpversion('libsodium'), '1.0.14', '>=')) {
return \Sodium\crypto_pwhash_str_verify($hashedPassword, $plainPassword);
}
return password_verify($plainPassword, $hashedPassword);
}
/**
* {@inheritdoc}
*/
public function needsRehash(string $hashedPassword): bool
{
return password_needs_rehash($hashedPassword, $this->algo, $this->options);
}
}

View File

@ -0,0 +1,26 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Hasher;
/**
* @author Christophe Coevoet <stof@notk.org>
*/
interface PasswordHasherAwareInterface
{
/**
* Gets the name of the password hasher used to hash the password.
*
* If the method returns null, the standard way to retrieve the password hasher
* will be used instead.
*/
public function getPasswordHasherName(): ?string;
}

View File

@ -0,0 +1,216 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Hasher;
use Symfony\Component\Security\Core\Encoder\EncoderAwareInterface;
use Symfony\Component\PasswordHasher\Exception\LogicException;
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
/**
* A generic hasher factory implementation.
*
* @author Nicolas Grekas <p@tchwork.com>
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class PasswordHasherFactory implements PasswordHasherFactoryInterface
{
private $passwordHashers;
public function __construct(array $passwordHashers)
{
$this->passwordHashers = $passwordHashers;
}
/**
* {@inheritdoc}
*/
public function getPasswordHasher($user): PasswordHasherInterface
{
$hasherKey = null;
if (($user instanceof PasswordHasherAwareInterface && null !== $hasherName = $user->getPasswordHasherName()) || ($user instanceof EncoderAwareInterface && null !== $hasherName = $user->getEncoderName())) {
if (!\array_key_exists($hasherName, $this->passwordHashers)) {
throw new \RuntimeException(sprintf('The password hasher "%s" was not configured.', $hasherName));
}
$hasherKey = $hasherName;
} else {
foreach ($this->passwordHashers as $class => $hasher) {
if ((\is_object($user) && $user instanceof $class) || (!\is_object($user) && (is_subclass_of($user, $class) || $user == $class))) {
$hasherKey = $class;
break;
}
}
}
if (null === $hasherKey) {
throw new \RuntimeException(sprintf('No password hasher has been configured for account "%s".', \is_object($user) ? get_debug_type($user) : $user));
}
if (!$this->passwordHashers[$hasherKey] instanceof PasswordHasherInterface) {
$this->passwordHashers[$hasherKey] = $this->createHasher($this->passwordHashers[$hasherKey]);
}
return $this->passwordHashers[$hasherKey];
}
/**
* Creates the actual hasher instance.
*
* @throws \InvalidArgumentException
*/
private function createHasher(array $config, bool $isExtra = false): PasswordHasherInterface
{
if (isset($config['algorithm'])) {
$rawConfig = $config;
$config = $this->getHasherConfigFromAlgorithm($config);
}
if (!isset($config['class'])) {
throw new \InvalidArgumentException('"class" must be set in '.json_encode($config));
}
if (!isset($config['arguments'])) {
throw new \InvalidArgumentException('"arguments" must be set in '.json_encode($config));
}
$hasher = new $config['class'](...$config['arguments']);
if ($isExtra || !\in_array($config['class'], [NativePasswordHasher::class, SodiumPasswordHasher::class], true)) {
return $hasher;
}
if ($rawConfig ?? null) {
$extrapasswordHashers = array_map(function (string $algo) use ($rawConfig): PasswordHasherInterface {
$rawConfig['algorithm'] = $algo;
return $this->createHasher($rawConfig);
}, ['pbkdf2', $rawConfig['hash_algorithm'] ?? 'sha512']);
} else {
$extrapasswordHashers = [new Pbkdf2PasswordHasher(), new MessageDigestPasswordHasher()];
}
return new MigratingPasswordHasher($hasher, ...$extrapasswordHashers);
}
private function getHasherConfigFromAlgorithm(array $config): array
{
if ('auto' === $config['algorithm']) {
$hasherChain = [];
// "plaintext" is not listed as any leaked hashes could then be used to authenticate directly
foreach ([SodiumPasswordHasher::isSupported() ? 'sodium' : 'native', 'pbkdf2', $config['hash_algorithm']] as $algo) {
$config['algorithm'] = $algo;
$hasherChain[] = $this->createHasher($config, true);
}
return [
'class' => MigratingPasswordHasher::class,
'arguments' => $hasherChain,
];
}
if ($frompasswordHashers = ($config['migrate_from'] ?? false)) {
unset($config['migrate_from']);
$hasherChain = [$this->createHasher($config, true)];
foreach ($frompasswordHashers as $name) {
if ($hasher = $this->passwordHashers[$name] ?? false) {
$hasher = $hasher instanceof PasswordHasherInterface ? $hasher : $this->createHasher($hasher, true);
} else {
$hasher = $this->createHasher(['algorithm' => $name], true);
}
$hasherChain[] = $hasher;
}
return [
'class' => MigratingPasswordHasher::class,
'arguments' => $hasherChain,
];
}
switch ($config['algorithm']) {
case 'plaintext':
return [
'class' => PlaintextPasswordHasher::class,
'arguments' => [$config['ignore_case'] ?? false],
];
case 'pbkdf2':
return [
'class' => Pbkdf2PasswordHasher::class,
'arguments' => [
$config['hash_algorithm'] ?? 'sha512',
$config['encode_as_base64'] ?? true,
$config['iterations'] ?? 1000,
$config['key_length'] ?? 40,
],
];
case 'bcrypt':
$config['algorithm'] = 'native';
$config['native_algorithm'] = \PASSWORD_BCRYPT;
return $this->getHasherConfigFromAlgorithm($config);
case 'native':
return [
'class' => NativePasswordHasher::class,
'arguments' => [
$config['time_cost'] ?? null,
(($config['memory_cost'] ?? 0) << 10) ?: null,
$config['cost'] ?? null,
] + (isset($config['native_algorithm']) ? [3 => $config['native_algorithm']] : []),
];
case 'sodium':
return [
'class' => SodiumPasswordHasher::class,
'arguments' => [
$config['time_cost'] ?? null,
(($config['memory_cost'] ?? 0) << 10) ?: null,
],
];
case 'argon2i':
if (SodiumPasswordHasher::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) {
$config['algorithm'] = 'sodium';
} elseif (\defined('PASSWORD_ARGON2I')) {
$config['algorithm'] = 'native';
$config['native_algorithm'] = \PASSWORD_ARGON2I;
} else {
throw new LogicException(sprintf('Algorithm "argon2i" is not available. Either use %s"auto" or upgrade to PHP 7.2+ instead.', \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13') ? '"argon2id", ' : ''));
}
return $this->getHasherConfigFromAlgorithm($config);
case 'argon2id':
if (($hasSodium = SodiumPasswordHasher::isSupported()) && \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) {
$config['algorithm'] = 'sodium';
} elseif (\defined('PASSWORD_ARGON2ID')) {
$config['algorithm'] = 'native';
$config['native_algorithm'] = \PASSWORD_ARGON2ID;
} else {
throw new LogicException(sprintf('Algorithm "argon2id" is not available. Either use %s"auto", upgrade to PHP 7.3+ or use libsodium 1.0.15+ instead.', \defined('PASSWORD_ARGON2I') || $hasSodium ? '"argon2i", ' : ''));
}
return $this->getHasherConfigFromAlgorithm($config);
}
return [
'class' => MessageDigestPasswordHasher::class,
'arguments' => [
$config['algorithm'],
$config['encode_as_base64'] ?? true,
$config['iterations'] ?? 5000,
],
];
}
}

View File

@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Hasher;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
/**
* PasswordHasherFactoryInterface to support different password hashers for different user accounts.
*
* @author Robin Chalas <robin.chalas@gmail.com>
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
interface PasswordHasherFactoryInterface
{
/**
* Returns the password hasher to use for the given user.
*
* @param UserInterface|string $user A UserInterface instance or a class name
*
* @throws \RuntimeException When no password hasher could be found for the user
*/
public function getPasswordHasher($user): PasswordHasherInterface;
}

View File

@ -0,0 +1,90 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Hasher;
use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException;
use Symfony\Component\PasswordHasher\Exception\LogicException;
use Symfony\Component\PasswordHasher\LegacyPasswordHasherInterface;
/**
* Pbkdf2PasswordHasher uses the PBKDF2 (Password-Based Key Derivation Function 2).
*
* Providing a high level of Cryptographic security,
* PBKDF2 is recommended by the National Institute of Standards and Technology (NIST).
*
* But also warrants a warning, using PBKDF2 (with a high number of iterations) slows down the process.
* PBKDF2 should be used with caution and care.
*
* @author Sebastiaan Stok <s.stok@rollerscapes.net>
* @author Andrew Johnson
* @author Fabien Potencier <fabien@symfony.com>
*/
final class Pbkdf2PasswordHasher implements LegacyPasswordHasherInterface
{
use CheckPasswordLengthTrait;
private $algorithm;
private $encodeHashAsBase64;
private $iterations = 1;
private $length;
private $encodedLength = -1;
/**
* @param string $algorithm The digest algorithm to use
* @param bool $encodeHashAsBase64 Whether to base64 encode the password hash
* @param int $iterations The number of iterations to use to stretch the password hash
* @param int $length Length of derived key to create
*/
public function __construct(string $algorithm = 'sha512', bool $encodeHashAsBase64 = true, int $iterations = 1000, int $length = 40)
{
$this->algorithm = $algorithm;
$this->encodeHashAsBase64 = $encodeHashAsBase64;
$this->length = $length;
try {
$this->encodedLength = \strlen($this->hash('', 'salt'));
} catch (\LogicException $e) {
// ignore unsupported algorithm
}
$this->iterations = $iterations;
}
public function hash(string $plainPassword, ?string $salt = null): string
{
if ($this->isPasswordTooLong($plainPassword)) {
throw new InvalidPasswordException();
}
if (!\in_array($this->algorithm, hash_algos(), true)) {
throw new LogicException(sprintf('The algorithm "%s" is not supported.', $this->algorithm));
}
$digest = hash_pbkdf2($this->algorithm, $plainPassword, $salt, $this->iterations, $this->length, true);
return $this->encodeHashAsBase64 ? base64_encode($digest) : bin2hex($digest);
}
public function verify(string $hashedPassword, string $plainPassword, ?string $salt = null): bool
{
if (\strlen($hashedPassword) !== $this->encodedLength || false !== strpos($hashedPassword, '$')) {
return false;
}
return !$this->isPasswordTooLong($plainPassword) && hash_equals($hashedPassword, $this->hash($plainPassword, $salt));
}
public function needsRehash(string $hashedPassword): bool
{
return false;
}
}

View File

@ -0,0 +1,82 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Hasher;
use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException;
use Symfony\Component\PasswordHasher\LegacyPasswordHasherInterface;
/**
* PlaintextPasswordHasher does not do any hashing but is useful in testing environments.
*
* As this hasher is not cryptographically secure, usage of it in production environments is discouraged.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class PlaintextPasswordHasher implements LegacyPasswordHasherInterface
{
use CheckPasswordLengthTrait;
private $ignorePasswordCase;
/**
* @param bool $ignorePasswordCase Compare password case-insensitive
*/
public function __construct(bool $ignorePasswordCase = false)
{
$this->ignorePasswordCase = $ignorePasswordCase;
}
/**
* {@inheritdoc}
*/
public function hash(string $plainPassword, ?string $salt = null): string
{
if ($this->isPasswordTooLong($plainPassword)) {
throw new InvalidPasswordException();
}
return $this->mergePasswordAndSalt($plainPassword, $salt);
}
public function verify(string $hashedPassword, string $plainPassword, ?string $salt = null): bool
{
if ($this->isPasswordTooLong($plainPassword)) {
return false;
}
$pass2 = $this->mergePasswordAndSalt($plainPassword, $salt);
if (!$this->ignorePasswordCase) {
return hash_equals($hashedPassword, $pass2);
}
return hash_equals(strtolower($hashedPassword), strtolower($pass2));
}
public function needsRehash(string $hashedPassword): bool
{
return false;
}
private function mergePasswordAndSalt(string $password, ?string $salt): string
{
if (empty($salt)) {
return $password;
}
if (false !== strrpos($salt, '{') || false !== strrpos($salt, '}')) {
throw new \InvalidArgumentException('Cannot use { or } in salt.');
}
return $password.'{'.$salt.'}';
}
}

View File

@ -0,0 +1,110 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Hasher;
use Symfony\Component\PasswordHasher\Exception\LogicException;
use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException;
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
/**
* Hashes passwords using libsodium.
*
* @author Robin Chalas <robin.chalas@gmail.com>
* @author Zan Baldwin <hello@zanbaldwin.com>
* @author Dominik Müller <dominik.mueller@jkweb.ch>
*/
final class SodiumPasswordHasher implements PasswordHasherInterface
{
use CheckPasswordLengthTrait;
private $opsLimit;
private $memLimit;
public function __construct(int $opsLimit = null, int $memLimit = null)
{
if (!self::isSupported()) {
throw new LogicException('Libsodium is not available. You should either install the sodium extension or use a different password hasher.');
}
$this->opsLimit = $opsLimit ?? max(4, \defined('SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE') ? \SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE : 4);
$this->memLimit = $memLimit ?? max(64 * 1024 * 1024, \defined('SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE') ? \SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE : 64 * 1024 * 1024);
if (3 > $this->opsLimit) {
throw new \InvalidArgumentException('$opsLimit must be 3 or greater.');
}
if (10 * 1024 > $this->memLimit) {
throw new \InvalidArgumentException('$memLimit must be 10k or greater.');
}
}
public static function isSupported(): bool
{
return version_compare(\extension_loaded('sodium') ? \SODIUM_LIBRARY_VERSION : phpversion('libsodium'), '1.0.14', '>=');
}
public function hash(string $plainPassword): string
{
if ($this->isPasswordTooLong($plainPassword)) {
throw new InvalidPasswordException();
}
if (\function_exists('sodium_crypto_pwhash_str')) {
return sodium_crypto_pwhash_str($plainPassword, $this->opsLimit, $this->memLimit);
}
if (\extension_loaded('libsodium')) {
return \Sodium\crypto_pwhash_str($plainPassword, $this->opsLimit, $this->memLimit);
}
throw new LogicException('Libsodium is not available. You should either install the sodium extension or use a different password hasher.');
}
public function verify(string $hashedPassword, string $plainPassword): bool
{
if ('' === $plainPassword) {
return false;
}
if ($this->isPasswordTooLong($plainPassword)) {
return false;
}
if (0 !== strpos($hashedPassword, '$argon')) {
// Accept validating non-argon passwords for seamless migrations
return (72 >= \strlen($plainPassword) || 0 !== strpos($hashedPassword, '$2')) && password_verify($plainPassword, $hashedPassword);
}
if (\function_exists('sodium_crypto_pwhash_str_verify')) {
return sodium_crypto_pwhash_str_verify($hashedPassword, $plainPassword);
}
if (\extension_loaded('libsodium')) {
return \Sodium\crypto_pwhash_str_verify($hashedPassword, $plainPassword);
}
return false;
}
public function needsRehash(string $hashedPassword): bool
{
if (\function_exists('sodium_crypto_pwhash_str_needs_rehash')) {
return sodium_crypto_pwhash_str_needs_rehash($hashedPassword, $this->opsLimit, $this->memLimit);
}
if (\extension_loaded('libsodium')) {
return \Sodium\crypto_pwhash_str_needs_rehash($hashedPassword, $this->opsLimit, $this->memLimit);
}
throw new LogicException('Libsodium is not available. You should either install the sodium extension or use a different password hasher.');
}
}

View File

@ -0,0 +1,64 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Hasher;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* Hashes passwords based on the user and the PasswordHasherFactory.
*
* @author Ariel Ferrandini <arielferrandini@gmail.com>
*/
class UserPasswordHasher implements UserPasswordHasherInterface
{
private $hasherFactory;
public function __construct(PasswordHasherFactoryInterface $hasherFactory)
{
$this->hasherFactory = $hasherFactory;
}
public function hashPassword(UserInterface $user, string $plainPassword): string
{
$hasher = $this->hasherFactory->getPasswordHasher($user);
return $hasher->hash($plainPassword, $user->getSalt());
}
/**
* {@inheritdoc}
*/
public function isPasswordValid(UserInterface $user, string $plainPassword): bool
{
if (null === $user->getPassword()) {
return false;
}
$hasher = $this->hasherFactory->getPasswordHasher($user);
return $hasher->verify($user->getPassword(), $plainPassword, $user->getSalt());
}
/**
* {@inheritdoc}
*/
public function needsRehash(UserInterface $user): bool
{
if (null === $user->getPassword()) {
return false;
}
$hasher = $this->hasherFactory->getPasswordHasher($user);
return $hasher->needsRehash($user->getPassword());
}
}

View File

@ -0,0 +1,37 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Hasher;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* Interface for the user password hasher service.
*
* @author Ariel Ferrandini <arielferrandini@gmail.com>
*/
interface UserPasswordHasherInterface
{
/**
* Hashes the plain password for the given user.
*/
public function hashPassword(UserInterface $user, string $plainPassword): string;
/**
* Checks if the plaintext password matches the user's password.
*/
public function isPasswordValid(UserInterface $user, string $plainPassword): bool;
/**
* Checks if a password hash would benefit from rehashing.
*/
public function needsRehash(UserInterface $user): bool;
}

View File

@ -0,0 +1,19 @@
Copyright (c) 2004-2021 Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,38 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher;
use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException;
/**
* Provides password hashing and verification capabilities for "legacy" hashers that require external salts.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Nicolas Grekas <p@tchwork.com>
* @author Robin Chalas <robin.chalas@gmail.com>
*/
interface LegacyPasswordHasherInterface extends PasswordHasherInterface
{
/**
* Hashes a plain password.
*
* @return string The hashed password
*
* @throws InvalidPasswordException If the plain password is invalid, e.g. excessively long
*/
public function hash(string $plainPassword, ?string $salt = null): string;
/**
* Checks that a plain password and a salt match a password hash.
*/
public function verify(string $hashedPassword, string $plainPassword, ?string $salt = null): bool;
}

View File

@ -0,0 +1,43 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher;
use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException;
/**
* Provides password hashing capabilities.
*
* @author Robin Chalas <robin.chalas@gmail.com>
* @author Fabien Potencier <fabien@symfony.com>
* @author Nicolas Grekas <p@tchwork.com>
*/
interface PasswordHasherInterface
{
public const MAX_PASSWORD_LENGTH = 4096;
/**
* Hashes a plain password.
*
* @throws InvalidPasswordException When the plain password is invalid, e.g. excessively long
*/
public function hash(string $plainPassword): string;
/**
* Verifies a plain password against a hash.
*/
public function verify(string $hashedPassword, string $plainPassword): bool;
/**
* Checks if a password hash would benefit from rehashing.
*/
public function needsRehash(string $hashedPassword): bool;
}

View File

@ -0,0 +1,40 @@
PasswordHasher Component
========================
The PasswordHasher component provides secure password hashing utilities.
Getting Started
---------------
```
$ composer require symfony/password-hasher
```
```php
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory;
// Configure different password hashers via the factory
$factory = new PasswordHasherFactory([
'common' => ['algorithm' => 'bcrypt'],
'memory-hard' => ['algorithm' => 'sodium'],
]);
// Retrieve the right password hasher by its name
$passwordHasher = $factory->getPasswordHasher('common');
// Hash a plain password
$hash = $passwordHasher->hash('plain'); // returns a bcrypt hash
// Verify that a given plain password matches the hash
$passwordHasher->verify($hash, 'wrong'); // returns false
$passwordHasher->verify($hash, 'plain'); // returns true (valid)
```
Resources
---------
* [Documentation](https://symfony.com/doc/current/password-hasher.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)

View File

@ -0,0 +1,362 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Tests\Command;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\SecurityBundle\Command\UserPasswordHasherCommand;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\Security\Core\User\User;
use Symfony\Component\PasswordHasher\Command\UserPasswordHashCommand;
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory;
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
use Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher;
use Symfony\Component\PasswordHasher\Hasher\Pbkdf2PasswordHasher;
use Symfony\Component\PasswordHasher\Hasher\SodiumPasswordHasher;
class UserPasswordHashCommandTest extends TestCase
{
/** @var CommandTester */
private $passwordHasherCommandTester;
public function testEncodePasswordEmptySalt()
{
$this->passwordHasherCommandTester->execute([
'password' => 'password',
'user-class' => 'Symfony\Component\Security\Core\User\User',
'--empty-salt' => true,
], ['decorated' => false]);
$this->assertStringContainsString(' Password hash password', $this->passwordHasherCommandTester->getDisplay());
}
public function testEncodeNoPasswordNoInteraction()
{
$statusCode = $this->passwordHasherCommandTester->execute([
], ['interactive' => false]);
$this->assertStringContainsString('[ERROR] The password must not be empty.', $this->passwordHasherCommandTester->getDisplay());
$this->assertEquals(1, $statusCode);
}
public function testEncodePasswordBcrypt()
{
$this->setupBcrypt();
$this->passwordHasherCommandTester->execute([
'password' => 'password',
'user-class' => 'Custom\Class\Bcrypt\User',
], ['interactive' => false]);
$output = $this->passwordHasherCommandTester->getDisplay();
$this->assertStringContainsString('Password hashing succeeded', $output);
$hasher = new NativePasswordHasher(null, null, 17, \PASSWORD_BCRYPT);
preg_match('# Password hash\s{1,}([\w+\/$.]+={0,2})\s+#', $output, $matches);
$hash = $matches[1];
$this->assertTrue($hasher->verify($hash, 'password', null));
}
public function testEncodePasswordArgon2i()
{
if (!($sodium = SodiumPasswordHasher::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) && !\defined('PASSWORD_ARGON2I')) {
$this->markTestSkipped('Argon2i algorithm not available.');
}
$this->setupArgon2i();
$this->passwordHasherCommandTester->execute([
'password' => 'password',
'user-class' => 'Custom\Class\Argon2i\User',
], ['interactive' => false]);
$output = $this->passwordHasherCommandTester->getDisplay();
$this->assertStringContainsString('Password hashing succeeded', $output);
$hasher = $sodium ? new SodiumPasswordHasher() : new NativePasswordHasher(null, null, null, \PASSWORD_ARGON2I);
preg_match('# Password hash\s+(\$argon2i?\$[\w,=\$+\/]+={0,2})\s+#', $output, $matches);
$hash = $matches[1];
$this->assertTrue($hasher->verify($hash, 'password', null));
}
public function testEncodePasswordArgon2id()
{
if (!($sodium = (SodiumPasswordHasher::isSupported() && \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13'))) && !\defined('PASSWORD_ARGON2ID')) {
$this->markTestSkipped('Argon2id algorithm not available.');
}
$this->setupArgon2id();
$this->passwordHasherCommandTester->execute([
'password' => 'password',
'user-class' => 'Custom\Class\Argon2id\User',
], ['interactive' => false]);
$output = $this->passwordHasherCommandTester->getDisplay();
$this->assertStringContainsString('Password hashing succeeded', $output);
$hasher = $sodium ? new SodiumPasswordHasher() : new NativePasswordHasher(null, null, null, \PASSWORD_ARGON2ID);
preg_match('# Password hash\s+(\$argon2id?\$[\w,=\$+\/]+={0,2})\s+#', $output, $matches);
$hash = $matches[1];
$this->assertTrue($hasher->verify($hash, 'password', null));
}
public function testEncodePasswordNative()
{
$this->passwordHasherCommandTester->execute([
'password' => 'password',
'user-class' => 'Custom\Class\Native\User',
], ['interactive' => false]);
$output = $this->passwordHasherCommandTester->getDisplay();
$this->assertStringContainsString('Password hashing succeeded', $output);
$hasher = new NativePasswordHasher();
preg_match('# Password hash\s{1,}([\w+\/$.,=]+={0,2})\s+#', $output, $matches);
$hash = $matches[1];
$this->assertTrue($hasher->verify($hash, 'password', null));
}
public function testEncodePasswordSodium()
{
if (!SodiumPasswordHasher::isSupported()) {
$this->markTestSkipped('Libsodium is not available.');
}
$this->setupSodium();
$this->passwordHasherCommandTester->execute([
'password' => 'password',
'user-class' => 'Custom\Class\Sodium\User',
], ['interactive' => false]);
$output = $this->passwordHasherCommandTester->getDisplay();
$this->assertStringContainsString('Password hashing succeeded', $output);
preg_match('# Password hash\s+(\$?\$[\w,=\$+\/]+={0,2})\s+#', $output, $matches);
$hash = $matches[1];
$this->assertTrue((new SodiumPasswordHasher())->verify($hash, 'password', null));
}
public function testEncodePasswordPbkdf2()
{
$this->passwordHasherCommandTester->execute([
'password' => 'password',
'user-class' => 'Custom\Class\Pbkdf2\User',
], ['interactive' => false]);
$output = $this->passwordHasherCommandTester->getDisplay();
$this->assertStringContainsString('Password hashing succeeded', $output);
$hasher = new Pbkdf2PasswordHasher('sha512', true, 1000);
preg_match('# Password hash\s{1,}([\w+\/]+={0,2})\s+#', $output, $matches);
$hash = $matches[1];
preg_match('# Generated salt\s{1,}([\w+\/]+={0,2})\s+#', $output, $matches);
$salt = $matches[1];
$this->assertTrue($hasher->verify($hash, 'password', $salt));
}
public function testEncodePasswordOutput()
{
$this->passwordHasherCommandTester->execute(
[
'password' => 'p@ssw0rd',
], ['interactive' => false]
);
$this->assertStringContainsString('Password hashing succeeded', $this->passwordHasherCommandTester->getDisplay());
$this->assertStringContainsString(' Password hash p@ssw0rd', $this->passwordHasherCommandTester->getDisplay());
$this->assertStringContainsString(' Generated salt ', $this->passwordHasherCommandTester->getDisplay());
}
public function testEncodePasswordEmptySaltOutput()
{
$this->passwordHasherCommandTester->execute([
'password' => 'p@ssw0rd',
'user-class' => 'Symfony\Component\Security\Core\User\User',
'--empty-salt' => true,
]);
$this->assertStringContainsString('Password hashing succeeded', $this->passwordHasherCommandTester->getDisplay());
$this->assertStringContainsString(' Password hash p@ssw0rd', $this->passwordHasherCommandTester->getDisplay());
$this->assertStringNotContainsString(' Generated salt ', $this->passwordHasherCommandTester->getDisplay());
}
public function testEncodePasswordNativeOutput()
{
$this->passwordHasherCommandTester->execute([
'password' => 'p@ssw0rd',
'user-class' => 'Custom\Class\Native\User',
], ['interactive' => false]);
$this->assertStringNotContainsString(' Generated salt ', $this->passwordHasherCommandTester->getDisplay());
}
public function testEncodePasswordArgon2iOutput()
{
if (!(SodiumPasswordHasher::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) && !\defined('PASSWORD_ARGON2I')) {
$this->markTestSkipped('Argon2i algorithm not available.');
}
$this->setupArgon2i();
$this->passwordHasherCommandTester->execute([
'password' => 'p@ssw0rd',
'user-class' => 'Custom\Class\Argon2i\User',
], ['interactive' => false]);
$this->assertStringNotContainsString(' Generated salt ', $this->passwordHasherCommandTester->getDisplay());
}
public function testEncodePasswordArgon2idOutput()
{
if (!(SodiumPasswordHasher::isSupported() && \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) && !\defined('PASSWORD_ARGON2ID')) {
$this->markTestSkipped('Argon2id algorithm not available.');
}
$this->setupArgon2id();
$this->passwordHasherCommandTester->execute([
'password' => 'p@ssw0rd',
'user-class' => 'Custom\Class\Argon2id\User',
], ['interactive' => false]);
$this->assertStringNotContainsString(' Generated salt ', $this->passwordHasherCommandTester->getDisplay());
}
public function testEncodePasswordSodiumOutput()
{
if (!SodiumPasswordHasher::isSupported()) {
$this->markTestSkipped('Libsodium is not available.');
}
$this->setupSodium();
$this->passwordHasherCommandTester->execute([
'password' => 'p@ssw0rd',
'user-class' => 'Custom\Class\Sodium\User',
], ['interactive' => false]);
$this->assertStringNotContainsString(' Generated salt ', $this->passwordHasherCommandTester->getDisplay());
}
public function testEncodePasswordNoConfigForGivenUserClass()
{
$this->expectException('\RuntimeException');
$this->expectExceptionMessage('No password hasher has been configured for account "Foo\Bar\User".');
$this->passwordHasherCommandTester->execute([
'password' => 'password',
'user-class' => 'Foo\Bar\User',
], ['interactive' => false]);
}
public function testEncodePasswordAsksNonProvidedUserClass()
{
$this->passwordHasherCommandTester->setInputs(['Custom\Class\Pbkdf2\User', "\n"]);
$this->passwordHasherCommandTester->execute([
'password' => 'password',
], ['decorated' => false]);
$this->assertStringContainsString(<<<EOTXT
For which user class would you like to hash a password? [Custom\Class\Native\User]:
[0] Custom\Class\Native\User
[1] Custom\Class\Pbkdf2\User
[2] Custom\Class\Test\User
[3] Symfony\Component\Security\Core\User\User
EOTXT
, $this->passwordHasherCommandTester->getDisplay(true));
}
public function testNonInteractiveEncodePasswordUsesFirstUserClass()
{
$this->passwordHasherCommandTester->execute([
'password' => 'password',
], ['interactive' => false]);
$this->assertStringContainsString('Hasher used Symfony\Component\PasswordHasher\Hasher\PlaintextPasswordHasher', $this->passwordHasherCommandTester->getDisplay());
}
public function testThrowsExceptionOnNoConfiguredHashers()
{
$this->expectException('RuntimeException');
$this->expectExceptionMessage('There are no configured password hashers for the "security" extension.');
$tester = new CommandTester(new UserPasswordHashCommand($this->getMockBuilder(PasswordHasherFactoryInterface::class)->getMock(), []));
$tester->execute([
'password' => 'password',
], ['interactive' => false]);
}
protected function setUp(): void
{
putenv('COLUMNS='.(119 + \strlen(\PHP_EOL)));
$hasherFactory = new PasswordHasherFactory([
User::class => ['algorithm' => 'plaintext'],
'Custom\Class\Native\User' => ['algorithm' => 'native', 'cost' => 10],
'Custom\Class\Pbkdf2\User' => ['algorithm' => 'pbkdf2', 'hash_algorithm' => 'sha512', 'iterations' => 1000, 'encode_as_base64' => true],
'Custom\Class\Test\User' => ['algorithm' => 'test'],
]);
$this->passwordHasherCommandTester = new CommandTester(new UserPasswordHashCommand(
$hasherFactory,
[User::class, 'Custom\Class\Native\User', 'Custom\Class\Pbkdf2\User', 'Custom\Class\Test\User']
));
}
protected function tearDown(): void
{
$this->passwordHasherCommandTester = null;
}
private function setupArgon2i()
{
putenv('COLUMNS='.(119 + \strlen(\PHP_EOL)));
$hasherFactory = new PasswordHasherFactory([
'Custom\Class\Argon2i\User' => ['algorithm' => 'argon2i'],
]);
$this->passwordHasherCommandTester = new CommandTester(
new UserPasswordHashCommand($hasherFactory, ['Custom\Class\Argon2i\User'])
);
}
private function setupArgon2id()
{
putenv('COLUMNS='.(119 + \strlen(\PHP_EOL)));
$hasherFactory = new PasswordHasherFactory([
'Custom\Class\Argon2id\User' => ['algorithm' => 'argon2id'],
]);
$this->passwordHasherCommandTester = new CommandTester(
new UserPasswordHashCommand($hasherFactory, ['Custom\Class\Argon2id\User'])
);
}
private function setupBcrypt()
{
putenv('COLUMNS='.(119 + \strlen(\PHP_EOL)));
$hasherFactory = new PasswordHasherFactory([
'Custom\Class\Bcrypt\User' => ['algorithm' => 'bcrypt'],
]);
$this->passwordHasherCommandTester = new CommandTester(new UserPasswordHashCommand(
$hasherFactory,
[User::class, 'Custom\Class\Pbkdf2\User', 'Custom\Class\Test\User']
));
}
private function setupSodium()
{
putenv('COLUMNS='.(119 + \strlen(\PHP_EOL)));
$hasherFactory = new PasswordHasherFactory([
'Custom\Class\Sodium\User' => ['algorithm' => 'sodium'],
]);
$this->passwordHasherCommandTester = new CommandTester(
new UserPasswordHashCommand($hasherFactory, ['Custom\Class\Sodium\User'])
);
}
}

View File

@ -0,0 +1,60 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Tests\Hasher;
use PHPUnit\Framework\TestCase;
use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException;
use Symfony\Component\PasswordHasher\Hasher\MessageDigestPasswordHasher;
class MessageDigestPasswordHasherTest extends TestCase
{
public function testVerify()
{
$hasher = new MessageDigestPasswordHasher('sha256', false, 1);
$this->assertTrue($hasher->verify(hash('sha256', 'password'), 'password', ''));
}
public function testHash()
{
$hasher = new MessageDigestPasswordHasher('sha256', false, 1);
$this->assertSame(hash('sha256', 'password'), $hasher->hash('password', ''));
$hasher = new MessageDigestPasswordHasher('sha256', true, 1);
$this->assertSame(base64_encode(hash('sha256', 'password', true)), $hasher->hash('password', ''));
$hasher = new MessageDigestPasswordHasher('sha256', false, 2);
$this->assertSame(hash('sha256', hash('sha256', 'password', true).'password'), $hasher->hash('password', ''));
}
public function testHashAlgorithmDoesNotExist()
{
$this->expectException('LogicException');
$hasher = new MessageDigestPasswordHasher('foobar');
$hasher->hash('password', '');
}
public function testHashLength()
{
$this->expectException(InvalidPasswordException::class);
$hasher = new MessageDigestPasswordHasher();
$hasher->hash(str_repeat('a', 5000), 'salt');
}
public function testCheckPasswordLength()
{
$hasher = new MessageDigestPasswordHasher();
$this->assertFalse($hasher->verify('encoded', str_repeat('a', 5000), 'salt'));
}
}

View File

@ -0,0 +1,68 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Tests\Hasher;
use PHPUnit\Framework\TestCase;
use Symfony\Component\PasswordHasher\Hasher\MigratingPasswordHasher;
use Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher;
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
class MigratingPasswordHasherTest extends TestCase
{
public function testValidation()
{
$bestHasher = new NativePasswordHasher(4, 12000, 4);
$extraHasher = $this->createMock(PasswordHasherInterface::class);
$extraHasher->expects($this->never())->method('hash');
$extraHasher->expects($this->never())->method('verify');
$extraHasher->expects($this->never())->method('needsRehash');
$hasher = new MigratingPasswordHasher($bestHasher, $extraHasher);
$this->assertTrue($hasher->needsRehash('foo'));
$hash = $hasher->hash('foo', 'salt');
$this->assertFalse($hasher->needsRehash($hash));
$this->assertTrue($hasher->verify($hash, 'foo', 'salt'));
$this->assertFalse($hasher->verify($hash, 'bar', 'salt'));
}
public function testFallback()
{
$bestHasher = new NativePasswordHasher(4, 12000, 4);
$extraHasher1 = $this->createMock(PasswordHasherInterface::class);
$extraHasher1->expects($this->any())
->method('verify')
->with('abc', 'foo', 'salt')
->willReturn(true);
$hasher = new MigratingPasswordHasher($bestHasher, $extraHasher1);
$this->assertTrue($hasher->verify('abc', 'foo', 'salt'));
$extraHasher2 = $this->createMock(PasswordHasherInterface::class);
$extraHasher2->expects($this->any())
->method('verify')
->willReturn(false);
$hasher = new MigratingPasswordHasher($bestHasher, $extraHasher2);
$this->assertFalse($hasher->verify('abc', 'foo', 'salt'));
$hasher = new MigratingPasswordHasher($bestHasher, $extraHasher2, $extraHasher1);
$this->assertTrue($hasher->verify('abc', 'foo', 'salt'));
}
}

View File

@ -0,0 +1,105 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Tests\Hasher;
use PHPUnit\Framework\TestCase;
use Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher;
/**
* @author Elnur Abdurrakhimov <elnur@elnur.pro>
*/
class NativePasswordHasherTest extends TestCase
{
public function testCostBelowRange()
{
$this->expectException('InvalidArgumentException');
new NativePasswordHasher(null, null, 3);
}
public function testCostAboveRange()
{
$this->expectException('InvalidArgumentException');
new NativePasswordHasher(null, null, 32);
}
/**
* @dataProvider validRangeData
*/
public function testCostInRange($cost)
{
$this->assertInstanceOf(NativePasswordHasher::class, new NativePasswordHasher(null, null, $cost));
}
public function validRangeData()
{
$costs = range(4, 31);
array_walk($costs, function (&$cost) { $cost = [$cost]; });
return $costs;
}
public function testValidation()
{
$hasher = new NativePasswordHasher();
$result = $hasher->hash('password', null);
$this->assertTrue($hasher->verify($result, 'password', null));
$this->assertFalse($hasher->verify($result, 'anotherPassword', null));
$this->assertFalse($hasher->verify($result, '', null));
}
public function testNonArgonValidation()
{
$hasher = new NativePasswordHasher();
$this->assertTrue($hasher->verify('$5$abcdefgh$ZLdkj8mkc2XVSrPVjskDAgZPGjtj1VGVaa1aUkrMTU/', 'password', null));
$this->assertFalse($hasher->verify('$5$abcdefgh$ZLdkj8mkc2XVSrPVjskDAgZPGjtj1VGVaa1aUkrMTU/', 'anotherPassword', null));
$this->assertTrue($hasher->verify('$6$abcdefgh$yVfUwsw5T.JApa8POvClA1pQ5peiq97DUNyXCZN5IrF.BMSkiaLQ5kvpuEm/VQ1Tvh/KV2TcaWh8qinoW5dhA1', 'password', null));
$this->assertFalse($hasher->verify('$6$abcdefgh$yVfUwsw5T.JApa8POvClA1pQ5peiq97DUNyXCZN5IrF.BMSkiaLQ5kvpuEm/VQ1Tvh/KV2TcaWh8qinoW5dhA1', 'anotherPassword', null));
}
public function testConfiguredAlgorithm()
{
$hasher = new NativePasswordHasher(null, null, null, \PASSWORD_BCRYPT);
$result = $hasher->hash('password', null);
$this->assertTrue($hasher->verify($result, 'password', null));
$this->assertStringStartsWith('$2', $result);
}
public function testConfiguredAlgorithmWithLegacyConstValue()
{
$hasher = new NativePasswordHasher(null, null, null, '1');
$result = $hasher->hash('password', null);
$this->assertTrue($hasher->verify($result, 'password', null));
$this->assertStringStartsWith('$2', $result);
}
public function testCheckPasswordLength()
{
$hasher = new NativePasswordHasher(null, null, 4);
$result = password_hash(str_repeat('a', 72), \PASSWORD_BCRYPT, ['cost' => 4]);
$this->assertFalse($hasher->verify($result, str_repeat('a', 73), 'salt'));
$this->assertTrue($hasher->verify($result, str_repeat('a', 72), 'salt'));
}
public function testNeedsRehash()
{
$hasher = new NativePasswordHasher(4, 11000, 4);
$this->assertTrue($hasher->needsRehash('dummyhash'));
$hash = $hasher->hash('foo', 'salt');
$this->assertFalse($hasher->needsRehash($hash));
$hasher = new NativePasswordHasher(5, 11000, 5);
$this->assertTrue($hasher->needsRehash($hash));
}
}

View File

@ -0,0 +1,216 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Tests\Hasher;
use PHPUnit\Framework\TestCase;
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherAwareInterface;
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory;
use Symfony\Component\PasswordHasher\Hasher\MessageDigestPasswordHasher;
use Symfony\Component\PasswordHasher\Hasher\MigratingPasswordHasher;
use Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher;
use Symfony\Component\PasswordHasher\Hasher\SodiumPasswordHasher;
use Symfony\Component\Security\Core\User\User;
use Symfony\Component\Security\Core\User\UserInterface;
class PasswordHasherFactoryTest extends TestCase
{
public function testGetHasherWithMessageDigestHasher()
{
$factory = new PasswordHasherFactory([UserInterface::class => [
'class' => MessageDigestPasswordHasher::class,
'arguments' => ['sha512', true, 5],
]]);
$hasher = $factory->getPasswordHasher($this->createMock(UserInterface::class));
$expectedHasher = new MessageDigestPasswordHasher('sha512', true, 5);
$this->assertEquals($expectedHasher->hash('foo', 'moo'), $hasher->hash('foo', 'moo'));
}
public function testGetHasherWithService()
{
$factory = new PasswordHasherFactory([
UserInterface::class => new MessageDigestPasswordHasher('sha1'),
]);
$hasher = $factory->getPasswordHasher($this->createMock(UserInterface::class));
$expectedHasher = new MessageDigestPasswordHasher('sha1');
$this->assertEquals($expectedHasher->hash('foo', ''), $hasher->hash('foo', ''));
$hasher = $factory->getPasswordHasher(new User('user', 'pass'));
$expectedHasher = new MessageDigestPasswordHasher('sha1');
$this->assertEquals($expectedHasher->hash('foo', ''), $hasher->hash('foo', ''));
}
public function testGetHasherWithClassName()
{
$factory = new PasswordHasherFactory([
UserInterface::class => new MessageDigestPasswordHasher('sha1'),
]);
$hasher = $factory->getPasswordHasher(SomeChildUser::class);
$expectedHasher = new MessageDigestPasswordHasher('sha1');
$this->assertEquals($expectedHasher->hash('foo', ''), $hasher->hash('foo', ''));
}
public function testGetHasherConfiguredForConcreteClassWithService()
{
$factory = new PasswordHasherFactory([
'Symfony\Component\Security\Core\User\User' => new MessageDigestPasswordHasher('sha1'),
]);
$hasher = $factory->getPasswordHasher(new User('user', 'pass'));
$expectedHasher = new MessageDigestPasswordHasher('sha1');
$this->assertEquals($expectedHasher->hash('foo', ''), $hasher->hash('foo', ''));
}
public function testGetHasherConfiguredForConcreteClassWithClassName()
{
$factory = new PasswordHasherFactory([
'Symfony\Component\PasswordHasher\Tests\Hasher\SomeUser' => new MessageDigestPasswordHasher('sha1'),
]);
$hasher = $factory->getPasswordHasher(SomeChildUser::class);
$expectedHasher = new MessageDigestPasswordHasher('sha1');
$this->assertEquals($expectedHasher->hash('foo', ''), $hasher->hash('foo', ''));
}
public function testGetNamedHasherForHasherAware()
{
$factory = new PasswordHasherFactory([
HasherAwareUser::class => new MessageDigestPasswordHasher('sha256'),
'hasher_name' => new MessageDigestPasswordHasher('sha1'),
]);
$hasher = $factory->getPasswordHasher(new HasherAwareUser('user', 'pass'));
$expectedHasher = new MessageDigestPasswordHasher('sha1');
$this->assertEquals($expectedHasher->hash('foo', ''), $hasher->hash('foo', ''));
}
public function testGetNullNamedHasherForHasherAware()
{
$factory = new PasswordHasherFactory([
HasherAwareUser::class => new MessageDigestPasswordHasher('sha1'),
'hasher_name' => new MessageDigestPasswordHasher('sha256'),
]);
$user = new HasherAwareUser('mathilde', 'krogulec');
$user->hasherName = null;
$hasher = $factory->getPasswordHasher($user);
$expectedHasher = new MessageDigestPasswordHasher('sha1');
$this->assertEquals($expectedHasher->hash('foo', ''), $hasher->hash('foo', ''));
}
public function testGetInvalidNamedHasherForHasherAware()
{
$this->expectException('RuntimeException');
$factory = new PasswordHasherFactory([
HasherAwareUser::class => new MessageDigestPasswordHasher('sha1'),
'hasher_name' => new MessageDigestPasswordHasher('sha256'),
]);
$user = new HasherAwareUser('user', 'pass');
$user->hasherName = 'invalid_hasher_name';
$factory->getPasswordHasher($user);
}
public function testGetHasherForHasherAwareWithClassName()
{
$factory = new PasswordHasherFactory([
HasherAwareUser::class => new MessageDigestPasswordHasher('sha1'),
'hasher_name' => new MessageDigestPasswordHasher('sha256'),
]);
$hasher = $factory->getPasswordHasher(HasherAwareUser::class);
$expectedHasher = new MessageDigestPasswordHasher('sha1');
$this->assertEquals($expectedHasher->hash('foo', ''), $hasher->hash('foo', ''));
}
public function testMigrateFrom()
{
if (!SodiumPasswordHasher::isSupported()) {
$this->markTestSkipped('Sodium is not available');
}
$factory = new PasswordHasherFactory([
'digest_hasher' => $digest = new MessageDigestPasswordHasher('sha256'),
SomeUser::class => ['algorithm' => 'sodium', 'migrate_from' => ['bcrypt', 'digest_hasher']],
]);
$hasher = $factory->getPasswordHasher(SomeUser::class);
$this->assertInstanceOf(MigratingPasswordHasher::class, $hasher);
$this->assertTrue($hasher->verify((new SodiumPasswordHasher())->hash('foo', null), 'foo', null));
$this->assertTrue($hasher->verify((new NativePasswordHasher(null, null, null, \PASSWORD_BCRYPT))->hash('foo', null), 'foo', null));
$this->assertTrue($hasher->verify($digest->hash('foo', null), 'foo', null));
$this->assertStringStartsWith(\SODIUM_CRYPTO_PWHASH_STRPREFIX, $hasher->hash('foo', null));
}
public function testDefaultMigratingHashers()
{
$this->assertInstanceOf(
MigratingPasswordHasher::class,
(new PasswordHasherFactory([SomeUser::class => ['class' => NativePasswordHasher::class, 'arguments' => []]]))->getPasswordHasher(SomeUser::class)
);
$this->assertInstanceOf(
MigratingPasswordHasher::class,
(new PasswordHasherFactory([SomeUser::class => ['algorithm' => 'bcrypt', 'cost' => 11]]))->getPasswordHasher(SomeUser::class)
);
if (!SodiumPasswordHasher::isSupported()) {
return;
}
$this->assertInstanceOf(
MigratingPasswordHasher::class,
(new PasswordHasherFactory([SomeUser::class => ['class' => SodiumPasswordHasher::class, 'arguments' => []]]))->getPasswordHasher(SomeUser::class)
);
}
}
class SomeUser implements UserInterface
{
public function getRoles(): array
{
}
public function getPassword(): ?string
{
}
public function getSalt(): ?string
{
}
public function getUsername(): string
{
}
public function eraseCredentials()
{
}
}
class SomeChildUser extends SomeUser
{
}
class HasherAwareUser extends SomeUser implements PasswordHasherAwareInterface
{
public $hasherName = 'hasher_name';
public function getPasswordHasherName(): ?string
{
return $this->hasherName;
}
}

View File

@ -0,0 +1,60 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Tests\Hasher;
use PHPUnit\Framework\TestCase;
use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException;
use Symfony\Component\PasswordHasher\Hasher\Pbkdf2PasswordHasher;
class Pbkdf2PasswordHasherTest extends TestCase
{
public function testVerify()
{
$hasher = new Pbkdf2PasswordHasher('sha256', false, 1, 40);
$this->assertTrue($hasher->verify('c1232f10f62715fda06ae7c0a2037ca19b33cf103b727ba56d870c11f290a2ab106974c75607c8a3', 'password', ''));
}
public function testHash()
{
$hasher = new Pbkdf2PasswordHasher('sha256', false, 1, 40);
$this->assertSame('c1232f10f62715fda06ae7c0a2037ca19b33cf103b727ba56d870c11f290a2ab106974c75607c8a3', $hasher->hash('password', ''));
$hasher = new Pbkdf2PasswordHasher('sha256', true, 1, 40);
$this->assertSame('wSMvEPYnFf2gaufAogN8oZszzxA7cnulbYcMEfKQoqsQaXTHVgfIow==', $hasher->hash('password', ''));
$hasher = new Pbkdf2PasswordHasher('sha256', false, 2, 40);
$this->assertSame('8bc2f9167a81cdcfad1235cd9047f1136271c1f978fcfcb35e22dbeafa4634f6fd2214218ed63ebb', $hasher->hash('password', ''));
}
public function testHashAlgorithmDoesNotExist()
{
$this->expectException('LogicException');
$hasher = new Pbkdf2PasswordHasher('foobar');
$hasher->hash('password', '');
}
public function testHashLength()
{
$this->expectException(InvalidPasswordException::class);
$hasher = new Pbkdf2PasswordHasher('foobar');
$hasher->hash(str_repeat('a', 5000), 'salt');
}
public function testCheckPasswordLength()
{
$hasher = new Pbkdf2PasswordHasher('foobar');
$this->assertFalse($hasher->verify('encoded', str_repeat('a', 5000), 'salt'));
}
}

View File

@ -0,0 +1,56 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Tests\Hasher;
use PHPUnit\Framework\TestCase;
use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException;
use Symfony\Component\PasswordHasher\Hasher\PlaintextPasswordHasher;
class PlaintextPasswordHasherTest extends TestCase
{
public function testVerify()
{
$hasher = new PlaintextPasswordHasher();
$this->assertTrue($hasher->verify('foo', 'foo', ''));
$this->assertFalse($hasher->verify('bar', 'foo', ''));
$this->assertFalse($hasher->verify('FOO', 'foo', ''));
$hasher = new PlaintextPasswordHasher(true);
$this->assertTrue($hasher->verify('foo', 'foo', ''));
$this->assertFalse($hasher->verify('bar', 'foo', ''));
$this->assertTrue($hasher->verify('FOO', 'foo', ''));
}
public function testHash()
{
$hasher = new PlaintextPasswordHasher();
$this->assertSame('foo', $hasher->hash('foo', ''));
}
public function testHashLength()
{
$this->expectException(InvalidPasswordException::class);
$hasher = new PlaintextPasswordHasher();
$hasher->hash(str_repeat('a', 5000), 'salt');
}
public function testCheckPasswordLength()
{
$hasher = new PlaintextPasswordHasher();
$this->assertFalse($hasher->verify('encoded', str_repeat('a', 5000), 'salt'));
}
}

View File

@ -0,0 +1,85 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Tests\Hasher;
use PHPUnit\Framework\TestCase;
use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException;
use Symfony\Component\PasswordHasher\Hasher\SodiumPasswordHasher;
class SodiumPasswordHasherTest extends TestCase
{
protected function setUp(): void
{
if (!SodiumPasswordHasher::isSupported()) {
$this->markTestSkipped('Libsodium is not available.');
}
}
public function testValidation()
{
$hasher = new SodiumPasswordHasher();
$result = $hasher->hash('password', null);
$this->assertTrue($hasher->verify($result, 'password', null));
$this->assertFalse($hasher->verify($result, 'anotherPassword', null));
$this->assertFalse($hasher->verify($result, '', null));
}
public function testBCryptValidation()
{
$hasher = new SodiumPasswordHasher();
$this->assertTrue($hasher->verify('$2y$04$M8GDODMoGQLQRpkYCdoJh.lbiZPee3SZI32RcYK49XYTolDGwoRMm', 'abc', null));
}
public function testNonArgonValidation()
{
$hasher = new SodiumPasswordHasher();
$this->assertTrue($hasher->verify('$5$abcdefgh$ZLdkj8mkc2XVSrPVjskDAgZPGjtj1VGVaa1aUkrMTU/', 'password', null));
$this->assertFalse($hasher->verify('$5$abcdefgh$ZLdkj8mkc2XVSrPVjskDAgZPGjtj1VGVaa1aUkrMTU/', 'anotherPassword', null));
$this->assertTrue($hasher->verify('$6$abcdefgh$yVfUwsw5T.JApa8POvClA1pQ5peiq97DUNyXCZN5IrF.BMSkiaLQ5kvpuEm/VQ1Tvh/KV2TcaWh8qinoW5dhA1', 'password', null));
$this->assertFalse($hasher->verify('$6$abcdefgh$yVfUwsw5T.JApa8POvClA1pQ5peiq97DUNyXCZN5IrF.BMSkiaLQ5kvpuEm/VQ1Tvh/KV2TcaWh8qinoW5dhA1', 'anotherPassword', null));
}
public function testHashLength()
{
$this->expectException(InvalidPasswordException::class);
$hasher = new SodiumPasswordHasher();
$hasher->hash(str_repeat('a', 4097), 'salt');
}
public function testCheckPasswordLength()
{
$hasher = new SodiumPasswordHasher();
$result = $hasher->hash(str_repeat('a', 4096), null);
$this->assertFalse($hasher->verify($result, str_repeat('a', 4097), null));
$this->assertTrue($hasher->verify($result, str_repeat('a', 4096), null));
}
public function testUserProvidedSaltIsNotUsed()
{
$hasher = new SodiumPasswordHasher();
$result = $hasher->hash('password', 'salt');
$this->assertTrue($hasher->verify($result, 'password', 'anotherSalt'));
}
public function testNeedsRehash()
{
$hasher = new SodiumPasswordHasher(4, 11000);
$this->assertTrue($hasher->needsRehash('dummyhash'));
$hash = $hasher->hash('foo', 'salt');
$this->assertFalse($hasher->needsRehash($hash));
$hasher = new SodiumPasswordHasher(5, 11000);
$this->assertTrue($hasher->needsRehash($hash));
}
}

View File

@ -0,0 +1,95 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Tests\Hasher;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
use Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasher;
use Symfony\Component\Security\Core\User\User;
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
class UserPasswordHasherTest extends TestCase
{
public function testHash()
{
$userMock = $this->createMock('Symfony\Component\Security\Core\User\UserInterface');
$userMock->expects($this->any())
->method('getSalt')
->willReturn('userSalt');
$mockHasher = $this->createMock(PasswordHasherInterface::class);
$mockHasher->expects($this->any())
->method('hash')
->with($this->equalTo('plainPassword'), $this->equalTo('userSalt'))
->willReturn('hash');
$mockPasswordHasherFactory = $this->createMock(PasswordHasherFactoryInterface::class);
$mockPasswordHasherFactory->expects($this->any())
->method('getPasswordHasher')
->with($this->equalTo($userMock))
->willReturn($mockHasher);
$passwordHasher = new UserPasswordHasher($mockPasswordHasherFactory);
$encoded = $passwordHasher->hashPassword($userMock, 'plainPassword');
$this->assertEquals('hash', $encoded);
}
public function testVerify()
{
$userMock = $this->createMock(UserInterface::class);
$userMock->expects($this->any())
->method('getSalt')
->willReturn('userSalt');
$userMock->expects($this->any())
->method('getPassword')
->willReturn('hash');
$mockHasher = $this->createMock(PasswordHasherInterface::class);
$mockHasher->expects($this->any())
->method('verify')
->with($this->equalTo('hash'), $this->equalTo('plainPassword'), $this->equalTo('userSalt'))
->willReturn(true);
$mockPasswordHasherFactory = $this->createMock(PasswordHasherFactoryInterface::class);
$mockPasswordHasherFactory->expects($this->any())
->method('getPasswordHasher')
->with($this->equalTo($userMock))
->willReturn($mockHasher);
$passwordHasher = new UserPasswordHasher($mockPasswordHasherFactory);
$isValid = $passwordHasher->isPasswordValid($userMock, 'plainPassword');
$this->assertTrue($isValid);
}
public function testNeedsRehash()
{
$user = new User('username', null);
$hasher = new NativePasswordHasher(4, 20000, 4);
$mockPasswordHasherFactory = $this->createMock(PasswordHasherFactoryInterface::class);
$mockPasswordHasherFactory->expects($this->any())
->method('getPasswordHasher')
->with($user)
->will($this->onConsecutiveCalls($hasher, $hasher, new NativePasswordHasher(5, 20000, 5), $hasher));
$passwordHasher = new UserPasswordHasher($mockPasswordHasherFactory);
$user->setPassword($passwordHasher->hashPassword($user, 'foo', 'salt'));
$this->assertFalse($passwordHasher->needsRehash($user));
$this->assertTrue($passwordHasher->needsRehash($user));
$this->assertFalse($passwordHasher->needsRehash($user));
}
}

View File

@ -0,0 +1,33 @@
{
"name": "symfony/password-hasher",
"type": "library",
"description": "Provides password hashing utilities",
"keywords": ["password", "hashing"],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Robin Chalas",
"email": "robin.chalas@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=7.2.5",
"symfony/polyfill-php80": "^1.15"
},
"require-dev": {
"symfony/security-core": "^5.3",
"symfony/console": "^5"
},
"autoload": {
"psr-4": { "Symfony\\Component\\PasswordHasher\\": "" },
"exclude-from-classmap": [
"/Tests/"
]
},
"minimum-stability": "dev"
}

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/5.2/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="vendor/autoload.php"
failOnRisky="true"
failOnWarning="true"
>
<php>
<ini name="error_reporting" value="-1" />
</php>
<testsuites>
<testsuite name="Symfony Security Password Component Suite">
<directory>./Tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./</directory>
<exclude>
<directory>./Resources</directory>
<directory>./Tests</directory>
<directory>./vendor</directory>
</exclude>
</whitelist>
</filter>
</phpunit>

View File

@ -4,6 +4,7 @@ CHANGELOG
5.3
---
* Deprecate all classes in the `Core\Encoder\` sub-namespace, use the `PasswordHasher` component instead
* Deprecate the `SessionInterface $session` constructor argument of `SessionTokenStorage`, inject a `\Symfony\Component\HttpFoundation\RequestStack $requestStack` instead
* Deprecate the `session` service provided by the ServiceLocator injected in `UsageTrackingTokenStorage`, provide a `request_stack` service instead
* Deprecate using `SessionTokenStorage` outside a request context, it will throw a `SessionNotFoundException` in Symfony 6.0

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\Security\Core\Authentication;
use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException;
use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\AuthenticationEvents;
@ -18,6 +19,7 @@ use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent;
use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent;
use Symfony\Component\Security\Core\Exception\AccountStatusException;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\Exception\ProviderNotFoundException;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
@ -89,6 +91,8 @@ class AuthenticationProviderManager implements AuthenticationManagerInterface
break;
} catch (AuthenticationException $e) {
$lastException = $e;
} catch (InvalidPasswordException $e) {
$lastException = new BadCredentialsException('Bad credentials.', 0, $e);
}
}

Some files were not shown because too many files have changed in this diff Show More