8000 [Security] Add migrating encoder configuration · symfony/symfony@80955be · GitHub
[go: up one dir, main page]

Skip to content

Commit 80955be

Browse files
committed
[Security] Add migrating encoder configuration
1 parent fd7c676 commit 80955be

File tree

9 files changed

+204
-3
lines changed

9 files changed

+204
-3
lines changed

src/Symfony/Bundle/SecurityBundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
4.4.0
55
-----
66

7+
* Added `migrate_from` option to encoders configuration.
78
* Added new `argon2id` encoder, undeprecated the `bcrypt` and `argon2i` ones (using `auto` is still recommended by default.)
89
* Deprecated the usage of "query_string" without a "search_dn" and a "search_password" config key in Ldap factories.
910
* Marked the `SecurityDataCollector` class as `@final`.

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,10 @@ private function addEncodersSection(ArrayNodeDefinition $rootNode)
394394
->beforeNormalization()->ifString()->then(function ($v) { return ['algorithm' => $v]; })->end()
395395
->children()
396396
->scalarNode('algorithm')->cannotBeEmpty()->end()
397+
->arrayNode('migrate_from')
398+
->prototype('scalar')->end()
399+
->beforeNormalization()->castToArray()->end()
400+
->end()
397401
->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()
398402
->scalarNode('key_length')->defaultValue(40)->end()
399403
->booleanNode('ignore_case')->defaultFalse()->end()

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,10 @@ private function createEncoder(array $config)
512512
return new Reference($config['id']);
513513
}
514514

515+
if ($config['migrate_from'] ?? false) {
516+
return $config;
517+
}
518+
515519
// plaintext encoder
516520
if ('plaintext' === $config['algorithm']) {
517521
$arguments = [$config['ignore_case']];

src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ public function testEncoders()
287287
'memory_cost' => null,
288288
'time_cost' => null,
289289
'threads' => null,
290+
'migrate_from' => [],
290291
],
291292
'JMS\FooBundle\Entity\User3' => [
292293
'algorithm' => 'md5',
@@ -299,6 +300,7 @@ public function testEncoders()
299300
'memory_cost' => null,
300301
'time_cost' => null,
301302
'threads' => null,
303+
'migrate_from' => [],
302304
],
303305
'JMS\FooBundle\Entity\User4' => new Reference('security.encoder.foo'),
304306
'JMS\FooBundle\Entity\User5' => [
@@ -320,6 +322,7 @@ public function testEncoders()
320322
'memory_cost' => null,
321323
'time_cost' => null,
322324
'threads' => null,
325+
'migrate_from' => [],
323326
],
324327
]], $container->getDefinition('security.encoder_factory.generic')->getArguments());
325328
}
@@ -348,6 +351,7 @@ public function testEncodersWithLibsodium()
348351
'memory_cost' => null,
349352
'time_cost' => null,
350353
'threads' => null,
354+
'migrate_from' => [],
351355
],
352356
'JMS\FooBundle\Entity\User3' => [
353357
'algorithm' => 'md5',
@@ -360,6 +364,7 @@ public function testEncodersWithLibsodium()
360364
'memory_cost' => null,
361365
'time_cost' => null,
362366
'threads' => null,
367+
'migrate_from' => [],
363368
],
364369
'JMS\FooBundle\Entity\User4' => new Reference('security.encoder.foo'),
365370
'JMS\FooBundle\Entity\User5' => [
@@ -401,6 +406,7 @@ public function testEncodersWithArgon2i()
401406
'memory_cost' => null,
402407
'time_cost' => null,
403408
'threads' => null,
409+
'migrate_from' => [],
404410
],
405411
'JMS\FooBundle\Entity\User3' => [
406412
'algorithm' => 'md5',
@@ -413,6 +419,7 @@ public function testEncodersWithArgon2i()
413419
'memory_cost' => null,
414420
'time_cost' => null,
415421
'threads' => null,
422+
'migrate_from' => [],
416423
],
417424
'JMS\FooBundle\Entity\User4' => new Reference('security.encoder.foo'),
418425
'JMS\FooBundle\Entity\User5' => [
@@ -430,9 +437,74 @@ public function testEncodersWithArgon2i()
430437
]], $container->getDefinition('security.encoder_factory.generic')->getArguments());
431438
}
432439

