From 8ad5af5de6b2644053c70b0f246bca4a33366835 Mon Sep 17 00:00:00 2001 From: ogizanagi Date: Tue, 24 Mar 2015 01:44:27 +0100 Subject: [PATCH 1/6] [SecurityBundle] UserPasswordEncoderCommand: Improve & simplify the command usage - Remove the salt option. Always generate a salt. - Provide a short version by setting the default user-class to Symfony\Component\Security\Core\User\User. - Add an empty-salt option in order to let encoders generate their built-in salts (BCryptPasswordEncoder), or for custom encoders requiring no salt. --- .../Command/UserPasswordEncoderCommand.php | 138 ++++++------------ .../UserPasswordEncoderCommandTest.php | 72 +++++++-- .../Functional/app/PasswordEncode/bcrypt.txt | 22 --- .../Functional/app/PasswordEncode/config.yml | 10 +- .../{plaintext.txt => emptysalt.txt} | 4 +- .../Functional/app/PasswordEncode/pbkdf2.txt | 22 --- 6 files changed, 116 insertions(+), 152 deletions(-) delete mode 100644 src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/bcrypt.txt rename src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/{plaintext.txt => emptysalt.txt} (84%) delete mode 100644 src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/pbkdf2.txt diff --git a/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php b/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php index 6bb54825e756b..6f340dbc2e96f 100644 --- a/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php +++ b/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php @@ -12,11 +12,12 @@ namespace Symfony\Bundle\SecurityBundle\Command; use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; +use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\Question; -use Symfony\Component\Console\Helper\Table; /** * Encode a user's password. @@ -32,35 +33,45 @@ protected function configure() { $this ->setName('security:encode-password') - ->setDescription('Encode a password.') - ->addArgument('password', InputArgument::OPTIONAL, 'Enter a password') - ->addArgument('user-class', InputArgument::OPTIONAL, 'Enter the user class configured to find the encoder you need.') - ->addArgument('salt', InputArgument::OPTIONAL, 'Enter the salt you want to use to encode your password.') + ->setDescription('Encodes a password.') + ->addArgument('password', InputArgument::OPTIONAL, 'The plain password to encode.') + ->addArgument('user-class', InputArgument::OPTIONAL, 'The User entity class path associated with the encoder used to encode the password.', 'Symfony\Component\Security\Core\User\User') + ->addOption('empty-salt', null, InputOption::VALUE_NONE, 'Do not generate a salt or let the encoder generate one.') ->setHelp(<<%command.name% command allows to encode a password using encoders -that are configured in the application configuration file, under the security.encoders. +The %command.name% command encodes passwords according to your +security configuration. This command is mainly used to generate passwords for +the in_memory user provider type and for changing passwords +in the database while developing the application. + +Suppose that you have the following security configuration in your application: -For instance, if you have the following configuration for your application: - security: - encoders: - Symfony\Component\Security\Core\User\User: plaintext - AppBundle\Model\User: bcrypt +# app/config/security.yml +security: + encoders: + Symfony\Component\Security\Core\User\User: plaintext + AppBundle\Entity\User: bcrypt -According to the response you will give to the question "Provide your configured user class" your -password will be encoded the way it was configured. - - If you answer "Symfony\Component\Security\Core\User\User", the password provided will be encoded - with the plaintext encoder. - - If you answer AppBundle\Model\User, the password provided will be encoded - with the bcrypt encoder. +If you execute the command non-interactively, the default Symfony User class +is used and a random salt is generated to encode the password: + + php %command.full_name% --no-interaction [password] + +Pass the full user class path as the second argument to encode passwords for +your own entities: + + php %command.full_name% --no-interaction [password] AppBundle\Entity\User + +Executing the command interactively allows you to generate a random salt for +encoding the password: -The command allows you to provide your own salt. If you don't provide any, -the command will take care about that for you. + php %command.full_name% [password] AppBundle\Entity\User -You can also use the non interactive way by typing the following command: - php %command.full_name% [password] [user-class] [salt] +In case your encoder doesn't require a salt, add the empty-salt option: + + php %command.full_name% --empty-salt [password] AppBundle\Entity\User EOF ) @@ -75,27 +86,20 @@ protected function execute(InputInterface $input, OutputInterface $output) $this->writeIntroduction($output); $password = $input->getArgument('password'); - $salt = $input->getArgument('salt'); + $emptySalt = $input->getOption('empty-salt'); $userClass = $input->getArgument('user-class'); $helper = $this->getHelper('question'); if (!$password) { + if (!$input->isInteractive()) { + throw new \Exception('The password must not be empty.'); + } $passwordQuestion = $this->createPasswordQuestion($input, $output); $password = $helper->ask($input, $output, $passwordQuestion); } - if (!$salt) { - $saltQuestion = $this->createSaltQuestion($input, $output); - $salt = $helper->ask($input, $output, $saltQuestion); - } - - $output->writeln("\n Encoders are configured by user type in the security.yml file."); - - if (!$userClass) { - $userClassQuestion = $this->createUserClassQuestion($input, $output); - $userClass = $helper->ask($input, $output, $userClassQuestion); - } + $salt = $emptySalt ? null : base64_encode($this->getContainer()->get('security.secure_random')->nextBytes(30)); $encoder = $this->getContainer()->get('security.encoder_factory')->getEncoder($userClass); $encodedPassword = $encoder->encodePassword($password, $salt); @@ -106,10 +110,15 @@ protected function execute(InputInterface $input, OutputInterface $output) $table ->setHeaders(array('Key', 'Value')) ->addRow(array('Encoder used', get_class($encoder))) - ->addRow(array('Encoded password', $encodedPassword)) - ; - - $table->render(); + ->addRow(array('Encoded password', $encodedPassword)); + + if ($emptySalt) { + $table->render(); + } else { + $table->addRow(array('Generated salt', $salt)); + $table->render(); + $output->writeln(sprintf("Make sure that your salt storage field fits this salt length: %s chars.\n", strlen($salt))); + } } /** @@ -137,59 +146,6 @@ private function createPasswordQuestion(InputInterface $input, OutputInterface $ return $passwordQuestion; } - /** - * Create the question that asks for the salt to perform the encoding. - * If there is no provided salt, a random one is automatically generated. - * - * @param InputInterface $input - * @param OutputInterface $output - * - * @return Question - */ - private function createSaltQuestion(InputInterface $input, OutputInterface $output) - { - $saltQuestion = new Question("\n > (Optional) Provide a salt (press to generate one): "); - - $container = $this->getContainer(); - $saltQuestion->setValidator(function ($value) use ($output, $container) { - if ('' === trim($value)) { - $value = base64_encode($container->get('security.secure_random')->nextBytes(30)); - - $output->writeln("\nThe salt has been generated: ".$value); - $output->writeln(sprintf("Make sure that your salt storage field fits this salt length: %s chars.\n", strlen($value))); - } - - return $value; - }); - - return $saltQuestion; - } - - /** - * Create the question that asks for the configured user class. - * - * @param InputInterface $input - * @param OutputInterface $output - * - * @return Question - */ - private function createUserClassQuestion(InputInterface $input, OutputInterface $output) - { - $userClassQuestion = new Question(" > Provide your configured user class: "); - $userClassQuestion->setAutocompleterValues(array('Symfony\Component\Security\Core\User\User')); - - $userClassQuestion->setValidator(function ($value) use ($output) { - if ('' === trim($value)) { - $value = 'Symfony\Component\Security\Core\User\User'; - $output->writeln("You did not provide any user class. The user class used is: Symfony\Component\Security\Core\User\User \n"); - } - - return $value; - }); - - return $userClassQuestion; - } - private function writeIntroduction(OutputInterface $output) { $output->writeln(array( diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php index 90b54bca5d0b5..2369af77797ee 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php @@ -14,6 +14,8 @@ use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\SecurityBundle\Command\UserPasswordEncoderCommand; use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder; +use Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder; /** * Tests UserPasswordEncoderCommand @@ -24,30 +26,43 @@ class UserPasswordEncoderCommandTest extends WebTestCase { private $passwordEncoderCommandTester; - public function testEncodePasswordPasswordPlainText() + public function testEncodePasswordEmptySalt() { $this->passwordEncoderCommandTester->execute(array( 'command' => 'security:encode-password', 'password' => 'password', 'user-class' => 'Symfony\Component\Security\Core\User\User', - 'salt' => 'AZERTYUIOPOfghjklytrertyuiolnbcxdfghjkytrfghjk', + '--empty-salt' => true, )); - $expected = file_get_contents(__DIR__.'/app/PasswordEncode/plaintext.txt'); + $expected = file_get_contents(__DIR__.'/app/PasswordEncode/emptysalt.txt'); $this->assertEquals($expected, $this->passwordEncoderCommandTester->getDisplay()); } + public function testEncodeNoPasswordNoInteraction() + { + $this->setExpectedException('\Exception', 'The password must not be empty.'); + + $this->passwordEncoderCommandTester->execute(array( + 'command' => 'security:encode-password', + ), array('interactive' => false)); + } + public function testEncodePasswordBcrypt() { $this->passwordEncoderCommandTester->execute(array( 'command' => 'security:encode-password', 'password' => 'password', 'user-class' => 'Custom\Class\Bcrypt\User', - 'salt' => 'AZERTYUIOPOfghjklytrertyuiolnbcxdfghjkytrfghjk', )); - $expected = file_get_contents(__DIR__.'/app/PasswordEncode/bcrypt.txt'); - $this->assertEquals($expected, $this->passwordEncoderCommandTester->getDisplay()); + $output = $this->passwordEncoderCommandTester->getDisplay(); + $this->assertContains('Password encoding succeeded', $output); + + $encoder = new BCryptPasswordEncoder(17); + preg_match('#\| Encoded password \| ([a-zA-Z0-9+\/$.]+={0,2})\s+\|#', $output, $matches); + $hash = $matches[1]; + $this->assertTrue($encoder->isPasswordValid($hash, 'password', null)); } public function testEncodePasswordPbkdf2() @@ -56,23 +71,56 @@ public function testEncodePasswordPbkdf2() 'command' => 'security:encode-password', 'password' => 'password', 'user-class' => 'Custom\Class\Pbkdf2\User', - 'salt' => 'AZERTYUIOPOfghjklytrertyuiolnbcxdfghjkytrfghjk', )); - $expected = file_get_contents(__DIR__.'/app/PasswordEncode/pbkdf2.txt'); + $output = $this->passwordEncoderCommandTester->getDisplay(); + $this->assertContains('Password encoding succeeded', $output); - $this->assertEquals($expected, $this->passwordEncoderCommandTester->getDisplay()); + $encoder = new Pbkdf2PasswordEncoder('sha512', true, 1000); + preg_match('#\| Encoded password \| ([a-zA-Z0-9+\/$.]+={0,2})\s+\|#', $output, $matches); + $hash = $matches[1]; + preg_match('#\| Generated salt \| ([a-zA-Z0-9+\/]+={0,2})\s+\|#', $output, $matches); + $salt = $matches[1]; + $this->assertTrue($encoder->isPasswordValid($hash, 'password', $salt)); + } + + public function testEncodePasswordOutput() + { + $this->passwordEncoderCommandTester->execute( + array( + 'command' => 'security:encode-password', + 'password' => 'p@ssw0rd', + ) + ); + + $this->assertContains('Password encoding succeeded', $this->passwordEncoderCommandTester->getDisplay()); + $this->assertContains('| Encoded password | p@ssw0rd', $this->passwordEncoderCommandTester->getDisplay()); + $this->assertContains('| Generated salt |', $this->passwordEncoderCommandTester->getDisplay()); + } + + public function testEncodePasswordEmptySaltOutput() + { + $this->passwordEncoderCommandTester->execute( + array( + 'command' => 'security:encode-password', + 'password' => 'p@ssw0rd', + '--empty-salt' => true, + ) + ); + + $this->assertContains('Password encoding succeeded', $this->passwordEncoderCommandTester->getDisplay()); + $this->assertContains('| Encoded password | p@ssw0rd', $this->passwordEncoderCommandTester->getDisplay()); + $this->assertNotContains('| Generated salt |', $this->passwordEncoderCommandTester->getDisplay()); } public function testEncodePasswordNoConfigForGivenUserClass() { - $this->setExpectedException('\RuntimeException', 'No encoder has been configured for account "Wrong/User/Class".'); + $this->setExpectedException('\RuntimeException', 'No encoder has been configured for account "Foo\Bar\User".'); $this->passwordEncoderCommandTester->execute(array( 'command' => 'security:encode-password', 'password' => 'password', - 'user-class' => 'Wrong/User/Class', - 'salt' => 'AZERTYUIOPOfghjklytrertyuiolnbcxdfghjkytrfghjk', + 'user-class' => 'Foo\Bar\User', )); } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/bcrypt.txt b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/bcrypt.txt deleted file mode 100644 index ad7622649bc2a..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/bcrypt.txt +++ /dev/null @@ -1,22 +0,0 @@ - - - Symfony Password Encoder Utility - - - -This command encodes any password you want according to the configuration you -made in your configuration file containing the security.encoders key. - - - Encoders are configured by user type in the security.yml file. - - - ✔ Password encoding succeeded - - -+------------------+---------------------------------------------------------------+ -| Key | Value | -+------------------+---------------------------------------------------------------+ -| Encoder used | Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder | -| Encoded password | $2y$13$AZERTYUIOPOfghjklytreeBTRM4Wd.D3IW7dtnQ6xGA7z3fY8zg4. | -+------------------+---------------------------------------------------------------+ diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/config.yml index bceff770219f4..82416b095748e 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/config.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/config.yml @@ -4,8 +4,14 @@ imports: security: encoders: Symfony\Component\Security\Core\User\User: plaintext - Custom\Class\Bcrypt\User: bcrypt - Custom\Class\Pbkdf2\User: pbkdf2 + Custom\Class\Bcrypt\User: + algorithm: bcrypt + cost: 10 + Custom\Class\Pbkdf2\User: + algorithm: pbkdf2 + hash_algorithm: sha512 + encode_as_base64: true + iterations: 1000 Custom\Class\Test\User: test providers: diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/plaintext.txt b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/emptysalt.txt similarity index 84% rename from src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/plaintext.txt rename to src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/emptysalt.txt index 0300aa8056970..aacd463ae2afa 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/plaintext.txt +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/emptysalt.txt @@ -8,8 +8,6 @@ This command encodes any password you want according to the configuration you made in your configuration file containing the security.encoders key. - Encoders are configured by user type in the security.yml file. - ✔ Password encoding succeeded @@ -18,5 +16,5 @@ made in your configuration file containing the security.encoders key. | Key | Value | +------------------+------------------------------------------------------------------+ | Encoder used | Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder | -| Encoded password | password{AZERTYUIOPOfghjklytrertyuiolnbcxdfghjkytrfghjk} | +| Encoded password | password | +------------------+------------------------------------------------------------------+ diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/pbkdf2.txt b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/pbkdf2.txt deleted file mode 100644 index 9b30572cce28f..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/pbkdf2.txt +++ /dev/null @@ -1,22 +0,0 @@ - - - Symfony Password Encoder Utility - - - -This command encodes any password you want according to the configuration you -made in your configuration file containing the security.encoders key. - - - Encoders are configured by user type in the security.yml file. - - - ✔ Password encoding succeeded - - -+------------------+---------------------------------------------------------------+ -| Key | Value | -+------------------+---------------------------------------------------------------+ -| Encoder used | Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder | -| Encoded password | nvGk/kUwqj6PHzmqUqXxJA6GEhxD1TSJziV8P4ThqsEi4ZHF6yHp6g== | -+------------------+---------------------------------------------------------------+ From df4640dd95f81ed8c691e4262057c70a2c75d8cb Mon Sep 17 00:00:00 2001 From: ogizanagi Date: Thu, 26 Mar 2015 01:41:49 +0100 Subject: [PATCH 2/6] [SecurityBundle] UserPasswordEncoderCommand: ask user confirmation about salt generation when using the interactive command --- .../Command/UserPasswordEncoderCommand.php | 37 ++++++++++++++++++- .../UserPasswordEncoderCommandTest.php | 8 ++-- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php b/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php index 6f340dbc2e96f..7dcdb541be797 100644 --- a/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php +++ b/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php @@ -17,6 +17,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Question\Question; /** @@ -99,7 +100,17 @@ protected function execute(InputInterface $input, OutputInterface $output) $password = $helper->ask($input, $output, $passwordQuestion); } - $salt = $emptySalt ? null : base64_encode($this->getContainer()->get('security.secure_random')->nextBytes(30)); + $salt = null; + + if ($input->isInteractive() && !$emptySalt) { + $emptySalt = true; + if ($helper->ask($input, $output, $this->createSaltQuestion($output))) { + $salt = $this->generateSalt(); + $emptySalt = false; + } + } elseif (!$emptySalt) { + $salt = $this->generateSalt(); + } $encoder = $this->getContainer()->get('security.encoder_factory')->getEncoder($userClass); $encodedPassword = $encoder->encodePassword($password, $salt); @@ -146,6 +157,25 @@ private function createPasswordQuestion(InputInterface $input, OutputInterface $ return $passwordQuestion; } + /** + * Create the question that asks for the salt generation confirmation. + * + * @param OutputInterface $output + * + * @return ConfirmationQuestion + */ + private function createSaltQuestion(OutputInterface $output) + { + $output->writeln(array( + '! [NOTE] The command will take care of generating a salt for you.', + '! Be aware that some encoders advise to let them generate their own salt.', + '! If you\'re using the bcrypt encoder, please answer \'no\' to the question below.', + '! Provide the \'empty-salt\' option in order to let the encoder handle the generation itself.'.PHP_EOL, + )); + + return new ConfirmationQuestion('Confirm salt generation ? (yes/no) [yes]:', true); + } + private function writeIntroduction(OutputInterface $output) { $output->writeln(array( @@ -178,4 +208,9 @@ private function writeResult(OutputInterface $output) '', )); } + + private function generateSalt() + { + return base64_encode($this->getContainer()->get('security.secure_random')->nextBytes(30)); + } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php index 2369af77797ee..78e873db2483e 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php @@ -54,7 +54,7 @@ public function testEncodePasswordBcrypt() 'command' => 'security:encode-password', 'password' => 'password', 'user-class' => 'Custom\Class\Bcrypt\User', - )); + ), array('interactive' => false)); $output = $this->passwordEncoderCommandTester->getDisplay(); $this->assertContains('Password encoding succeeded', $output); @@ -71,7 +71,7 @@ public function testEncodePasswordPbkdf2() 'command' => 'security:encode-password', 'password' => 'password', 'user-class' => 'Custom\Class\Pbkdf2\User', - )); + ), array('interactive' => false)); $output = $this->passwordEncoderCommandTester->getDisplay(); $this->assertContains('Password encoding succeeded', $output); @@ -90,7 +90,7 @@ public function testEncodePasswordOutput() array( 'command' => 'security:encode-password', 'password' => 'p@ssw0rd', - ) + ), array('interactive' => false) ); $this->assertContains('Password encoding succeeded', $this->passwordEncoderCommandTester->getDisplay()); @@ -121,7 +121,7 @@ public function testEncodePasswordNoConfigForGivenUserClass() 'command' => 'security:encode-password', 'password' => 'password', 'user-class' => 'Foo\Bar\User', - )); + ), array('interactive' => false)); } protected function setUp() From 289de2faf86b331f02319f2bd59af2a84570c7cd Mon Sep 17 00:00:00 2001 From: ogizanagi Date: Tue, 24 Mar 2015 02:27:20 +0100 Subject: [PATCH 3/6] [SecurityBundle] UserPasswordEncoderCommand: Let the bcrypt encoder generate its built-in salt --- .../Command/UserPasswordEncoderCommand.php | 5 +++-- .../Functional/UserPasswordEncoderCommandTest.php | 13 +++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php b/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php index 7dcdb541be797..ed8ef6609b861 100644 --- a/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php +++ b/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php @@ -19,6 +19,7 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Question\Question; +use Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder; /** * Encode a user's password. @@ -87,8 +88,9 @@ protected function execute(InputInterface $input, OutputInterface $output) $this->writeIntroduction($output); $password = $input->getArgument('password'); - $emptySalt = $input->getOption('empty-salt'); $userClass = $input->getArgument('user-class'); + $encoder = $this->getContainer()->get('security.encoder_factory')->getEncoder($userClass); + $emptySalt = $input->getOption('empty-salt') || $encoder instanceof BCryptPasswordEncoder; $helper = $this->getHelper('question'); @@ -112,7 +114,6 @@ protected function execute(InputInterface $input, OutputInterface $output) $salt = $this->generateSalt(); } - $encoder = $this->getContainer()->get('security.encoder_factory')->getEncoder($userClass); $encodedPassword = $encoder->encodePassword($password, $salt); $this->writeResult($output); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php index 78e873db2483e..e595fe0baecad 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php @@ -113,6 +113,19 @@ public function testEncodePasswordEmptySaltOutput() $this->assertNotContains('| Generated salt |', $this->passwordEncoderCommandTester->getDisplay()); } + public function testEncodePasswordBcryptOutput() + { + $this->passwordEncoderCommandTester->execute( + array( + 'command' => 'security:encode-password', + 'password' => 'p@ssw0rd', + 'user-class' => 'Custom\Class\Bcrypt\User', + ) + ); + + $this->assertNotContains('| Generated salt |', $this->passwordEncoderCommandTester->getDisplay()); + } + public function testEncodePasswordNoConfigForGivenUserClass() { $this->setExpectedException('\RuntimeException', 'No encoder has been configured for account "Foo\Bar\User".'); From 2a6abf7a2c71b64817e2981e9230db3f9d33af42 Mon Sep 17 00:00:00 2001 From: ogizanagi Date: Thu, 26 Mar 2015 02:22:47 +0100 Subject: [PATCH 4/6] [SecurityBundle] UserPasswordEncoderCommand: Add a note when bcrypt detected without empty-salt option --- .../Command/UserPasswordEncoderCommand.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php b/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php index ed8ef6609b861..2cb86dc7b4f97 100644 --- a/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php +++ b/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php @@ -89,8 +89,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $password = $input->getArgument('password'); $userClass = $input->getArgument('user-class'); - $encoder = $this->getContainer()->get('security.encoder_factory')->getEncoder($userClass); - $emptySalt = $input->getOption('empty-salt') || $encoder instanceof BCryptPasswordEncoder; + $emptySalt = $input->getOption('empty-salt'); $helper = $this->getHelper('question'); @@ -102,6 +101,13 @@ protected function execute(InputInterface $input, OutputInterface $output) $password = $helper->ask($input, $output, $passwordQuestion); } + $encoder = $this->getContainer()->get('security.encoder_factory')->getEncoder($userClass); + + if (!$emptySalt && $encoder instanceof BCryptPasswordEncoder) { + $output->writeln('! [NOTE] Bcrypt encoder detected. The command will assume the salt should be generated by the encoder.'.PHP_EOL); + $emptySalt = true; + } + $salt = null; if ($input->isInteractive() && !$emptySalt) { @@ -170,7 +176,7 @@ private function createSaltQuestion(OutputInterface $output) $output->writeln(array( '! [NOTE] The command will take care of generating a salt for you.', '! Be aware that some encoders advise to let them generate their own salt.', - '! If you\'re using the bcrypt encoder, please answer \'no\' to the question below.', + '! If you\'re using one of those encoders, please answer \'no\' to the question below.', '! Provide the \'empty-salt\' option in order to let the encoder handle the generation itself.'.PHP_EOL, )); From b9374cc54f80c06aec5050293de5fa02d9eb4fe2 Mon Sep 17 00:00:00 2001 From: ogizanagi Date: Mon, 30 Mar 2015 21:10:47 +0200 Subject: [PATCH 5/6] [SecurityBundle] UserPasswordEncoderCommand: Tweak output, complying with console style guide --- .../Command/UserPasswordEncoderCommand.php | 126 +++++------------- .../UserPasswordEncoderCommandTest.php | 23 ++-- .../app/PasswordEncode/emptysalt.txt | 25 ++-- 3 files changed, 57 insertions(+), 117 deletions(-) diff --git a/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php b/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php index 2cb86dc7b4f97..5dbd830a39da5 100644 --- a/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php +++ b/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php @@ -12,13 +12,12 @@ namespace Symfony\Bundle\SecurityBundle\Command; use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; -use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Question\Question; +use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder; /** @@ -85,34 +84,39 @@ protected function configure() */ protected function execute(InputInterface $input, OutputInterface $output) { - $this->writeIntroduction($output); + $output = new SymfonyStyle($input, $output); + + $input->isInteractive() ? $output->title('Symfony Password Encoder Utility') : $output->newLine(); $password = $input->getArgument('password'); $userClass = $input->getArgument('user-class'); $emptySalt = $input->getOption('empty-salt'); - $helper = $this->getHelper('question'); + $encoder = $this->getContainer()->get('security.encoder_factory')->getEncoder($userClass); + $bcryptWithoutEmptySalt = !$emptySalt && $encoder instanceof BCryptPasswordEncoder; + + if ($bcryptWithoutEmptySalt) { + $emptySalt = true; + } if (!$password) { if (!$input->isInteractive()) { - throw new \Exception('The password must not be empty.'); + $output->error('The password must not be empty.'); + + return 1; } $passwordQuestion = $this->createPasswordQuestion($input, $output); - $password = $helper->ask($input, $output, $passwordQuestion); - } - - $encoder = $this->getContainer()->get('security.encoder_factory')->getEncoder($userClass); - - if (!$emptySalt && $encoder instanceof BCryptPasswordEncoder) { - $output->writeln('! [NOTE] Bcrypt encoder detected. The command will assume the salt should be generated by the encoder.'.PHP_EOL); - $emptySalt = true; + $password = $output->askQuestion($passwordQuestion); } $salt = null; if ($input->isInteractive() && !$emptySalt) { $emptySalt = true; - if ($helper->ask($input, $output, $this->createSaltQuestion($output))) { + + $output->note('The command will take care of generating a salt for you. Be aware that some encoders advise to let them generate their own salt. If you\'re using one of those encoders, please answer \'no\' to the question below. '.PHP_EOL.'Provide the \'empty-salt\' option in order to let the encoder handle the generation itself.'); + + if ($output->confirm('Confirm salt generation ?')) { $salt = $this->generateSalt(); $emptySalt = false; } @@ -122,98 +126,40 @@ protected function execute(InputInterface $input, OutputInterface $output) $encodedPassword = $encoder->encodePassword($password, $salt); - $this->writeResult($output); - - $table = new Table($output); - $table - ->setHeaders(array('Key', 'Value')) - ->addRow(array('Encoder used', get_class($encoder))) - ->addRow(array('Encoded password', $encodedPassword)); + $rows = array( + array('Encoder used', get_class($encoder)), + array('Encoded password', $encodedPassword), + ); + if (!$emptySalt) { + $rows[] = array('Generated salt', $salt); + } + $output->table(array('Key', 'Value'), $rows); - if ($emptySalt) { - $table->render(); - } else { - $table->addRow(array('Generated salt', $salt)); - $table->render(); - $output->writeln(sprintf("Make sure that your salt storage field fits this salt length: %s chars.\n", strlen($salt))); + if (!$emptySalt) { + $output->note(sprintf('Make sure that your salt storage field fits the salt length: %s chars', strlen($salt))); + } elseif ($bcryptWithoutEmptySalt) { + $output->note('Bcrypt encoder used: the encoder generated its own built-in salt.'); } + + $output->success('Password encoding succeeded'); } /** * Create the password question to ask the user for the password to be encoded. * - * @param InputInterface $input - * @param OutputInterface $output - * * @return Question */ - private function createPasswordQuestion(InputInterface $input, OutputInterface $output) + private function createPasswordQuestion() { - $passwordQuestion = new Question("\n > Type in your password to be encoded: "); + $passwordQuestion = new Question('Type in your password to be encoded'); - $passwordQuestion->setValidator(function ($value) { + return $passwordQuestion->setValidator(function ($value) { if ('' === trim($value)) { throw new \Exception('The password must not be empty.'); } return $value; - }); - $passwordQuestion->setHidden(true); - $passwordQuestion->setMaxAttempts(20); - - return $passwordQuestion; - } - - /** - * Create the question that asks for the salt generation confirmation. - * - * @param OutputInterface $output - * - * @return ConfirmationQuestion - */ - private function createSaltQuestion(OutputInterface $output) - { - $output->writeln(array( - '! [NOTE] The command will take care of generating a salt for you.', - '! Be aware that some encoders advise to let them generate their own salt.', - '! If you\'re using one of those encoders, please answer \'no\' to the question below.', - '! Provide the \'empty-salt\' option in order to let the encoder handle the generation itself.'.PHP_EOL, - )); - - return new ConfirmationQuestion('Confirm salt generation ? (yes/no) [yes]:', true); - } - - private function writeIntroduction(OutputInterface $output) - { - $output->writeln(array( - '', - $this->getHelperSet()->get('formatter')->formatBlock( - 'Symfony Password Encoder Utility', - 'bg=blue;fg=white', - true - ), - '', - )); - - $output->writeln(array( - '', - 'This command encodes any password you want according to the configuration you', - 'made in your configuration file containing the security.encoders key.', - '', - )); - } - - private function writeResult(OutputInterface $output) - { - $output->writeln(array( - '', - $this->getHelperSet()->get('formatter')->formatBlock( - '✔ Password encoding succeeded', - 'bg=green;fg=white', - true - ), - '', - )); + })->setHidden(true)->setMaxAttempts(20); } private function generateSalt() diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php index e595fe0baecad..868ff0b8434e3 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php @@ -41,11 +41,12 @@ public function testEncodePasswordEmptySalt() public function testEncodeNoPasswordNoInteraction() { - $this->setExpectedException('\Exception', 'The password must not be empty.'); - - $this->passwordEncoderCommandTester->execute(array( + $statusCode = $this->passwordEncoderCommandTester->execute(array( 'command' => 'security:encode-password', ), array('interactive' => false)); + + $this->assertContains('[ERROR] The password must not be empty.', $this->passwordEncoderCommandTester->getDisplay()); + $this->assertEquals($statusCode, 1); } public function testEncodePasswordBcrypt() @@ -60,7 +61,7 @@ public function testEncodePasswordBcrypt() $this->assertContains('Password encoding succeeded', $output); $encoder = new BCryptPasswordEncoder(17); - preg_match('#\| Encoded password \| ([a-zA-Z0-9+\/$.]+={0,2})\s+\|#', $output, $matches); + preg_match('# Encoded password\s{1,}([\w+\/$.]+={0,2})\s+#', $output, $matches); $hash = $matches[1]; $this->assertTrue($encoder->isPasswordValid($hash, 'password', null)); } @@ -77,9 +78,9 @@ public function testEncodePasswordPbkdf2() $this->assertContains('Password encoding succeeded', $output); $encoder = new Pbkdf2PasswordEncoder('sha512', true, 1000); - preg_match('#\| Encoded password \| ([a-zA-Z0-9+\/$.]+={0,2})\s+\|#', $output, $matches); + preg_match('# Encoded password\s{1,}([\w+\/]+={0,2})\s+#', $output, $matches); $hash = $matches[1]; - preg_match('#\| Generated salt \| ([a-zA-Z0-9+\/]+={0,2})\s+\|#', $output, $matches); + preg_match('# Generated salt\s{1,}([\w+\/]+={0,2})\s+#', $output, $matches); $salt = $matches[1]; $this->assertTrue($encoder->isPasswordValid($hash, 'password', $salt)); } @@ -94,8 +95,8 @@ public function testEncodePasswordOutput() ); $this->assertContains('Password encoding succeeded', $this->passwordEncoderCommandTester->getDisplay()); - $this->assertContains('| Encoded password | p@ssw0rd', $this->passwordEncoderCommandTester->getDisplay()); - $this->assertContains('| Generated salt |', $this->passwordEncoderCommandTester->getDisplay()); + $this->assertContains(' Encoded password p@ssw0rd', $this->passwordEncoderCommandTester->getDisplay()); + $this->assertContains(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay()); } public function testEncodePasswordEmptySaltOutput() @@ -109,8 +110,8 @@ public function testEncodePasswordEmptySaltOutput() ); $this->assertContains('Password encoding succeeded', $this->passwordEncoderCommandTester->getDisplay()); - $this->assertContains('| Encoded password | p@ssw0rd', $this->passwordEncoderCommandTester->getDisplay()); - $this->assertNotContains('| Generated salt |', $this->passwordEncoderCommandTester->getDisplay()); + $this->assertContains(' Encoded password p@ssw0rd', $this->passwordEncoderCommandTester->getDisplay()); + $this->assertNotContains(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay()); } public function testEncodePasswordBcryptOutput() @@ -123,7 +124,7 @@ public function testEncodePasswordBcryptOutput() ) ); - $this->assertNotContains('| Generated salt |', $this->passwordEncoderCommandTester->getDisplay()); + $this->assertNotContains(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay()); } public function testEncodePasswordNoConfigForGivenUserClass() diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/emptysalt.txt b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/emptysalt.txt index aacd463ae2afa..9c8d3deb1b462 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/emptysalt.txt +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/emptysalt.txt @@ -1,20 +1,13 @@ - - Symfony Password Encoder Utility - +Symfony Password Encoder Utility +================================ + ------------------ ------------------------------------------------------------------ + Key Value + ------------------ ------------------------------------------------------------------ + Encoder used Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder + Encoded password password + ------------------ ------------------------------------------------------------------ -This command encodes any password you want according to the configuration you -made in your configuration file containing the security.encoders key. + [OK] Password encoding succeeded - - - ✔ Password encoding succeeded - - -+------------------+------------------------------------------------------------------+ -| Key | Value | -+------------------+------------------------------------------------------------------+ -| Encoder used | Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder | -| Encoded password | password | -+------------------+------------------------------------------------------------------+ From 42b17dce3393f9025379a2b9be223a6700cb9852 Mon Sep 17 00:00:00 2001 From: ogizanagi Date: Mon, 6 Apr 2015 22:00:44 +0200 Subject: [PATCH 6/6] [SecurityBundle] updates symfony/console min version --- src/Symfony/Bundle/SecurityBundle/composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Bundle/SecurityBundle/composer.json b/src/Symfony/Bundle/SecurityBundle/composer.json index c5219093e639b..3e4a91761c0ce 100644 --- a/src/Symfony/Bundle/SecurityBundle/composer.json +++ b/src/Symfony/Bundle/SecurityBundle/composer.json @@ -23,7 +23,7 @@ "require-dev": { "symfony/phpunit-bridge": "~2.7|~3.0.0", "symfony/browser-kit": "~2.4|~3.0.0", - "symfony/console": "~2.5|~3.0.0", + "symfony/console": "~2.7|~3.0.0", "symfony/css-selector": "~2.0,>=2.0.5|~3.0.0", "symfony/dependency-injection": "~2.6,>=2.6.6|~3.0.0", "symfony/dom-crawler": "~2.0,>=2.0.5|~3.0.0",