Skip to content

Commit 2005da4

Browse files
committed
use tag for user context hash request invalidation
1 parent 6219f7a commit 2005da4

File tree

11 files changed

+91
-63
lines changed

11 files changed

+91
-63
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
Changelog
22
=========
33

4+
2.6.0
5+
-----
6+
7+
### Changed
8+
9+
* User context lookup now tags the hash lookup response. The logout listener can now invalidate that tag instead of
10+
doing a BAN request. The previous varnish BAN request has been incorrect and banned all cache entries on Varnish.
11+
The logout handler is now also activated by default for the Symfony HttpCache in addition to Varnish and Noop.
12+
413
2.5.1
514
-----
615

Resources/doc/reference/configuration/user-context.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,11 +139,12 @@ or you will end up with mixed up caches.
139139
~~~~~~~~~~~~~~~~~~
140140

141141
The logout handler will invalidate any cached user hashes when the user logs
142-
out.
142+
out. This will make sure that the session cookie of the logged out session can
143+
not be abused to see protected cached content.
143144

144145
For the handler to work:
145146

146-
* your caching proxy should be :ref:`configured for BANs <foshttpcache:proxy-configuration>`
147+
* your caching proxy must be :ref:`configured for tag invalidation <foshttpcache:proxy-configuration>`
147148
* Symfony’s default behavior of regenerating the session id when users log in
148149
and out must be enabled (``invalidate_session``).
149150

src/DependencyInjection/Configuration.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,17 @@ function ($v) {
120120
return $v;
121121
}
122122

