Skip to content

Commit 41cbc66

Browse files
committed
[Security] Extract password hashing from security-core - using the right naming
1 parent 386555b commit 41cbc66

File tree

135 files changed

+4042
-489
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

135 files changed

+4042
-489
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
"symfony/monolog-bridge": "self.version",
8787
"symfony/notifier": "self.version",
8888
"symfony/options-resolver": "self.version",
89+
"symfony/password-hasher": "self.version",
8990
"symfony/process": "self.version",
9091
"symfony/property-access": "self.version",
9192
"symfony/property-info": "self.version",

src/Symfony/Bundle/FrameworkBundle/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
"symfony/messenger": "^5.2",
5252
"symfony/mime": "^4.4|^5.0",
5353
"symfony/process": "^4.4|^5.0",
54-
"symfony/security-bundle": "^5.2",
54+
"symfony/security-bundle": "^5.3",
5555
"symfony/serializer": "^5.2",
5656
"symfony/stopwatch": "^4.4|^5.0",
5757
"symfony/string": "^5.0",

src/Symfony/Bundle/SecurityBundle/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
CHANGELOG
22
=========
33

4+
5.3
5+
---
6+
7+
* Deprecate `UserPasswordEncoderCommand` class and the corresponding `user:encode-password` command,
8+
use `UserPasswordHashCommand` and `user:hash-password` instead.
9+
* Deprecate the `security.encoder_factory.generic` service, the `security.encoder_factory` and `Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface` aliases,
10+
use `security.password_hasher_factory` and `Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface` instead.
11+
* Deprecate the `security.user_password_encoder.generic` service, the `security.password_encoder` and the `Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface` aliases,
12+
use `security.user_password_hasher`, `security.password_hasher` and `Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface` instead.
13+
414
5.2.0
515
-----
616

src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Symfony\Component\Console\Output\OutputInterface;
2222
use Symfony\Component\Console\Question\Question;
2323
use Symfony\Component\Console\Style\SymfonyStyle;
24+
use Symfony\Component\PasswordHasher\Command\UserPasswordHashCommand;
2425
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
2526
use Symfony\Component\Security\Core\Encoder\SelfSaltingEncoderInterface;
2627

@@ -30,6 +31,8 @@
3031
* @author Sarah Khalil <mkhalil.sarah@gmail.com>
3132
*
3233
* @final
34+
*
35+
* @deprecated since Symfony 5.3, use {@link UserPasswordHashCommand} instead
3336
*/
3437
class UserPasswordEncoderCommand extends Command
3538
{
@@ -107,6 +110,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
107110
$io = new SymfonyStyle($input, $output);
108111
$errorIo = $output instanceof ConsoleOutputInterface ? new SymfonyStyle($input, $output->getErrorOutput()) : $io;
109112

113+
$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.');
114+
110115
$input->isInteractive() ? $errorIo->title('Symfony Password Encoder Utility') : $errorIo->newLine();
111116

112117
$password = $input->getArgument('password');

src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,23 @@ public function getConfigTreeBuilder()
6565
return $v;
6666
})
6767
->end()
68+
->beforeNormalization()
69+
->ifTrue(function ($v) {
70+
if ($v['encoders'] ?? false) {
71+
trigger_deprecation('symfony/security-bundle', '5.3', 'The child node "encoders" at path "security" is deprecated, use "password_hashers" instead.');
72+
73+
return true;
74+
}
75+
76+
return $v['password_hashers'] ?? false;
77+
})
78+
->then(function ($v) {
79+
$v['password_hashers'] = array_merge($v['password_hashers'] ?? [], $v['encoders'] ?? []);
80+
$v['encoders'] = $v['password_hashers'];
81+
82+
return $v;
83+
})
84+
->end()
6885
->children()
6986
->scalarNode('access_denied_url')->defaultNull()->example('/foo/error403')->end()
7087
->enumNode('session_fixation_strategy')
@@ -94,6 +111,7 @@ public function getConfigTreeBuilder()
94111
;
95112

