From f576173a96ce9409a829b416500649a6857be98a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arnaud=20Fr=C3=A9zet?= Date: Wed, 19 May 2021 16:17:32 +0200 Subject: [PATCH 1/3] [Security] Add a method in the security helper to ease programmatic logout (#40663) --- .../Bundle/SecurityBundle/CHANGELOG.md | 1 + .../DependencyInjection/SecurityExtension.php | 7 + .../Resources/config/security.php | 1 + .../Resources/config/security_listeners.php | 4 + .../SecurityBundle/Security/Security.php | 24 +++ .../Tests/Functional/SecurityTest.php | 32 ++++ .../Functional/app/SecurityHelper/config.yml | 4 + .../Functional/app/SecurityHelper/routing.yml | 6 +- .../Tests/Security/SecurityTest.php | 180 +++++++++++++++++- 9 files changed, 257 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md index 55b08604f4e41..5fc8ddbd80c74 100644 --- a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md @@ -8,6 +8,7 @@ CHANGELOG * Deprecate the `Symfony\Component\Security\Core\Security` service alias, use `Symfony\Bundle\SecurityBundle\Security\Security` instead * Add `Security::getFirewallConfig()` to help to get the firewall configuration associated to the Request * Add `Security::login()` to login programmatically + * Add `Security::logout()` to logout programmatically 6.1 --- diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index e9432f8662933..5941ba856e6f4 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -376,6 +376,13 @@ private function createFirewall(ContainerBuilder $container, string $id, array $ $container->register($firewallEventDispatcherId, EventDispatcher::class) ->addTag('event_dispatcher.dispatcher', ['name' => $firewallEventDispatcherId]); + $eventDispatcherLocator = $container->getDefinition('security.firewall.event_dispatcher_locator'); + $eventDispatcherLocator + ->replaceArgument(0, array_merge($eventDispatcherLocator->getArgument(0), [ + $id => new ServiceClosureArgument(new Reference($firewallEventDispatcherId)), + ])) + ; + // Register listeners $listeners = []; $listenerKeys = []; diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.php index f584dae33b2eb..eaf7e0bf52e92 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.php +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.php @@ -86,6 +86,7 @@ 'request_stack' => service('request_stack'), 'security.firewall.map' => service('security.firewall.map'), 'security.user_checker' => service('security.user_checker'), + 'security.firewall.event_dispatcher_locator' => service('security.firewall.event_dispatcher_locator'), ]), abstract_arg('authenticators'), ]) diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.php index dfd94ad0d6f49..9b7ef071d84e6 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.php +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.php @@ -11,6 +11,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; +use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\Security\Http\AccessMap; use Symfony\Component\Security\Http\Authentication\CustomAuthenticationFailureHandler; use Symfony\Component\Security\Http\Authentication\CustomAuthenticationSuccessHandler; @@ -160,5 +161,8 @@ service('security.access_map'), ]) ->tag('monolog.logger', ['channel' => 'security']) + + ->set('security.firewall.event_dispatcher_locator', ServiceLocator::class) + ->args([[]]) ; }; diff --git a/src/Symfony/Bundle/SecurityBundle/Security/Security.php b/src/Symfony/Bundle/SecurityBundle/Security/Security.php index f733fc12c81e6..05add6341787a 100644 --- a/src/Symfony/Bundle/SecurityBundle/Security/Security.php +++ b/src/Symfony/Bundle/SecurityBundle/Security/Security.php @@ -13,10 +13,12 @@ use Psr\Container\ContainerInterface; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Exception\LogicException; use Symfony\Component\Security\Core\Security as LegacySecurity; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Event\LogoutEvent; use Symfony\Contracts\Service\ServiceProviderInterface; /** @@ -60,6 +62,28 @@ public function login(UserInterface $user, string $authenticatorName = null, str $this->container->get('security.user_authenticator')->authenticateUser($user, $authenticator, $request); } + /** + * Logout the current user by dispatching the LogoutEvent. + * + * @return Response|null The LogoutEvent's Response if any + */ + public function logout(): ?Response + { + $request = $this->container->get('request_stack')->getMainRequest(); + $logoutEvent = new LogoutEvent($request, $this->container->get('security.token_storage')->getToken()); + $firewallConfig = $this->container->get('security.firewall.map')->getFirewallConfig($request); + + if (!$firewallConfig) { + throw new LogicException('It is not possible to logout, as the request is not behind a firewall.'); + } + $firewallName = $firewallConfig->getName(); + + $this->container->get('security.firewall.event_dispatcher_locator')->get($firewallName)->dispatch($logoutEvent); + $this->container->get('security.token_storage')->setToken(); + + return $logoutEvent->getResponse(); + } + private function getAuthenticator(?string $authenticatorName, string $firewallName): AuthenticatorInterface { if (!\array_key_exists($firewallName, $this->authenticators)) { diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityTest.php index de357a998df23..09e86e95d7f87 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityTest.php @@ -99,6 +99,24 @@ public function testLoginWithBuiltInAuthenticator(string $authenticator) $this->assertSame(200, $response->getStatusCode()); $this->assertSame(['message' => 'Welcome @chalasr!'], json_decode($response->getContent(), true)); } + + /** + * @testWith ["json_login"] + * ["Symfony\\Bundle\\SecurityBundle\\Tests\\Functional\\Bundle\\AuthenticatorBundle\\ApiAuthenticator"] + */ + public function testLogout(string $authenticator) + { + $client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config.yml', 'debug' => true]); + static::getContainer()->get(WelcomeController::class)->authenticator = $authenticator; + $client->request('GET', '/welcome'); + $this->assertEquals('chalasr', static::getContainer()->get('security.helper')->getUser()->getUserIdentifier()); + + $client->request('GET', '/auto-logout'); + $response = $client->getResponse(); + $this->assertSame(200, $response->getStatusCode()); + $this->assertNull(static::getContainer()->get('security.helper')->getUser()); + $this->assertSame(['message' => 'Logout successful'], json_decode($response->getContent(), true)); + } } final class UserWithoutEquatable implements UserInterface, PasswordAuthenticatedUserInterface @@ -224,3 +242,17 @@ public function welcome() return new JsonResponse(['message' => sprintf('Welcome @%s!', $this->security->getUser()->getUserIdentifier())]); } } + +class LogoutController +{ + public function __construct(private Security $security) + { + } + + public function logout() + { + $this->security->logout(); + + return new JsonResponse(['message' => 'Logout successful']); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/config.yml index f46a1fa682159..f50bc81a3d283 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/config.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/config.yml @@ -15,6 +15,10 @@ services: arguments: ['@security.helper'] public: true + Symfony\Bundle\SecurityBundle\Tests\Functional\LogoutController: + arguments: ['@security.helper'] + public: true + Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ApiAuthenticator: ~ security: diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/routing.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/routing.yml index cbe982ae6f310..3970c3e0da0fc 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/routing.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/routing.yml @@ -1,3 +1,7 @@ welcome: path: /welcome - defaults: { _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\WelcomeController::welcome } + controller: Symfony\Bundle\SecurityBundle\Tests\Functional\WelcomeController::welcome + +logout: + path: /auto-logout + controller: Symfony\Bundle\SecurityBundle\Tests\Functional\LogoutController::logout diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Security/SecurityTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Security/SecurityTest.php index 1929d688701c2..b41e241333ce7 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Security/SecurityTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Security/SecurityTest.php @@ -19,15 +19,19 @@ use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; +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\Authorization\AuthorizationCheckerInterface; +use Symfony\Component\Security\Core\Exception\LogicException; use Symfony\Component\Security\Core\User\InMemoryUser; use Symfony\Component\Security\Core\User\UserCheckerInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Http\Authentication\UserAuthenticatorInterface; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Event\LogoutEvent; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Symfony\Contracts\Service\ServiceProviderInterface; class SecurityTest extends TestCase @@ -117,7 +121,7 @@ public function getFirewallConfigTests() yield [$request, new FirewallConfig('main', 'acme_user_checker')]; } - public function testAutoLogin() + public function testLogin() { $request = new Request(); $authenticator = $this->createMock(AuthenticatorInterface::class); @@ -163,6 +167,180 @@ public function testAutoLogin() $security->login($user); } + public function testLogout() + { + $request = new Request(); + $requestStack = $this->createMock(RequestStack::class); + $requestStack + ->expects($this->once()) + ->method('getMainRequest') + ->willReturn($request) + ; + + $token = $this->createMock(TokenInterface::class); + $tokenStorage = $this->createMock(TokenStorageInterface::class); + $tokenStorage + ->expects($this->once()) + ->method('getToken') + ->willReturn($token) + ; + $tokenStorage + ->expects($this->once()) + ->method('setToken') + ; + + $eventDispatcher = $this->createMock(EventDispatcherInterface::class); + $eventDispatcher + ->expects($this->once()) + ->method('dispatch') + ->with(new LogoutEvent($request, $token)) + ; + + $firewallMap = $this->createMock(FirewallMap::class); + $firewallConfig = new FirewallConfig('my_firewall', 'user_checker'); + $firewallMap + ->expects($this->once()) + ->method('getFirewallConfig') + ->willReturn($firewallConfig) + ; + + $eventDispatcherLocator = $this->createMock(ContainerInterface::class); + $eventDispatcherLocator + ->expects($this->atLeastOnce()) + ->method('get') + ->willReturnMap([ + ['my_firewall', $eventDispatcher], + ]) + ; + + $container = $this->createMock(ContainerInterface::class); + $container + ->expects($this->atLeastOnce()) + ->method('get') + ->willReturnMap([ + ['request_stack', $requestStack], + ['security.token_storage', $tokenStorage], + ['security.firewall.map', $firewallMap], + ['security.firewall.event_dispatcher_locator', $eventDispatcherLocator], + ]) + ; + $security = new Security($container); + $security->logout(); + } + + public function testLogoutWithoutFirewall() + { + $request = new Request(); + $requestStack = $this->createMock(RequestStack::class); + $requestStack + ->expects($this->once()) + ->method('getMainRequest') + ->willReturn($request) + ; + + $token = $this->createMock(TokenInterface::class); + $tokenStorage = $this->createMock(TokenStorageInterface::class); + $tokenStorage + ->expects($this->once()) + ->method('getToken') + ->willReturn($token) + ; + + $firewallMap = $this->createMock(FirewallMap::class); + $firewallMap + ->expects($this->once()) + ->method('getFirewallConfig') + ->willReturn(null) + ; + + $container = $this->createMock(ContainerInterface::class); + $container + ->expects($this->atLeastOnce()) + ->method('get') + ->willReturnMap([ + ['request_stack', $requestStack], + ['security.token_storage', $tokenStorage], + ['security.firewall.map', $firewallMap], + ]) + ; + + $this->expectException(LogicException::class); + $security = new Security($container); + $security->logout(); + } + + public function testLogoutWithResponse() + { + $request = new Request(); + $requestStack = $this->createMock(RequestStack::class); + $requestStack + ->expects($this->once()) + ->method('getMainRequest') + ->willReturn($request) + ; + + $token = $this->createMock(TokenInterface::class); + $tokenStorage = $this->createMock(TokenStorageInterface::class); + $tokenStorage + ->expects($this->once()) + ->method('getToken') + ->willReturn($token) + ; + $tokenStorage + ->expects($this->once()) + ->method('setToken') + ; + + $eventDispatcher = $this->createMock(EventDispatcherInterface::class); + $eventDispatcher + ->expects($this->once()) + ->method('dispatch') + ->willReturnCallback(function ($event) use ($request, $token) { + $this->assertInstanceOf(LogoutEvent::class, $event); + $this->assertEquals($request, $event->getRequest()); + $this->assertEquals($token, $event->getToken()); + + $event->setResponse(new Response('a custom response')); + + return $event; + }) + ; + + $firewallMap = $this->createMock(FirewallMap::class); + $firewallConfig = new FirewallConfig('my_firewall', 'user_checker'); + $firewallMap + ->expects($this->once()) + ->method('getFirewallConfig') + ->willReturn($firewallConfig) + ; + + $eventDispatcherLocator = $this->createMock(ContainerInterface::class); + $eventDispatcherLocator + ->expects($this->atLeastOnce()) + ->method('get') + ->willReturnMap([ + ['my_firewall', $eventDispatcher], + ]) + ; + + $container = $this->createMock(ContainerInterface::class); + $container + ->expects($this->atLeastOnce()) + ->method('get') + ->willReturnMap([ + ['request_stack', $requestStack], + ['security.token_storage', $tokenStorage], + ['security.firewall.map', $firewallMap], + ['security.firewall.event_dispatcher_locator', $eventDispatcherLocator], + ]) + ; + $security = new Security($container); + $response = $security->logout(); + + $this->assertInstanceOf(Response::class, $response); + $this->assertEquals('a custom response', $response->getContent()); + } + private function createContainer(string $serviceId, object $serviceObject): ContainerInterface { return new ServiceLocator([$serviceId => fn () => $serviceObject]); From f41a18403dceec49f4156efe76a62e9007616fdd Mon Sep 17 00:00:00 2001 From: Robin Chalas Date: Thu, 7 Jul 2022 15:25:23 +0200 Subject: [PATCH 2/3] Add CSRF protection --- .../DependencyInjection/SecurityExtension.php | 2 + .../Resources/config/security.php | 2 + .../Security/FirewallConfig.php | 47 ++++---- .../SecurityBundle/Security/Security.php | 31 ++++-- .../CompleteConfigurationTest.php | 11 ++ .../Tests/Functional/SecurityTest.php | 38 ++++--- .../app/SecurityHelper/config_logout_csrf.yml | 42 +++++++ .../Functional/app/SecurityHelper/routing.yml | 4 +- .../Tests/Security/SecurityTest.php | 103 +++++++++++------- 9 files changed, 192 insertions(+), 88 deletions(-) create mode 100644 src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/config_logout_csrf.yml diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 5941ba856e6f4..e4f1fddc61f30 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -455,6 +455,8 @@ private function createFirewall(ContainerBuilder $container, string $id, array $ false === $firewall['stateless'] && isset($firewall['context']) ? $firewall['context'] : null, ]) ; + + $config->replaceArgument(12, $firewall['logout']); } // Determine default entry point diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.php index eaf7e0bf52e92..781c782d4ccf7 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.php +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.php @@ -87,6 +87,7 @@ 'security.firewall.map' => service('security.firewall.map'), 'security.user_checker' => service('security.user_checker'), 'security.firewall.event_dispatcher_locator' => service('security.firewall.event_dispatcher_locator'), + 'security.csrf.token_manager' => service('security.csrf.token_manager')->ignoreOnInvalid(), ]), abstract_arg('authenticators'), ]) @@ -207,6 +208,7 @@ null, [], // listeners null, // switch_user + null, // logout ]) ->set('security.logout_url_generator', LogoutUrlGenerator::class) diff --git a/src/Symfony/Bundle/SecurityBundle/Security/FirewallConfig.php b/src/Symfony/Bundle/SecurityBundle/Security/FirewallConfig.php index ce9373f91db72..6525a23e4b9c5 100644 --- a/src/Symfony/Bundle/SecurityBundle/Security/FirewallConfig.php +++ b/src/Symfony/Bundle/SecurityBundle/Security/FirewallConfig.php @@ -16,33 +16,21 @@ */ final class FirewallConfig { - private string $name; - private string $userChecker; - private ?string $requestMatcher; - private bool $securityEnabled; - private bool $stateless; - private ?string $provider; - private ?string $context; - private ?string $entryPoint; - private ?string $accessDeniedHandler; - private ?string $accessDeniedUrl; - private array $authenticators; - private ?array $switchUser; - - public function __construct(string $name, string $userChecker, string $requestMatcher = null, bool $securityEnabled = true, bool $stateless = false, string $provider = null, string $context = null, string $entryPoint = null, string $accessDeniedHandler = null, string $accessDeniedUrl = null, array $authenticators = [], array $switchUser = null) - { - $this->name = $name; - $this->userChecker = $userChecker; - $this->requestMatcher = $requestMatcher; - $this->securityEnabled = $securityEnabled; - $this->stateless = $stateless; - $this->provider = $provider; - $this->context = $context; - $this->entryPoint = $entryPoint; - $this->accessDeniedHandler = $accessDeniedHandler; - $this->accessDeniedUrl = $accessDeniedUrl; - $this->authenticators = $authenticators; - $this->switchUser = $switchUser; + public function __construct( + private readonly string $name, + private readonly string $userChecker, + private readonly ?string $requestMatcher = null, + private readonly bool $securityEnabled = true, + private readonly bool $stateless = false, + private readonly ?string $provider = null, + private readonly ?string $context = null, + private readonly ?string $entryPoint = null, + private readonly ?string $accessDeniedHandler = null, + private readonly ?string $accessDeniedUrl = null, + private readonly array $authenticators = [], + private readonly ?array $switchUser = null, + private readonly ?array $logout = null + ) { } public function getName(): string @@ -111,4 +99,9 @@ public function getSwitchUser(): ?array { return $this->switchUser; } + + public function getLogout(): ?array + { + return $this->logout; + } } diff --git a/src/Symfony/Bundle/SecurityBundle/Security/Security.php b/src/Symfony/Bundle/SecurityBundle/Security/Security.php index 05add6341787a..ef94ea432cc33 100644 --- a/src/Symfony/Bundle/SecurityBundle/Security/Security.php +++ b/src/Symfony/Bundle/SecurityBundle/Security/Security.php @@ -14,11 +14,15 @@ use Psr\Container\ContainerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Exception\LogicException; +use Symfony\Component\Security\Core\Exception\LogoutException; use Symfony\Component\Security\Core\Security as LegacySecurity; use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Csrf\CsrfToken; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; use Symfony\Component\Security\Http\Event\LogoutEvent; +use Symfony\Component\Security\Http\ParameterBagUtils; use Symfony\Contracts\Service\ServiceProviderInterface; /** @@ -69,17 +73,30 @@ public function login(UserInterface $user, string $authenticatorName = null, str */ public function logout(): ?Response { + /** @var TokenStorageInterface $tokenStorage */ + $tokenStorage = $this->container->get('security.token_storage'); + + if (!($token = $tokenStorage->getToken()) || !$token->getUser()) { + throw new LogicException('Unable to logout as there is no logged-in user.'); + } + $request = $this->container->get('request_stack')->getMainRequest(); - $logoutEvent = new LogoutEvent($request, $this->container->get('security.token_storage')->getToken()); - $firewallConfig = $this->container->get('security.firewall.map')->getFirewallConfig($request); - if (!$firewallConfig) { - throw new LogicException('It is not possible to logout, as the request is not behind a firewall.'); + if (!$firewallConfig = $this->container->get('security.firewall.map')->getFirewallConfig($request)) { + throw new LogicException('Unable to logout as the request is not behind a firewall.'); } - $firewallName = $firewallConfig->getName(); - $this->container->get('security.firewall.event_dispatcher_locator')->get($firewallName)->dispatch($logoutEvent); - $this->container->get('security.token_storage')->setToken(); + if ($this->container->has('security.csrf.token_manager') && $logoutConfig = $firewallConfig->getLogout()) { + $csrfToken = ParameterBagUtils::getRequestParameterValue($request, $logoutConfig['csrf_parameter']); + if (!\is_string($csrfToken) || false === $this->container->get('security.csrf.token_manager')->isTokenValid(new CsrfToken($logoutConfig['csrf_token_id'], $csrfToken))) { + throw new LogoutException('Invalid CSRF token.'); + } + } + + $logoutEvent = new LogoutEvent($request, $token); + $this->container->get('security.firewall.event_dispatcher_locator')->get($firewallConfig->getName())->dispatch($logoutEvent); + + $tokenStorage->setToken(); return $logoutEvent->getResponse(); } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php index 7d8384456cea8..8a431f8e416f4 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php @@ -141,6 +141,7 @@ public function testFirewalls() '', [], null, + null, ], [ 'secure', @@ -165,6 +166,14 @@ public function testFirewalls() 'parameter' => '_switch_user', 'role' => 'ROLE_ALLOWED_TO_SWITCH', ], + [ + 'csrf_parameter' => '_csrf_token', + 'csrf_token_id' => 'logout', + 'path' => '/logout', + 'target' => '/', + 'invalidate_session' => true, + 'delete_cookies' => [], + ], ], [ 'host', @@ -181,6 +190,7 @@ public function testFirewalls() 'http_basic', ], null, + null, ], [ 'with_user_checker', @@ -197,6 +207,7 @@ public function testFirewalls() 'http_basic', ], null, + null, ], ], $configs); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityTest.php index 09e86e95d7f87..2e7032bd2e21e 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityTest.php @@ -20,6 +20,7 @@ use Symfony\Component\Security\Core\User\InMemoryUser; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; class SecurityTest extends AbstractWebTestCase { @@ -88,9 +89,9 @@ public function userWillBeMarkedAsChangedIfRolesHasChangedProvider() * @testWith ["json_login"] * ["Symfony\\Bundle\\SecurityBundle\\Tests\\Functional\\Bundle\\AuthenticatorBundle\\ApiAuthenticator"] */ - public function testLoginWithBuiltInAuthenticator(string $authenticator) + public function testLogin(string $authenticator) { - $client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config.yml', 'debug' => true]); + $client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config.yml']); static::getContainer()->get(WelcomeController::class)->authenticator = $authenticator; $client->request('GET', '/welcome'); $response = $client->getResponse(); @@ -98,21 +99,26 @@ public function testLoginWithBuiltInAuthenticator(string $authenticator) $this->assertInstanceOf(JsonResponse::class, $response); $this->assertSame(200, $response->getStatusCode()); $this->assertSame(['message' => 'Welcome @chalasr!'], json_decode($response->getContent(), true)); + $this->assertSame('chalasr', static::getContainer()->get('security.helper')->getUser()->getUserIdentifier()); } - /** - * @testWith ["json_login"] - * ["Symfony\\Bundle\\SecurityBundle\\Tests\\Functional\\Bundle\\AuthenticatorBundle\\ApiAuthenticator"] - */ - public function testLogout(string $authenticator) + public function testLogout() { - $client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config.yml', 'debug' => true]); - static::getContainer()->get(WelcomeController::class)->authenticator = $authenticator; - $client->request('GET', '/welcome'); - $this->assertEquals('chalasr', static::getContainer()->get('security.helper')->getUser()->getUserIdentifier()); + $client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config.yml']); + $client->request('GET', '/force-logout'); + $response = $client->getResponse(); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertNull(static::getContainer()->get('security.helper')->getUser()); + $this->assertSame(['message' => 'Logout successful'], json_decode($response->getContent(), true)); + } - $client->request('GET', '/auto-logout'); + public function testLogoutWithCsrf() + { + $client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config_logout_csrf.yml']); + $client->request('GET', '/force-logout'); $response = $client->getResponse(); + $this->assertSame(200, $response->getStatusCode()); $this->assertNull(static::getContainer()->get('security.helper')->getUser()); $this->assertSame(['message' => 'Logout successful'], json_decode($response->getContent(), true)); @@ -245,12 +251,16 @@ public function welcome() class LogoutController { - public function __construct(private Security $security) + public function __construct(private Security $security, private ?CsrfTokenManagerInterface $csrfTokenManager = null) { } - public function logout() + public function logout(Request $request) { + $this->security->login(new InMemoryUser('chalasr', '', ['ROLE_USER']), 'json_login', 'default'); + if ($this->csrfTokenManager) { + $request->query->set('_csrf_token', (string) $this->csrfTokenManager->getToken('logout')); + } $this->security->logout(); return new JsonResponse(['message' => 'Logout successful']); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/config_logout_csrf.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/config_logout_csrf.yml new file mode 100644 index 0000000000000..652c4c0b686e6 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/config_logout_csrf.yml @@ -0,0 +1,42 @@ +imports: + - { resource: ./../config/framework.yml } + +services: + # alias the service so we can access it in the tests + functional_test.security.helper: + alias: security.helper + public: true + + functional.test.security.token_storage: + alias: security.token_storage + public: true + + Symfony\Bundle\SecurityBundle\Tests\Functional\WelcomeController: + arguments: ['@security.helper'] + public: true + + Symfony\Bundle\SecurityBundle\Tests\Functional\LogoutController: + arguments: ['@security.helper', '@security.csrf.token_manager'] + public: true + + Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ApiAuthenticator: ~ + +security: + enable_authenticator_manager: true + providers: + in_memory: + memory: + users: [] + + firewalls: + default: + json_login: + username_path: user.login + password_path: user.password + logout: + path: /regular-logout + custom_authenticators: + - 'Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ApiAuthenticator' + + access_control: + - { path: ^/foo, roles: PUBLIC_ACCESS } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/routing.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/routing.yml index 3970c3e0da0fc..b862fc3185d38 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/routing.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/routing.yml @@ -2,6 +2,6 @@ welcome: path: /welcome controller: Symfony\Bundle\SecurityBundle\Tests\Functional\WelcomeController::welcome -logout: - path: /auto-logout +force-logout: + path: /force-logout controller: Symfony\Bundle\SecurityBundle\Tests\Functional\LogoutController::logout diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Security/SecurityTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Security/SecurityTest.php index b41e241333ce7..8a91f0f7c4a92 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Security/SecurityTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Security/SecurityTest.php @@ -171,23 +171,13 @@ public function testLogout() { $request = new Request(); $requestStack = $this->createMock(RequestStack::class); - $requestStack - ->expects($this->once()) - ->method('getMainRequest') - ->willReturn($request) - ; + $requestStack->expects($this->once())->method('getMainRequest')->willReturn($request); $token = $this->createMock(TokenInterface::class); + $token->method('getUser')->willReturn(new InMemoryUser('foo', 'bar')); $tokenStorage = $this->createMock(TokenStorageInterface::class); - $tokenStorage - ->expects($this->once()) - ->method('getToken') - ->willReturn($token) - ; - $tokenStorage - ->expects($this->once()) - ->method('setToken') - ; + $tokenStorage->expects($this->once())->method('getToken')->willReturn($token); + $tokenStorage->expects($this->once())->method('setToken'); $eventDispatcher = $this->createMock(EventDispatcherInterface::class); $eventDispatcher @@ -232,13 +222,10 @@ public function testLogoutWithoutFirewall() { $request = new Request(); $requestStack = $this->createMock(RequestStack::class); - $requestStack - ->expects($this->once()) - ->method('getMainRequest') - ->willReturn($request) - ; + $requestStack->expects($this->once())->method('getMainRequest')->willReturn($request); $token = $this->createMock(TokenInterface::class); + $token->method('getUser')->willReturn(new InMemoryUser('foo', 'bar')); $tokenStorage = $this->createMock(TokenStorageInterface::class); $tokenStorage ->expects($this->once()) @@ -273,23 +260,69 @@ public function testLogoutWithResponse() { $request = new Request(); $requestStack = $this->createMock(RequestStack::class); - $requestStack - ->expects($this->once()) - ->method('getMainRequest') - ->willReturn($request) - ; + $requestStack->expects($this->once())->method('getMainRequest')->willReturn($request); $token = $this->createMock(TokenInterface::class); + $token->method('getUser')->willReturn(new InMemoryUser('foo', 'bar')); $tokenStorage = $this->createMock(TokenStorageInterface::class); - $tokenStorage + $tokenStorage->expects($this->once())->method('getToken')->willReturn($token); + $tokenStorage->expects($this->once())->method('setToken')->with(); + + $eventDispatcher = $this->createMock(EventDispatcherInterface::class); + $eventDispatcher ->expects($this->once()) - ->method('getToken') - ->willReturn($token) + ->method('dispatch') + ->willReturnCallback(function ($event) use ($request, $token) { + $this->assertInstanceOf(LogoutEvent::class, $event); + $this->assertEquals($request, $event->getRequest()); + $this->assertEquals($token, $event->getToken()); + + $event->setResponse(new Response('a custom response')); + + return $event; + }) ; - $tokenStorage - ->expects($this->once()) - ->method('setToken') + + $firewallMap = $this->createMock(FirewallMap::class); + $firewallConfig = new FirewallConfig('my_firewall', 'user_checker'); + $firewallMap->expects($this->once())->method('getFirewallConfig')->willReturn($firewallConfig); + + $eventDispatcherLocator = $this->createMock(ContainerInterface::class); + $eventDispatcherLocator + ->expects($this->atLeastOnce()) + ->method('get') + ->willReturnMap([['my_firewall', $eventDispatcher]]) + ; + + $container = $this->createMock(ContainerInterface::class); + $container + ->expects($this->atLeastOnce()) + ->method('get') + ->willReturnMap([ + ['request_stack', $requestStack], + ['security.token_storage', $tokenStorage], + ['security.firewall.map', $firewallMap], + ['security.firewall.event_dispatcher_locator', $eventDispatcherLocator], + ]) ; + $security = new Security($container); + $response = $security->logout(); + + $this->assertInstanceOf(Response::class, $response); + $this->assertEquals('a custom response', $response->getContent()); + } + + public function testLogoutWithCsrf() + { + $request = new Request(); + $requestStack = $this->createMock(RequestStack::class); + $requestStack->expects($this->once())->method('getMainRequest')->willReturn($request); + + $token = $this->createMock(TokenInterface::class); + $token->method('getUser')->willReturn(new InMemoryUser('foo', 'bar')); + $tokenStorage = $this->createMock(TokenStorageInterface::class); + $tokenStorage->expects($this->once())->method('getToken')->willReturn($token); + $tokenStorage->expects($this->once())->method('setToken')->with(); $eventDispatcher = $this->createMock(EventDispatcherInterface::class); $eventDispatcher @@ -308,19 +341,13 @@ public function testLogoutWithResponse() $firewallMap = $this->createMock(FirewallMap::class); $firewallConfig = new FirewallConfig('my_firewall', 'user_checker'); - $firewallMap - ->expects($this->once()) - ->method('getFirewallConfig') - ->willReturn($firewallConfig) - ; + $firewallMap->expects($this->once())->method('getFirewallConfig')->willReturn($firewallConfig); $eventDispatcherLocator = $this->createMock(ContainerInterface::class); $eventDispatcherLocator ->expects($this->atLeastOnce()) ->method('get') - ->willReturnMap([ - ['my_firewall', $eventDispatcher], - ]) + ->willReturnMap([['my_firewall', $eventDispatcher]]) ; $container = $this->createMock(ContainerInterface::class); From e5e7d5ece408af6df4dc31f7f7891759679e702f Mon Sep 17 00:00:00 2001 From: Robin Chalas Date: Sun, 17 Jul 2022 17:53:58 +0200 Subject: [PATCH 3/3] Make CSRF validation opt-in --- .../SecurityBundle/Security/Security.php | 15 +++- .../Tests/Functional/SecurityTest.php | 83 ++++++++++++++----- .../Functional/app/SecurityHelper/config.yml | 26 ++++-- .../app/SecurityHelper/config_logout_csrf.yml | 30 ++++--- .../Functional/app/SecurityHelper/routing.yml | 12 ++- .../Tests/Security/SecurityTest.php | 25 ++++-- 6 files changed, 133 insertions(+), 58 deletions(-) diff --git a/src/Symfony/Bundle/SecurityBundle/Security/Security.php b/src/Symfony/Bundle/SecurityBundle/Security/Security.php index ef94ea432cc33..94d0370510d45 100644 --- a/src/Symfony/Bundle/SecurityBundle/Security/Security.php +++ b/src/Symfony/Bundle/SecurityBundle/Security/Security.php @@ -69,9 +69,13 @@ public function login(UserInterface $user, string $authenticatorName = null, str /** * Logout the current user by dispatching the LogoutEvent. * + * @param bool $validateCsrfToken Whether to look for a valid CSRF token based on the `logout` listener configuration + * * @return Response|null The LogoutEvent's Response if any + * + * @throws LogoutException When $validateCsrfToken is true and the CSRF token is not found or invalid */ - public function logout(): ?Response + public function logout(bool $validateCsrfToken = true): ?Response { /** @var TokenStorageInterface $tokenStorage */ $tokenStorage = $this->container->get('security.token_storage'); @@ -86,9 +90,12 @@ public function logout(): ?Response throw new LogicException('Unable to logout as the request is not behind a firewall.'); } - if ($this->container->has('security.csrf.token_manager') && $logoutConfig = $firewallConfig->getLogout()) { + if ($validateCsrfToken) { + if (!$this->container->has('security.csrf.token_manager') || !$logoutConfig = $firewallConfig->getLogout()) { + throw new LogicException(sprintf('Unable to logout with CSRF token validation. Either make sure that CSRF protection is enabled and "logout" is configured on the "%s" firewall, or bypass CSRF token validation explicitly by passing false to the $validateCsrfToken argument of this method.', $firewallConfig->getName())); + } $csrfToken = ParameterBagUtils::getRequestParameterValue($request, $logoutConfig['csrf_parameter']); - if (!\is_string($csrfToken) || false === $this->container->get('security.csrf.token_manager')->isTokenValid(new CsrfToken($logoutConfig['csrf_token_id'], $csrfToken))) { + if (!\is_string($csrfToken) || !$this->container->get('security.csrf.token_manager')->isTokenValid(new CsrfToken($logoutConfig['csrf_token_id'], $csrfToken))) { throw new LogoutException('Invalid CSRF token.'); } } @@ -96,7 +103,7 @@ public function logout(): ?Response $logoutEvent = new LogoutEvent($request, $token); $this->container->get('security.firewall.event_dispatcher_locator')->get($firewallConfig->getName())->dispatch($logoutEvent); - $tokenStorage->setToken(); + $tokenStorage->setToken(null); return $logoutEvent->getResponse(); } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityTest.php index 2e7032bd2e21e..95161c26ec6db 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityTest.php @@ -14,13 +14,16 @@ use Symfony\Bundle\SecurityBundle\Security\FirewallConfig; use Symfony\Bundle\SecurityBundle\Security\Security; use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\SecuredPageBundle\Security\Core\User\ArrayUserProvider; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\User\InMemoryUser; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\UserInterface; -use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; class SecurityTest extends AbstractWebTestCase { @@ -38,8 +41,10 @@ public function testServiceIsFunctional() $security = $container->get('functional_test.security.helper'); $this->assertTrue($security->isGranted('ROLE_USER')); $this->assertSame($token, $security->getToken()); - $this->assertInstanceOf(FirewallConfig::class, $firewallConfig = $security->getFirewallConfig(new Request())); - $this->assertSame('default', $firewallConfig->getName()); + $request = new Request(); + $request->server->set('REQUEST_URI', '/main/foo'); + $this->assertInstanceOf(FirewallConfig::class, $firewallConfig = $security->getFirewallConfig($request)); + $this->assertSame('main', $firewallConfig->getName()); } /** @@ -86,14 +91,14 @@ public function userWillBeMarkedAsChangedIfRolesHasChangedProvider() } /** - * @testWith ["json_login"] + * @testWith ["form_login"] * ["Symfony\\Bundle\\SecurityBundle\\Tests\\Functional\\Bundle\\AuthenticatorBundle\\ApiAuthenticator"] */ public function testLogin(string $authenticator) { - $client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config.yml']); - static::getContainer()->get(WelcomeController::class)->authenticator = $authenticator; - $client->request('GET', '/welcome'); + $client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config.yml', 'debug' > true]); + static::getContainer()->get(ForceLoginController::class)->authenticator = $authenticator; + $client->request('GET', '/main/force-login'); $response = $client->getResponse(); $this->assertInstanceOf(JsonResponse::class, $response); @@ -104,8 +109,10 @@ public function testLogin(string $authenticator) public function testLogout() { - $client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config.yml']); - $client->request('GET', '/force-logout'); + $client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config.yml', 'debug' => true]); + $client->loginUser(new InMemoryUser('chalasr', 'the-password', ['ROLE_FOO']), 'main'); + + $client->request('GET', '/main/force-logout'); $response = $client->getResponse(); $this->assertSame(200, $response->getStatusCode()); @@ -114,9 +121,39 @@ public function testLogout() } public function testLogoutWithCsrf() + { + $client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config_logout_csrf.yml', 'debug' => true]); + $client->loginUser(new InMemoryUser('chalasr', 'the-password', ['ROLE_FOO']), 'main'); + + // put a csrf token in the storage + /** @var EventDispatcherInterface $eventDispatcher */ + $eventDispatcher = static::getContainer()->get(EventDispatcherInterface::class); + $setCsrfToken = function (RequestEvent $event) { + static::getContainer()->get('security.csrf.token_storage')->setToken('logout', 'bar'); + $event->setResponse(new Response('')); + }; + $eventDispatcher->addListener(KernelEvents::REQUEST, $setCsrfToken); + try { + $client->request('GET', '/'.uniqid('', true)); + } finally { + $eventDispatcher->removeListener(KernelEvents::REQUEST, $setCsrfToken); + } + + static::getContainer()->get(LogoutController::class)->checkCsrf = true; + $client->request('GET', '/main/force-logout', ['_csrf_token' => 'bar']); + $response = $client->getResponse(); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertNull(static::getContainer()->get('security.helper')->getUser()); + $this->assertSame(['message' => 'Logout successful'], json_decode($response->getContent(), true)); + } + + public function testLogoutBypassCsrf() { $client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config_logout_csrf.yml']); - $client->request('GET', '/force-logout'); + $client->loginUser(new InMemoryUser('chalasr', 'the-password', ['ROLE_FOO']), 'main'); + + $client->request('GET', '/main/force-logout'); $response = $client->getResponse(); $this->assertSame(200, $response->getStatusCode()); @@ -232,9 +269,9 @@ public function eraseCredentials(): void } } -class WelcomeController +class ForceLoginController { - public $authenticator = 'json_login'; + public $authenticator = 'form_login'; public function __construct(private Security $security) { @@ -242,7 +279,7 @@ public function __construct(private Security $security) public function welcome() { - $user = new InMemoryUser('chalasr', '', ['ROLE_USER']); + $user = new InMemoryUser('chalasr', 'the-password', ['ROLE_FOO']); $this->security->login($user, $this->authenticator); return new JsonResponse(['message' => sprintf('Welcome @%s!', $this->security->getUser()->getUserIdentifier())]); @@ -251,18 +288,24 @@ public function welcome() class LogoutController { - public function __construct(private Security $security, private ?CsrfTokenManagerInterface $csrfTokenManager = null) + public $checkCsrf = false; + + public function __construct(private Security $security) { } - public function logout(Request $request) + public function logout(UserInterface $user) { - $this->security->login(new InMemoryUser('chalasr', '', ['ROLE_USER']), 'json_login', 'default'); - if ($this->csrfTokenManager) { - $request->query->set('_csrf_token', (string) $this->csrfTokenManager->getToken('logout')); - } - $this->security->logout(); + $this->security->logout($this->checkCsrf); return new JsonResponse(['message' => 'Logout successful']); } } + +class LoggedInController +{ + public function __invoke(UserInterface $user) + { + return new JsonResponse(['message' => sprintf('Welcome back @%s', $user->getUserIdentifier())]); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/config.yml index f50bc81a3d283..1a93b66275a1c 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/config.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/config.yml @@ -2,7 +2,6 @@ imports: - { resource: ./../config/framework.yml } services: - # alias the service so we can access it in the tests functional_test.security.helper: alias: security.helper public: true @@ -11,7 +10,7 @@ services: alias: security.token_storage public: true - Symfony\Bundle\SecurityBundle\Tests\Functional\WelcomeController: + Symfony\Bundle\SecurityBundle\Tests\Functional\ForceLoginController: arguments: ['@security.helper'] public: true @@ -19,22 +18,31 @@ services: arguments: ['@security.helper'] public: true + Symfony\Bundle\SecurityBundle\Tests\Functional\LoggedInController: + arguments: ['@security.helper'] + public: true + Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ApiAuthenticator: ~ security: enable_authenticator_manager: true + providers: - in_memory: + main: memory: - users: [] + users: + chalasr: { password: the-password, roles: ['ROLE_FOO'] } + no-role-username: { password: the-password, roles: [] } firewalls: - default: - json_login: - username_path: user.login - password_path: user.password + main: + pattern: ^/main + form_login: + check_path: /main/login/check custom_authenticators: - 'Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ApiAuthenticator' + provider: main access_control: - - { path: ^/foo, roles: PUBLIC_ACCESS } + - { path: '^/main/login/check$', roles: IS_AUTHENTICATED_FULLY } + - { path: '^/main/logged-in$', roles: IS_AUTHENTICATED_FULLY } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/config_logout_csrf.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/config_logout_csrf.yml index 652c4c0b686e6..86cfe2b1e6d8b 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/config_logout_csrf.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/config_logout_csrf.yml @@ -2,7 +2,6 @@ imports: - { resource: ./../config/framework.yml } services: - # alias the service so we can access it in the tests functional_test.security.helper: alias: security.helper public: true @@ -11,32 +10,39 @@ services: alias: security.token_storage public: true - Symfony\Bundle\SecurityBundle\Tests\Functional\WelcomeController: + Symfony\Bundle\SecurityBundle\Tests\Functional\ForceLoginController: arguments: ['@security.helper'] public: true Symfony\Bundle\SecurityBundle\Tests\Functional\LogoutController: - arguments: ['@security.helper', '@security.csrf.token_manager'] + arguments: ['@security.helper'] + public: true + + Symfony\Bundle\SecurityBundle\Tests\Functional\LoggedInController: + arguments: ['@security.helper'] public: true Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ApiAuthenticator: ~ security: enable_authenticator_manager: true + providers: - in_memory: + main: memory: - users: [] + users: + chalasr: { password: the-password, roles: ['ROLE_FOO'] } + no-role-username: { password: the-password, roles: [] } firewalls: - default: - json_login: - username_path: user.login - password_path: user.password - logout: - path: /regular-logout + main: + pattern: ^/main + form_login: + check_path: /main/login/check custom_authenticators: - 'Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ApiAuthenticator' + provider: main access_control: - - { path: ^/foo, roles: PUBLIC_ACCESS } + - { path: '^/main/logged-in$', roles: IS_AUTHENTICATED_FULLY } + - { path: '^/main/force-logout$', roles: IS_AUTHENTICATED_FULLY } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/routing.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/routing.yml index b862fc3185d38..1f74d27501207 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/routing.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/routing.yml @@ -1,7 +1,11 @@ -welcome: - path: /welcome - controller: Symfony\Bundle\SecurityBundle\Tests\Functional\WelcomeController::welcome +force-login: + path: /main/force-login + controller: Symfony\Bundle\SecurityBundle\Tests\Functional\ForceLoginController::welcome + +logged-in: + path: /main/logged-in + controller: Symfony\Bundle\SecurityBundle\Tests\Functional\LoggedInController force-logout: - path: /force-logout + path: /main/force-logout controller: Symfony\Bundle\SecurityBundle\Tests\Functional\LogoutController::logout diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Security/SecurityTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Security/SecurityTest.php index 8a91f0f7c4a92..3eefbc8bccc68 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Security/SecurityTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Security/SecurityTest.php @@ -28,6 +28,8 @@ use Symfony\Component\Security\Core\User\InMemoryUser; use Symfony\Component\Security\Core\User\UserCheckerInterface; use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Csrf\CsrfToken; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Security\Http\Authentication\UserAuthenticatorInterface; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; use Symfony\Component\Security\Http\Event\LogoutEvent; @@ -177,7 +179,7 @@ public function testLogout() $token->method('getUser')->willReturn(new InMemoryUser('foo', 'bar')); $tokenStorage = $this->createMock(TokenStorageInterface::class); $tokenStorage->expects($this->once())->method('getToken')->willReturn($token); - $tokenStorage->expects($this->once())->method('setToken'); + $tokenStorage->expects($this->once())->method('setToken')->with(null); $eventDispatcher = $this->createMock(EventDispatcherInterface::class); $eventDispatcher @@ -215,7 +217,7 @@ public function testLogout() ]) ; $security = new Security($container); - $security->logout(); + $security->logout(false); } public function testLogoutWithoutFirewall() @@ -253,7 +255,7 @@ public function testLogoutWithoutFirewall() $this->expectException(LogicException::class); $security = new Security($container); - $security->logout(); + $security->logout(false); } public function testLogoutWithResponse() @@ -266,7 +268,7 @@ public function testLogoutWithResponse() $token->method('getUser')->willReturn(new InMemoryUser('foo', 'bar')); $tokenStorage = $this->createMock(TokenStorageInterface::class); $tokenStorage->expects($this->once())->method('getToken')->willReturn($token); - $tokenStorage->expects($this->once())->method('setToken')->with(); + $tokenStorage->expects($this->once())->method('setToken')->with(null); $eventDispatcher = $this->createMock(EventDispatcherInterface::class); $eventDispatcher @@ -306,15 +308,15 @@ public function testLogoutWithResponse() ]) ; $security = new Security($container); - $response = $security->logout(); + $response = $security->logout(false); $this->assertInstanceOf(Response::class, $response); $this->assertEquals('a custom response', $response->getContent()); } - public function testLogoutWithCsrf() + public function testLogoutWithValidCsrf() { - $request = new Request(); + $request = new Request(['_csrf_token' => 'dummytoken']); $requestStack = $this->createMock(RequestStack::class); $requestStack->expects($this->once())->method('getMainRequest')->willReturn($request); @@ -322,7 +324,7 @@ public function testLogoutWithCsrf() $token->method('getUser')->willReturn(new InMemoryUser('foo', 'bar')); $tokenStorage = $this->createMock(TokenStorageInterface::class); $tokenStorage->expects($this->once())->method('getToken')->willReturn($token); - $tokenStorage->expects($this->once())->method('setToken')->with(); + $tokenStorage->expects($this->once())->method('setToken')->with(null); $eventDispatcher = $this->createMock(EventDispatcherInterface::class); $eventDispatcher @@ -340,7 +342,7 @@ public function testLogoutWithCsrf() ; $firewallMap = $this->createMock(FirewallMap::class); - $firewallConfig = new FirewallConfig('my_firewall', 'user_checker'); + $firewallConfig = new FirewallConfig(name: 'my_firewall', userChecker: 'user_checker', logout: ['csrf_parameter' => '_csrf_token', 'csrf_token_id' => 'logout']); $firewallMap->expects($this->once())->method('getFirewallConfig')->willReturn($firewallConfig); $eventDispatcherLocator = $this->createMock(ContainerInterface::class); @@ -350,7 +352,11 @@ public function testLogoutWithCsrf() ->willReturnMap([['my_firewall', $eventDispatcher]]) ; + $csrfTokenManager = $this->createMock(CsrfTokenManagerInterface::class); + $csrfTokenManager->expects($this->once())->method('isTokenValid')->with($this->equalTo(new CsrfToken('logout', 'dummytoken')))->willReturn(true); + $container = $this->createMock(ContainerInterface::class); + $container->expects($this->once())->method('has')->with('security.csrf.token_manager')->willReturn(true); $container ->expects($this->atLeastOnce()) ->method('get') @@ -359,6 +365,7 @@ public function testLogoutWithCsrf() ['security.token_storage', $tokenStorage], ['security.firewall.map', $firewallMap], ['security.firewall.event_dispatcher_locator', $eventDispatcherLocator], + ['security.csrf.token_manager', $csrfTokenManager], ]) ; $security = new Security($container);