Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 44c9856

Browse filesBrowse 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 44c9856
Copy full SHA for 44c9856

File tree

11 files changed

+90
-4
lines changed
Filter options

11 files changed

+90
-4
lines changed

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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php
+25Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php
+1Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/Resources/config/schema/security-1.0.xsd
+1Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.php
+1Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php
+7-1Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/authenticator_manager.php
+3Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/authenticator_manager.xml
+2Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/authenticator_manager.yml
+3Lines 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/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
5.3
55
---
66

7+
* Add `CheckRequiredBadgesListener` to validate which badges are present
78
* Deprecate `PersistentTokenInterface::getUsername()` in favor of `PersistentTokenInterface::getUserIdentifier()`
89
* Deprecate `UsernameNotFoundException` in favor of `UserNotFoundException` and `getUsername()`/`setUsername()` in favor of `getUserIdentifier()`/`setUserIdentifier()`
910
* Deprecate `UserProviderInterface::loadUserByUsername()` in favor of `UserProviderInterface::loadUserByIdentifier()`

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php
+11-1Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php
+35-2Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@
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;
2425
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
26+
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
2527
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
2628
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
2729
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
@@ -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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.