Skip to content
This repository was archived by the owner on May 31, 2024. It is now read-only.

Commit 0a1081e

Browse files
committed
[Security] Add migrating encoder configuration
1 parent 526ae0a commit 0a1081e

File tree

2 files changed

+79
-3
lines changed

2 files changed

+79
-3
lines changed

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 [

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)