123-
if (isset($v['proxy_client']['default']) && in_array($v['proxy_client']['default'], ['varnish', 'noop'])) {
123+
if (isset($v['proxy_client']['default'])
124+
&& in_array($v['proxy_client']['default'], ['varnish', 'symfony', 'noop'])
125+
) {
124126
$v['user_context']['logout_handler']['enabled'] = true;
125127

126128
return $v;
127129
}
128-
if (isset($v['proxy_client']['varnish']) || isset($v['proxy_client']['noop'])) {
130+
if (isset($v['proxy_client']['varnish'])
131+
|| isset($v['proxy_client']['symfony'])
132+
|| isset($v['proxy_client']['noop'])
133+
) {
129134
$v['user_context']['logout_handler']['enabled'] = true;
130135

131136
return $v;

src/DependencyInjection/FOSHttpCacheExtension.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -313,10 +313,6 @@ private function loadUserContext(ContainerBuilder $container, XmlFileLoader $loa
313313
->replaceArgument(0, $options);
314314

315315
if ($config['logout_handler']['enabled']) {
316-
$container->getDefinition('fos_http_cache.user_context_invalidator')
317-
->replaceArgument(1, $completeUserIdentifierHeaders)
318-
->replaceArgument(2, $config['match']['accept']);
319-
320316
$container->setAlias('security.logout.handler.session', 'fos_http_cache.user_context.session_logout_handler');
321317
} else {
322318
$container->removeDefinition('fos_http_cache.user_context.logout_handler');

src/EventListener/UserContextListener.php

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111

1212
namespace FOS\HttpCacheBundle\EventListener;
1313

14+
use FOS\HttpCache\ResponseTagger;
1415
use FOS\HttpCache\UserContext\HashGenerator;
16+
use FOS\HttpCacheBundle\UserContextInvalidator;
1517
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
1618
use Symfony\Component\HttpFoundation\Request;
1719
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
@@ -45,6 +47,13 @@ class UserContextListener implements EventSubscriberInterface
4547
*/
4648
private $hashGenerator;
4749

50+
/**
51+
* If the response tagger is set, the hash lookup response is tagged with the session id for later invalidation.
52+
*
53+
* @var ResponseTagger|null
54+
*/
55+
private $responseTagger;
56+
4857
/**
4958
* @var array
5059
*/
@@ -60,26 +69,20 @@ class UserContextListener implements EventSubscriberInterface
6069
* It prevents issues when the hash generator that is used returns a customized value for anonymous users,
6170
* that differs from the documented, hardcoded one.
6271
*
63-
* @var RequestMatcherInterface
72+
* @var RequestMatcherInterface|null
6473
*/
6574
private $anonymousRequestMatcher;
6675

67-
/**
68-
* Constructor.
69-
*
70-
* @param RequestMatcherInterface $requestMatcher
71-
* @param HashGenerator $hashGenerator
72-
* @param RequestMatcherInterface|null $anonymousRequestMatcher
73-
* @param array $options
74-
*/
7576
public function __construct(
7677
RequestMatcherInterface $requestMatcher,
7778
HashGenerator $hashGenerator,
7879
RequestMatcherInterface $anonymousRequestMatcher = null,
80+
ResponseTagger $responseTagger = null,
7981
array $options = []
8082
) {
8183
$this->requestMatcher = $requestMatcher;
8284
$this->hashGenerator = $hashGenerator;
85+
$this->responseTagger = $responseTagger;
8386
$this->anonymousRequestMatcher = $anonymousRequestMatcher;
8487

8588
$resolver = new OptionsResolver();
@@ -115,7 +118,8 @@ public function onKernelRequest(GetResponseEvent $event)
115118
return;
116119
}
117120

118-
if (!$this->requestMatcher->matches($event->getRequest())) {
121+
$request = $event->getRequest();
122+
if (!$this->requestMatcher->matches($request)) {
119123
if ($event->getRequest()->headers->has($this->options['user_hash_header'])
120124
&& !$this->isAnonymous($event->getRequest())
121125
) {
@@ -127,6 +131,11 @@ public function onKernelRequest(GetResponseEvent $event)
127131

128132
$hash = $this->hashGenerator->generateHash();
129133

134+
if ($this->responseTagger && $request->hasSession()) {
135+
$tag = UserContextInvalidator::buildTag($request->getSession()->getId());
136+
$this->responseTagger->addTags([$tag]);
137+
}
138+
130139
// status needs to be 200 as otherwise varnish will not cache the response.
131140
$response = new Response('', 200, [
132141
$this->options['user_hash_header'] => $hash,

src/Resources/config/user_context.xml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<argument type="service" id="fos_http_cache.user_context.request_matcher" />
1818
<argument type="service" id="fos_http_cache.user_context.hash_generator" />
1919
<argument type="service" id="fos_http_cache.user_context.anonymous_request_matcher" />
20+
<argument type="service" id="fos_http_cache.http.symfony_response_tagger" on-invalid="ignore" />
2021
<argument>%fos_http_cache.event_listener.user_context.options%</argument>
2122
<tag name="kernel.event_subscriber" />
2223
</service>
@@ -27,8 +28,6 @@
2728

2829
<service id="fos_http_cache.user_context_invalidator" class="FOS\HttpCacheBundle\UserContextInvalidator">
2930
<argument type="service" id="fos_http_cache.default_proxy_client" />
30-
<argument />
31-
<argument />
3231
</service>
3332

3433
<service id="fos_http_cache.user_context.logout_handler" class="FOS\HttpCacheBundle\Security\Http\Logout\ContextInvalidationLogoutHandler" public="false">

src/UserContextInvalidator.php

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,36 +11,20 @@
1111

1212
namespace FOS\HttpCacheBundle;
1313

14-
use FOS\HttpCache\ProxyClient\Invalidation\BanCapable;
14+
use FOS\HttpCache\ProxyClient\Invalidation\TagCapable;
1515

1616
class UserContextInvalidator
1717
{
18-
/**
19-
* Service used to ban hash request.
20-
*
21-
* @var BanCapable
22-
*/
23-
private $banner;
18+
const USER_CONTEXT_TAG_PREFIX = 'fos_http_cache_hashlookup-';
2419

2520
/**
26-
* Accept header.
27-
*
28-
* @var string
21+
* @var TagCapable
2922
*/
30-
private $acceptHeader;
23+
private $tagger;
3124

32-
/**
33-
* User identifier headers.
34-
*
35-
* @var string[]
36-
*/
37-
private $userIdentifierHeaders;
38-
39-
public function __construct(BanCapable $banner, $userIdentifierHeaders, $acceptHeader)
25+
public function __construct(TagCapable $tagger)
4026
{
41-
$this->banner = $banner;
42-
$this->acceptHeader = $acceptHeader;
43-
$this->userIdentifierHeaders = $userIdentifierHeaders;
27+
$this->tagger = $tagger;
4428
}
4529

4630
/**
@@ -50,11 +34,11 @@ public function __construct(BanCapable $banner, $userIdentifierHeaders, $acceptH
5034
*/
5135
public function invalidateContext($sessionId)
5236
{
53-
foreach ($this->userIdentifierHeaders as $header) {
54-
$this->banner->ban([
55-
'accept' => $this->acceptHeader,
56-
$header => sprintf('.*%s.*', $sessionId),
57-
]);
58-
}
37+
$this->tagger->invalidateTags([static::buildTag($sessionId)]);
38+
}
39+
40+
public static function buildTag($hash)
41+
{
42+
return static::USER_CONTEXT_TAG_PREFIX.$hash;
5943
}
6044
}

tests/Functional/EventListener/UserContextListenerTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace FOS\HttpCacheBundle\Tests\Functional\EventListener;
1313

1414
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
15+
use Symfony\Component\HttpFoundation\Session\SessionInterface;
1516

1617
class UserContextListenerTest extends WebTestCase
1718
{
@@ -21,6 +22,9 @@ public function testHashLookup()
2122
'PHP_AUTH_USER' => 'user',
2223
'PHP_AUTH_PW' => 'user',
2324
]);
25+
/** @var SessionInterface $session */
26+
$session = $client->getContainer()->get('session');
27+
$session->setId('test');
2428

2529
$client->request('GET', '/secured_area/_fos_user_context_hash', [], [], [
2630
'HTTP_ACCEPT' => 'application/vnd.fos.user-context-hash',
@@ -29,6 +33,7 @@ public function testHashLookup()
2933

3034
$this->assertTrue($response->headers->has('X-User-Context-Hash'), 'X-User-Context-Hash header missing on the response');
3135
$this->assertEquals('5224d8f5b85429624e2160e538a3376a479ec87b89251b295c44ecbf7498ea3c', $response->headers->get('X-User-Context-Hash'), 'Not the expected context hash');
36+
$this->assertEquals('fos_http_cache_hashlookup-test', $response->headers->get('X-Cache-Tags'));
3237
$this->assertEquals('max-age=60, public', $response->headers->get('Cache-Control'));
3338
}
3439

tests/Functional/Security/Http/Logout/ContextInvalidationLogoutHandlerTest.php

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,30 +32,20 @@ public function testLogout()
3232
$session->save();
3333

3434
$mock = \Mockery::mock(Varnish::class);
35-
$mock->shouldReceive('ban')
35+
$mock->shouldReceive('invalidateTags')
3636
->once()
37-
->with([
38-
'accept' => 'application/vnd.fos.user-context-hash',
39-
'cookie' => '.*test.*',
40-
])
41-
;
42-
$mock->shouldReceive('ban')
43-
->once()
44-
->with([
45-
'accept' => 'application/vnd.fos.user-context-hash',
46-
'authorization' => '.*test.*',
47-
])
37+
->with(['fos_http_cache_hashlookup-test'])
4838
;
4939
$mock->shouldReceive('flush')
5040
->once()
51-
->andReturn(2)
41+
->andReturn(1)
5242
;
5343

5444
$client->getContainer()->set('fos_http_cache.proxy_client.varnish', $mock);
5545

5646
$client->getCookieJar()->set(new Cookie($session->getName(), 'test'));
5747
$client->request('GET', '/secured_area/logout');
5848

59-
$this->assertEquals(302, $client->getResponse()->getStatusCode(), $client->getResponse()->getContent());
49+
$this->assertEquals(302, $client->getResponse()->getStatusCode(), substr($client->getResponse()->getContent(), 0, 2000));
6050
}
6151
}

tests/Unit/DependencyInjection/ConfigurationTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ public function testSupportsSymfony()
278278
$expectedConfiguration['cache_manager']['generate_url_type'] = 'auto';
279279
$expectedConfiguration['tags']['enabled'] = 'auto';
280280
$expectedConfiguration['invalidation']['enabled'] = 'auto';
281-
$expectedConfiguration['user_context']['logout_handler']['enabled'] = false;
281+
$expectedConfiguration['user_context']['logout_handler']['enabled'] = true;
282282

283283
$formats = array_map(function ($path) {
284284
return __DIR__.'/../../Resources/Fixtures/'.$path;

0 commit comments

Comments
 (0)