Skip to content

Commit f41a184

Browse files
committed
Add CSRF protection
1 parent f576173 commit f41a184

File tree

9 files changed

+192
-88
lines changed

9 files changed

+192
-88
lines changed

src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,8 @@ private function createFirewall(ContainerBuilder $container, string $id, array $
455455
false === $firewall['stateless'] && isset($firewall['context']) ? $firewall['context'] : null,
456456
])
457457
;
458+
459+
$config->replaceArgument(12, $firewall['logout']);
458460
}
459461

460462
// Determine default entry point

src/Symfony/Bundle/SecurityBundle/Resources/config/security.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
'security.firewall.map' => service('security.firewall.map'),
8888
'security.user_checker' => service('security.user_checker'),
8989
'security.firewall.event_dispatcher_locator' => service('security.firewall.event_dispatcher_locator'),
90+
'security.csrf.token_manager' => service('security.csrf.token_manager')->ignoreOnInvalid(),
9091
]),
9192
abstract_arg('authenticators'),
9293
])
@@ -207,6 +208,7 @@
207208
null,
208209
[], // listeners
209210
null, // switch_user
211+
null, // logout
210212
])
211213

212214
->set('security.logout_url_generator', LogoutUrlGenerator::class)

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

Lines changed: 20 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,33 +16,21 @@
1616
*/
1717
final class FirewallConfig
1818
{
19-
private string $name;
20-
private string $userChecker;
21-
private ?string $requestMatcher;
22-
private bool $securityEnabled;
23-
private bool $stateless;
24-
private ?string $provider;
25-
private ?string $context;
26-
private ?string $entryPoint;
27-
private ?string $accessDeniedHandler;
28-
private ?string $accessDeniedUrl;
29-
private array $authenticators;
30-
private ?array $switchUser;
31-
32-
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)
33-
{
34-
$this->name = $name;
35-
$this->userChecker = $userChecker;
36-
$this->requestMatcher = $requestMatcher;
37-
$this->securityEnabled = $securityEnabled;
38-
$this->stateless = $stateless;
39-
$this->provider = $provider;
40-
$this->context = $context;
41-
$this->entryPoint = $entryPoint;
42-
$this->accessDeniedHandler = $accessDeniedHandler;
43-
$this->accessDeniedUrl = $accessDeniedUrl;
44-
$this->authenticators = $authenticators;
45-
$this->switchUser = $switchUser;
19+
public function __construct(
20+
private readonly string $name,
21+
private readonly string $userChecker,
22+
private readonly ?string $requestMatcher = null,
23+
private readonly bool $securityEnabled = true,
24+
private readonly bool $stateless = false,
25+
private readonly ?string $provider = null,
26+
private readonly ?string $context = null,
27+
private readonly ?string $entryPoint = null,
28+
private readonly ?string $accessDeniedHandler = null,
29+
private readonly ?string $accessDeniedUrl = null,
30+
private readonly array $authenticators = [],
31+
private readonly ?array $switchUser = null,
32+
private readonly ?array $logout = null
33+
) {
4634
}
4735

4836
public function getName(): string
@@ -111,4 +99,9 @@ public function getSwitchUser(): ?array
11199
{
112100
return $this->switchUser;
113101
}
102+
103+
public function getLogout(): ?array
104+
{
105+
return $this->logout;
106+
}
114107
}

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

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,15 @@
1414
use Psr\Container\ContainerInterface;
1515
use Symfony\Component\HttpFoundation\Request;
1616
use Symfony\Component\HttpFoundation\Response;
17+
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
1718
use Symfony\Component\Security\Core\Exception\LogicException;
19+
use Symfony\Component\Security\Core\Exception\LogoutException;
1820
use Symfony\Component\Security\Core\Security as LegacySecurity;
1921
use Symfony\Component\Security\Core\User\UserInterface;
22+
use Symfony\Component\Security\Csrf\CsrfToken;
2023
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
2124
use Symfony\Component\Security\Http\Event\LogoutEvent;
25+
use Symfony\Component\Security\Http\ParameterBagUtils;
2226
use Symfony\Contracts\Service\ServiceProviderInterface;
2327

