Skip to content

Commit c91a98f

Browse files
ogizanagifabpot
authored andcommitted
[SecurityBundle] UserPasswordEncoderCommand: Improve & simplify the command usage
1 parent fb703c3 commit c91a98f

File tree

8 files changed

+180
-221
lines changed

8 files changed

+180
-221
lines changed

Command/UserPasswordEncoderCommand.php

Lines changed: 81 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
1515
use Symfony\Component\Console\Input\InputArgument;
1616
use Symfony\Component\Console\Input\InputInterface;
17+
use Symfony\Component\Console\Input\InputOption;
1718
use Symfony\Component\Console\Output\OutputInterface;
1819
use Symfony\Component\Console\Question\Question;
19-
use Symfony\Component\Console\Helper\Table;
20+
use Symfony\Component\Console\Style\SymfonyStyle;
21+
use Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder;
2022

2123
/**
2224
* Encode a user's password.
@@ -32,35 +34,45 @@ protected function configure()
3234
{
3335
$this
3436
->setName('security:encode-password')
35-
->setDescription('Encode a password.')
36-
->addArgument('password', InputArgument::OPTIONAL, 'Enter a password')
37-
->addArgument('user-class', InputArgument::OPTIONAL, 'Enter the user class configured to find the encoder you need.')
38-
->addArgument('salt', InputArgument::OPTIONAL, 'Enter the salt you want to use to encode your password.')
37+
->setDescription('Encodes a password.')
38+
->addArgument('password', InputArgument::OPTIONAL, 'The plain password to encode.')
39+
->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')
40+
->addOption('empty-salt', null, InputOption::VALUE_NONE, 'Do not generate a salt or let the encoder generate one.')
3941
->setHelp(<<<EOF
4042
41-
The <info>%command.name%</info> command allows to encode a password using encoders
42-
that are configured in the application configuration file, under the <comment>security.encoders</comment>.
43+
The <info>%command.name%</info> command encodes passwords according to your
44+
security configuration. This command is mainly used to generate passwords for
45+
the <comment>in_memory</comment> user provider type and for changing passwords
46+
in the database while developing the application.
47+
48+
Suppose that you have the following security configuration in your application:
4349
44-
For instance, if you have the following configuration for your application:
4550
<comment>
46-
security:
47-
encoders:
48-
Symfony\Component\Security\Core\User\User: plaintext
49-
AppBundle\Model\User: bcrypt
51+
# app/config/security.yml
52+
security:
53+
encoders:
54+
Symfony\Component\Security\Core\User\User: plaintext
55+
AppBundle\Entity\User: bcrypt
5056
</comment>
5157
52-
According to the response you will give to the question "<question>Provide your configured user class</question>" your
53-
password will be encoded the way it was configured.
54-
- If you answer "<comment>Symfony\Component\Security\Core\User\User</comment>", the password provided will be encoded
55-
with the <comment>plaintext</comment> encoder.
56-
- If you answer <comment>AppBundle\Model\User</comment>, the password provided will be encoded
57-
with the <comment>bcrypt</comment> encoder.
58+
If you execute the command non-interactively, the default Symfony User class
59+
is used and a random salt is generated to encode the password:
60+
61+
<info>php %command.full_name% --no-interaction [password]</info>
62+
63+
Pass the full user class path as the second argument to encode passwords for
64+
your own entities:
65+
66+
<info>php %command.full_name% --no-interaction [password] AppBundle\Entity\User</info>
5867
59-
The command allows you to provide your own <comment>salt</comment>. If you don't provide any,
60-
the command will take care about that for you.
68+
Executing the command interactively allows you to generate a random salt for
69+
encoding the password:
6170
62-
You can also use the non interactive way by typing the following command:
63-
<info>php %command.full_name% [password] [user-class] [salt]</info>
71+
<info>php %command.full_name% [password] AppBundle\Entity\User</info>
72+
73+
In case your encoder doesn't require a salt, add the <comment>empty-salt</comment> option:
74+
75+
<info>php %command.full_name% --empty-salt [password] AppBundle\Entity\User</info>
6476
6577
EOF
6678
)
@@ -72,154 +84,86 @@ protected function configure()
7284
*/
7385
protected function execute(InputInterface $input, OutputInterface $output)
7486
{
75-
$this->writeIntroduction($output);
87+
$output = new SymfonyStyle($input, $output);
88+
89+
$input->isInteractive() ? $output->title('Symfony Password Encoder Utility') : $output->newLine();
7690

7791
$password = $input->getArgument('password');
78-
$salt = $input->getArgument('salt');
7992
$userClass = $input->getArgument('user-class');
93+
$emptySalt = $input->getOption('empty-salt');
8094

81-
$helper = $this->getHelper('question');
95+
$encoder = $this->getContainer()->get('security.encoder_factory')->getEncoder($userClass);
96+
$bcryptWithoutEmptySalt = !$emptySalt && $encoder instanceof BCryptPasswordEncoder;
97+
98+
if ($bcryptWithoutEmptySalt) {
99+
$emptySalt = true;
100+
}
82101

83102
if (!$password) {
103+
if (!$input->isInteractive()) {
104+
$output->error('The password must not be empty.');
105+
106+
return 1;
107+
}
84108
$passwordQuestion = $this->createPasswordQuestion($input, $output);
85-
$password = $helper->ask($input, $output, $passwordQuestion);
109+
$password = $output->askQuestion($passwordQuestion);
86110
}
87111

88-
if (!$salt) {
89-
$saltQuestion = $this->createSaltQuestion($input, $output);
90-
$salt = $helper->ask($input, $output, $saltQuestion);
91-
}
112+
$salt = null;
113+
114+
if ($input->isInteractive() && !$emptySalt) {
115+
$emptySalt = true;
92116

93-
$output->writeln("\n <comment>Encoders are configured by user type in the security.yml file.</comment>");
117+
$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.');
94118

95-
if (!$userClass) {
96-
$userClassQuestion = $this->createUserClassQuestion($input, $output);
97-
$userClass = $helper->ask($input, $output, $userClassQuestion);
119+
if ($output->confirm('Confirm salt generation ?')) {
120+
$salt = $this->generateSalt();
121+
$emptySalt = false;
122+
}
123+
} elseif (!$emptySalt) {
124+
$salt = $this->generateSalt();
98125
}
99126

100-
$encoder = $this->getContainer()->get('security.encoder_factory')->getEncoder($userClass);
101127
$encodedPassword = $encoder->encodePassword($password, $salt);
102128

103-
$this->writeResult($output);
129+
$rows = array(
130+
array('Encoder used', get_class($encoder)),
131+
array('Encoded password', $encodedPassword),
132+
);
133+
if (!$emptySalt) {
134+
$rows[] = array('Generated salt', $salt);
135+
}
136+
$output->table(array('Key', 'Value'), $rows);
104137

105-
$table = new Table($output);
106-
$table
107-
->setHeaders(array('Key', 'Value'))
108-
->addRow(array('Encoder used', get_class($encoder)))
109-
->addRow(array('Encoded password', $encodedPassword))
110-
;
138+
if (!$emptySalt) {
139+
$output->note(sprintf('Make sure that your salt storage field fits the salt length: %s chars', strlen($salt)));
140+
} elseif ($bcryptWithoutEmptySalt) {
141+
$output->note('Bcrypt encoder used: the encoder generated its own built-in salt.');
142+
}
111143

112-
$table->render();
144+
$output->success('Password encoding succeeded');
113145
}
114146

115147
/**
116148
* Create the password question to ask the user for the password to be encoded.
117149
*
118-
* @param InputInterface $input
119-
* @param OutputInterface $output
120-
*
121150
* @return Question
122151
*/
123-
private function createPasswordQuestion(InputInterface $input, OutputInterface $output)
152+
private function createPasswordQuestion()
124153
{
125-
$passwordQuestion = new Question("\n > <question>Type in your password to be encoded:</question> ");
154+
$passwordQuestion = new Question('Type in your password to be encoded');
126155

127-
$passwordQuestion->setValidator(function ($value) {
156+
return $passwordQuestion->setValidator(function ($value) {
128157
if ('' === trim($value)) {
129158
throw new \Exception('The password must not be empty.');
130159
}
131160

132161
return $value;
133-
});
134-
$passwordQuestion->setHidden(true);
135-
$passwordQuestion->setMaxAttempts(20);
136-
137-
return $passwordQuestion;
138-
}
139-
140-
/**
141-
* Create the question that asks for the salt to perform the encoding.
142-
* If there is no provided salt, a random one is automatically generated.
143-
*
144-
* @param InputInterface $input
145-
* @param OutputInterface $output
146-
*
147-
* @return Question
148-
*/
149-
private function createSaltQuestion(InputInterface $input, OutputInterface $output)
150-
{
151-
$saltQuestion = new Question("\n > (Optional) <question>Provide a salt (press <enter> to generate one):</question> ");
152-
153-
$container = $this->getContainer();
154-
$saltQuestion->setValidator(function ($value) use ($output, $container) {
155-
if ('' === trim($value)) {
156-
$value = base64_encode($container->get('security.secure_random')->nextBytes(30));
157-
158-
$output->writeln("\n<comment>The salt has been generated: </comment>".$value);
159-
$output->writeln(sprintf("<comment>Make sure that your salt storage field fits this salt length: %s chars.</comment>\n", strlen($value)));
160-
}
161-
162-
return $value;
163-
});
164-
165-
return $saltQuestion;
166-
}
167-
168-
/**
169-
* Create the question that asks for the configured user class.
170-
*
171-
* @param InputInterface $input
172-
* @param OutputInterface $output
173-
*
174-
* @return Question
175-
*/
176-
private function createUserClassQuestion(InputInterface $input, OutputInterface $output)
177-
{
178-
$userClassQuestion = new Question(" > <question>Provide your configured user class:</question> ");
179-
$userClassQuestion->setAutocompleterValues(array('Symfony\Component\Security\Core\User\User'));
180-
181-
$userClassQuestion->setValidator(function ($value) use ($output) {
182-
if ('' === trim($value)) {
183-
$value = 'Symfony\Component\Security\Core\User\User';
184-
$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");
185-
}
186-
187-
return $value;
188-
});
189-
190-
return $userClassQuestion;
191-
}
192-
193-
private function writeIntroduction(OutputInterface $output)
194-
{
195-
$output->writeln(array(
196-
'',
197-
$this->getHelperSet()->get('formatter')->formatBlock(
198-
'Symfony Password Encoder Utility',
199-
'bg=blue;fg=white',
200-
true
201-
),
202-
'',
203-
));
204-
205-
$output->writeln(array(
206-
'',
207-
'This command encodes any password you want according to the configuration you',
208-
'made in your configuration file containing the <comment>security.encoders</comment> key.',
209-
'',
210-
));
162+
})->setHidden(true)->setMaxAttempts(20);
211163
}
212164

213-
private function writeResult(OutputInterface $output)
165+
private function generateSalt()
214166
{
215-
$output->writeln(array(
216-
'',
217-
$this->getHelperSet()->get('formatter')->formatBlock(
218-
'✔ Password encoding succeeded',
219-
'bg=green;fg=white',
220-
true
221-
),
222-
'',
223-
));
167+
return base64_encode($this->getContainer()->get('security.secure_random')->nextBytes(30));
224168
}
225169
}

0 commit comments

Comments
 (0)