96113
$this->addEncodersSection($rootNode);
114+
$this->addPasswordHashersSection($rootNode);
97115
$this->addProvidersSection($rootNode);
98116
$this->addFirewallsSection($rootNode, $this->factories);
99117
$this->addAccessControlSection($rootNode);
@@ -401,6 +419,57 @@ private function addEncodersSection(ArrayNodeDefinition $rootNode)
401419
;
402420
}
403421

422+
private function addPasswordHashersSection(ArrayNodeDefinition $rootNode)
423+
{
424+
$rootNode
425+
->fixXmlConfig('password_hasher')
426+
->children()
427+
->arrayNode('password_hashers')
428+
->example([
429+
'App\Entity\User1' => 'auto',
430+
'App\Entity\User2' => [
431+
'algorithm' => 'auto',
432+
'time_cost' => 8,
433+
'cost' => 13,
434+
],
435+
])
436+
->requiresAtLeastOneElement()
437+
->useAttributeAsKey('class')
438+
->prototype('array')
439+
->canBeUnset()
440+
->performNoDeepMerging()
441+
->beforeNormalization()->ifString()->then(function ($v) { return ['algorithm' => $v]; })->end()
442+
->children()
443+
->scalarNode('algorithm')
444+
->cannotBeEmpty()
445+
->validate()
446+
->ifTrue(function ($v) { return !\is_string($v); })
447+
->thenInvalid('You must provide a string value.')
448+
->end()
449+
->end()
450+
->arrayNode('migrate_from')
451+
->prototype('scalar')->end()
452+
->beforeNormalization()->castToArray()->end()
453+
->end()
454+
->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()
455+
->scalarNode('key_length')->defaultValue(40)->end()
456+
->booleanNode('ignore_case')->defaultFalse()->end()
457+
->booleanNode('encode_as_base64')->defaultTrue()->end()
458+
->scalarNode('iterations')->defaultValue(5000)->end()
459+
->integerNode('cost')
460+
->min(4)
461+
->max(31)
462+
->defaultNull()
463+
->end()
464+
->scalarNode('memory_cost')->defaultNull()->end()
465+
->scalarNode('time_cost')->defaultNull()->end()
466+
->scalarNode('id')->end()
467+
->end()
468+
->end()
469+
->end()
470+
->end();
471+
}
472+
404473
private function getAccessDecisionStrategies()
405474
{
406475
$strategies = [

src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php

Lines changed: 130 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
use Symfony\Component\DependencyInjection\Reference;
3333
use Symfony\Component\EventDispatcher\EventDispatcher;
3434
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
35+
use Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher;
36+
use Symfony\Component\PasswordHasher\Hasher\Pbkdf2PasswordHasher;
37+
use Symfony\Component\PasswordHasher\Hasher\PlaintextPasswordHasher;
38+
use Symfony\Component\PasswordHasher\Hasher\SodiumPasswordHasher;
3539
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
3640
use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder;
3741
use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder;
@@ -105,6 +109,7 @@ public function load(array $configs, ContainerBuilder $container)
105109
$loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/Resources/config'));
106110

107111
$loader->load('security.php');
112+
$loader->load('password_hasher.php');
108113
$loader->load('security_listeners.php');
109114
$loader->load('security_rememberme.php');
110115

@@ -166,13 +171,22 @@ public function load(array $configs, ContainerBuilder $container)
166171
$container->getDefinition('security.authentication.guard_handler')
167172
->replaceArgument(2, $this->statelessFirewallKeys);
168173

174+
// @deprecated since Symfony 5.3
169175
if ($config['encoders']) {
170176
$this->createEncoders($config['encoders'], $container);
171177
}
172178

179+
if ($config['password_hashers']) {
180+
$this->createHashers($config['password_hashers'], $container);
181+
}
182+
173183
if (class_exists(Application::class)) {
174184
$loader->load('console.php');
185+
186+
// @deprecated since Symfony 5.3
175187
$container->getDefinition('security.command.user_password_encoder')->replaceArgument(1, array_keys($config['encoders']));
188+
189+
$container->getDefinition('security.command.user_password_hash')->replaceArgument(1, array_keys($config['password_hashers']));
176190
}
177191

178192
$container->registerForAutoconfiguration(VoterInterface::class)
@@ -689,7 +703,7 @@ private function createEncoder(array $config)
689703

690704
// Argon2i encoder
691705
if ('argon2i' === $config['algorithm']) {
692-
if (SodiumPasswordEncoder::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) {
706+
if (SodiumPasswordHasher::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) {
693707
$config['algorithm'] = 'sodium';
694708
} elseif (\defined('PASSWORD_ARGON2I')) {
695709
$config['algorithm'] = 'native';
@@ -702,7 +716,7 @@ private function createEncoder(array $config)
702716
}
703717

704718
if ('argon2id' === $config['algorithm']) {
705-
if (($hasSodium = SodiumPasswordEncoder::isSupported()) && \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) {
719+
if (($hasSodium = SodiumPasswordHasher::isSupported()) && \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) {
706720
$config['algorithm'] = 'sodium';
707721
} elseif (\defined('PASSWORD_ARGON2ID')) {
708722
$config['algorithm'] = 'native';
@@ -717,6 +731,117 @@ private function createEncoder(array $config)
717731
if ('native' === $config['algorithm']) {
718732
return [
719733
'class' => NativePasswordEncoder::class,
734+
'arguments' => [
735+
$config['time_cost'],
736+
(($config['memory_cost'] ?? 0) << 10) ?: null,
737+
$config['cost'],
738+
] + (isset($config['native_algorithm']) ? [3 => $config['native_algorithm']] : []),
739+
];
740+
}
741+
742+
if ('sodium' === $config['algorithm']) {
743+
if (!SodiumPasswordHasher::isSupported()) {
744+
throw new InvalidConfigurationException('Libsodium is not available. Install the sodium extension or use "auto" instead.');
745+
}
746+
747+
return [
748+
'class' => SodiumPasswordEncoder::class,
749+
'arguments' => [
750+
$config['time_cost'],
751+
(($config['memory_cost'] ?? 0) << 10) ?: null,
752+
],
753+
];
754+
}
755+
756+
// run-time configured encoder
757+
return $config;
758+
}
759+
760+
private function createHashers(array $hashers, ContainerBuilder $container)
761+
{
762+
$hasherMap = [];
763+
foreach ($hashers as $class => $hasher) {
764+
$hasherMap[$class] = $this->createHasher($hasher);
765+
}
766+
767+
$container
768+
->getDefinition('security.password_hasher_factory')
769+
->setArguments([$hasherMap])
770+
;
771+
}
772+
773+
private function createHasher(array $config)
774+
{
775+
// a custom hasher service
776+
if (isset($config['id'])) {
777+
return new Reference($config['id']);
778+
}
779+
780+
if ($config['migrate_from'] ?? false) {
781+
return $config;
782+
}
783+
784+
// plaintext hasher
785+
if ('plaintext' === $config['algorithm']) {
786+
$arguments = [$config['ignore_case']];
787+
788+
return [
789+
'class' => PlaintextPasswordHasher::class,
790+
'arguments' => $arguments,
791+
];
792+
}
793+
794+
// pbkdf2 hasher
795+
if ('pbkdf2' === $config['algorithm']) {
796+
return [
797+
'class' => Pbkdf2PasswordHasher::class,
798+
'arguments' => [
799+
$config['hash_algorithm'],
800+
$config['encode_as_base64'],
801+
$config['iterations'],
802+
$config['key_length'],
803+
],
804+
];
805+
}
806+
807+
// bcrypt hasher
808+
if ('bcrypt' === $config['algorithm']) {
809+
$config['algorithm'] = 'native';
810+
$config['native_algorithm'] = \PASSWORD_BCRYPT;
811+
812+
return $this->createHasher($config);
813+
}
814+
815+
// Argon2i hasher
816+
if ('argon2i' === $config['algorithm']) {
817+
if (SodiumPasswordHasher::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) {
818+
$config['algorithm'] = 'sodium';
819+
} elseif (\defined('PASSWORD_ARGON2I')) {
820+
$config['algorithm'] = 'native';
821+
$config['native_algorithm'] = \PASSWORD_ARGON2I;
822+
} else {
823+
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'));
824+
}
825+
826+
return $this->createHasher($config);
827+
}
828+
829+
if ('argon2id' === $config['algorithm']) {
830+
if (($hasSodium = SodiumPasswordHasher::isSupported()) && \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) {
831+
$config['algorithm'] = 'sodium';
832+
} elseif (\defined('PASSWORD_ARGON2ID')) {
833+
$config['algorithm'] = 'native';
834+
$config['native_algorithm'] = \PASSWORD_ARGON2ID;
835+
} else {
836+
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'));
837+
}
838+
839+
return $this->createHasher($config);
840+
}
841+
842+
if ('native' === $config['algorithm']) {
843+
return [
844+
'class' => NativePasswordHasher::class,
720845
'arguments' => [
721846
$config['time_cost'],
722847
(($config['memory_cost'] ?? 0) << 10) ?: null,
@@ -726,20 +851,20 @@ private function createEncoder(array $config)
726851
}
727852

728853
if ('sodium' === $config['algorithm']) {
729-
if (!SodiumPasswordEncoder::isSupported()) {
854+
if (!SodiumPasswordHasher::isSupported()) {
730855
throw new InvalidConfigurationException('Libsodium is not available. Install the sodium extension or use "auto" instead.');
731856
}
732857

733858
return [
734-
'class' => SodiumPasswordEncoder::class,
859+
'class' => SodiumPasswordHasher::class,
735860
'arguments' => [
736861
$config['time_cost'],
737862
(($config['memory_cost'] ?? 0) << 10) ?: null,
738863
],
739864
];
740865
}
741866

742-
// run-time configured encoder
867+
// run-time configured hasher
743868
return $config;
744869
}
745870

src/Symfony/Bundle/SecurityBundle/Resources/config/console.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
1313

1414
use Symfony\Bundle\SecurityBundle\Command\UserPasswordEncoderCommand;
15+
use Symfony\Component\PasswordHasher\Command\UserPasswordHashCommand;
1516

1617
return static function (ContainerConfigurator $container) {
1718
$container->services()
@@ -20,6 +21,16 @@
2021
service('security.encoder_factory'),
2122
abstract_arg('encoders user classes'),
2223
])
23-
->tag('console.command')
24+
->tag('console.command', ['command' => 'security:encode-password'])
25+
->deprecate('symfony/security-bundle', '5.3', 'The "%service_id%" service is deprecated, use "security.command.user_password_hash" instead.')
26+
;
27+
28+
$container->services()
29+
->set('security.command.user_password_hash', UserPasswordHashCommand::class)
30+
->args([
31+
service('security.password_hasher_factory'),
32+
abstract_arg('list of user classes'),
33+
])
34+
->tag('console.command')
2435
;
2536
};

src/Symfony/Bundle/SecurityBundle/Resources/config/guard.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
abstract_arg('User Provider'),
3535
abstract_arg('Provider-shared Key'),
3636
abstract_arg('User Checker'),
37-
service('security.password_encoder'),
37+
service('security.password_hasher'),
3838
])
3939

4040
->set('security.authentication.listener.guard', GuardAuthenticationListener::class)

0 commit comments

Comments
 (0)