Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
[SecurityBundle] UserPasswordEncoderCommand: Improve & simplify the c…
…ommand usage
  • Loading branch information
ogizanagi authored and fabpot committed Apr 7, 2015
1 parent aa82fb0 commit b3f6340
Show file tree
Hide file tree
Showing 8 changed files with 180 additions and 221 deletions.
218 changes: 81 additions & 137 deletions src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php
Expand Up @@ -14,9 +14,11 @@
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.
Expand All @@ -32,35 +34,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(<<<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>.
The <info>%command.name%</info> command encodes passwords according to your
security configuration. This command is mainly used to generate passwords for
the <comment>in_memory</comment> 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:
<comment>
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
</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.
If you execute the command non-interactively, the default Symfony User class
is used and a random salt is generated to encode the password:
<info>php %command.full_name% --no-interaction [password]</info>
Pass the full user class path as the second argument to encode passwords for
your own entities:
<info>php %command.full_name% --no-interaction [password] AppBundle\Entity\User</info>
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.
Executing the command interactively allows you to generate a random salt for
encoding the password:
You can also use the non interactive way by typing the following command:
<info>php %command.full_name% [password] [user-class] [salt]</info>
<info>php %command.full_name% [password] AppBundle\Entity\User</info>
In case your encoder doesn't require a salt, add the <comment>empty-salt</comment> option:
<info>php %command.full_name% --empty-salt [password] AppBundle\Entity\User</info>
EOF
)
Expand All @@ -72,154 +84,86 @@ 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');
$salt = $input->getArgument('salt');
$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()) {
$output->error('The password must not be empty.');

return 1;
}
$passwordQuestion = $this->createPasswordQuestion($input, $output);
$password = $helper->ask($input, $output, $passwordQuestion);
$password = $output->askQuestion($passwordQuestion);
}

if (!$salt) {
$saltQuestion = $this->createSaltQuestion($input, $output);
$salt = $helper->ask($input, $output, $saltQuestion);
}
$salt = null;

if ($input->isInteractive() && !$emptySalt) {
$emptySalt = true;

$output->writeln("\n <comment>Encoders are configured by user type in the security.yml file.</comment>");
$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 (!$userClass) {
$userClassQuestion = $this->createUserClassQuestion($input, $output);
$userClass = $helper->ask($input, $output, $userClassQuestion);
if ($output->confirm('Confirm salt generation ?')) {
$salt = $this->generateSalt();
$emptySalt = false;
}
} elseif (!$emptySalt) {
$salt = $this->generateSalt();
}

$encoder = $this->getContainer()->get('security.encoder_factory')->getEncoder($userClass);
$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 > <question>Type in your password to be encoded:</question> ");
$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 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 = base64_encode($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.',
'',
));
})->setHidden(true)->setMaxAttempts(20);
}

private function writeResult(OutputInterface $output)
private function generateSalt()
{
$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));
}
}

0 comments on commit b3f6340

Please sign in to comment.