diff --git a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md index ffb44752149b4..3750985e24a29 100644 --- a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG --- * Add `Security::isGrantedForUser()` to test user authorization without relying on the session. For example, users not currently logged in, or while processing a message from a message queue + * Add `security.firewalls.not_full_fledged_handler` option to configure behavior where user is not full fledged 7.2 --- diff --git a/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php b/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php index f3c1cd1fe34af..23d031087b2db 100644 --- a/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php +++ b/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php @@ -176,6 +176,7 @@ public function collect(Request $request, Response $response, ?\Throwable $excep 'access_denied_url' => $firewallConfig->getAccessDeniedUrl(), 'user_checker' => $firewallConfig->getUserChecker(), 'authenticators' => $firewallConfig->getAuthenticators(), + 'not_full_fledged_handler' => $firewallConfig->getNotFullFledgedHandler(), ]; // generate exit impersonation path from current request diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php index a45276066484c..2d5fb8cf53f75 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -17,6 +17,8 @@ use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; +use Symfony\Component\Security\Http\Authorization\NotFullFledgedEqualNormalLoginHandler; +use Symfony\Component\Security\Http\Authorization\NotFullFledgedRedirectToStartAuthenticationHandler; use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface; use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy; @@ -214,6 +216,17 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto ->booleanNode('stateless')->defaultFalse()->end() ->booleanNode('lazy')->defaultFalse()->end() ->scalarNode('context')->cannotBeEmpty()->end() + ->scalarNode('not_full_fledged_handler') + ->defaultValue(NotFullFledgedRedirectToStartAuthenticationHandler::class) + ->beforeNormalization() + ->ifTrue(fn ($v): bool => 'redirect' == $v) + ->then(fn ($v) => NotFullFledgedRedirectToStartAuthenticationHandler::class) + ->end() + ->beforeNormalization() + ->ifTrue(fn ($v): bool => 'equal' == $v) + ->then(fn ($v) => NotFullFledgedEqualNormalLoginHandler::class) + ->end() + ->end() ->arrayNode('logout') ->treatTrueLike([]) ->canBeUnset() diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index f454b9318c183..e834df36b6002 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -582,6 +582,7 @@ private function createFirewall(ContainerBuilder $container, string $id, array $ $config->replaceArgument(10, $listenerKeys); $config->replaceArgument(11, $firewall['switch_user'] ?? null); + $config->replaceArgument(13, $firewall['not_full_fledged_handler'] ?? null); return [$matcher, $listeners, $exceptionListener, null !== $logoutListenerId ? new Reference($logoutListenerId) : null, $firewallAuthenticationProviders]; } @@ -890,6 +891,10 @@ private function createExceptionListener(ContainerBuilder $container, array $con $listener->replaceArgument(5, $config['access_denied_url']); } + if (isset($config['not_full_fledged_handler'])) { + $listener->replaceArgument(9, new Reference($config['not_full_fledged_handler'])); + } + return $exceptionListenerId; } diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.php index bd879973b49a3..0d60bbd2091ac 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.php +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.php @@ -45,6 +45,8 @@ use Symfony\Component\Security\Core\User\MissingUserProvider; use Symfony\Component\Security\Core\Validator\Constraints\UserPasswordValidator; use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; +use Symfony\Component\Security\Http\Authorization\NotFullFledgedEqualNormalLoginHandler; +use Symfony\Component\Security\Http\Authorization\NotFullFledgedRedirectToStartAuthenticationHandler; use Symfony\Component\Security\Http\Controller\SecurityTokenValueResolver; use Symfony\Component\Security\Http\Controller\UserValueResolver; use Symfony\Component\Security\Http\EventListener\IsGrantedAttributeListener; @@ -230,6 +232,7 @@ [], // listeners null, // switch_user null, // logout + null, // not_full_fledged_handler ]) ->set('security.logout_url_generator', LogoutUrlGenerator::class) @@ -322,5 +325,8 @@ ->set('cache.security_is_csrf_token_valid_attribute_expression_language') ->parent('cache.system') ->tag('cache.pool') + + ->set(NotFullFledgedRedirectToStartAuthenticationHandler::class) + ->set(NotFullFledgedEqualNormalLoginHandler::class) ; }; diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.php index 952b1d75625ad..a378e393773df 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.php +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.php @@ -139,6 +139,7 @@ service('security.access.denied_handler')->nullOnInvalid(), service('logger')->nullOnInvalid(), false, // Stateless + service('security.not.full.fledged_handler')->nullOnInvalid(), ]) ->tag('monolog.logger', ['channel' => 'security']) diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig b/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig index 635d61e2dd2c8..5694ef3f42754 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig +++ b/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig @@ -334,6 +334,10 @@ authenticators {{ collector.firewall.authenticators is empty ? '(none)' : profiler_dump(collector.firewall.authenticators, maxDepth=1) }} + + not_full_fledged_handler + {{ collector.firewall.not_full_fledged_handler ?: '(none)' }} + {% endif %} diff --git a/src/Symfony/Bundle/SecurityBundle/Security/FirewallConfig.php b/src/Symfony/Bundle/SecurityBundle/Security/FirewallConfig.php index 16edc6319a806..c4615b6be2fd5 100644 --- a/src/Symfony/Bundle/SecurityBundle/Security/FirewallConfig.php +++ b/src/Symfony/Bundle/SecurityBundle/Security/FirewallConfig.php @@ -30,6 +30,7 @@ public function __construct( private readonly array $authenticators = [], private readonly ?array $switchUser = null, private readonly ?array $logout = null, + private readonly ?string $notFullFledgedHandler = null, ) { } @@ -104,4 +105,9 @@ public function getLogout(): ?array { return $this->logout; } + + public function getNotFullFledgedHandler(): ?string + { + return $this->notFullFledgedHandler; + } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTestCase.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTestCase.php index 04fba9fe584d3..bc8a468a237b9 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTestCase.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTestCase.php @@ -35,6 +35,7 @@ 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; +use Symfony\Component\Security\Http\Authorization\NotFullFledgedRedirectToStartAuthenticationHandler; abstract class CompleteConfigurationTestCase extends TestCase { @@ -149,6 +150,7 @@ public function testFirewalls() [], null, null, + null, ], [ 'secure', @@ -184,6 +186,7 @@ public function testFirewalls() 'enable_csrf' => null, 'clear_site_data' => [], ], + NotFullFledgedRedirectToStartAuthenticationHandler::class, ], [ 'host', @@ -201,6 +204,7 @@ public function testFirewalls() ], null, null, + NotFullFledgedRedirectToStartAuthenticationHandler::class, ], [ 'with_user_checker', @@ -218,6 +222,7 @@ public function testFirewalls() ], null, null, + NotFullFledgedRedirectToStartAuthenticationHandler::class, ], ], $configs); diff --git a/src/Symfony/Component/Security/Http/Authorization/NotFullFledgedEqualNormalLoginHandler.php b/src/Symfony/Component/Security/Http/Authorization/NotFullFledgedEqualNormalLoginHandler.php new file mode 100644 index 0000000000000..19759ed5fbf93 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authorization/NotFullFledgedEqualNormalLoginHandler.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Authorization; + +use Psr\Log\LoggerInterface; +use Symfony\Component\HttpKernel\Event\ExceptionEvent; +use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; +use Symfony\Component\Security\Core\Exception\InsufficientAuthenticationException; + +/** + * NotFullFledgedHandler for considering NotFullFledged Login equal to Normal Login except if IS_AUTHENTICATED_FULLY is asked + * If IS_AUTHENTICATED_FULLY is in access denied Exception Attrribute, user is redirect to + * startAuthentication function in AuthenticationTrustResolver + * Otherwise The original AccessDeniedException is passing to accessDeniedHandler or ErrorPage or Thrown. + * + * @author Roman JOLY + */ +class NotFullFledgedEqualNormalLoginHandler implements NotFullFledgedHandlerInterface +{ + public function handle(ExceptionEvent $event, AccessDeniedException $exception, AuthenticationTrustResolverInterface $trustResolver, ?TokenInterface $token, ?LoggerInterface $logger, callable $startAuthenticationCallback): bool + { + if (!$trustResolver->isAuthenticated($token)) { + $this->reauthenticate($startAuthenticationCallback, $event, $token, $exception, $logger); + } + + foreach ($exception->getAttributes() as $attribute) { + if (\in_array($attribute, [AuthenticatedVoter::IS_AUTHENTICATED_FULLY])) { + $this->reauthenticate($startAuthenticationCallback, $event, $token, $exception, $logger); + + return true; + } + } + + return false; + } + + private function reauthenticate(callable $startAuthenticationCallback, ExceptionEvent $event, ?TokenInterface $token, AccessDeniedException $exception, ?LoggerInterface $logger): void + { + $logger?->debug('Access denied, the user is not fully authenticated; redirecting to authentication entry point.', ['exception' => $exception]); + + try { + $insufficientAuthenticationException = new InsufficientAuthenticationException('Full authentication is required to access this resource.', 0, $exception); + if (null !== $token) { + $insufficientAuthenticationException->setToken($token); + } + + $event->setResponse($startAuthenticationCallback($event->getRequest(), $insufficientAuthenticationException)); + } catch (\Exception $e) { + $event->setThrowable($e); + } + } +} diff --git a/src/Symfony/Component/Security/Http/Authorization/NotFullFledgedHandlerInterface.php b/src/Symfony/Component/Security/Http/Authorization/NotFullFledgedHandlerInterface.php new file mode 100644 index 0000000000000..bffaf9c2ab7bb --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authorization/NotFullFledgedHandlerInterface.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Authorization; + +use Psr\Log\LoggerInterface; +use Symfony\Component\HttpKernel\Event\ExceptionEvent; +use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; + +/** + * This is used by the ExceptionListener to translate an AccessDeniedException + * to a Response object. + * + * @author Roman JOLY + */ +interface NotFullFledgedHandlerInterface +{ + /** + * Allow to manage NotFullFledged cases when ExceptionListener catch AccessDeniedException + * This function can make checks and event / exception changes to change the Response + * It returns a boolean for break or not after that or continue the ExceptionListener process to decorate Exception and their response. + * + * @param $startAuthenticationCallback callable for call start function from + * + * @return bool break handleAccessDeniedException function in ExceptionListener after handle + */ + public function handle(ExceptionEvent $event, AccessDeniedException $exception, AuthenticationTrustResolverInterface $trustResolver, ?TokenInterface $token, ?LoggerInterface $logger, callable $startAuthenticationCallback): bool; +} diff --git a/src/Symfony/Component/Security/Http/Authorization/NotFullFledgedRedirectToStartAuthenticationHandler.php b/src/Symfony/Component/Security/Http/Authorization/NotFullFledgedRedirectToStartAuthenticationHandler.php new file mode 100644 index 0000000000000..d959b017d5e65 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authorization/NotFullFledgedRedirectToStartAuthenticationHandler.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Authorization; + +use Psr\Log\LoggerInterface; +use Symfony\Component\HttpKernel\Event\ExceptionEvent; +use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; +use Symfony\Component\Security\Core\Exception\InsufficientAuthenticationException; + +/** + * NotFullFledgedHandler for considering NotFullFledged Login has to be redirect to login page if AccessDeniedException is thrown + * When an AccessDeniedException is thrown and user is not full fledged logged, he is redirected to login page with + * start function from authenticationEntryPoint. + * + * @author Roman JOLY + */ +class NotFullFledgedRedirectToStartAuthenticationHandler implements NotFullFledgedHandlerInterface +{ + public function handle(ExceptionEvent $event, AccessDeniedException $exception, AuthenticationTrustResolverInterface $trustResolver, ?TokenInterface $token, ?LoggerInterface $logger, callable $startAuthenticationCallback): bool + { + if (!$trustResolver->isFullFledged($token)) { + $logger?->debug('Access denied, the user is not fully authenticated; redirecting to authentication entry point.', ['exception' => $exception]); + + try { + $insufficientAuthenticationException = new InsufficientAuthenticationException('Full authentication is required to access this resource.', 0, $exception); + if (null !== $token) { + $insufficientAuthenticationException->setToken($token); + } + + $event->setResponse($startAuthenticationCallback($event->getRequest(), $insufficientAuthenticationException)); + } catch (\Exception $e) { + $event->setThrowable($e); + } + + return true; + } + + return false; + } +} diff --git a/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php b/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php index a85ff958f2049..220b9991e0ccd 100644 --- a/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php @@ -25,10 +25,10 @@ use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Security\Core\Exception\AccountStatusException; use Symfony\Component\Security\Core\Exception\AuthenticationException; -use Symfony\Component\Security\Core\Exception\InsufficientAuthenticationException; use Symfony\Component\Security\Core\Exception\LazyResponseException; use Symfony\Component\Security\Core\Exception\LogoutException; use Symfony\Component\Security\Http\Authorization\AccessDeniedHandlerInterface; +use Symfony\Component\Security\Http\Authorization\NotFullFledgedHandlerInterface; use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface; use Symfony\Component\Security\Http\EntryPoint\Exception\NotAnEntryPointException; use Symfony\Component\Security\Http\HttpUtils; @@ -57,6 +57,7 @@ public function __construct( private ?AccessDeniedHandlerInterface $accessDeniedHandler = null, private ?LoggerInterface $logger = null, private bool $stateless = false, + private ?NotFullFledgedHandlerInterface $notFullFledgedHandler = null, ) { } @@ -124,22 +125,9 @@ private function handleAuthenticationException(ExceptionEvent $event, Authentica private function handleAccessDeniedException(ExceptionEvent $event, AccessDeniedException $exception): void { $event->setThrowable(new AccessDeniedHttpException($exception->getMessage(), $exception)); - $token = $this->tokenStorage->getToken(); - if (!$this->authenticationTrustResolver->isFullFledged($token)) { - $this->logger?->debug('Access denied, the user is not fully authenticated; redirecting to authentication entry point.', ['exception' => $exception]); - - try { - $insufficientAuthenticationException = new InsufficientAuthenticationException('Full authentication is required to access this resource.', 0, $exception); - if (null !== $token) { - $insufficientAuthenticationException->setToken($token); - } - - $event->setResponse($this->startAuthentication($event->getRequest(), $insufficientAuthenticationException)); - } catch (\Exception $e) { - $event->setThrowable($e); - } + if ($this->notFullFledgedHandler?->handle($event, $exception, $this->authenticationTrustResolver, $token, $this->logger, function ($request, $exception) {return $this->startAuthentication($request, $exception); })) { return; } diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/ExceptionListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/ExceptionListenerTest.php index 8bcc958854275..bf0b881224bb0 100644 --- a/src/Symfony/Component/Security/Http/Tests/Firewall/ExceptionListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/ExceptionListenerTest.php @@ -25,6 +25,7 @@ use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\LogoutException; use Symfony\Component\Security\Http\Authorization\AccessDeniedHandlerInterface; +use Symfony\Component\Security\Http\Authorization\NotFullFledgedHandlerInterface; use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface; use Symfony\Component\Security\Http\Firewall\ExceptionListener; use Symfony\Component\Security\Http\HttpUtils; @@ -142,7 +143,55 @@ public function testAccessDeniedExceptionNotFullFledged(\Exception $exception, ? $listener = $this->createExceptionListener($tokenStorage, $this->createTrustResolver(false), null, $this->createEntryPoint()); $listener->onKernelException($event); - $this->assertEquals('OK', $event->getResponse()->getContent()); + $this->assertSame($eventException ?? $exception, $event->getThrowable()->getPrevious()); + } + + /** + * @dataProvider getAccessDeniedExceptionProvider + */ + public function testAccessDeniedExceptionNotFullFledgedWithHandlerResponseCustomNotAuthenticated(\Exception $exception, ?\Exception $eventException = null) + { + $event = $this->createEvent($exception); + + $tokenStorage = $this->createMock(TokenStorageInterface::class); + $tokenStorage->expects($this->once())->method('getToken')->willReturn($this->createMock(TokenInterface::class)); + + $listener = $this->createExceptionListener($tokenStorage, $this->createTrustResolver(false, false), null, $this->createEntryPointWithoutStartCalled(), null, null, $this->createNotFullFledgedHandler(true)); + $listener->onKernelException($event); + + $this->assertSame($eventException ?? $exception, $event->getThrowable()->getPrevious()); + } + + /** + * @dataProvider getAccessDeniedExceptionProvider + */ + public function testAccessDeniedExceptionNotFullFledgedWithoutHandlerResponseAuthenticated(\Exception $exception, ?\Exception $eventException = null) + { + $event = $this->createEvent($exception); + + $tokenStorage = $this->createMock(TokenStorageInterface::class); + $tokenStorage->expects($this->once())->method('getToken')->willReturn($this->createMock(TokenInterface::class)); + + $listener = $this->createExceptionListener($tokenStorage, $this->createTrustResolver(false, true), null, $this->createEntryPointWithoutStartCalled(), null, null, $this->createNotFullFledgedHandler(false)); + $listener->onKernelException($event); + + $this->assertNull($event->getResponse()); + $this->assertEquals($eventException?->getMessage() ?? $exception->getMessage(), $event->getThrowable()->getMessage()); + } + + /** + * @dataProvider getAccessDeniedExceptionProvider + */ + public function testAccessDeniedExceptionNotFullFledgedWithHandlerResponseCustomAuthenticated(\Exception $exception, ?\Exception $eventException = null) + { + $event = $this->createEvent($exception); + + $tokenStorage = $this->createMock(TokenStorageInterface::class); + $tokenStorage->expects($this->once())->method('getToken')->willReturn($this->createMock(TokenInterface::class)); + + $listener = $this->createExceptionListener($tokenStorage, $this->createTrustResolver(false, true), null, $this->createEntryPointWithoutStartCalled(), null, null, $this->createNotFullFledgedHandler(true)); + $listener->onKernelException($event); + $this->assertSame($eventException ?? $exception, $event->getThrowable()->getPrevious()); } @@ -183,15 +232,24 @@ public static function getAccessDeniedExceptionProvider() private function createEntryPoint(?Response $response = null) { $entryPoint = $this->createMock(AuthenticationEntryPointInterface::class); - $entryPoint->expects($this->once())->method('start')->willReturn($response ?? new Response('OK')); + $entryPoint->method('start')->willReturn($response ?? new Response('OK')); return $entryPoint; } - private function createTrustResolver($fullFledged) + private function createEntryPointWithoutStartCalled() + { + $entryPoint = $this->createMock(AuthenticationEntryPointInterface::class); + $entryPoint->expects($this->never())->method('start'); + + return $entryPoint; + } + + private function createTrustResolver($fullFledged, $authenticate = false) { $trustResolver = $this->createMock(AuthenticationTrustResolverInterface::class); - $trustResolver->expects($this->once())->method('isFullFledged')->willReturn($fullFledged); + $trustResolver->method('isFullFledged')->willReturn($fullFledged); + $trustResolver->method('isAuthenticated')->willReturn($authenticate); return $trustResolver; } @@ -203,7 +261,7 @@ private function createEvent(\Exception $exception, $kernel = null) return new ExceptionEvent($kernel, Request::create('/'), HttpKernelInterface::MAIN_REQUEST, $exception); } - private function createExceptionListener(?TokenStorageInterface $tokenStorage = null, ?AuthenticationTrustResolverInterface $trustResolver = null, ?HttpUtils $httpUtils = null, ?AuthenticationEntryPointInterface $authenticationEntryPoint = null, $errorPage = null, ?AccessDeniedHandlerInterface $accessDeniedHandler = null) + private function createExceptionListener(?TokenStorageInterface $tokenStorage = null, ?AuthenticationTrustResolverInterface $trustResolver = null, ?HttpUtils $httpUtils = null, ?AuthenticationEntryPointInterface $authenticationEntryPoint = null, $errorPage = null, ?AccessDeniedHandlerInterface $accessDeniedHandler = null, ?NotFullFledgedHandlerInterface $notFullFledgedHandler = null) { return new ExceptionListener( $tokenStorage ?? $this->createMock(TokenStorageInterface::class), @@ -212,7 +270,18 @@ private function createExceptionListener(?TokenStorageInterface $tokenStorage = 'key', $authenticationEntryPoint, $errorPage, - $accessDeniedHandler + $accessDeniedHandler, + null, + false, + $notFullFledgedHandler, ); } + + private function createNotFullFledgedHandler(bool $break = false) + { + $handler = $this->createMock(NotFullFledgedHandlerInterface::class); + $handler->method('handle')->willReturn($break); + + return $handler; + } }