-
-
Notifications
You must be signed in to change notification settings - Fork 9.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added a command to encode a password
- Loading branch information
Showing
8 changed files
with
417 additions
and
2 deletions.
There are no files selected for viewing
225 changes: 225 additions & 0 deletions
225
src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,225 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
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\Output\OutputInterface; | ||
use Symfony\Component\Console\Question\Question; | ||
use Symfony\Component\Console\Helper\Table; | ||
|
||
/** | ||
* Encode a user's password. | ||
* | ||
* @author Sarah Khalil <mkhalil.sarah@gmail.com> | ||
*/ | ||
class UserPasswordEncoderCommand extends ContainerAwareCommand | ||
{ | ||
/** | ||
* {@inheritdoc} | ||
*/ | ||
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.') | ||
->setHelp(<<<EOF | ||
The <info>%command.name%</info> command allows to encode a password using encoders | ||
that are configured in the application configuration file, under the <comment>security.encoders</comment>. | ||
For instance, if you have the following configuration for your application: | ||
<comment> | ||
security: | ||
encoders: | ||
Symfony\Component\Security\Core\User\User: plaintext | ||
AppBundle\Model\User: bcrypt | ||
</comment> | ||
According to the response you will give to the question "<question>Provide your configured user class</question>" your | ||
password will be encoded the way it was configured. | ||
- If you answer "<comment>Symfony\Component\Security\Core\User\User</comment>", the password provided will be encoded | ||
with the <comment>plaintext</comment> encoder. | ||
- If you answer <comment>AppBundle\Model\User</comment>, the password provided will be encoded | ||
with the <comment>bcrypt</comment> encoder. | ||
The command allows you to provide your own <comment>salt</comment>. If you don't provide any, | ||
the command will take care about that for you. | ||
You can also use the non interactive way by typing the following command: | ||
<info>php %command.full_name% [password] [salt] [user-class]</info> | ||
EOF | ||
) | ||
; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
protected function execute(InputInterface $input, OutputInterface $output) | ||
{ | ||
$this->writeIntroduction($output); | ||
|
||
$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 <comment>Encoders are configured by user type in the security.yml file.</comment>"); | ||
|
||
if (!$userClass) { | ||
$userClassQuestion = $this->createUserClassQuestion($input, $output); | ||
$userClass = $helper->ask($input, $output, $userClassQuestion); | ||
} | ||
|
||
$encoder = $this->getContainer()->get('security.encoder_factory')->getEncoder($userClass); | ||
$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)) | ||
; | ||
|
||
$table->render(); | ||
} | ||
|
||
/** | ||
* 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) | ||
{ | ||
$passwordQuestion = new Question("\n > <question>Type in your password to be encoded:</question> "); | ||
|
||
$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 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) <question>Provide a salt (press <enter> to generate one):</question> "); | ||
|
||
$container = $this->getContainer(); | ||
$saltQuestion->setValidator(function ($value) use ($output, $container) { | ||
if ('' === trim($value)) { | ||
$value = hash('sha512', $container->get('security.secure_random')->nextBytes(30)); | ||
|
||
$output->writeln("\n<comment>The salt has been generated: </comment>".$value); | ||
$output->writeln(sprintf("<comment>Make sure that your salt storage field fits this salt length: %s chars.</comment>\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(" > <question>Provide your configured user class:</question> "); | ||
$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("<info>You did not provide any user class.</info> <comment>The user class used is: Symfony\Component\Security\Core\User\User</comment> \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 <comment>security.encoders</comment> key.', | ||
'', | ||
)); | ||
} | ||
|
||
private function writeResult(OutputInterface $output) | ||
{ | ||
$output->writeln(array( | ||
'', | ||
$this->getHelperSet()->get('formatter')->formatBlock( | ||
'✔ Password encoding succeeded', | ||
'bg=green;fg=white', | ||
true | ||
), | ||
'', | ||
)); | ||
} | ||
} |
96 changes: 96 additions & 0 deletions
96
src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Bundle\SecurityBundle\Tests\Functional; | ||
|
||
use Symfony\Bundle\FrameworkBundle\Console\Application; | ||
use Symfony\Bundle\SecurityBundle\Command\UserPasswordEncoderCommand; | ||
use Symfony\Component\Console\Tester\CommandTester; | ||
|
||
/** | ||
* Tests UserPasswordEncoderCommand | ||
* | ||
* @author Sarah Khalil <mkhalil.sarah@gmail.com> | ||
*/ | ||
class UserPasswordEncoderCommandTest extends WebTestCase | ||
{ | ||
private $passwordEncoderCommandTester; | ||
|
||
public function testEncodePasswordPasswordPlainText() | ||
{ | ||
$this->passwordEncoderCommandTester->execute(array( | ||
'command' => 'security:encode-password', | ||
'password' => 'password', | ||
'user-class' => 'Symfony\Component\Security\Core\User\User', | ||
'salt' => 'AZERTYUIOPOfghjklytrertyuiolnbcxdfghjkytrfghjk', | ||
)); | ||
$expected = file_get_contents(__DIR__.'/app/PasswordEncode/plaintext.txt'); | ||
|
||
$this->assertEquals($expected, $this->passwordEncoderCommandTester->getDisplay()); | ||
} | ||
|
||
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()); | ||
} | ||
|
||
public function testEncodePasswordPbkdf2() | ||
{ | ||
$this->passwordEncoderCommandTester->execute(array( | ||
'command' => 'security:encode-password', | ||
'password' => 'password', | ||
'user-class' => 'Custom\Class\Pbkdf2\User', | ||
'salt' => 'AZERTYUIOPOfghjklytrertyuiolnbcxdfghjkytrfghjk', | ||
)); | ||
|
||
$expected = file_get_contents(__DIR__.'/app/PasswordEncode/pbkdf2.txt'); | ||
|
||
$this->assertEquals($expected, $this->passwordEncoderCommandTester->getDisplay()); | ||
} | ||
|
||
public function testEncodePasswordNoConfigForGivenUserClass() | ||
{ | ||
$this->setExpectedException('\RuntimeException', 'No encoder has been configured for account "Wrong/User/Class".'); | ||
|
||
$this->passwordEncoderCommandTester->execute(array( | ||
'command' => 'security:encode-password', | ||
'password' => 'password', | ||
'user-class' => 'Wrong/User/Class', | ||
'salt' => 'AZERTYUIOPOfghjklytrertyuiolnbcxdfghjkytrfghjk', | ||
)); | ||
} | ||
|
||
protected function setUp() | ||
{ | ||
$kernel = $this->createKernel(array('test_case' => 'PasswordEncode')); | ||
$kernel->boot(); | ||
|
||
$application = new Application($kernel); | ||
|
||
$application->add(new UserPasswordEncoderCommand()); | ||
$passwordEncoderCommand = $application->find('security:encode-password'); | ||
|
||
$this->passwordEncoderCommandTester = new CommandTester($passwordEncoderCommand); | ||
} | ||
|
||
protected function tearDown() | ||
{ | ||
$this->passwordEncoderCommandTester = null; | ||
} | ||
} |
22 changes: 22 additions & 0 deletions
22
src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/bcrypt.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
|
||
|
||
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. | | ||
+------------------+---------------------------------------------------------------+ |
6 changes: 6 additions & 0 deletions
6
src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/bundles.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
<?php | ||
|
||
return array( | ||
new Symfony\Bundle\SecurityBundle\SecurityBundle(), | ||
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), | ||
); |
21 changes: 21 additions & 0 deletions
21
src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/config.yml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
imports: | ||
- { resource: ./../config/framework.yml } | ||
|
||
security: | ||
encoders: | ||
Symfony\Component\Security\Core\User\User: plaintext | ||
Custom\Class\Bcrypt\User: bcrypt | ||
Custom\Class\Pbkdf2\User: pbkdf2 | ||
Custom\Class\Test\User: test | ||
|
||
providers: | ||
in_memory: | ||
memory: | ||
users: | ||
user: { password: userpass, roles: [ 'ROLE_USER' ] } | ||
admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] } | ||
|
||
firewalls: | ||
test: | ||
pattern: ^/ | ||
security: false |
22 changes: 22 additions & 0 deletions
22
src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/pbkdf2.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
|
||
|
||
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== | | ||
+------------------+---------------------------------------------------------------+ |
Oops, something went wrong.
a7bd0fc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why allow salt to be specified? In fact it's required if you don't want to give command line input? Traditionally people generate weaker salts than desirable and I feel like in a simple convenience method like this it's… well, inconvenient. If you need a specific salt, then perhaps that's sufficiently specialised that you could just do it yourself. As in, the command should hit the common use case and not worry about special cases especially when the cost for everyone else is a potentially weak salt.
If some of the encoders require one to be given instead of generated (I.e not bcrypt) then I'd still recommend changing the argument order: start with the class, then the password, then an optional salt which is generated if not provided. That'd be better DX imo.
a7bd0fc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also didn't mean to sound negative, this is a great idea and a useful contribution, thanks!
a7bd0fc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@inanimatt see #13988 ;)