Skip to content

Commit 11d7d37

Browse files
committed
Add IsCsrfTokenValid attribute
1 parent c4e97eb commit 11d7d37

File tree

6 files changed

+300
-0
lines changed

6 files changed

+300
-0
lines changed

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
4545
use Symfony\Component\Security\Http\Controller\SecurityTokenValueResolver;
4646
use Symfony\Component\Security\Http\Controller\UserValueResolver;
47+
use Symfony\Component\Security\Http\EventListener\IsCsrfTokenValidAttributeListener;
4748
use Symfony\Component\Security\Http\EventListener\IsGrantedAttributeListener;
4849
use Symfony\Component\Security\Http\Firewall;
4950
use Symfony\Component\Security\Http\FirewallMapInterface;
@@ -297,6 +298,12 @@
297298
])
298299
->tag('kernel.event_subscriber')
299300

301+
->set('controller.is_csrf_token_valid_attribute_listener', IsCsrfTokenValidAttributeListener::class)
302+
->args([
303+
service('security.csrf.token_manager'),
304+
])
305+
->tag('kernel.event_subscriber')
306+
300307
->set('security.is_granted_attribute_expression_language', BaseExpressionLanguage::class)
301308
->args([service('cache.security_is_granted_attribute_expression_language')->nullOnInvalid()])
302309

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Http\Attribute;
13+
14+
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION)]
15+
final class IsCsrfTokenValid
16+
{
17+
public function __construct(
18+
/**
19+
* Sets the id used when generating the token.
20+
*/
21+
public string $id,
22+
23+
/**
24+
* Sets the key of the request that contains the actual token value that should be validated.
25+
*/
26+
public ?string $tokenKey = '_token',
27+
) {
28+
}
29+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Http\EventListener;
13+
14+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
15+
use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;
16+
use Symfony\Component\HttpKernel\KernelEvents;
17+
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
18+
use Symfony\Component\Security\Csrf\CsrfToken;
19+
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
20+
use Symfony\Component\Security\Http\Attribute\IsCsrfTokenValid;
21+
22+
/**
23+
* Handles the IsCsrfTokenValid attribute on controllers.
24+
*/
25+
class IsCsrfTokenValidAttributeListener implements EventSubscriberInterface
26+
{
27+
public function __construct(
28+
private readonly CsrfTokenManagerInterface $csrfTokenManager,
29+
) {
30+
}
31+
32+
public function onKernelControllerArguments(ControllerArgumentsEvent $event): void
33+
{
34+
/** @var IsCsrfTokenValid[] $attributes */
35+
if (!\is_array($attributes = $event->getAttributes()[IsCsrfTokenValid::class] ?? null)) {
36+
return;
37+
}
38+
39+
$request = $event->getRequest();
40+
41+
foreach ($attributes as $attribute) {
42+
if (!$this->csrfTokenManager->isTokenValid(new CsrfToken($attribute->id, $request->request->getString($attribute->tokenKey)))) {
43+
throw new InvalidCsrfTokenException('Invalid CSRF token.');
44+
}
45+
}
46+
}
47+
48+
public static function getSubscribedEvents(): array
49+
{
50+
return [KernelEvents::CONTROLLER_ARGUMENTS => ['onKernelControllerArguments', 20]];
51+
}
52+
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace EventListener;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;
17+
use Symfony\Component\HttpKernel\HttpKernelInterface;
18+
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
19+
use Symfony\Component\Security\Csrf\CsrfToken;
20+
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
21+
use Symfony\Component\Security\Http\EventListener\IsCsrfTokenValidAttributeListener;
22+
use Symfony\Component\Security\Http\Tests\Fixtures\IsCsrfTokenValidAttributeController;
23+
use Symfony\Component\Security\Http\Tests\Fixtures\IsCsrfTokenValidAttributeMethodsController;
24+
25+
class IsCsrfTokenValidAttributeListenerTest extends TestCase
26+
{
27+
public function testIsCsrfTokenValidCalledCorrectlyOnInvokableClass()
28+
{
29+
$request = new Request(request: ['_token' => 'bar']);
30+
31+
$csrfTokenManager = $this->createMock(CsrfTokenManagerInterface::class);
32+
$csrfTokenManager->expects($this->once())
33+
->method('isTokenValid')
34+
->with(new CsrfToken('foo', 'bar'))
35+
->willReturn(true);
36+
37+
$event = new ControllerArgumentsEvent(
38+
$this->createMock(HttpKernelInterface::class),
39+
new IsCsrfTokenValidAttributeController(),
40+
[],
41+
$request,
42+
null
43+
);
44+
45+
$listener = new IsCsrfTokenValidAttributeListener($csrfTokenManager);
46+
$listener->onKernelControllerArguments($event);
47+
}
48+
49+
public function testNothingHappensWithNoConfig()
50+
{
51+
$csrfTokenManager = $this->createMock(CsrfTokenManagerInterface::class);
52+
$csrfTokenManager->expects($this->never())
53+
->method('isTokenValid');
54+
55+
$event = new ControllerArgumentsEvent(
56+
$this->createMock(HttpKernelInterface::class),
57+
[new IsCsrfTokenValidAttributeMethodsController(), 'noAttribute'],
58+
[],
59+
new Request(),
60+
null
61+
);
62+
63+
$listener = new IsCsrfTokenValidAttributeListener($csrfTokenManager);
64+
$listener->onKernelControllerArguments($event);
65+
}
66+
67+
public function testIsCsrfTokenValidCalledCorrectly()
68+
{
69+
$request = new Request(request: ['_token' => 'bar']);
70+
71+
$csrfTokenManager = $this->createMock(CsrfTokenManagerInterface::class);
72+
$csrfTokenManager->expects($this->once())
73+
->method('isTokenValid')
74+
->with(new CsrfToken('foo', 'bar'))
75+
->willReturn(true);
76+
77+
$event = new ControllerArgumentsEvent(
78+
$this->createMock(HttpKernelInterface::class),
79+
[new IsCsrfTokenValidAttributeMethodsController(), 'withDefaultTokenKey'],
80+
[],
81+
$request,
82+
null
83+
);
84+
85+
$listener = new IsCsrfTokenValidAttributeListener($csrfTokenManager);
86+
$listener->onKernelControllerArguments($event);
87+
}
88+
89+
public function testIsCsrfTokenValidCalledCorrectlyWithCustomTokenKey()
90+
{
91+
$request = new Request(request: ['my_token_key' => 'bar']);
92+
93+
$csrfTokenManager = $this->createMock(CsrfTokenManagerInterface::class);
94+
$csrfTokenManager->expects($this->once())
95+
->method('isTokenValid')
96+
->with(new CsrfToken('foo', 'bar'))
97+
->willReturn(true);
98+
99+
$event = new ControllerArgumentsEvent(
100+
$this->createMock(HttpKernelInterface::class),
101+
[new IsCsrfTokenValidAttributeMethodsController(), 'withCustomTokenKey'],
102+
[],
103+
$request,
104+
null
105+
);
106+
107+
$listener = new IsCsrfTokenValidAttributeListener($csrfTokenManager);
108+
$listener->onKernelControllerArguments($event);
109+
}
110+
111+
public function testIsCsrfTokenValidCalledCorrectlyWithInvalidTokenKey()
112+
{
113+
$request = new Request(request: ['_token' => 'bar']);
114+
115+
$csrfTokenManager = $this->createMock(CsrfTokenManagerInterface::class);
116+
$csrfTokenManager->expects($this->once())
117+
->method('isTokenValid')
118+
->with(new CsrfToken('foo', ''))
119+
->willReturn(true);
120+
121+
$event = new ControllerArgumentsEvent(
122+
$this->createMock(HttpKernelInterface::class),
123+
[new IsCsrfTokenValidAttributeMethodsController(), 'withInvalidTokenKey'],
124+
[],
125+
$request,
126+
null
127+
);
128+
129+
$listener = new IsCsrfTokenValidAttributeListener($csrfTokenManager);
130+
$listener->onKernelControllerArguments($event);
131+
}
132+
133+
public function testExceptionWhenInvalidToken()
134+
{
135+
$this->expectException(InvalidCsrfTokenException::class);
136+
137+
$csrfTokenManager = $this->createMock(CsrfTokenManagerInterface::class);
138+
$csrfTokenManager->expects($this->once())
139+
->method('isTokenValid')
140+
->withAnyParameters()
141+
->willReturn(false);
142+
143+
$event = new ControllerArgumentsEvent(
144+
$this->createMock(HttpKernelInterface::class),
145+
[new IsCsrfTokenValidAttributeMethodsController(), 'withDefaultTokenKey'],
146+
[],
147+
new Request(),
148+
null
149+
);
150+
151+
$listener = new IsCsrfTokenValidAttributeListener($csrfTokenManager);
152+
$listener->onKernelControllerArguments($event);
153+
}
154+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Http\Tests\Fixtures;
13+
14+
use Symfony\Component\Security\Http\Attribute\IsCsrfTokenValid;
15+
16+
#[IsCsrfTokenValid('foo')]
17+
class IsCsrfTokenValidAttributeController
18+
{
19+
public function __invoke()
20+
{
21+
}
22+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Http\Tests\Fixtures;
13+
14+
use Symfony\Component\Security\Http\Attribute\IsCsrfTokenValid;
15+
16+
class IsCsrfTokenValidAttributeMethodsController
17+
{
18+
public function noAttribute()
19+
{
20+
}
21+
22+
#[IsCsrfTokenValid('foo')]
23+
public function withDefaultTokenKey()
24+
{
25+
}
26+
27+
#[IsCsrfTokenValid('foo', tokenKey: 'my_token_key')]
28+
public function withCustomTokenKey()
29+
{
30+
}
31+
32+
#[IsCsrfTokenValid('foo', tokenKey: 'invalid_token_key')]
33+
public function withInvalidTokenKey()
34+
{
35+
}
36+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy