diff --git a/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php b/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php index 6bb54825e7..5dbd830a39 100644 --- a/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php +++ b/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php @@ -14,9 +14,11 @@ namespace Symfony\Bundle\SecurityBundle\Command; use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; 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; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder; /** * Encode a user's password. @@ -32,35 +34,45 @@ class UserPasswordEncoderCommand extends ContainerAwareCommand { $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: -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% --no-interaction [password] -You can also use the non interactive way by typing the following command: - php %command.full_name% [password] [user-class] [salt] +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: + + php %command.full_name% [password] AppBundle\Entity\User + +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 ) @@ -72,154 +84,86 @@ EOF */ 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'); - $salt = $input->getArgument('salt'); $userClass = $input->getArgument('user-class'); - - $helper = $this->getHelper('question'); - - if (!$password) { - $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); - } + $emptySalt = $input->getOption('empty-salt'); $encoder = $this->getContainer()->get('security.encoder_factory')->getEncoder($userClass); + $bcryptWithoutEmptySalt = !$emptySalt && $encoder instanceof BCryptPasswordEncoder; + + if ($bcryptWithoutEmptySalt) { + $emptySalt = true; + } + + if (!$password) { + if (!$input->isInteractive()) { + $output->error('The password must not be empty.'); + + return 1; + } + $passwordQuestion = $this->createPasswordQuestion($input, $output); + $password = $output->askQuestion($passwordQuestion); + } + + $salt = null; + + if ($input->isInteractive() && !$emptySalt) { + $emptySalt = true; + + $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; + } + } elseif (!$emptySalt) { + $salt = $this->generateSalt(); + } + $encodedPassword = $encoder->encodePassword($password, $salt); - $this->writeResult($output); + $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); - $table = new Table($output); - $table - ->setHeaders(array('Key', 'Value')) - ->addRow(array('Encoder used', get_class($encoder))) - ->addRow(array('Encoded password', $encodedPassword)) - ; + 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.'); + } - $table->render(); + $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; + })->setHidden(true)->setMaxAttempts(20); } - /** - * 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) + private function generateSalt() { - $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( - '', - $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 - ), - '', - )); + 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 90b54bca5d..868ff0b843 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php @@ -14,6 +14,8 @@ namespace Symfony\Bundle\SecurityBundle\Tests\Functional; 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,44 @@ 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() + { + $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() { $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'); + ), array('interactive' => false)); - $this->assertEquals($expected, $this->passwordEncoderCommandTester->getDisplay()); + $output = $this->passwordEncoderCommandTester->getDisplay(); + $this->assertContains('Password encoding succeeded', $output); + + $encoder = new BCryptPasswordEncoder(17); + preg_match('# Encoded password\s{1,}([\w+\/$.]+={0,2})\s+#', $output, $matches); + $hash = $matches[1]; + $this->assertTrue($encoder->isPasswordValid($hash, 'password', null)); } public function testEncodePasswordPbkdf2() @@ -56,24 +72,70 @@ class UserPasswordEncoderCommandTest extends WebTestCase 'command' => 'security:encode-password', 'password' => 'password', 'user-class' => 'Custom\Class\Pbkdf2\User', - 'salt' => 'AZERTYUIOPOfghjklytrertyuiolnbcxdfghjkytrfghjk', - )); + ), array('interactive' => false)); - $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\s{1,}([\w+\/]+={0,2})\s+#', $output, $matches); + $hash = $matches[1]; + preg_match('# Generated salt\s{1,}([\w+\/]+={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', + ), array('interactive' => false) + ); + + $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 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 "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', + ), array('interactive' => false)); } protected function setUp() 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 ad7622649b..0000000000 --- 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 bceff77021..82416b0957 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/emptysalt.txt b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/emptysalt.txt new file mode 100644 index 0000000000..9c8d3deb1b --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/emptysalt.txt @@ -0,0 +1,13 @@ + +Symfony Password Encoder Utility +================================ + + ------------------ ------------------------------------------------------------------ + Key Value + ------------------ ------------------------------------------------------------------ + Encoder used Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder + Encoded password password + ------------------ ------------------------------------------------------------------ + + [OK] Password encoding succeeded + 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 9b30572cce..0000000000 --- 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== | -+------------------+---------------------------------------------------------------+ diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/plaintext.txt b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/plaintext.txt deleted file mode 100644 index 0300aa8056..0000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/plaintext.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\PlaintextPasswordEncoder | -| Encoded password | password{AZERTYUIOPOfghjklytrertyuiolnbcxdfghjkytrfghjk} | -+------------------+------------------------------------------------------------------+ diff --git a/src/Symfony/Bundle/SecurityBundle/composer.json b/src/Symfony/Bundle/SecurityBundle/composer.json index c5219093e6..3e4a91761c 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",