diff --git a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md index fe22755dd3..4294c012c0 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 6befc9319b..942e27d7ec 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -15,6 +15,7 @@ use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AbstractF 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 @@ class MainConfiguration implements ConfigurationInterface ->disallowNewKeysInSubsequentConfigs() ->useAttributeAsKey('name') ->prototype('array') + ->fixXmlConfig('required_badge') ->children() ; @@ -266,6 +268,29 @@ class MainConfiguration implements ConfigurationInterface ->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 3f358b7319..2f5c674fb8 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -495,6 +495,7 @@ class SecurityExtension extends Extension implements PrependExtensionInterface ->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 f7b9790f4a..3de6b98b38 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 1bd7723634..57c2afeada 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 @@ return static function (ContainerConfigurator $container) { 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 f192ee614c..be3e8d5e43 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\Authorization\AccessDecisionManager; 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 @@ abstract class CompleteConfigurationTest extends TestCase { $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 31a37fe210..fa53fb980f 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 2a3b643a6e..0185b81c44 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 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent $this->firewallName = $firewallName; $this->logger = $logger; $this->eraseCredentials = $eraseCredentials; + $this->requiredBadges = $requiredBadges; } /** @@ -170,10 +172,18 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent $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 89f0ddf07a..f03bd85bd2 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\Request; 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 @@ class AuthenticatorManagerTest extends TestCase $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 @@ class AuthenticatorManagerTest extends TestCase 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); } }