Skip to content

Commit e5e7d5e

Browse files
committed
Make CSRF validation opt-in
1 parent f41a184 commit e5e7d5e

File tree

6 files changed

+133
-58
lines changed

6 files changed

+133
-58
lines changed

src/Symfony/Bundle/SecurityBundle/Security/Security.php

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,13 @@ public function login(UserInterface $user, string $authenticatorName = null, str
6969
/**
7070
* Logout the current user by dispatching the LogoutEvent.
7171
*
72+
* @param bool $validateCsrfToken Whether to look for a valid CSRF token based on the `logout` listener configuration
73+
*
7274
* @return Response|null The LogoutEvent's Response if any
75+
*
76+
* @throws LogoutException When $validateCsrfToken is true and the CSRF token is not found or invalid
7377
*/
74-
public function logout(): ?Response
78+
public function logout(bool $validateCsrfToken = true): ?Response
7579
{
7680
/** @var TokenStorageInterface $tokenStorage */
7781
$tokenStorage = $this->container->get('security.token_storage');
@@ -86,17 +90,20 @@ public function logout(): ?Response
8690
throw new LogicException('Unable to logout as the request is not behind a firewall.');
8791
}
8892

89-
if ($this->container->has('security.csrf.token_manager') && $logoutConfig = $firewallConfig->getLogout()) {
93+
if ($validateCsrfToken) {
94+
if (!$this->container->has('security.csrf.token_manager') || !$logoutConfig = $firewallConfig->getLogout()) {
95+
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()));
96+
}
9097
$csrfToken = ParameterBagUtils::getRequestParameterValue($request, $logoutConfig['csrf_parameter']);
91-
if (!\is_string($csrfToken) || false === $this->container->get('security.csrf.token_manager')->isTokenValid(new CsrfToken($logoutConfig['csrf_token_id'], $csrfToken))) {
98+
if (!\is_string($csrfToken) || !$this->container->get('security.csrf.token_manager')->isTokenValid(new CsrfToken($logoutConfig['csrf_token_id'], $csrfToken))) {
9299
throw new LogoutException('Invalid CSRF token.');
93100
}
94101
}
95102

96103
$logoutEvent = new LogoutEvent($request, $token);
97104
$this->container->get('security.firewall.event_dispatcher_locator')->get($firewallConfig->getName())->dispatch($logoutEvent);
98105

99-
$tokenStorage->setToken();
106+
$tokenStorage->setToken(null);
100107

101108
return $logoutEvent->getResponse();
102109
}

src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityTest.php

Lines changed: 63 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,16 @@
1414
use Symfony\Bundle\SecurityBundle\Security\FirewallConfig;
1515
use Symfony\Bundle\SecurityBundle\Security\Security;
1616
use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\SecuredPageBundle\Security\Core\User\ArrayUserProvider;
17+
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
1718
use Symfony\Component\HttpFoundation\JsonResponse;
1819
use Symfony\Component\HttpFoundation\Request;
20+
use Symfony\Component\HttpFoundation\Response;
21+
use Symfony\Component\HttpKernel\Event\RequestEvent;
22+
use Symfony\Component\HttpKernel\KernelEvents;
1923
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
2024
use Symfony\Component\Security\Core\User\InMemoryUser;
2125
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
2226
use Symfony\Component\Security\Core\User\UserInterface;
23-
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
2427

2528
class SecurityTest extends AbstractWebTestCase
2629
{
@@ -38,8 +41,10 @@ public function testServiceIsFunctional()
3841
$security = $container->get('functional_test.security.helper');
3942
$this->assertTrue($security->isGranted('ROLE_USER'));
4043
$this->assertSame($token, $security->getToken());
41-
$this->assertInstanceOf(FirewallConfig::class, $firewallConfig = $security->getFirewallConfig(new Request()));
42-
$this->assertSame('default', $firewallConfig->getName());
44+
$request = new Request();
45+
$request->server->set('REQUEST_URI', '/main/foo');
46+
$this->assertInstanceOf(FirewallConfig::class, $firewallConfig = $security->getFirewallConfig($request));
47+
$this->assertSame('main', $firewallConfig->getName());
4348
}
4449

