Skip to content

Commit c7012ce

Browse files
CopilotKevinrob
andauthored
Fix Authorization header caching in PublicCacheStrategy according to HTTP Caching RFC 9111 (#200)
* Initial plan * Add Authorization header caching restrictions per RFC 9111 Co-authored-by: Kevinrob <4509277+Kevinrob@users.noreply.github.com> * Move Authorization header caching logic from PrivateCacheStrategy to PublicCacheStrategy per RFC 9111 Co-authored-by: Kevinrob <4509277+Kevinrob@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Kevinrob <4509277+Kevinrob@users.noreply.github.com> Co-authored-by: Kevin Robatel <kevinrob2@gmail.com>
1 parent a63dfa7 commit c7012ce

File tree

2 files changed

+275
-0
lines changed

2 files changed

+275
-0
lines changed

src/Strategy/PublicCacheStrategy.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,18 @@ protected function getCacheObject(RequestInterface $request, ResponseInterface $
3737
return;
3838
}
3939

40+
// RFC 9111 Section 3.5: Check Authorization header caching restrictions for shared caches
41+
if ($request->hasHeader('Authorization')) {
42+
// Requests with Authorization header should only be cached if response contains
43+
// one of the following directives: public, must-revalidate, s-maxage
44+
if (!$cacheControl->has('public')
45+
&& !$cacheControl->has('must-revalidate')
46+
&& !$cacheControl->has('s-maxage')) {
47+
// No explicit authorization to cache authenticated requests
48+
return;
49+
}
50+
}
51+
4052
return parent::getCacheObject($request, $response);
4153
}
4254
}

