diff --git a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md
index fe22755dd3099..4294c012c0ee6 100644
--- a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md
+++ b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md
@@ -4,6 +4,7 @@ CHANGELOG
5.3
---
+ * Add `required_badges` firewall config option
* [BC break] Add `login_throttling.lock_factory` setting defaulting to `null` (instead of `lock.factory`)
* Add a `login_throttling.interval` (in `security.firewalls`) option to change the default throttling interval.
* Add the `debug:firewall` command.
diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php
index 6befc9319bfef..942e27d7ec109 100644
--- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php
+++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php
@@ -15,6 +15,7 @@
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
+use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManager;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
use Symfony\Component\Security\Http\Event\LogoutEvent;
@@ -194,6 +195,7 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto
->disallowNewKeysInSubsequentConfigs()
->useAttributeAsKey('name')
->prototype('array')
+ ->fixXmlConfig('required_badge')
->children()
;
@@ -266,6 +268,29 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto
->scalarNode('role')->defaultValue('ROLE_ALLOWED_TO_SWITCH')->end()
->end()
->end()
+ ->arrayNode('required_badges')
+ ->info('A list of badges that must be present on the authenticated passport.')
+ ->validate()
+ ->always()
+ ->then(function ($requiredBadges) {
+ return array_map(function ($requiredBadge) {
+ if (class_exists($requiredBadge)) {
+ return $requiredBadge;
+ }
+
+ if (false === strpos($requiredBadge, '\\')) {
+ $fqcn = 'Symfony\Component\Security\Http\Authenticator\Passport\Badge\\'.$requiredBadge;
+ if (class_exists($fqcn)) {
+ return $fqcn;
+ }
+ }
+
+ throw new InvalidConfigurationException(sprintf('Undefined security Badge class "%s" set in "security.firewall.required_badges".', $requiredBadge));
+ }, $requiredBadges);
+ })
+ ->end()
+ ->prototype('scalar')->end()
+ ->end()
;
$abstractFactoryKeys = [];
diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php
index 3f358b731903f..2f5c674fb873e 100644
--- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php
+++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php
@@ -495,6 +495,7 @@ private function createFirewall(ContainerBuilder $container, string $id, array $
->replaceArgument(0, $authenticators)
->replaceArgument(2, new Reference($firewallEventDispatcherId))
->replaceArgument(3, $id)
+ ->replaceArgument(6, $firewall['required_badges'] ?? [])
->addTag('monolog.logger', ['channel' => 'security'])
;
diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/schema/security-1.0.xsd b/src/Symfony/Bundle/SecurityBundle/Resources/config/schema/security-1.0.xsd
index f7b9790f4a810..3de6b98b384e0 100644
--- a/src/Symfony/Bundle/SecurityBundle/Resources/config/schema/security-1.0.xsd
+++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/schema/security-1.0.xsd
@@ -172,6 +172,7 @@
+
diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.php
index 1bd7723634f38..57c2afeadadd3 100644
--- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.php
+++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.php
@@ -46,6 +46,7 @@
abstract_arg('provider key'),
service('logger')->nullOnInvalid(),
param('security.authentication.manager.erase_credentials'),
+ abstract_arg('required badges'),
])
->tag('monolog.logger', ['channel' => 'security'])
diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php
index f192ee614cbb6..be3e8d5e4307d 100644
--- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php
+++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php
@@ -26,6 +26,8 @@
use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder;
use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder;
use Symfony\Component\Security\Http\Authentication\AuthenticatorManager;
+use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
+use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
abstract class CompleteConfigurationTest extends TestCase
{
@@ -37,7 +39,11 @@ public function testAuthenticatorManager()
{
$container = $this->getContainer('authenticator_manager');
- $this->assertEquals(AuthenticatorManager::class, $container->getDefinition('security.authenticator.manager.main')->getClass());
+ $authenticatorManager = $container->getDefinition('security.authenticator.manager.main');
+ $this->assertEquals(AuthenticatorManager::class, $authenticatorManager->getClass());
+
+ // required badges
+ $this->assertEquals([CsrfTokenBadge::class, RememberMeBadge::class], $authenticatorManager->getArgument(6));
// login link
$expiredStorage = $container->getDefinition($expiredStorageId = 'security.authenticator.expired_login_link_storage.main');
diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/authenticator_manager.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/authenticator_manager.php
index 31a37fe2103f9..fa53fb980f67a 100644
--- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/authenticator_manager.php
+++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/authenticator_manager.php
@@ -1,9 +1,12 @@
loadFromExtension('security', [
'enable_authenticator_manager' => true,
'firewalls' => [
'main' => [
+ 'required_badges' => [CsrfTokenBadge::class, 'RememberMeBadge'],
'login_link' => [
'check_route' => 'login_check',
'check_post_only' => true,
diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/authenticator_manager.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/authenticator_manager.xml
index 2a3b643a6e905..0185b81c440c8 100644
--- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/authenticator_manager.xml
+++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/authenticator_manager.xml
@@ -9,6 +9,8 @@
+ Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge
+ RememberMeBadge
authenticators = $authenticators;
$this->tokenStorage = $tokenStorage;
@@ -62,6 +63,7 @@ public function __construct(iterable $authenticators, TokenStorageInterface $tok
$this->firewallName = $firewallName;
$this->logger = $logger;
$this->eraseCredentials = $eraseCredentials;
+ $this->requiredBadges = $requiredBadges;
}
/**
@@ -170,10 +172,18 @@ private function executeAuthenticator(AuthenticatorInterface $authenticator, Req
$this->eventDispatcher->dispatch($event);
// check if all badges are resolved
+ $resolvedBadges = [];
foreach ($passport->getBadges() as $badge) {
if (!$badge->isResolved()) {
throw new BadCredentialsException(sprintf('Authentication failed: Security badge "%s" is not resolved, did you forget to register the correct listeners?', get_debug_type($badge)));
}
+
+ $resolvedBadges[] = \get_class($badge);
+ }
+
+ $missingRequiredBadges = array_diff($this->requiredBadges, $resolvedBadges);
+ if ($missingRequiredBadges) {
+ throw new BadCredentialsException(sprintf('Authentication failed; Some badges marked as required by the firewall config are not available on the passport: "%s".', implode('", "', $missingRequiredBadges)));
}
// create the authenticated token
diff --git a/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php b/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php
index 89f0ddf07a10a..f03bd85bd27b9 100644
--- a/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php
+++ b/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php
@@ -17,10 +17,12 @@
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
+use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\User\InMemoryUser;
use Symfony\Component\Security\Http\Authentication\AuthenticatorManager;
use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface;
+use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
@@ -133,6 +135,37 @@ public function testNoCredentialsValidated()
$manager->authenticateRequest($this->request);
}
+ public function testRequiredBadgeMissing()
+ {
+ $authenticator = $this->createAuthenticator();
+ $this->request->attributes->set('_security_authenticators', [$authenticator]);
+
+ $authenticator->expects($this->any())->method('authenticate')->willReturn(new SelfValidatingPassport(new UserBadge('wouter')));
+
+ $authenticator->expects($this->once())->method('onAuthenticationFailure')->with($this->anything(), $this->callback(function ($exception) {
+ return 'Authentication failed; Some badges marked as required by the firewall config are not available on the passport: "'.CsrfTokenBadge::class.'".' === $exception->getMessage();
+ }));
+
+ $manager = $this->createManager([$authenticator], 'main', true, [CsrfTokenBadge::class]);
+ $manager->authenticateRequest($this->request);
+ }
+
+ public function testAllRequiredBadgesPresent()
+ {
+ $authenticator = $this->createAuthenticator();
+ $this->request->attributes->set('_security_authenticators', [$authenticator]);
+
+ $csrfBadge = new CsrfTokenBadge('csrfid', 'csrftoken');
+ $csrfBadge->markResolved();
+ $authenticator->expects($this->any())->method('authenticate')->willReturn(new SelfValidatingPassport(new UserBadge('wouter'), [$csrfBadge]));
+ $authenticator->expects($this->any())->method('createAuthenticatedToken')->willReturn(new UsernamePasswordToken($this->user, null, 'main'));
+
+ $authenticator->expects($this->once())->method('onAuthenticationSuccess');
+
+ $manager = $this->createManager([$authenticator], 'main', true, [CsrfTokenBadge::class]);
+ $manager->authenticateRequest($this->request);
+ }
+
/**
* @dataProvider provideEraseCredentialsData
*/
@@ -243,8 +276,8 @@ private function createAuthenticator($supports = true)
return $authenticator;
}
- private function createManager($authenticators, $firewallName = 'main', $eraseCredentials = true)
+ private function createManager($authenticators, $firewallName = 'main', $eraseCredentials = true, array $requiredBadges = [])
{
- return new AuthenticatorManager($authenticators, $this->tokenStorage, $this->eventDispatcher, $firewallName, null, $eraseCredentials);
+ return new AuthenticatorManager($authenticators, $this->tokenStorage, $this->eventDispatcher, $firewallName, null, $eraseCredentials, $requiredBadges);
}
}