4550
/**
@@ -86,14 +91,14 @@ public function userWillBeMarkedAsChangedIfRolesHasChangedProvider()
8691
}
8792

8893
/**
89-
* @testWith ["json_login"]
94+
* @testWith ["form_login"]
9095
* ["Symfony\\Bundle\\SecurityBundle\\Tests\\Functional\\Bundle\\AuthenticatorBundle\\ApiAuthenticator"]
9196
*/
9297
public function testLogin(string $authenticator)
9398
{
94-
$client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config.yml']);
95-
static::getContainer()->get(WelcomeController::class)->authenticator = $authenticator;
96-
$client->request('GET', '/welcome');
99+
$client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config.yml', 'debug' > true]);
100+
static::getContainer()->get(ForceLoginController::class)->authenticator = $authenticator;
101+
$client->request('GET', '/main/force-login');
97102
$response = $client->getResponse();
98103

99104
$this->assertInstanceOf(JsonResponse::class, $response);
@@ -104,8 +109,10 @@ public function testLogin(string $authenticator)
104109

105110
public function testLogout()
106111
{
107-
$client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config.yml']);
108-
$client->request('GET', '/force-logout');
112+
$client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config.yml', 'debug' => true]);
113+
$client->loginUser(new InMemoryUser('chalasr', 'the-password', ['ROLE_FOO']), 'main');
114+
115+
$client->request('GET', '/main/force-logout');
109116
$response = $client->getResponse();
110117

111118
$this->assertSame(200, $response->getStatusCode());
@@ -114,9 +121,39 @@ public function testLogout()
114121
}
115122

116123
public function testLogoutWithCsrf()
124+
{
125+
$client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config_logout_csrf.yml', 'debug' => true]);
126+
$client->loginUser(new InMemoryUser('chalasr', 'the-password', ['ROLE_FOO']), 'main');
127+
128+
// put a csrf token in the storage
129+
/** @var EventDispatcherInterface $eventDispatcher */
130+
$eventDispatcher = static::getContainer()->get(EventDispatcherInterface::class);
131+
$setCsrfToken = function (RequestEvent $event) {
132+
static::getContainer()->get('security.csrf.token_storage')->setToken('logout', 'bar');
133+
$event->setResponse(new Response(''));
134+
};
135+
$eventDispatcher->addListener(KernelEvents::REQUEST, $setCsrfToken);
136+
try {
137+
$client->request('GET', '/'.uniqid('', true));
138+
} finally {
139+
$eventDispatcher->removeListener(KernelEvents::REQUEST, $setCsrfToken);
140+
}
141+
142+
static::getContainer()->get(LogoutController::class)->checkCsrf = true;
143+
$client->request('GET', '/main/force-logout', ['_csrf_token' => 'bar']);
144+
$response = $client->getResponse();
145+
146+
$this->assertSame(200, $response->getStatusCode());
147+
$this->assertNull(static::getContainer()->get('security.helper')->getUser());
148+
$this->assertSame(['message' => 'Logout successful'], json_decode($response->getContent(), true));
149+
}
150+
151+
public function testLogoutBypassCsrf()
117152
{
118153
$client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config_logout_csrf.yml']);
119-
$client->request('GET', '/force-logout');
154+
$client->loginUser(new InMemoryUser('chalasr', 'the-password', ['ROLE_FOO']), 'main');
155+
156+
$client->request('GET', '/main/force-logout');
120157
$response = $client->getResponse();
121158

122159
$this->assertSame(200, $response->getStatusCode());
@@ -232,17 +269,17 @@ public function eraseCredentials(): void
232269
}
233270
}
234271