tests/AuthorizationCacheTest.php

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
<?php
2+
3+
namespace Kevinrob\GuzzleCache\Tests;
4+
5+
use GuzzleHttp\Psr7\Request;
6+
use GuzzleHttp\Psr7\Response;
7+
use Kevinrob\GuzzleCache\Storage\VolatileRuntimeStorage;
8+
use Kevinrob\GuzzleCache\Strategy\PrivateCacheStrategy;
9+
use Kevinrob\GuzzleCache\Strategy\PublicCacheStrategy;
10+
use PHPUnit\Framework\TestCase;
11+
12+
class AuthorizationCacheTest extends TestCase
13+
{
14+
/**
15+
* Test that requests with Authorization header are NOT cached when response only has max-age
16+
*/
17+
public function testAuthorizationHeaderNotCachedWithMaxAge()
18+
{
19+
$storage = new VolatileRuntimeStorage();
20+
$strategy = new PublicCacheStrategy($storage);
21+
22+
$request = new Request('GET', 'https://api.example.com/data', [
23+
'Authorization' => 'Bearer secret-token'
24+
]);
25+
26+
$response = new Response(200, [
27+
'Cache-Control' => 'max-age=3600'
28+
], 'Private data');
29+
30+
$result = $strategy->cache($request, $response);
31+
$this->assertFalse($result, 'Request with Authorization header should not be cached with only max-age');
32+
33+
$cached = $strategy->fetch($request);
34+
$this->assertNull($cached, 'No cache entry should exist for authorized request with only max-age');
35+
}
36+
37+
/**
38+
* Test that requests with Authorization header ARE cached when response has Cache-Control: public
39+
*/
40+
public function testAuthorizationHeaderCachedWithPublic()
41+
{
42+
$storage = new VolatileRuntimeStorage();
43+
$strategy = new PublicCacheStrategy($storage);
44+
45+
$request = new Request('GET', 'https://api.example.com/data', [
46+
'Authorization' => 'Bearer secret-token'
47+
]);
48+
49+
$response = new Response(200, [
50+
'Cache-Control' => 'public, max-age=3600'
51+
], 'Public data');
52+
53+
$result = $strategy->cache($request, $response);
54+
$this->assertTrue($result, 'Request with Authorization header should be cached with public directive');
55+
56+
$cached = $strategy->fetch($request);
57+
$this->assertNotNull($cached, 'Cache entry should exist for authorized request with public directive');
58+
$this->assertEquals('Public data', (string) $cached->getResponse()->getBody());
59+
}
60+
61+
/**
62+
* Test that requests with Authorization header ARE cached when response has Cache-Control: must-revalidate
63+
*/
64+
public function testAuthorizationHeaderCachedWithMustRevalidate()
65+
{
66+
$storage = new VolatileRuntimeStorage();
67+
$strategy = new PublicCacheStrategy($storage);
68+
69+
$request = new Request('GET', 'https://api.example.com/data', [
70+
'Authorization' => 'Bearer secret-token'
71+
]);
72+
73+
$response = new Response(200, [
74+
'Cache-Control' => 'must-revalidate, max-age=3600'
75+
], 'Revalidate data');
76+
77+
$result = $strategy->cache($request, $response);
78+
$this->assertTrue($result, 'Request with Authorization header should be cached with must-revalidate directive');
79+
80+
$cached = $strategy->fetch($request);
81+
$this->assertNotNull($cached, 'Cache entry should exist for authorized request with must-revalidate directive');
82+
$this->assertEquals('Revalidate data', (string) $cached->getResponse()->getBody());
83+
}
84+
85+
/**
86+
* Test that requests with Authorization header ARE cached when response has Cache-Control: s-maxage
87+
*/
88+
public function testAuthorizationHeaderCachedWithSMaxage()
89+
{
90+
$storage = new VolatileRuntimeStorage();
91+
$strategy = new PublicCacheStrategy($storage);
92+
93+
$request = new Request('GET', 'https://api.example.com/data', [
94+
'Authorization' => 'Bearer secret-token'
95+
]);
96+
97+
$response = new Response(200, [
98+
'Cache-Control' => 's-maxage=1800, max-age=3600'
99+
], 'Shared cache data');
100+
101+
$result = $strategy->cache($request, $response);
102+
$this->assertTrue($result, 'Request with Authorization header should be cached with s-maxage directive');
103+
104+
$cached = $strategy->fetch($request);
105+
$this->assertNotNull($cached, 'Cache entry should exist for authorized request with s-maxage directive');
106+
$this->assertEquals('Shared cache data', (string) $cached->getResponse()->getBody());
107+
}
108+
109+
/**
110+
* Test that requests WITHOUT Authorization header are cached normally with max-age
111+
*/
112+
public function testNoAuthorizationHeaderCachedWithMaxAge()
113+
{
114+
$storage = new VolatileRuntimeStorage();
115+
$strategy = new PrivateCacheStrategy($storage);
116+
117+
$request = new Request('GET', 'https://api.example.com/data');
118+
119+
$response = new Response(200, [
120+
'Cache-Control' => 'max-age=3600'
121+
], 'Public data');
122+
123+
$result = $strategy->cache($request, $response);
124+
$this->assertTrue($result, 'Request without Authorization header should be cached normally');
125+
126+
$cached = $strategy->fetch($request);
127+
$this->assertNotNull($cached, 'Cache entry should exist for non-authorized request');
128+
$this->assertEquals('Public data', (string) $cached->getResponse()->getBody());
129+
}
130+
131+
/**
132+
* Test PublicCacheStrategy behavior with Authorization headers
133+
*/
134+
public function testPublicCacheStrategyWithAuthorization()
135+
{
136+
$storage = new VolatileRuntimeStorage();
137+
$strategy = new PublicCacheStrategy($storage);
138+
139+
// Test that private cache with authorization is not cached in public strategy
140+
$request = new Request('GET', 'https://api.example.com/data', [
141+
'Authorization' => 'Bearer secret-token'
142+
]);
143+
144+
$response = new Response(200, [
145+
'Cache-Control' => 'private, max-age=3600'
146+
], 'Private data');
147+
148+
$result = $strategy->cache($request, $response);
149+
$this->assertFalse($result, 'Private response should not be cached in public strategy');
150+
151+
// Test that public response with authorization is cached
152+
$response2 = new Response(200, [
153+
'Cache-Control' => 'public, max-age=3600'
154+
], 'Public data');
155+
156+
$result2 = $strategy->cache($request, $response2);
157+
$this->assertTrue($result2, 'Public response with authorization should be cached in public strategy');
158+
159+
$cached = $strategy->fetch($request);
160+
$this->assertNotNull($cached, 'Cache entry should exist for public authorized request');
161+
$this->assertEquals('Public data', (string) $cached->getResponse()->getBody());
162+
}
163+
164+
/**
165+
* Test multiple allowed directives together
166+
*/
167+
public function testMultipleAllowedDirectives()
168+
{
169+
$storage = new VolatileRuntimeStorage();
170+
$strategy = new PublicCacheStrategy($storage);
171+
172+
$request = new Request('GET', 'https://api.example.com/data', [
173+
'Authorization' => 'Bearer secret-token'
174+
]);
175+
176+
$response = new Response(200, [
177+
'Cache-Control' => 'public, must-revalidate, s-maxage=1800, max-age=3600'
178+
], 'Multi directive data');
179+
180+
$result = $strategy->cache($request, $response);
181+
$this->assertTrue($result, 'Request with Authorization header should be cached with multiple allowed directives');
182+
183+
$cached = $strategy->fetch($request);
184+
$this->assertNotNull($cached, 'Cache entry should exist for authorized request with multiple allowed directives');
185+
$this->assertEquals('Multi directive data', (string) $cached->getResponse()->getBody());
186+
}
187+
188+
/**
189+
* Test case sensitivity of Authorization header
190+
*/
191+
public function testAuthorizationHeaderCaseSensitivity()
192+
{
193+
$storage = new VolatileRuntimeStorage();
194+
$strategy = new PublicCacheStrategy($storage);
195+
196+
$requests = [
197+
new Request('GET', 'https://api.example.com/data', ['Authorization' => 'Bearer token']),
198+
new Request('GET', 'https://api.example.com/data', ['authorization' => 'Bearer token']),
199+
new Request('GET', 'https://api.example.com/data', ['AUTHORIZATION' => 'Bearer token']),
200+
];
201+
202+
$response = new Response(200, [
203+
'Cache-Control' => 'max-age=3600'
204+
], 'Test data');
205+
206+
foreach ($requests as $request) {
207+
$result = $strategy->cache($request, $response);
208+
$this->assertFalse($result, 'Authorization header should be detected regardless of case');
209+
210+
$cached = $strategy->fetch($request);
211+
$this->assertNull($cached, 'No cache entry should exist for any case variation of Authorization header');
212+
}
213+
}
214+
215+
/**
216+
* Test that other cache control directives still work as expected
217+
*/
218+
public function testOtherCacheControlDirectivesStillWork()
219+
{
220+
$storage = new VolatileRuntimeStorage();
221+
$strategy = new PublicCacheStrategy($storage);
222+
223+
// Test no-store still prevents caching even with authorization allowances
224+
$request = new Request('GET', 'https://api.example.com/data', [
225+
'Authorization' => 'Bearer secret-token'
226+
]);
227+
228+
$response = new Response(200, [
229+
'Cache-Control' => 'public, no-store, max-age=3600'
230+
], 'No store data');
231+
232+
$result = $strategy->cache($request, $response);
233+
$this->assertFalse($result, 'no-store should still prevent caching even with public directive');
234+
235+
$cached = $strategy->fetch($request);
236+
$this->assertNull($cached, 'No cache entry should exist when no-store is present');
237+
}
238+
239+
/**
240+
* Test that PrivateCacheStrategy still caches requests with Authorization header normally
241+
*/
242+
public function testPrivateCacheStrategyAllowsAuthorizationCaching()
243+
{
244+
$storage = new VolatileRuntimeStorage();
245+
$strategy = new PrivateCacheStrategy($storage);
246+
247+
$request = new Request('GET', 'https://api.example.com/data', [
248+
'Authorization' => 'Bearer secret-token'
249+
]);
250+
251+
// Private cache should cache this even with just max-age
252+
$response = new Response(200, [
253+
'Cache-Control' => 'max-age=3600'
254+
], 'Private cache data');
255+
256+
$result = $strategy->cache($request, $response);
257+
$this->assertTrue($result, 'Private cache should allow caching of authenticated requests');
258+
259+
$cached = $strategy->fetch($request);
260+
$this->assertNotNull($cached, 'Cache entry should exist in private cache for authenticated request');
261+
$this->assertEquals('Private cache data', (string) $cached->getResponse()->getBody());
262+
}
263+
}

0 commit comments

Comments
 (0)