2428
/**
@@ -69,17 +73,30 @@ public function login(UserInterface $user, string $authenticatorName = null, str
6973
*/
7074
public function logout(): ?Response
7175
{
76+
/** @var TokenStorageInterface $tokenStorage */
77+
$tokenStorage = $this->container->get('security.token_storage');
78+
79+
if (!($token = $tokenStorage->getToken()) || !$token->getUser()) {
80+
throw new LogicException('Unable to logout as there is no logged-in user.');
81+
}
82+
7283
$request = $this->container->get('request_stack')->getMainRequest();
73-
$logoutEvent = new LogoutEvent($request, $this->container->get('security.token_storage')->getToken());
74-
$firewallConfig = $this->container->get('security.firewall.map')->getFirewallConfig($request);
7584

76-
if (!$firewallConfig) {
77-
throw new LogicException('It is not possible to logout, as the request is not behind a firewall.');
85+
if (!$firewallConfig = $this->container->get('security.firewall.map')->getFirewallConfig($request)) {
86+
throw new LogicException('Unable to logout as the request is not behind a firewall.');
7887
}
79-
$firewallName = $firewallConfig->getName();
8088

81-
$this->container->get('security.firewall.event_dispatcher_locator')->get($firewallName)->dispatch($logoutEvent);
82-
$this->container->get('security.token_storage')->setToken();
89+
if ($this->container->has('security.csrf.token_manager') && $logoutConfig = $firewallConfig->getLogout()) {
90+
$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))) {
92+
throw new LogoutException('Invalid CSRF token.');
93+
}
94+
}
95+
96+
$logoutEvent = new LogoutEvent($request, $token);
97+
$this->container->get('security.firewall.event_dispatcher_locator')->get($firewallConfig->getName())->dispatch($logoutEvent);
98+
99+
$tokenStorage->setToken();
83100

84101
return $logoutEvent->getResponse();
85102
}

src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ public function testFirewalls()
141141
'',
142142
[],
143143
null,
144+
null,
144145
],
145146
[
146147
'secure',
@@ -165,6 +166,14 @@ public function testFirewalls()
165166
'parameter' => '_switch_user',
166167
'role' => 'ROLE_ALLOWED_TO_SWITCH',
167168
],
169+
[
170+
'csrf_parameter' => '_csrf_token',
171+
'csrf_token_id' => 'logout',
172+
'path' => '/logout',
173+
'target' => '/',
174+
'invalidate_session' => true,
175+
'delete_cookies' => [],
176+
],
168177
],
169178
[
170179
'host',
@@ -181,6 +190,7 @@ public function testFirewalls()
181190
'http_basic',
182191
],
183192
null,
193+
null,
184194
],
185195
[
186196
'with_user_checker',
@@ -197,6 +207,7 @@ public function testFirewalls()
197207
'http_basic',
198208
],
199209
null,
210+
null,
200211
],
201212
], $configs);
202213

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

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Symfony\Component\Security\Core\User\InMemoryUser;
2121
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
2222
use Symfony\Component\Security\Core\User\UserInterface;
23+
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
2324