235-
class WelcomeController
272+
class ForceLoginController
236273
{
237-
public $authenticator = 'json_login';
274+
public $authenticator = 'form_login';
238275

239276
public function __construct(private Security $security)
240277
{
241278
}
242279

243280
public function welcome()
244281
{
245-
$user = new InMemoryUser('chalasr', '', ['ROLE_USER']);
282+
$user = new InMemoryUser('chalasr', 'the-password', ['ROLE_FOO']);
246283
$this->security->login($user, $this->authenticator);
247284

248285
return new JsonResponse(['message' => sprintf('Welcome @%s!', $this->security->getUser()->getUserIdentifier())]);
@@ -251,18 +288,24 @@ public function welcome()
251288

252289
class LogoutController
253290
{
254-
public function __construct(private Security $security, private ?CsrfTokenManagerInterface $csrfTokenManager = null)
291+
public $checkCsrf = false;
292+
293+
public function __construct(private Security $security)
255294
{
256295
}
257296

258-
public function logout(Request $request)
297+
public function logout(UserInterface $user)
259298
{
260-
$this->security->login(new InMemoryUser('chalasr', '', ['ROLE_USER']), 'json_login', 'default');
261-
if ($this->csrfTokenManager) {
262-
$request->query->set('_csrf_token', (string) $this->csrfTokenManager->getToken('logout'));
263-
}
264-
$this->security->logout();
299+
$this->security->logout($this->checkCsrf);
265300

266301
return new JsonResponse(['message' => 'Logout successful']);
267302
}
268303
}
304+
305+
class LoggedInController
306+
{
307+
public function __invoke(UserInterface $user)
308+
{
309+
return new JsonResponse(['message' => sprintf('Welcome back @%s', $user->getUserIdentifier())]);
310+
}
311+
}

src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/config.yml

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ imports:
22
- { resource: ./../config/framework.yml }
33

44
services:
5-
# alias the service so we can access it in the tests
65
functional_test.security.helper:
76
alias: security.helper
87
public: true
@@ -11,30 +10,39 @@ services:
1110
alias: security.token_storage
1211
public: true
1312

14-
Symfony\Bundle\SecurityBundle\Tests\Functional\WelcomeController:
13+
Symfony\Bundle\SecurityBundle\Tests\Functional\ForceLoginController:
1514
arguments: ['@security.helper']
1615
public: true
1716

1817
Symfony\Bundle\SecurityBundle\Tests\Functional\LogoutController:
1918
arguments: ['@security.helper']
2019
public: true
2120

21+
Symfony\Bundle\SecurityBundle\Tests\Functional\LoggedInController:
22+
arguments: ['@security.helper']
23+
public: true
24+
2225
Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ApiAuthenticator: ~
2326

2427
security:
2528
enable_authenticator_manager: true
29+
2630
providers:
27-
in_memory:
31+
main:
2832
memory:
29-
users: []
33+
users:
34+
chalasr: { password: the-password, roles: ['ROLE_FOO'] }
35+
no-role-username: { password: the-password, roles: [] }
3036

3137
firewalls:
32-
default:
33-
json_login:
34-
username_path: user.login
35-
password_path: user.password
38+
main:
39+
pattern: ^/main
40+
form_login:
41+
check_path: /main/login/check
3642
custom_authenticators:
3743
- 'Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ApiAuthenticator'
44+
provider: main
3845

3946
access_control:
40-
- { path: ^/foo, roles: PUBLIC_ACCESS }
47+
- { path: '^/main/login/check$', roles: IS_AUTHENTICATED_FULLY }
48+
- { path: '^/main/logged-in$', roles: IS_AUTHENTICATED_FULLY }

src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/config_logout_csrf.yml

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ imports:
22
- { resource: ./../config/framework.yml }
33

44
services:
5-
# alias the service so we can access it in the tests
65
functional_test.security.helper:
76
alias: security.helper
87
public: true
@@ -11,32 +10,39 @@ services:
1110
alias: security.token_storage
1211
public: true
1312

14-
Symfony\Bundle\SecurityBundle\Tests\Functional\WelcomeController:
13+
Symfony\Bundle\SecurityBundle\Tests\Functional\ForceLoginController:
1514
arguments: ['@security.helper']
1615
public: true
1716

1817
Symfony\Bundle\SecurityBundle\Tests\Functional\LogoutController:
19-
arguments: ['@security.helper', '@security.csrf.token_manager']
18+
arguments: ['@security.helper']
19+
public: true
20+
21+
Symfony\Bundle\SecurityBundle\Tests\Functional\LoggedInController:
22+
arguments: ['@security.helper']
2023
public: true
2124

2225
Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ApiAuthenticator: ~
2326

2427
security:
2528
enable_authenticator_manager: true
29+
2630
providers:
27-
in_memory:
31+
main:
2832
memory:
29-
users: []
33+
users:
34+
chalasr: { password: the-password, roles: ['ROLE_FOO'] }
35+
no-role-username: { password: the-password, roles: [] }
3036

3137
firewalls:
32-
default:
33-
json_login:
34-
username_path: user.login
35-
password_path: user.password
36-
logout:
37-
path: /regular-logout
38+
main:
39+
pattern: ^/main
40+
form_login:
41+
check_path: /main/login/check
3842
custom_authenticators:
3943
- 'Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ApiAuthenticator'
44+
provider: main
4045

4146
access_control:
42-
- { path: ^/foo, roles: PUBLIC_ACCESS }
47+
- { path: '^/main/logged-in$', roles: IS_AUTHENTICATED_FULLY }
48+
- { path: '^/main/force-logout$', roles: IS_AUTHENTICATED_FULLY }
Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
welcome:
2-
path: /welcome
3-
controller: Symfony\Bundle\SecurityBundle\Tests\Functional\WelcomeController::welcome
1+
force-login:
2+
path: /main/force-login
3+
controller: Symfony\Bundle\SecurityBundle\Tests\Functional\ForceLoginController::welcome
4+
5+
logged-in:
6+
path: /main/logged-in
7+
controller: Symfony\Bundle\SecurityBundle\Tests\Functional\LoggedInController
48

59
force-logout:
6-
path: /force-logout
10+
path: /main/force-logout
711
controller: Symfony\Bundle\SecurityBundle\Tests\Functional\LogoutController::logout

0 commit comments

Comments
 (0)