Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 2a8a101

Browse files
committed
[Security] Add concept of required passport badges
A badge on a passport is a critical security element, it determines which security checks are run during authentication. Using the `required_badges` setting, applications can make sure the expected security checks are run.
1 parent bb1e1e5 commit 2a8a101

File tree

10 files changed

+89
-4
lines changed

10 files changed

+89
-4
lines changed

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
1616
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
1717
use Symfony\Component\Config\Definition\ConfigurationInterface;
18+
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
1819
use Symfony\Component\Security\Core\Authorization\AccessDecisionManager;
1920
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
2021
use Symfony\Component\Security\Http\Event\LogoutEvent;
@@ -194,6 +195,7 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto
194195
->disallowNewKeysInSubsequentConfigs()
195196
->useAttributeAsKey('name')
196197
->prototype('array')
198+
->fixXmlConfig('required_badge')
197199
->children()
198200
;
199201

@@ -266,6 +268,29 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto
266268
->scalarNode('role')->defaultValue('ROLE_ALLOWED_TO_SWITCH')->end()
267269
->end()
268270
->end()
271+
->arrayNode('required_badges')
272+
->info('A list of badges that must be present on the authenticated passport.')
273+
->validate()
274+
->always()
275+
->then(function ($requiredBadges) {
276+
return array_map(function ($requiredBadge) {
277+
if (class_exists($requiredBadge)) {
278+
return $requiredBadge;
279+
}
280+
281+
if (false === strpos($requiredBadge, '\\')) {
282+
$fqcn = 'Symfony\Component\Security\Http\Authenticator\Passport\Badge\\'.$requiredBadge;
283+
if (class_exists($fqcn)) {
284+
return $fqcn;
285+
}
286+
}
287+
288+
throw new InvalidConfigurationException(sprintf('Undefined security Badge class "%s" set in "security.firewall.required_badges".', $requiredBadge));
289+
}, $requiredBadges);
290+
})
291+
->end()
292+
->prototype('scalar')->end()
293+
->end()
269294
;
270295

271296
$abstractFactoryKeys = [];

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,7 @@ private function createFirewall(ContainerBuilder $container, string $id, array $
495495
->replaceArgument(0, $authenticators)
496496
->replaceArgument(2, new Reference($firewallEventDispatcherId))
497497
->replaceArgument(3, $id)
498+
->replaceArgument(6, $firewall['required_badges'] ?? [])
498499
->addTag('monolog.logger', ['channel' => 'security'])
499500
;
500501

src/Symfony/Bundle/SecurityBundle/Resources/config/schema/security-1.0.xsd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@
172172
<xsd:element name="remember-me" type="remember_me" minOccurs="0" maxOccurs="1" />
173173
<xsd:element name="remote-user" type="remote_user" minOccurs="0" maxOccurs="1" />
174174
<xsd:element name="x509" type="x509" minOccurs="0" maxOccurs="1" />
175+
<xsd:element name="required-badge" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
175176
<!-- allow factories to use dynamic elements -->
176177
<xsd:any processContents="lax" minOccurs="0" maxOccurs="unbounded" />
177178
</xsd:choice>

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
abstract_arg('provider key'),
4747
service('logger')->nullOnInvalid(),
4848
param('security.authentication.manager.erase_credentials'),
49+
abstract_arg('required badges'),
4950
])
5051
->tag('monolog.logger', ['channel' => 'security'])
5152

src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder;
2727
use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder;
2828
use Symfony\Component\Security\Http\Authentication\AuthenticatorManager;
29+
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
30+
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
2931