2425
class SecurityTest extends AbstractWebTestCase
2526
{
@@ -88,31 +89,36 @@ public function userWillBeMarkedAsChangedIfRolesHasChangedProvider()
8889
* @testWith ["json_login"]
8990
* ["Symfony\\Bundle\\SecurityBundle\\Tests\\Functional\\Bundle\\AuthenticatorBundle\\ApiAuthenticator"]
9091
*/
91-
public function testLoginWithBuiltInAuthenticator(string $authenticator)
92+
public function testLogin(string $authenticator)
9293
{
93-
$client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config.yml', 'debug' => true]);
94+
$client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config.yml']);
9495
static::getContainer()->get(WelcomeController::class)->authenticator = $authenticator;
9596
$client->request('GET', '/welcome');
9697
$response = $client->getResponse();
9798

9899
$this->assertInstanceOf(JsonResponse::class, $response);
99100
$this->assertSame(200, $response->getStatusCode());
100101
$this->assertSame(['message' => 'Welcome @chalasr!'], json_decode($response->getContent(), true));
102+
$this->assertSame('chalasr', static::getContainer()->get('security.helper')->getUser()->getUserIdentifier());
101103
}
102104

103-
/**
104-
* @testWith ["json_login"]
105-
* ["Symfony\\Bundle\\SecurityBundle\\Tests\\Functional\\Bundle\\AuthenticatorBundle\\ApiAuthenticator"]
106-
*/
107-
public function testLogout(string $authenticator)
105+
public function testLogout()
108106
{
109-
$client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config.yml', 'debug' => true]);
110-
static::getContainer()->get(WelcomeController::class)->authenticator = $authenticator;
111-
$client->request('GET', '/welcome');
112-
$this->assertEquals('chalasr', static::getContainer()->get('security.helper')->getUser()->getUserIdentifier());
107+
$client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config.yml']);
108+
$client->request('GET', '/force-logout');
109+
$response = $client->getResponse();
110+
111+
$this->assertSame(200, $response->getStatusCode());
112+
$this->assertNull(static::getContainer()->get('security.helper')->getUser());
113+
$this->assertSame(['message' => 'Logout successful'], json_decode($response->getContent(), true));
114+
}
113115

114-
$client->request('GET', '/auto-logout');
116+
public function testLogoutWithCsrf()
117+
{
118+
$client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config_logout_csrf.yml']);
119+
$client->request('GET', '/force-logout');
115120
$response = $client->getResponse();
121+
116122
$this->assertSame(200, $response->getStatusCode());
117123
$this->assertNull(static::getContainer()->get('security.helper')->getUser());
118124
$this->assertSame(['message' => 'Logout successful'], json_decode($response->getContent(), true));
@@ -245,12 +251,16 @@ public function welcome()
245251

246252
class LogoutController
247253
{
248-
public function __construct(private Security $security)
254+
public function __construct(private Security $security, private ?CsrfTokenManagerInterface $csrfTokenManager = null)
249255
{
250256
}
251257

252-
public function logout()
258+
public function logout(Request $request)
253259
{
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+
}
254264
$this->security->logout();
255265

256266
return new JsonResponse(['message' => 'Logout successful']);
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
imports:
2+
- { resource: ./../config/framework.yml }
3+
4+
services:
5+
# alias the service so we can access it in the tests
6+
functional_test.security.helper:
7+
alias: security.helper
8+
public: true
9+
10+
functional.test.security.token_storage:
11+
alias: security.token_storage
12+
public: true
13+
14+
Symfony\Bundle\SecurityBundle\Tests\Functional\WelcomeController:
15+
arguments: ['@security.helper']
16+
public: true
17+
18+
Symfony\Bundle\SecurityBundle\Tests\Functional\LogoutController:
19+
arguments: ['@security.helper', '@security.csrf.token_manager']
20+
public: true
21+
22+
Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ApiAuthenticator: ~
23+
24+
security:
25+
enable_authenticator_manager: true
26+
providers:
27+
in_memory:
28+
memory:
29+
users: []
30+
31+
firewalls:
32+
default:
33+
json_login:
34+
username_path: user.login
35+
password_path: user.password
36+
logout:
37+
path: /regular-logout
38+
custom_authenticators:
39+
- 'Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ApiAuthenticator'
40+
41+
access_control:
42+
- { path: ^/foo, roles: PUBLIC_ACCESS }

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ welcome:
22
path: /welcome
33
controller: Symfony\Bundle\SecurityBundle\Tests\Functional\WelcomeController::welcome
44

5-
logout:
6-
path: /auto-logout
5+
force-logout:
6+
path: /force-logout
77
controller: Symfony\Bundle\SecurityBundle\Tests\Functional\LogoutController::logout

0 commit comments

Comments
 (0)