diff --git a/src/Maker/MakeUser.php b/src/Maker/MakeUser.php index 302471cac..870debfd4 100644 --- a/src/Maker/MakeUser.php +++ b/src/Maker/MakeUser.php @@ -31,6 +31,7 @@ use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher; use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder; use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder; use Symfony\Component\Security\Core\Exception\UserNotFoundException; @@ -116,6 +117,11 @@ class_exists(DoctrineBundle::class) $userWillHavePassword = $io->confirm('Does this app need to hash/check user passwords?'); $input->setOption('with-password', $userWillHavePassword); + $symfonyGte53 = class_exists(NativePasswordHasher::class); + if ($symfonyGte53) { + return; + } + if ($userWillHavePassword && !class_exists(NativePasswordEncoder::class) && Argon2iPasswordEncoder::isSupported()) { $io->writeln('The newer Argon2i password hasher requires PHP 7.2, libsodium or paragonie/sodium_compat. Your system DOES support this algorithm.'); $io->writeln('You should use Argon2i unless your production system will not support it.'); diff --git a/src/Resources/skeleton/security/UserProvider.tpl.php b/src/Resources/skeleton/security/UserProvider.tpl.php index 9f2bf8cf5..6b8161eac 100644 --- a/src/Resources/skeleton/security/UserProvider.tpl.php +++ b/src/Resources/skeleton/security/UserProvider.tpl.php @@ -72,13 +72,13 @@ public function supportsClass($class) /** - * Upgrades the encoded password of a user, typically for using a better hash algorithm. + * Upgrades the hashed password of a user, typically for using a better hash algorithm. */ - public function upgradePassword(UserInterface $user, string $newEncodedPassword): void + public function upgradePassword(UserInterface $user, string $newHashedPassword): void { - // TODO: when encoded passwords are in use, this method should: + // TODO: when hashed passwords are in use, this method should: // 1. persist the new password in the user storage - // 2. update the $user object with $user->setPassword($newEncodedPassword); + // 2. update the $user object with $user->setPassword($newHashedPassword); } } diff --git a/src/Security/SecurityConfigUpdater.php b/src/Security/SecurityConfigUpdater.php index 8355df15c..e4475894a 100644 --- a/src/Security/SecurityConfigUpdater.php +++ b/src/Security/SecurityConfigUpdater.php @@ -13,6 +13,7 @@ use Symfony\Bundle\MakerBundle\Util\YamlSourceManipulator; use Symfony\Component\HttpKernel\Log\Logger; +use Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher; use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder; /** @@ -50,7 +51,8 @@ public function updateForUserClass(string $yamlSource, UserClassConfiguration $u $this->updateProviders($userConfig, $userClass); if ($userConfig->hasPassword()) { - $this->updateEncoders($userConfig, $userClass); + $symfonyGte53 = class_exists(NativePasswordHasher::class); + $this->updatePasswordHashers($userConfig, $userClass, $symfonyGte53 ? 'password_hashers' : 'encoders'); } $contents = $this->manipulator->getContents(); @@ -72,6 +74,10 @@ public function updateForAuthenticator(string $yamlSource, string $firewallName, $newData = $this->manipulator->getData(); if (!isset($newData['security']['firewalls'])) { + if ($newData['security']) { + $newData['security']['_firewalls'] = $this->manipulator->createEmptyLine(); + } + $newData['security']['firewalls'] = []; } @@ -153,6 +159,10 @@ private function updateProviders(UserClassConfiguration $userConfig, string $use $this->removeMemoryProviderIfIsSingleConfigured(); $newData = $this->manipulator->getData(); + if ($newData['security'] && !\array_key_exists('providers', $newData['security'])) { + $newData['security']['_providers'] = $this->manipulator->createEmptyLine(); + } + $newData['security']['providers']['__'] = $this->manipulator->createCommentLine( ' used to reload user from session & other features (e.g. switch_user)' ); @@ -175,18 +185,34 @@ private function updateProviders(UserClassConfiguration $userConfig, string $use $this->manipulator->setData($newData); } - private function updateEncoders(UserClassConfiguration $userConfig, string $userClass) + private function updatePasswordHashers(UserClassConfiguration $userConfig, string $userClass, string $keyName = 'password_hashers') { $newData = $this->manipulator->getData(); - if (!isset($newData['security']['encoders'])) { - // encoders is usually the first key, by convention - $newData['security'] = ['encoders' => []] + $newData['security']; + if ('password_hashers' === $keyName && isset($newData['security']['encoders'])) { + // fallback to "encoders" if the user already defined encoder config + $this->updatePasswordHashers($userClass, $userClass, 'encoders'); + + return; + } + + if (!isset($newData['security'][$keyName])) { + // by convention, password_hashers are put before the user provider option + $providersIndex = array_search('providers', array_keys($newData['security'])); + if (false === $providersIndex) { + $newData['security'] = [$keyName => []] + $newData['security']; + } else { + $newData['security'] = array_merge( + \array_slice($newData['security'], 0, $providersIndex), + [$keyName => []], + \array_slice($newData['security'], $providersIndex) + ); + } } - $newData['security']['encoders'][$userClass] = [ - 'algorithm' => $userConfig->shouldUseArgon2() ? 'argon2i' : (class_exists(NativePasswordEncoder::class) ? 'auto' : 'bcrypt'), + $newData['security'][$keyName][$userClass] = [ + 'algorithm' => $userConfig->shouldUseArgon2() ? 'argon2i' : ((class_exists(NativePasswordHasher::class) || class_exists(NativePasswordEncoder::class)) ? 'auto' : 'bcrypt'), ]; - $newData['security']['encoders']['_'] = $this->manipulator->createEmptyLine(); + $newData['security'][$keyName]['_'] = $this->manipulator->createEmptyLine(); $this->manipulator->setData($newData); } diff --git a/tests/Security/SecurityConfigUpdaterTest.php b/tests/Security/SecurityConfigUpdaterTest.php index 05e43cc9e..03d413630 100644 --- a/tests/Security/SecurityConfigUpdaterTest.php +++ b/tests/Security/SecurityConfigUpdaterTest.php @@ -16,6 +16,7 @@ use Symfony\Bundle\MakerBundle\Security\SecurityConfigUpdater; use Symfony\Bundle\MakerBundle\Security\UserClassConfiguration; use Symfony\Component\HttpKernel\Log\Logger; +use Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher; use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder; class SecurityConfigUpdaterTest extends TestCase @@ -48,9 +49,10 @@ public function testUpdateForUserClass(UserClassConfiguration $userConfig, strin $updater = new SecurityConfigUpdater($this->ysmLogger); $source = file_get_contents(__DIR__.'/yaml_fixtures/source/'.$startingSourceFilename); $actualSource = $updater->updateForUserClass($source, $userConfig, $userClass); - $expectedSource = file_get_contents(__DIR__.'/yaml_fixtures/expected_user_class/'.$expectedSourceFilename); + $symfonyVersion = class_exists(NativePasswordHasher::class) ? '5.3' : 'legacy'; + $expectedSource = file_get_contents(__DIR__.'/yaml_fixtures/expected_user_class/'.$symfonyVersion.'/'.$expectedSourceFilename); - $bcryptOrAuto = class_exists(NativePasswordEncoder::class) ? 'auto' : 'bcrypt'; + $bcryptOrAuto = ('5.3' === $symfonyVersion || class_exists(NativePasswordEncoder::class)) ? 'auto' : 'bcrypt'; $expectedSource = str_replace('{BCRYPT_OR_AUTO}', $bcryptOrAuto, $expectedSource); $this->assertSame($expectedSource, $actualSource); @@ -95,6 +97,18 @@ public function getUserClassTests() 'simple_security_with_single_memory_provider_configured.yaml', 'simple_security_with_single_memory_provider_configured.yaml', ]; + + yield 'security_52_empty_security' => [ + new UserClassConfiguration(true, 'email', true), + 'security_52_entity_email_with_password.yaml', + 'security_52_empty_security.yaml', + ]; + + yield 'security_52_with_firewalls_and_logout' => [ + new UserClassConfiguration(true, 'email', true), + 'security_52_complex_entity_email_with_password.yaml', + 'security_52_with_firewalls_and_logout.yaml', + ]; } /** diff --git a/tests/Security/yaml_fixtures/expected_authenticator/empty_source.yaml b/tests/Security/yaml_fixtures/expected_authenticator/empty_source.yaml index 699784f62..43063acf5 100644 --- a/tests/Security/yaml_fixtures/expected_authenticator/empty_source.yaml +++ b/tests/Security/yaml_fixtures/expected_authenticator/empty_source.yaml @@ -3,4 +3,4 @@ security: main: anonymous: lazy guard: - authenticators: [App\Security\AppCustomAuthenticator] \ No newline at end of file + authenticators: [App\Security\AppCustomAuthenticator] diff --git a/tests/Security/yaml_fixtures/expected_authenticator/security_52_empty_source.yaml b/tests/Security/yaml_fixtures/expected_authenticator/security_52_empty_source.yaml index 26d47aae2..82d0bdf16 100644 --- a/tests/Security/yaml_fixtures/expected_authenticator/security_52_empty_source.yaml +++ b/tests/Security/yaml_fixtures/expected_authenticator/security_52_empty_source.yaml @@ -1,5 +1,6 @@ security: enable_authenticator_manager: true + firewalls: main: lazy: true diff --git a/tests/Security/yaml_fixtures/expected_authenticator/security_52_with_multiple_authenticators.yaml b/tests/Security/yaml_fixtures/expected_authenticator/security_52_with_multiple_authenticators.yaml index 77b3bb419..84241d8cf 100644 --- a/tests/Security/yaml_fixtures/expected_authenticator/security_52_with_multiple_authenticators.yaml +++ b/tests/Security/yaml_fixtures/expected_authenticator/security_52_with_multiple_authenticators.yaml @@ -1,5 +1,6 @@ security: enable_authenticator_manager: true + firewalls: main: lazy: true diff --git a/tests/Security/yaml_fixtures/expected_user_class/5.3/empty_source_model_email_with_password.yaml b/tests/Security/yaml_fixtures/expected_user_class/5.3/empty_source_model_email_with_password.yaml new file mode 100644 index 000000000..732eea848 --- /dev/null +++ b/tests/Security/yaml_fixtures/expected_user_class/5.3/empty_source_model_email_with_password.yaml @@ -0,0 +1,9 @@ +security: + password_hashers: + App\Security\User: + algorithm: {BCRYPT_OR_AUTO} + + providers: + # used to reload user from session & other features (e.g. switch_user) + app_user_provider: + id: App\Security\UserProvider diff --git a/tests/Security/yaml_fixtures/expected_user_class/5.3/entity_email_with_password.yaml b/tests/Security/yaml_fixtures/expected_user_class/5.3/entity_email_with_password.yaml new file mode 100644 index 000000000..47866590a --- /dev/null +++ b/tests/Security/yaml_fixtures/expected_user_class/5.3/entity_email_with_password.yaml @@ -0,0 +1,16 @@ +security: + password_hashers: + App\Entity\User: + algorithm: {BCRYPT_OR_AUTO} + + # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers + providers: + # used to reload user from session & other features (e.g. switch_user) + app_user_provider: + entity: + class: App\Entity\User + property: email + + firewalls: + dev: ~ + main: ~ diff --git a/tests/Security/yaml_fixtures/expected_user_class/entity_username_no_password.yaml b/tests/Security/yaml_fixtures/expected_user_class/5.3/entity_username_no_password.yaml similarity index 100% rename from tests/Security/yaml_fixtures/expected_user_class/entity_username_no_password.yaml rename to tests/Security/yaml_fixtures/expected_user_class/5.3/entity_username_no_password.yaml diff --git a/tests/Security/yaml_fixtures/expected_user_class/5.3/model_email_password_existing_providers.yaml b/tests/Security/yaml_fixtures/expected_user_class/5.3/model_email_password_existing_providers.yaml new file mode 100644 index 000000000..db129ff31 --- /dev/null +++ b/tests/Security/yaml_fixtures/expected_user_class/5.3/model_email_password_existing_providers.yaml @@ -0,0 +1,16 @@ +security: + password_hashers: + App\Security\User: + algorithm: {BCRYPT_OR_AUTO} + + # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers + providers: + in_memory: { memory: ~ } + other_provider: { foo: true } + # used to reload user from session & other features (e.g. switch_user) + app_user_provider: + id: App\Security\UserProvider + + firewalls: + dev: ~ + main: ~ diff --git a/tests/Security/yaml_fixtures/expected_user_class/5.3/model_email_with_password.yaml b/tests/Security/yaml_fixtures/expected_user_class/5.3/model_email_with_password.yaml new file mode 100644 index 000000000..93623141e --- /dev/null +++ b/tests/Security/yaml_fixtures/expected_user_class/5.3/model_email_with_password.yaml @@ -0,0 +1,14 @@ +security: + password_hashers: + App\Security\User: + algorithm: {BCRYPT_OR_AUTO} + + # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers + providers: + # used to reload user from session & other features (e.g. switch_user) + app_user_provider: + id: App\Security\UserProvider + + firewalls: + dev: ~ + main: ~ diff --git a/tests/Security/yaml_fixtures/expected_user_class/model_username_no_password.yaml b/tests/Security/yaml_fixtures/expected_user_class/5.3/model_username_no_password.yaml similarity index 100% rename from tests/Security/yaml_fixtures/expected_user_class/model_username_no_password.yaml rename to tests/Security/yaml_fixtures/expected_user_class/5.3/model_username_no_password.yaml diff --git a/tests/Security/yaml_fixtures/expected_user_class/5.3/security_52_complex_entity_email_with_password.yaml b/tests/Security/yaml_fixtures/expected_user_class/5.3/security_52_complex_entity_email_with_password.yaml new file mode 100644 index 000000000..7b5119f4a --- /dev/null +++ b/tests/Security/yaml_fixtures/expected_user_class/5.3/security_52_complex_entity_email_with_password.yaml @@ -0,0 +1,20 @@ +security: + enable_authenticator_manager: true + password_hashers: + App\Entity\User: + algorithm: {BCRYPT_OR_AUTO} + + # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers + providers: + # used to reload user from session & other features (e.g. switch_user) + app_user_provider: + entity: + class: App\Entity\User + property: email + + firewalls: + dev: + pattern: ^/(_(profiler|wdt)|css|images|js)/ + security: false + main: + lazy: true diff --git a/tests/Security/yaml_fixtures/expected_user_class/5.3/security_52_entity_email_with_password.yaml b/tests/Security/yaml_fixtures/expected_user_class/5.3/security_52_entity_email_with_password.yaml new file mode 100644 index 000000000..c8c91a92e --- /dev/null +++ b/tests/Security/yaml_fixtures/expected_user_class/5.3/security_52_entity_email_with_password.yaml @@ -0,0 +1,13 @@ +security: + enable_authenticator_manager: true + + password_hashers: + App\Entity\User: + algorithm: {BCRYPT_OR_AUTO} + + providers: + # used to reload user from session & other features (e.g. switch_user) + app_user_provider: + entity: + class: App\Entity\User + property: email diff --git a/tests/Security/yaml_fixtures/expected_user_class/5.3/simple_security_with_single_memory_provider_configured.yaml b/tests/Security/yaml_fixtures/expected_user_class/5.3/simple_security_with_single_memory_provider_configured.yaml new file mode 100644 index 000000000..7ca690651 --- /dev/null +++ b/tests/Security/yaml_fixtures/expected_user_class/5.3/simple_security_with_single_memory_provider_configured.yaml @@ -0,0 +1,18 @@ +security: + password_hashers: + App\Entity\User: + algorithm: {BCRYPT_OR_AUTO} + + # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers + providers: + # used to reload user from session & other features (e.g. switch_user) + app_user_provider: + entity: + class: App\Entity\User + property: email + + firewalls: + dev: ~ + main: + anonymous: lazy + provider: app_user_provider diff --git a/tests/Security/yaml_fixtures/expected_user_class/empty_source_model_email_with_password.yaml b/tests/Security/yaml_fixtures/expected_user_class/legacy/empty_source_model_email_with_password.yaml similarity index 83% rename from tests/Security/yaml_fixtures/expected_user_class/empty_source_model_email_with_password.yaml rename to tests/Security/yaml_fixtures/expected_user_class/legacy/empty_source_model_email_with_password.yaml index 69698e348..35cd17454 100644 --- a/tests/Security/yaml_fixtures/expected_user_class/empty_source_model_email_with_password.yaml +++ b/tests/Security/yaml_fixtures/expected_user_class/legacy/empty_source_model_email_with_password.yaml @@ -6,4 +6,4 @@ security: providers: # used to reload user from session & other features (e.g. switch_user) app_user_provider: - id: App\Security\UserProvider \ No newline at end of file + id: App\Security\UserProvider diff --git a/tests/Security/yaml_fixtures/expected_user_class/entity_email_with_password.yaml b/tests/Security/yaml_fixtures/expected_user_class/legacy/entity_email_with_password.yaml similarity index 100% rename from tests/Security/yaml_fixtures/expected_user_class/entity_email_with_password.yaml rename to tests/Security/yaml_fixtures/expected_user_class/legacy/entity_email_with_password.yaml diff --git a/tests/Security/yaml_fixtures/expected_user_class/legacy/entity_username_no_password.yaml b/tests/Security/yaml_fixtures/expected_user_class/legacy/entity_username_no_password.yaml new file mode 100644 index 000000000..5cef6b8b0 --- /dev/null +++ b/tests/Security/yaml_fixtures/expected_user_class/legacy/entity_username_no_password.yaml @@ -0,0 +1,12 @@ +security: + # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers + providers: + # used to reload user from session & other features (e.g. switch_user) + app_user_provider: + entity: + class: App\Entity\User + property: username + + firewalls: + dev: ~ + main: ~ diff --git a/tests/Security/yaml_fixtures/expected_user_class/model_email_password_existing_providers.yaml b/tests/Security/yaml_fixtures/expected_user_class/legacy/model_email_password_existing_providers.yaml similarity index 100% rename from tests/Security/yaml_fixtures/expected_user_class/model_email_password_existing_providers.yaml rename to tests/Security/yaml_fixtures/expected_user_class/legacy/model_email_password_existing_providers.yaml diff --git a/tests/Security/yaml_fixtures/expected_user_class/model_email_with_password.yaml b/tests/Security/yaml_fixtures/expected_user_class/legacy/model_email_with_password.yaml similarity index 100% rename from tests/Security/yaml_fixtures/expected_user_class/model_email_with_password.yaml rename to tests/Security/yaml_fixtures/expected_user_class/legacy/model_email_with_password.yaml diff --git a/tests/Security/yaml_fixtures/expected_user_class/legacy/model_username_no_password.yaml b/tests/Security/yaml_fixtures/expected_user_class/legacy/model_username_no_password.yaml new file mode 100644 index 000000000..f66911320 --- /dev/null +++ b/tests/Security/yaml_fixtures/expected_user_class/legacy/model_username_no_password.yaml @@ -0,0 +1,10 @@ +security: + # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers + providers: + # used to reload user from session & other features (e.g. switch_user) + app_user_provider: + id: App\Security\UserProvider + + firewalls: + dev: ~ + main: ~ diff --git a/tests/Security/yaml_fixtures/expected_user_class/legacy/security_52_complex_entity_email_with_password.yaml b/tests/Security/yaml_fixtures/expected_user_class/legacy/security_52_complex_entity_email_with_password.yaml new file mode 100644 index 000000000..b43761262 --- /dev/null +++ b/tests/Security/yaml_fixtures/expected_user_class/legacy/security_52_complex_entity_email_with_password.yaml @@ -0,0 +1,20 @@ +security: + enable_authenticator_manager: true + encoders: + App\Entity\User: + algorithm: {BCRYPT_OR_AUTO} + + # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers + providers: + # used to reload user from session & other features (e.g. switch_user) + app_user_provider: + entity: + class: App\Entity\User + property: email + + firewalls: + dev: + pattern: ^/(_(profiler|wdt)|css|images|js)/ + security: false + main: + lazy: true diff --git a/tests/Security/yaml_fixtures/expected_user_class/legacy/security_52_entity_email_with_password.yaml b/tests/Security/yaml_fixtures/expected_user_class/legacy/security_52_entity_email_with_password.yaml new file mode 100644 index 000000000..8bf25fc6f --- /dev/null +++ b/tests/Security/yaml_fixtures/expected_user_class/legacy/security_52_entity_email_with_password.yaml @@ -0,0 +1,13 @@ +security: + enable_authenticator_manager: true + + encoders: + App\Entity\User: + algorithm: {BCRYPT_OR_AUTO} + + providers: + # used to reload user from session & other features (e.g. switch_user) + app_user_provider: + entity: + class: App\Entity\User + property: email diff --git a/tests/Security/yaml_fixtures/expected_user_class/simple_security_with_single_memory_provider_configured.yaml b/tests/Security/yaml_fixtures/expected_user_class/legacy/simple_security_with_single_memory_provider_configured.yaml similarity index 100% rename from tests/Security/yaml_fixtures/expected_user_class/simple_security_with_single_memory_provider_configured.yaml rename to tests/Security/yaml_fixtures/expected_user_class/legacy/simple_security_with_single_memory_provider_configured.yaml diff --git a/tests/Security/yaml_fixtures/source/empty_security.yaml b/tests/Security/yaml_fixtures/source/empty_security.yaml index 0e241393c..6dbd9183e 100644 --- a/tests/Security/yaml_fixtures/source/empty_security.yaml +++ b/tests/Security/yaml_fixtures/source/empty_security.yaml @@ -1 +1 @@ -security: {} \ No newline at end of file +security: {} diff --git a/tests/Security/yaml_fixtures/source/security_52_with_multiple_authenticators.yaml b/tests/Security/yaml_fixtures/source/security_52_with_multiple_authenticators.yaml index 3de191b3a..5ca600e75 100644 --- a/tests/Security/yaml_fixtures/source/security_52_with_multiple_authenticators.yaml +++ b/tests/Security/yaml_fixtures/source/security_52_with_multiple_authenticators.yaml @@ -1,5 +1,6 @@ security: enable_authenticator_manager: true + firewalls: main: lazy: true