3032
abstract class CompleteConfigurationTest extends TestCase
3133
{
@@ -37,7 +39,11 @@ public function testAuthenticatorManager()
3739
{
3840
$container = $this->getContainer('authenticator_manager');
3941

40-
$this->assertEquals(AuthenticatorManager::class, $container->getDefinition('security.authenticator.manager.main')->getClass());
42+
$authenticatorManager = $container->getDefinition('security.authenticator.manager.main');
43+
$this->assertEquals(AuthenticatorManager::class, $authenticatorManager->getClass());
44+
45+
// required badges
46+
$this->assertEquals([CsrfTokenBadge::class, RememberMeBadge::class], $authenticatorManager->getArgument(6));
4147

4248
// login link
4349
$expiredStorage = $container->getDefinition($expiredStorageId = 'security.authenticator.expired_login_link_storage.main');

src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/authenticator_manager.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
<?php
22

3+
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
4+
35
$container->loadFromExtension('security', [
46
'enable_authenticator_manager' => true,
57
'firewalls' => [
68
'main' => [
9+
'required_badges' => [CsrfTokenBadge::class, 'RememberMeBadge'],
710
'login_link' => [
811
'check_route' => 'login_check',
912
'check_post_only' => true,

src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/authenticator_manager.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
<config enable-authenticator-manager="true">
1111
<firewall name="main">
12+
<required-badge>Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge</required-badge>
13+
<required-badge>RememberMeBadge</required-badge>
1214
<login-link check-route="login_check"
1315
check-post-only="true"
1416
max-uses="1"

src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/authenticator_manager.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ security:
22
enable_authenticator_manager: true
33
firewalls:
44
main:
5+
required_badges:
6+
- 'Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge'
7+
- RememberMeBadge
58
login_link:
69
check_route: login_check
710
check_post_only: true

src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,18 +50,20 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent
5050
private $eraseCredentials;
5151
private $logger;
5252
private $firewallName;
53+
private $requiredBadges;
5354

5455
/**
5556
* @param AuthenticatorInterface[] $authenticators
5657
*/
57-
public function __construct(iterable $authenticators, TokenStorageInterface $tokenStorage, EventDispatcherInterface $eventDispatcher, string $firewallName, ?LoggerInterface $logger = null, bool $eraseCredentials = true)
58+
public function __construct(iterable $authenticators, TokenStorageInterface $tokenStorage, EventDispatcherInterface $eventDispatcher, string $firewallName, ?LoggerInterface $logger = null, bool $eraseCredentials = true, array $requiredBadges = [])
5859
{
5960
$this->authenticators = $authenticators;
6061
$this->tokenStorage = $tokenStorage;
6162
$this->eventDispatcher = $eventDispatcher;
6263
$this->firewallName = $firewallName;
6364
$this->logger = $logger;
6465
$this->eraseCredentials = $eraseCredentials;
66+
$this->requiredBadges = $requiredBadges;
6567
}
6668

6769
/**
@@ -170,10 +172,18 @@ private function executeAuthenticator(AuthenticatorInterface $authenticator, Req
170172
$this->eventDispatcher->dispatch($event);
171173

172174
// check if all badges are resolved
175+
$resolvedBadges = [];
173176
foreach ($passport->getBadges() as $badge) {
174177
if (!$badge->isResolved()) {
175178
throw new BadCredentialsException(sprintf('Authentication failed: Security badge "%s" is not resolved, did you forget to register the correct listeners?', get_debug_type($badge)));
176179
}
180+
181+
$resolvedBadges[] = \get_class($badge);
182+
}
183+
184+
$missingRequiredBadges = array_diff($this->requiredBadges, $resolvedBadges);
185+
if ($missingRequiredBadges) {
186+
throw new BadCredentialsException(sprintf('Authentication failed; Some badges marked as required by the firewall config are not available on the passport: "%s".', implode('", "', $missingRequiredBadges)));
177187
}
178188

179189
// create the authenticated token

src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@
1717
use Symfony\Component\HttpFoundation\Response;
1818
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
1919
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
20+
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
2021
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
2122
use Symfony\Component\Security\Core\User\InMemoryUser;
2223
use Symfony\Component\Security\Http\Authentication\AuthenticatorManager;
2324
use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface;
25+
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
2426
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
2527
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
2628
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
@@ -133,6 +135,37 @@ public function testNoCredentialsValidated()
133135
$manager->authenticateRequest($this->request);
134136
}
135137

138+
public function testRequiredBadgeMissing()
139+
{
140+
$authenticator = $this->createAuthenticator();
141+
$this->request->attributes->set('_security_authenticators', [$authenticator]);
142+
143+
$authenticator->expects($this->any())->method('authenticate')->willReturn(new SelfValidatingPassport(new UserBadge('wouter')));
144+
145+
$authenticator->expects($this->once())->method('onAuthenticationFailure')->with($this->anything(), $this->callback(function ($exception) {
146+
return 'Authentication failed; Some badges marked as required by the firewall config are not available on the passport: "'.CsrfTokenBadge::class.'".' === $exception->getMessage();
147+
}));
148+
149+
$manager = $this->createManager([$authenticator], 'main', true, [CsrfTokenBadge::class]);
150+
$manager->authenticateRequest($this->request);
151+
}
152+
153+
public function testAllRequiredBadgesPresent()
154+
{
155+
$authenticator = $this->createAuthenticator();
156+
$this->request->attributes->set('_security_authenticators', [$authenticator]);
157+
158+
$csrfBadge = new CsrfTokenBadge('csrfid', 'csrftoken');
159+
$csrfBadge->markResolved();
160+
$authenticator->expects($this->any())->method('authenticate')->willReturn(new SelfValidatingPassport(new UserBadge('wouter'), [$csrfBadge]));
161+
$authenticator->expects($this->any())->method('createAuthenticatedToken')->willReturn(new UsernamePasswordToken($this->user, null, 'main'));
162+
163+
$authenticator->expects($this->once())->method('onAuthenticationSuccess');
164+
165+
$manager = $this->createManager([$authenticator], 'main', true, [CsrfTokenBadge::class]);
166+
$manager->authenticateRequest($this->request);
167+
}
168+
136169
/**
137170
* @dataProvider provideEraseCredentialsData
138171
*/
@@ -243,8 +276,8 @@ private function createAuthenticator($supports = true)
243276
return $authenticator;
244277
}
245278

246-
private function createManager($authenticators, $firewallName = 'main', $eraseCredentials = true)
279+
private function createManager($authenticators, $firewallName = 'main', $eraseCredentials = true, array $requiredBadges = [])
247280
{
248-
return new AuthenticatorManager($authenticators, $this->tokenStorage, $this->eventDispatcher, $firewallName, null, $eraseCredentials);
281+
return new AuthenticatorManager($authenticators, $this->tokenStorage, $this->eventDispatcher, $firewallName, null, $eraseCredentials, $requiredBadges);
249282
}
250283
}

0 commit comments

Comments
 (0)