Skip to content

[2.7][SecurityBundle] Added a command to encode a password #12818

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 19, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member

Choose a reason for hiding this comment

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

we should allow to pass the class name as an argument for a non-interactive usage.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, let me do that

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';
Copy link
Contributor

Choose a reason for hiding this comment

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

IMHO this should be an class variable not "hidden" & hardcoded one...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What do you mean?

Copy link
Contributor

Choose a reason for hiding this comment

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

With your approach even if you extend this class you need to replace whole method (copy&paste) to replace default class value...

Copy link
Contributor

Choose a reason for hiding this comment

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

@stloyd I can't understand what you're meaning! This method has been made private because there are really few chances someone wants to override it IMO.

Copy link
Member

Choose a reason for hiding this comment

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

@saro0h Would it be possible to load the configuration and propose a choice amongst the configure user classes? That would be much more easier for end users (typing a fully-qualified class name without a typo is hard on the CLI -- at least for me.)

$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");
Copy link
Contributor

Choose a reason for hiding this comment

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

What about a default value?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can't because, there is no way to access the security.encoders key to get all user classes configured.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think he suggests to set Symfony\Component\Security\Core\User\User as the default value, no matter what user classes are configured under security.encoders.
I personally did this in my own implementation of this command, as I mainly use it for in_memory users.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, that is what I mean. Since you configure Symfony\Component\Security\Core\User\User in the setAutocompleterValues and you set the $value to Symfony\Component\Security\Core\User\User if null, I guess you can configure the default value of the user-class.

}

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
),
'',
));
}
}
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;
}
}
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. |
+------------------+---------------------------------------------------------------+
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(),
);
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
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== |
+------------------+---------------------------------------------------------------+
Loading