Skip to content

Commit

Permalink
Added a command to encode a password
Browse files Browse the repository at this point in the history
  • Loading branch information
saro0h committed Mar 17, 2015
1 parent 59ca5b3 commit a7bd0fc
Show file tree
Hide file tree
Showing 8 changed files with 417 additions and 2 deletions.
@@ -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
),
'',
));
}
}
@@ -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;
}
}
@@ -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. |
+------------------+---------------------------------------------------------------+
@@ -0,0 +1,6 @@
<?php

return array(
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
);
@@ -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
@@ -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== |
+------------------+---------------------------------------------------------------+

3 comments on commit a7bd0fc

@mattattui
Copy link
Contributor

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.

@mattattui
Copy link
Contributor

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!

@sstok
Copy link
Contributor

@sstok sstok commented on a7bd0fc Mar 22, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@inanimatt see #13988 ;)

Please sign in to comment.