Skip to content

Commit fa11c06

Browse files
committed
feature #12818 [2.7][SecurityBundle] Added a command to encode a password (saro0h)
This PR was merged into the 2.7 branch. Discussion ---------- [2.7][SecurityBundle] Added a command to encode a password | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #11206 | License | MIT | Doc PR | ~ - [x] Give some love to the UI (need your feedbacks) - [x] Write tests - [x] Write documentation ------------------------------------------------------- The encoder got depend directly from the configuration of the user class in the security.yml. So the user choose the user type, and, using the method `getEncoder($user)` of the `security.encoder_factory` service, I get the right encoder configured. *Here is the output for `security:encode-password`*: ![capture d ecran 2015-01-01 a 19 48 07](https://cloud.githubusercontent.com/assets/667519/5593008/3af45686-91ef-11e4-8024-e66a2e000fbe.png) Commits ------- a7bd0fc Added a command to encode a password
2 parents 3f0fd7c + a7bd0fc commit fa11c06

File tree

8 files changed

+417
-2
lines changed

8 files changed

+417
-2
lines changed
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\SecurityBundle\Command;
13+
14+
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
15+
use Symfony\Component\Console\Input\InputArgument;
16+
use Symfony\Component\Console\Input\InputInterface;
17+
use Symfony\Component\Console\Output\OutputInterface;
18+
use Symfony\Component\Console\Question\Question;
19+
use Symfony\Component\Console\Helper\Table;
20+
21+
/**
22+
* Encode a user's password.
23+
*
24+
* @author Sarah Khalil <mkhalil.sarah@gmail.com>
25+
*/
26+
class UserPasswordEncoderCommand extends ContainerAwareCommand
27+
{
28+
/**
29+
* {@inheritdoc}
30+
*/
31+
protected function configure()
32+
{
33+
$this
34+
->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.')
39+
->setHelp(<<<EOF
40+
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+
44+
For instance, if you have the following configuration for your application:
45+
<comment>
46+
security:
47+
encoders:
48+
Symfony\Component\Security\Core\User\User: plaintext
49+
AppBundle\Model\User: bcrypt
50+
</comment>
51+
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+
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.
61+
62+
You can also use the non interactive way by typing the following command:
63+
<info>php %command.full_name% [password] [salt] [user-class]</info>
64+
65+
EOF
66+
)
67+
;
68+
}
69+
70+
/**
71+
* {@inheritdoc}
72+
*/
73+
protected function execute(InputInterface $input, OutputInterface $output)
74+
{
75+
$this->writeIntroduction($output);
76+
77+
$password = $input->getArgument('password');
78+
$salt = $input->getArgument('salt');
79+
$userClass = $input->getArgument('user-class');
80+
81+
$helper = $this->getHelper('question');
82+
83+
if (!$password) {
84+
$passwordQuestion = $this->createPasswordQuestion($input, $output);
85+
$password = $helper->ask($input, $output, $passwordQuestion);
86+
}
87+
88+
if (!$salt) {
89+
$saltQuestion = $this->createSaltQuestion($input, $output);
90+
$salt = $helper->ask($input, $output, $saltQuestion);
91+
}
92+
93+
$output->writeln("\n <comment>Encoders are configured by user type in the security.yml file.</comment>");
94+
95+
if (!$userClass) {
96+
$userClassQuestion = $this->createUserClassQuestion($input, $output);
97+
$userClass = $helper->ask($input, $output, $userClassQuestion);
98+
}
99+
100+
$encoder = $this->getContainer()->get('security.encoder_factory')->getEncoder($userClass);
101+
$encodedPassword = $encoder->encodePassword($password, $salt);
102+
103+
$this->writeResult($output);
104+
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+
;
111+
112+
$table->render();
113+
}
114+
115+
/**
116+
* Create the password question to ask the user for the password to be encoded.
117+
*
118+
* @param InputInterface $input
119+
* @param OutputInterface $output
120+
*
121+
* @return Question
122+
*/
123+
private function createPasswordQuestion(InputInterface $input, OutputInterface $output)
124+
{
125+
$passwordQuestion = new Question("\n > <question>Type in your password to be encoded:</question> ");
126+
127+
$passwordQuestion->setValidator(function ($value) {
128+
if ('' === trim($value)) {
129+
throw new \Exception('The password must not be empty.');
130+
}
131+
132+
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 = hash('sha512', $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+
));
211+
}
212+
213+
private function writeResult(OutputInterface $output)
214+
{
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+
));
224+
}
225+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\SecurityBundle\Tests\Functional;
13+
14+
use Symfony\Bundle\FrameworkBundle\Console\Application;
15+
use Symfony\Bundle\SecurityBundle\Command\UserPasswordEncoderCommand;
16+
use Symfony\Component\Console\Tester\CommandTester;
17+
18+
/**
19+
* Tests UserPasswordEncoderCommand
20+
*
21+
* @author Sarah Khalil <mkhalil.sarah@gmail.com>
22+
*/
23+
class UserPasswordEncoderCommandTest extends WebTestCase
24+
{
25+
private $passwordEncoderCommandTester;
26+
27+
public function testEncodePasswordPasswordPlainText()
28+
{
29+
$this->passwordEncoderCommandTester->execute(array(
30+
'command' => 'security:encode-password',
31+
'password' => 'password',
32+
'user-class' => 'Symfony\Component\Security\Core\User\User',
33+
'salt' => 'AZERTYUIOPOfghjklytrertyuiolnbcxdfghjkytrfghjk',
34+
));
35+
$expected = file_get_contents(__DIR__.'/app/PasswordEncode/plaintext.txt');
36+
37+
$this->assertEquals($expected, $this->passwordEncoderCommandTester->getDisplay());
38+
}
39+
40+
public function testEncodePasswordBcrypt()
41+
{
42+
$this->passwordEncoderCommandTester->execute(array(
43+
'command' => 'security:encode-password',
44+
'password' => 'password',
45+
'user-class' => 'Custom\Class\Bcrypt\User',
46+
'salt' => 'AZERTYUIOPOfghjklytrertyuiolnbcxdfghjkytrfghjk',
47+
));
48+
$expected = file_get_contents(__DIR__.'/app/PasswordEncode/bcrypt.txt');
49+
50+
$this->assertEquals($expected, $this->passwordEncoderCommandTester->getDisplay());
51+
}
52+
53+
public function testEncodePasswordPbkdf2()
54+
{
55+
$this->passwordEncoderCommandTester->execute(array(
56+
'command' => 'security:encode-password',
57+
'password' => 'password',
58+
'user-class' => 'Custom\Class\Pbkdf2\User',
59+
'salt' => 'AZERTYUIOPOfghjklytrertyuiolnbcxdfghjkytrfghjk',
60+
));
61+
62+
$expected = file_get_contents(__DIR__.'/app/PasswordEncode/pbkdf2.txt');
63+
64+
$this->assertEquals($expected, $this->passwordEncoderCommandTester->getDisplay());
65+
}
66+
67+
public function testEncodePasswordNoConfigForGivenUserClass()
68+
{
69+
$this->setExpectedException('\RuntimeException', 'No encoder has been configured for account "Wrong/User/Class".');
70+
71+
$this->passwordEncoderCommandTester->execute(array(
72+
'command' => 'security:encode-password',
73+
'password' => 'password',
74+
'user-class' => 'Wrong/User/Class',
75+
'salt' => 'AZERTYUIOPOfghjklytrertyuiolnbcxdfghjkytrfghjk',
76+
));
77+
}
78+
79+
protected function setUp()
80+
{
81+
$kernel = $this->createKernel(array('test_case' => 'PasswordEncode'));
82+
$kernel->boot();
83+
84+
$application = new Application($kernel);
85+
86+
$application->add(new UserPasswordEncoderCommand());
87+
$passwordEncoderCommand = $application->find('security:encode-password');
88+
89+
$this->passwordEncoderCommandTester = new CommandTester($passwordEncoderCommand);
90+
}
91+
92+
protected function tearDown()
93+
{
94+
$this->passwordEncoderCommandTester = null;
95+
}
96+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
2+
3+
Symfony Password Encoder Utility
4+
5+
6+
7+
This command encodes any password you want according to the configuration you
8+
made in your configuration file containing the security.encoders key.
9+
10+
11+
Encoders are configured by user type in the security.yml file.
12+
13+
14+
✔ Password encoding succeeded
15+
16+
17+
+------------------+---------------------------------------------------------------+
18+
| Key | Value |
19+
+------------------+---------------------------------------------------------------+
20+
| Encoder used | Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder |
21+
| Encoded password | $2y$13$AZERTYUIOPOfghjklytreeBTRM4Wd.D3IW7dtnQ6xGA7z3fY8zg4. |
22+
+------------------+---------------------------------------------------------------+
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?php
2+
3+
return array(
4+
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
5+
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
6+
);
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
imports:
2+
- { resource: ./../config/framework.yml }
3+
4+
security:
5+
encoders:
6+
Symfony\Component\Security\Core\User\User: plaintext
7+
Custom\Class\Bcrypt\User: bcrypt
8+
Custom\Class\Pbkdf2\User: pbkdf2
9+
Custom\Class\Test\User: test
10+
11+
providers:
12+
in_memory:
13+
memory:
14+
users:
15+
user: { password: userpass, roles: [ 'ROLE_USER' ] }
16+
admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] }
17+
18+
firewalls:
19+
test:
20+
pattern: ^/
21+
security: false
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
2+
3+
Symfony Password Encoder Utility
4+
5+
6+
7+
This command encodes any password you want according to the configuration you
8+
made in your configuration file containing the security.encoders key.
9+
10+
11+
Encoders are configured by user type in the security.yml file.
12+
13+
14+
✔ Password encoding succeeded
15+
16+
17+
+------------------+---------------------------------------------------------------+
18+
| Key | Value |
19+
+------------------+---------------------------------------------------------------+
20+
| Encoder used | Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder |
21+
| Encoded password | nvGk/kUwqj6PHzmqUqXxJA6GEhxD1TSJziV8P4ThqsEi4ZHF6yHp6g== |
22+
+------------------+---------------------------------------------------------------+

0 commit comments

Comments
 (0)