440+
public function testMigratingEncoder()
441+
{
442+
if (!($sodium = SodiumPasswordEncoder::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) && !\defined('PASSWORD_ARGON2I')) {
443+
$this->markTestSkipped('Argon2i algorithm is not supported.');
444+
}
445+
446+
$container = $this->getContainer('migrating_encoder');
447+
448+
$this->assertEquals([[
449+
'JMS\FooBundle\Entity\User1' => [
450+
'class' => 'Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder',
451+
'arguments' => [false],
452+
],
453+
'JMS\FooBundle\Entity\User2' => [
454+
'algorithm' => 'sha1',
455+
'encode_as_base64' => false,
456+
'iterations' => 5,
457+
'hash_algorithm' => 'sha512',
458+
'key_length' => 40,
459+
'ignore_case' => false,
460+
'cost' => null,
461+
'memory_cost' => null,
462+
'time_cost' => null,
463+
'threads' => null,
464+
'migrate_from' => [],
465+
],
466+
'JMS\FooBundle\Entity\User3' => [
467+
'algorithm' => 'md5',
468+
'hash_algorithm' => 'sha512',
469+
'key_length' => 40,
470+
'ignore_case' => false,
471+
'encode_as_base64' => true,
472+
'iterations' => 5000,
473+
'cost' => null,
474+
'memory_cost' => null,
475+
'time_cost' => null,
476+
'threads' => null,
477+
'migrate_from' => [],
478+
],
479+
'JMS\FooBundle\Entity\User4' => new Reference('security.encoder.foo'),
480+
'JMS\FooBundle\Entity\User5' => [
481+
'class' => 'Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder',
482+
'arguments' => ['sha1', false, 5, 30],
483+
],
484+
'JMS\FooBundle\Entity\User6' => [
485+
'class' => 'Symfony\Component\Security\Core\Encoder\NativePasswordEncoder',
486+
'arguments' => [8, 102400, 15],
487+
],
488+
'JMS\FooBundle\Entity\User7' => [
489+
'algorithm' => 'argon2i',
490+
'hash_algorithm' => 'sha512',
491+
'key_length' => 40,
492+
'ignore_case' => false,
493+
'encode_as_base64' => true,
494+
'iterations' => 5000,
495+
'cost' => null,
496+
'memory_cost' => 256,
497+
'time_cost' => 1,
498+
'threads' => null,
499+
'migrate_from' => ['bcrypt'],
500+
],
501+
]], $container->getDefinition('security.encoder_factory.generic')->getArguments());
502+
}
503+
433504
public function testEncodersWithBCrypt()
434505
{
435506
$container = $this->getContainer('bcrypt_encoder');
507+
436508
$this->assertEquals([[
437509
'JMS\FooBundle\Entity\User1' => [
438510
'class' => 'Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder',
@@ -449,6 +521,7 @@ public function testEncodersWithBCrypt()
449521
'memory_cost' => null,
450522
'time_cost' => null,
451523
'threads' => null,
524+
'migrate_from' => [],
452525
],
453526
'JMS\FooBundle\Entity\User3' => [
454527
'algorithm' => 'md5',
@@ -461,6 +534,7 @@ public function testEncodersWithBCrypt()
461534
'memory_cost' => null,
462535
'time_cost' => null,
463536
'threads' => null,
537+
'migrate_from' => [],
464538
],
465539
'JMS\FooBundle\Entity\User4' => new Reference('security.encoder.foo'),
466540
'JMS\FooBundle\Entity\User5' => [
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
$this->load('container1.php', $container);
4+
5+
$container->loadFromExtension('security', [
6+
'encoders' => [
7+
'JMS\FooBundle\Entity\User7' => [
8+
'algorithm' => 'argon2i',
9+
'memory_cost' => 256,
10+
'time_cost' => 1,
11+
'migrate_from' => 'bcrypt',
12+
],
13+
],
14+
]);
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
3+
<container xmlns="http://symfony.com/schema/dic/services"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xmlns:sec="http://symfony.com/schema/dic/security"
6+
xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd">
7+
8+
<imports>
9+
<import resource="container1.xml"/>
10+
</imports>
11+
12+
<sec:config>
13+
<sec:encoder class="JMS\FooBundle\Entity\User7" algorithm="argon2i" memory-cost="256" time-cost="1">
14+
<sec:migrate-from>bcrypt</sec:migrate-from>
15+
</sec:encoder>
16+
</sec:config>
17+
18+
</container>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
imports:
2+
- { resource: container1.yml }
3+
4+
security:
5+
encoders:
6+
JMS\FooBundle\Entity\User7:
7+
algorithm: argon2i
8+
memory_cost: 256
9+
time_cost: 1
10+
migrate_from: bcrypt

src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,10 @@ public function getEncoder($user)
6565
*
6666
* @throws \InvalidArgumentException
6767
*/
68-
private function createEncoder(array $config): PasswordEncoderInterface
68+
private function createEncoder(array $config, bool $isExtra = false): PasswordEncoderInterface
6969
{
7070
if (isset($config['algorithm'])) {
71+
$rawConfig = $config;
7172
$config = $this->getEncoderConfigFromAlgorithm($config);
7273
}
7374
if (!isset($config['class'])) {
@@ -79,7 +80,23 @@ private function createEncoder(array $config): PasswordEncoderInterface
7980

8081
$reflection = new \ReflectionClass($config['class']);
8182

82-
return $reflection->newInstanceArgs($config['arguments']);
83+
$encoder = $reflection->newInstanceArgs($config['arguments']);
84+
85+
if ($isExtra || !\in_array($config['class'], [NativePasswordEncoder::class, SodiumPasswordEncoder::class], true)) {
86+
return $encoder;
87+
}
88+
89+
if ($rawConfig ?? null) {
90+
$extraEncoders = array_map(function (string $algo) use ($rawConfig): PasswordEncoderInterface {
91+
$rawConfig['algorithm'] = $algo;
92+
93+
return $this->createEncoder($rawConfig);
94+
}, ['pbkdf2', $rawConfig['hash_algorithm'] ?? 'sha512']);
95+
} else {
96+
$extraEncoders = [new Pbkdf2PasswordEncoder(), new MessageDigestPasswordEncoder()];
97+
}
98+
99+
return new MigratingPasswordEncoder($encoder, ...$extraEncoders);
83100
}
84101

85102
private function getEncoderConfigFromAlgorithm(array $config): array
@@ -89,7 +106,25 @@ private function getEncoderConfigFromAlgorithm(array $config): array
89106
// "plaintext" is not listed as any leaked hashes could then be used to authenticate directly
90107
foreach ([SodiumPasswordEncoder::isSupported() ? 'sodium' : 'native', 'pbkdf2', $config['hash_algorithm']] as $algo) {
91108
$config['algorithm'] = $algo;
92-
$encoderChain[] = $this->createEncoder($config);
109+
$encoderChain[] = $this->createEncoder($config, true);
110+
}
111+
112+
return [
113+
'class' => MigratingPasswordEncoder::class,
114+
'arguments' => $encoderChain,
115+
];
116+
}
117+
118+
if ($fromEncoders = ($config['migrate_from'] ?? false)) {
119+
$encoderChain = [];
120+
foreach ($fromEncoders as $name) {
121+
if ($encoder = $this->encoders[$name] ?? false) {
122+
$encoder = $encoder instanceof PasswordEncoderInterface ? $encoder : $this->createEncoder($encoder, true);
123+
} else {
124+
$encoder = $this->createEncoder(['algorithm' => $name], true);
125+
}
126+
127+
$encoderChain[] = $encoder;
93128
}
94129

95130
return [

src/Symfony/Component/Security/Core/Tests/Encoder/EncoderFactoryTest.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
use Symfony\Component\Security\Core\Encoder\EncoderAwareInterface;
1616
use Symfony\Component\Security\Core\Encoder\EncoderFactory;
1717
use Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder;
18+
use Symfony\Component\Security\Core\Encoder\MigratingPasswordEncoder;
19+
use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder;
20+
use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder;
1821
use Symfony\Component\Security\Core\User\User;
1922
use Symfony\Component\Security\Core\User\UserInterface;
2023

@@ -131,6 +134,44 @@ public function testGetEncoderForEncoderAwareWithClassName()
131134
$expectedEncoder = new MessageDigestPasswordEncoder('sha1');
132135
$this->assertEquals($expectedEncoder->encodePassword('foo', ''), $encoder->encodePassword('foo', ''));
133136
}
137+
138+
public function testMigrateFrom()
139+
{
140+
if (!SodiumPasswordEncoder::isSupported()) {
141+
$this->markTestSkipped('Sodium is not available');
142+
}
143+
144+
$factory = new EncoderFactory([
145+
'digest_encoder' => $digest = new MessageDigestPasswordEncoder('sha256'),
146+
'pbdkf2' => $digest = new MessageDigestPasswordEncoder('sha256'),
147+
'bcrypt_encoder' => ['algorithm' => 'bcrypt'],
148+
SomeUser::class => ['algorithm' => 'sodium', 'migrate_from' => ['bcrypt_encoder', 'digest_encoder']],
149+
]);
150+
151+
$encoder = $factory->getEncoder(SomeUser::class);
152+
$this->assertInstanceOf(MigratingPasswordEncoder::class, $encoder);
153+
154+
$this->assertTrue($encoder->isPasswordValid((new SodiumPasswordEncoder())->encodePassword('foo', null), 'foo', null));
155+
$this->assertTrue($encoder->isPasswordValid((new NativePasswordEncoder(null, null, null, \PASSWORD_BCRYPT))->encodePassword('foo', null), 'foo', null));
156+
$this->assertTrue($encoder->isPasswordValid($digest->encodePassword('foo', null), 'foo', null));
157+
}
158+
159+
public function testDefaultMigratingEncoders()
160+
{
161+
$this->assertInstanceOf(
162+
MigratingPasswordEncoder::class,
163+
(new EncoderFactory([SomeUser::class => ['class' => NativePasswordEncoder::class, 'arguments' => []]]))->getEncoder(SomeUser::class)
164+
);
165+
166+
if (!SodiumPasswordEncoder::isSupported()) {
167+
return;
168+
}
169+
170+
$this->assertInstanceOf(
171+
MigratingPasswordEncoder::class,
172+
(new EncoderFactory([SomeUser::class => ['class' => SodiumPasswordEncoder::class, 'arguments' => []]]))->getEncoder(SomeUser::class)
173+
);
174+
}
134175
}
135176

136177
class SomeUser implements UserInterface

0 commit comments

Comments
 (0)
0