Skip to content

Commit d77603d

Browse files
committed
Password Policy
1 parent 398830c commit d77603d

File tree

15 files changed

+372
-1
lines changed

15 files changed

+372
-1
lines changed

src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ public function getConfigTreeBuilder(): TreeBuilder
9898
$this->addFirewallsSection($rootNode, $this->factories);
9999
$this->addAccessControlSection($rootNode);
100100
$this->addRoleHierarchySection($rootNode);
101+
$this->addPasswordPolicySection($rootNode);
101102

102103
return $tb;
103104
}
@@ -461,6 +462,21 @@ private function addPasswordHashersSection(ArrayNodeDefinition $rootNode): void
461462
->end();
462463
}
463464

465+
private function addPasswordPolicySection(ArrayNodeDefinition $rootNode): void
466+
{
467+
$rootNode
468+
->fixXmlConfig('password_policy')
469+
->children()
470+
->arrayNode('password_policies')
471+
->treatNullLike([])
472+
->treatFalseLike([])
473+
->treatTrueLike([])
474+
->prototype('scalar')->end()
475+
->end()
476+
->end()
477+
;
478+
}
479+
464480
private function getAccessDecisionStrategies(): array
465481
{
466482
return [

src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,13 @@ public function load(array $configs, ContainerBuilder $container)
190190
$container->getDefinition('security.access_listener')->setArgument(3, false);
191191
$container->getDefinition('security.authorization_checker')->setArgument(2, false);
192192
$container->getDefinition('security.authorization_checker')->setArgument(3, false);
193+
194+
if (!$config['password_policies']) {
195+
$container->removeDefinition('security.password_policy_listener');
196+
} else {
197+
$policyServices = array_map(static fn (string $id) => new Reference($id), $config['password_policies']);
198+
$container->getDefinition('security.password_policy_listener')->setArgument(0, $policyServices);
199+
}
193200
}
194201

195202
/**

src/Symfony/Bundle/SecurityBundle/Resources/config/security.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
use Symfony\Component\Security\Http\Controller\SecurityTokenValueResolver;
4646
use Symfony\Component\Security\Http\Controller\UserValueResolver;
4747
use Symfony\Component\Security\Http\EventListener\IsGrantedAttributeListener;
48+
use Symfony\Component\Security\Http\EventListener\PasswordPolicyListener;
4849
use Symfony\Component\Security\Http\Firewall;
4950
use Symfony\Component\Security\Http\FirewallMapInterface;
5051
use Symfony\Component\Security\Http\HttpUtils;
@@ -298,5 +299,11 @@
298299
->set('cache.security_is_granted_attribute_expression_language')
299300
->parent('cache.system')
300301
->tag('cache.pool')
302+
303+
->set('security.password_policy_listener', PasswordPolicyListener::class)
304+
->args([
305+
[],
306+
])
307+
->tag('kernel.event_subscriber')
301308
;
302309
};
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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\Component\HttpFoundation\JsonResponse;
15+
16+
class PasswordPolicyTest extends AbstractWebTestCase
17+
{
18+
/**
19+
* @dataProvider providePassword
20+
*/
21+
public function testLoginFailBecauseThePasswordIsBlacklisted(string $password, string $expectedMessage)
22+
{
23+
// Given
24+
$client = $this->createClient(['test_case' => 'PasswordPolicy', 'root_config' => 'config.yml']);
25+
26+
// When
27+
$client->request('POST', '/chk', [], [], ['CONTENT_TYPE' => 'application/json'], '{"user": {"login": "dunglas", "password": "'.$password.'"}}');
28+
$response = $client->getResponse();
29+
30+
// Then
31+
$this->assertInstanceOf(JsonResponse::class, $response);
32+
$this->assertSame(401, $response->getStatusCode());
33+
$this->assertSame(['error' => $expectedMessage], json_decode($response->getContent(), true));
34+
}
35+
36+
public static function providePassword(): iterable
37+
{
38+
yield ['foo', 'The password does not fulfill the password policy.'];
39+
yield ['short?', 'The password does not fulfill the password policy.'];
40+
yield ['Good password?', 'The password does not fulfill the password policy.'];
41+
42+
// The following password fulfills the password policy, but is not valid.
43+
yield ['Good pas861g4c8r1xq68zfx1sword?', 'Invalid credentials.'];
44+
}
45+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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\app\PasswordPolicy;
13+
14+
use Symfony\Component\Security\Http\Authenticator\Passport\Policy\PasswordPolicyInterface;
15+
16+
final class BlocklistPasswordPolicy implements PasswordPolicyInterface
17+
{
18+
public function verify(#[\SensitiveParameter] string $plaintextPassword): bool
19+
{
20+
return 'foo' !== $plaintextPassword;
21+
}
22+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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+
return [
13+
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
14+
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
15+
new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle(),
16+
];
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
imports:
2+
- { resource: ./../config/framework.yml }
3+
4+
framework:
5+
http_method_override: false
6+
serializer: ~
7+
8+
security:
9+
password_policies:
10+
- 'policy.blocklist'
11+
- 'policy.constraint_password'
12+
13+
password_hashers:
14+
Symfony\Component\Security\Core\User\InMemoryUser: plaintext
15+
16+
providers:
17+
in_memory:
18+
memory:
19+
users:
20+
dunglas: { password: foo, roles: [ROLE_USER] }
21+
22+
firewalls:
23+
main:
24+
pattern: ^/
25+
json_login:
26+
check_path: /chk
27+
username_path: user.login
28+
password_path: user.password
29+
30+
access_control:
31+
- { path: ^/foo, roles: ROLE_USER }
32+
33+
34+
services:
35+
policy.constraint_password.length:
36+
class: Symfony\Component\Validator\Constraints\Length
37+
arguments:
38+
- min: 8
39+
policy.constraint_password.strength:
40+
class: Symfony\Component\Validator\Constraints\PasswordStrength
41+
arguments:
42+
- minScore: 2
43+
44+
policy.constraint_password:
45+
class: Symfony\Component\Security\Http\Authenticator\Passport\Policy\PasswordConstraintPolicy
46+
arguments:
47+
- '@validator'
48+
- ['@policy.constraint_password.length', '@policy.constraint_password.strength']
49+
50+
policy.blocklist:
51+
class: Symfony\Bundle\SecurityBundle\Tests\Functional\app\PasswordPolicy\BlocklistPasswordPolicy
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
login_check:
2+
path: /chk
3+
defaults: { _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\JsonLoginBundle\Controller\TestController::loginCheckAction }

src/Symfony/Component/Security/Core/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ CHANGELOG
2222
* Remove all classes in the `Core\Encoder\` sub-namespace, use the `PasswordHasher` component instead
2323
* Remove methods `getPassword()` and `getSalt()` from `UserInterface`, use `PasswordAuthenticatedUserInterface`
2424
or `LegacyPasswordAuthenticatedUserInterface` instead
25-
* `AccessDecisionManager` requires the strategy to be passed as in instance of `AccessDecisionStrategyInterface`
25+
* `AccessDecisionManager` requires the strategy to be passed as in instance of `AccessDecisionStrategyInterface`
2626

2727
5.4.21
2828
------
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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\Component\Security\Core\Exception;
13+
14+
final class PasswordPolicyException extends AuthenticationException
15+
{
16+
public function getMessageKey(): string
17+
{
18+
return 'The password does not fulfill the password policy.';
19+
}
20+
}

0 commit comments

Comments
 (0)