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

Skip to content

Commit e34b87d

Browse files
committed
[Security] Add migrating encoder configuration
1 parent 0472dbf commit e34b87d

File tree

9 files changed

+175
-10
lines changed

9 files changed

+175
-10
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 `migrating_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

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('migrating_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['migrating_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+
'migrating_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+
'migrating_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+
'migrating_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+
'migrating_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+
'migrating_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+
'migrating_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+
'migrating_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+
'migrating_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+
'migrating_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+
'migrating_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+
'migrating_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+
'migrating_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+
'migrating_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:migrating_from>bcrypt</sec:migrating_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+
migrating_from: bcrypt

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

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -85,17 +85,14 @@ private function createEncoder(array $config): PasswordEncoderInterface
8585
private function getEncoderConfigFromAlgorithm(array $config): array
8686
{
8787
if ('auto' === $config['algorithm']) {
88-
$encoderChain = [];
89-
// "plaintext" is not listed as any leaked hashes could then be used to authenticate directly
90-
foreach ([SodiumPasswordEncoder::isSupported() ? 'sodium' : 'native', 'pbkdf2', $config['hash_algorithm']] as $algo) {
91-
$config['algorithm'] = $algo;
92-
$encoderChain[] = $this->createEncoder($config);
93-
}
88+
return $this->createMigratingEncoder($config, [SodiumPasswordEncoder::isSupported() ? 'sodium' : 'native', 'pbkdf2', $config['hash_algorithm']]);
89+
}
90+
91+
if ($algos = ($config['migrating_from'] ?? false)) {
92+
array_unshift($algos, $config['algorithm']);
93+
unset($config['migrating_from']);
9494

95-
return [
96-
'class' => MigratingPasswordEncoder::class,
97-
'arguments' => $encoderChain,
98-
];
95+
return $this->createMigratingEncoder($config, $algos);
9996
}
10097

10198
switch ($config['algorithm']) {
@@ -175,4 +172,22 @@ private function getEncoderConfigFromAlgorithm(array $config): array
175172
],
176173
];
177174
}
175+
176+
private function createMigratingEncoder(array $config, array $algos)
177+
{
178+
foreach ($algos as $algo) {
179+
// "plaintext" is not allowed as any leaked hashes could then be used to authenticate directly
180+
if ('plaintext' === $algo) {
181+
throw new LogicException('Migrating plaintext passwords is not supported.');
182+
}
183+
184+
$config['algorithm'] = $algo;
185+
$encoderChain[] = $this->createEncoder($config);
186+
}
187+
188+
return [
189+
'class' => MigratingPasswordEncoder::class,
190+
'arguments' => $encoderChain,
191+
];
192+
}
178193
}

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

Lines changed: 25 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,28 @@ public function testGetEncoderForEncoderAwareWithClassName()
131134
$expectedEncoder = new MessageDigestPasswordEncoder('sha1');
132135
$this->assertEquals($expectedEncoder->encodePassword('foo', ''), $encoder->encodePassword('foo', ''));
133136
}
137+
138+
public function testMigratingPasswordEncoder()
139+
{
140+
if (!SodiumPasswordEncoder::isSupported()) {
141+
$this->markTestSkipped('Sodium is not available');
142+
}
143+
144+
$factory = new EncoderFactory([SomeUser::class => ['algorithm' => 'sodium', 'migrating_from' => ['bcrypt']]]);
145+
$encoder = $factory->getEncoder(SomeUser::class);
146+
147+
$this->assertInstanceOf(MigratingPasswordEncoder::class, $encoder);
148+
$this->assertTrue($encoder->isPasswordValid(
149+
(new SodiumPasswordEncoder())->encodePassword('foo', null),
150+
'foo',
151+
null
152+
));
153+
$this->assertTrue($encoder->isPasswordValid(
154+
(new NativePasswordEncoder(null, null, null, \PASSWORD_BCRYPT))->encodePassword('foo', null),
155+
'foo',
156+
null
157+
));
158+
}
134159
}
135160

136161
class SomeUser implements UserInterface

0 commit comments

Comments
 (0)
0