From 57adc7e71a03d189174d9187e61707e55365d548 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Jun 2025 07:26:59 +0000 Subject: [PATCH 1/3] Initial plan From 02b8ded0fb564ba549ab8f3d982027b2ed8f612d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Jun 2025 07:36:11 +0000 Subject: [PATCH 2/3] Add Authorization header caching restrictions per RFC 9111 Co-authored-by: Kevinrob <4509277+Kevinrob@users.noreply.github.com> --- src/Strategy/PrivateCacheStrategy.php | 12 ++ tests/AuthorizationCacheTest.php | 238 ++++++++++++++++++++++++++ 2 files changed, 250 insertions(+) create mode 100644 tests/AuthorizationCacheTest.php diff --git a/src/Strategy/PrivateCacheStrategy.php b/src/Strategy/PrivateCacheStrategy.php index 23db04e..87f05b4 100644 --- a/src/Strategy/PrivateCacheStrategy.php +++ b/src/Strategy/PrivateCacheStrategy.php @@ -81,6 +81,18 @@ protected function getCacheObject(RequestInterface $request, ResponseInterface $ return; } + // RFC 9111 Section 3.5: Check Authorization header caching restrictions + if ($request->hasHeader('Authorization')) { + // Requests with Authorization header should only be cached if response contains + // one of the following directives: public, must-revalidate, s-maxage + if (!$cacheControl->has('public') + && !$cacheControl->has('must-revalidate') + && !$cacheControl->has('s-maxage')) { + // No explicit authorization to cache authenticated requests + return; + } + } + if ($cacheControl->has('no-cache')) { // Stale response see RFC7234 section 5.2.1.4 $entry = new CacheEntry($request, $response, new \DateTime('-1 seconds')); diff --git a/tests/AuthorizationCacheTest.php b/tests/AuthorizationCacheTest.php new file mode 100644 index 0000000..9410e2e --- /dev/null +++ b/tests/AuthorizationCacheTest.php @@ -0,0 +1,238 @@ + 'Bearer secret-token' + ]); + + $response = new Response(200, [ + 'Cache-Control' => 'max-age=3600' + ], 'Private data'); + + $result = $strategy->cache($request, $response); + $this->assertFalse($result, 'Request with Authorization header should not be cached with only max-age'); + + $cached = $strategy->fetch($request); + $this->assertNull($cached, 'No cache entry should exist for authorized request with only max-age'); + } + + /** + * Test that requests with Authorization header ARE cached when response has Cache-Control: public + */ + public function testAuthorizationHeaderCachedWithPublic() + { + $storage = new VolatileRuntimeStorage(); + $strategy = new PrivateCacheStrategy($storage); + + $request = new Request('GET', 'https://api.example.com/data', [ + 'Authorization' => 'Bearer secret-token' + ]); + + $response = new Response(200, [ + 'Cache-Control' => 'public, max-age=3600' + ], 'Public data'); + + $result = $strategy->cache($request, $response); + $this->assertTrue($result, 'Request with Authorization header should be cached with public directive'); + + $cached = $strategy->fetch($request); + $this->assertNotNull($cached, 'Cache entry should exist for authorized request with public directive'); + $this->assertEquals('Public data', (string) $cached->getResponse()->getBody()); + } + + /** + * Test that requests with Authorization header ARE cached when response has Cache-Control: must-revalidate + */ + public function testAuthorizationHeaderCachedWithMustRevalidate() + { + $storage = new VolatileRuntimeStorage(); + $strategy = new PrivateCacheStrategy($storage); + + $request = new Request('GET', 'https://api.example.com/data', [ + 'Authorization' => 'Bearer secret-token' + ]); + + $response = new Response(200, [ + 'Cache-Control' => 'must-revalidate, max-age=3600' + ], 'Revalidate data'); + + $result = $strategy->cache($request, $response); + $this->assertTrue($result, 'Request with Authorization header should be cached with must-revalidate directive'); + + $cached = $strategy->fetch($request); + $this->assertNotNull($cached, 'Cache entry should exist for authorized request with must-revalidate directive'); + $this->assertEquals('Revalidate data', (string) $cached->getResponse()->getBody()); + } + + /** + * Test that requests with Authorization header ARE cached when response has Cache-Control: s-maxage + */ + public function testAuthorizationHeaderCachedWithSMaxage() + { + $storage = new VolatileRuntimeStorage(); + $strategy = new PrivateCacheStrategy($storage); + + $request = new Request('GET', 'https://api.example.com/data', [ + 'Authorization' => 'Bearer secret-token' + ]); + + $response = new Response(200, [ + 'Cache-Control' => 's-maxage=1800, max-age=3600' + ], 'Shared cache data'); + + $result = $strategy->cache($request, $response); + $this->assertTrue($result, 'Request with Authorization header should be cached with s-maxage directive'); + + $cached = $strategy->fetch($request); + $this->assertNotNull($cached, 'Cache entry should exist for authorized request with s-maxage directive'); + $this->assertEquals('Shared cache data', (string) $cached->getResponse()->getBody()); + } + + /** + * Test that requests WITHOUT Authorization header are cached normally with max-age + */ + public function testNoAuthorizationHeaderCachedWithMaxAge() + { + $storage = new VolatileRuntimeStorage(); + $strategy = new PrivateCacheStrategy($storage); + + $request = new Request('GET', 'https://api.example.com/data'); + + $response = new Response(200, [ + 'Cache-Control' => 'max-age=3600' + ], 'Public data'); + + $result = $strategy->cache($request, $response); + $this->assertTrue($result, 'Request without Authorization header should be cached normally'); + + $cached = $strategy->fetch($request); + $this->assertNotNull($cached, 'Cache entry should exist for non-authorized request'); + $this->assertEquals('Public data', (string) $cached->getResponse()->getBody()); + } + + /** + * Test PublicCacheStrategy behavior with Authorization headers + */ + public function testPublicCacheStrategyWithAuthorization() + { + $storage = new VolatileRuntimeStorage(); + $strategy = new PublicCacheStrategy($storage); + + // Test that private cache with authorization is not cached in public strategy + $request = new Request('GET', 'https://api.example.com/data', [ + 'Authorization' => 'Bearer secret-token' + ]); + + $response = new Response(200, [ + 'Cache-Control' => 'private, max-age=3600' + ], 'Private data'); + + $result = $strategy->cache($request, $response); + $this->assertFalse($result, 'Private response should not be cached in public strategy'); + + // Test that public response with authorization is cached + $response2 = new Response(200, [ + 'Cache-Control' => 'public, max-age=3600' + ], 'Public data'); + + $result2 = $strategy->cache($request, $response2); + $this->assertTrue($result2, 'Public response with authorization should be cached in public strategy'); + + $cached = $strategy->fetch($request); + $this->assertNotNull($cached, 'Cache entry should exist for public authorized request'); + $this->assertEquals('Public data', (string) $cached->getResponse()->getBody()); + } + + /** + * Test multiple allowed directives together + */ + public function testMultipleAllowedDirectives() + { + $storage = new VolatileRuntimeStorage(); + $strategy = new PrivateCacheStrategy($storage); + + $request = new Request('GET', 'https://api.example.com/data', [ + 'Authorization' => 'Bearer secret-token' + ]); + + $response = new Response(200, [ + 'Cache-Control' => 'public, must-revalidate, s-maxage=1800, max-age=3600' + ], 'Multi directive data'); + + $result = $strategy->cache($request, $response); + $this->assertTrue($result, 'Request with Authorization header should be cached with multiple allowed directives'); + + $cached = $strategy->fetch($request); + $this->assertNotNull($cached, 'Cache entry should exist for authorized request with multiple allowed directives'); + $this->assertEquals('Multi directive data', (string) $cached->getResponse()->getBody()); + } + + /** + * Test case sensitivity of Authorization header + */ + public function testAuthorizationHeaderCaseSensitivity() + { + $storage = new VolatileRuntimeStorage(); + $strategy = new PrivateCacheStrategy($storage); + + $requests = [ + new Request('GET', 'https://api.example.com/data', ['Authorization' => 'Bearer token']), + new Request('GET', 'https://api.example.com/data', ['authorization' => 'Bearer token']), + new Request('GET', 'https://api.example.com/data', ['AUTHORIZATION' => 'Bearer token']), + ]; + + $response = new Response(200, [ + 'Cache-Control' => 'max-age=3600' + ], 'Test data'); + + foreach ($requests as $request) { + $result = $strategy->cache($request, $response); + $this->assertFalse($result, 'Authorization header should be detected regardless of case'); + + $cached = $strategy->fetch($request); + $this->assertNull($cached, 'No cache entry should exist for any case variation of Authorization header'); + } + } + + /** + * Test that other cache control directives still work as expected + */ + public function testOtherCacheControlDirectivesStillWork() + { + $storage = new VolatileRuntimeStorage(); + $strategy = new PrivateCacheStrategy($storage); + + // Test no-store still prevents caching even with authorization allowances + $request = new Request('GET', 'https://api.example.com/data', [ + 'Authorization' => 'Bearer secret-token' + ]); + + $response = new Response(200, [ + 'Cache-Control' => 'public, no-store, max-age=3600' + ], 'No store data'); + + $result = $strategy->cache($request, $response); + $this->assertFalse($result, 'no-store should still prevent caching even with public directive'); + + $cached = $strategy->fetch($request); + $this->assertNull($cached, 'No cache entry should exist when no-store is present'); + } +} \ No newline at end of file From f4d47204867f44f345f253adb1f3a7648d42772c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 3 Aug 2025 08:24:55 +0000 Subject: [PATCH 3/3] Move Authorization header caching logic from PrivateCacheStrategy to PublicCacheStrategy per RFC 9111 Co-authored-by: Kevinrob <4509277+Kevinrob@users.noreply.github.com> --- src/Strategy/PrivateCacheStrategy.php | 12 --------- src/Strategy/PublicCacheStrategy.php | 12 +++++++++ tests/AuthorizationCacheTest.php | 39 ++++++++++++++++++++++----- 3 files changed, 44 insertions(+), 19 deletions(-) diff --git a/src/Strategy/PrivateCacheStrategy.php b/src/Strategy/PrivateCacheStrategy.php index 87f05b4..23db04e 100644 --- a/src/Strategy/PrivateCacheStrategy.php +++ b/src/Strategy/PrivateCacheStrategy.php @@ -81,18 +81,6 @@ protected function getCacheObject(RequestInterface $request, ResponseInterface $ return; } - // RFC 9111 Section 3.5: Check Authorization header caching restrictions - if ($request->hasHeader('Authorization')) { - // Requests with Authorization header should only be cached if response contains - // one of the following directives: public, must-revalidate, s-maxage - if (!$cacheControl->has('public') - && !$cacheControl->has('must-revalidate') - && !$cacheControl->has('s-maxage')) { - // No explicit authorization to cache authenticated requests - return; - } - } - if ($cacheControl->has('no-cache')) { // Stale response see RFC7234 section 5.2.1.4 $entry = new CacheEntry($request, $response, new \DateTime('-1 seconds')); diff --git a/src/Strategy/PublicCacheStrategy.php b/src/Strategy/PublicCacheStrategy.php index 411dda1..668502d 100644 --- a/src/Strategy/PublicCacheStrategy.php +++ b/src/Strategy/PublicCacheStrategy.php @@ -37,6 +37,18 @@ protected function getCacheObject(RequestInterface $request, ResponseInterface $ return; } + // RFC 9111 Section 3.5: Check Authorization header caching restrictions for shared caches + if ($request->hasHeader('Authorization')) { + // Requests with Authorization header should only be cached if response contains + // one of the following directives: public, must-revalidate, s-maxage + if (!$cacheControl->has('public') + && !$cacheControl->has('must-revalidate') + && !$cacheControl->has('s-maxage')) { + // No explicit authorization to cache authenticated requests + return; + } + } + return parent::getCacheObject($request, $response); } } diff --git a/tests/AuthorizationCacheTest.php b/tests/AuthorizationCacheTest.php index 9410e2e..14e6faa 100644 --- a/tests/AuthorizationCacheTest.php +++ b/tests/AuthorizationCacheTest.php @@ -17,7 +17,7 @@ class AuthorizationCacheTest extends TestCase public function testAuthorizationHeaderNotCachedWithMaxAge() { $storage = new VolatileRuntimeStorage(); - $strategy = new PrivateCacheStrategy($storage); + $strategy = new PublicCacheStrategy($storage); $request = new Request('GET', 'https://api.example.com/data', [ 'Authorization' => 'Bearer secret-token' @@ -40,7 +40,7 @@ public function testAuthorizationHeaderNotCachedWithMaxAge() public function testAuthorizationHeaderCachedWithPublic() { $storage = new VolatileRuntimeStorage(); - $strategy = new PrivateCacheStrategy($storage); + $strategy = new PublicCacheStrategy($storage); $request = new Request('GET', 'https://api.example.com/data', [ 'Authorization' => 'Bearer secret-token' @@ -64,7 +64,7 @@ public function testAuthorizationHeaderCachedWithPublic() public function testAuthorizationHeaderCachedWithMustRevalidate() { $storage = new VolatileRuntimeStorage(); - $strategy = new PrivateCacheStrategy($storage); + $strategy = new PublicCacheStrategy($storage); $request = new Request('GET', 'https://api.example.com/data', [ 'Authorization' => 'Bearer secret-token' @@ -88,7 +88,7 @@ public function testAuthorizationHeaderCachedWithMustRevalidate() public function testAuthorizationHeaderCachedWithSMaxage() { $storage = new VolatileRuntimeStorage(); - $strategy = new PrivateCacheStrategy($storage); + $strategy = new PublicCacheStrategy($storage); $request = new Request('GET', 'https://api.example.com/data', [ 'Authorization' => 'Bearer secret-token' @@ -167,7 +167,7 @@ public function testPublicCacheStrategyWithAuthorization() public function testMultipleAllowedDirectives() { $storage = new VolatileRuntimeStorage(); - $strategy = new PrivateCacheStrategy($storage); + $strategy = new PublicCacheStrategy($storage); $request = new Request('GET', 'https://api.example.com/data', [ 'Authorization' => 'Bearer secret-token' @@ -191,7 +191,7 @@ public function testMultipleAllowedDirectives() public function testAuthorizationHeaderCaseSensitivity() { $storage = new VolatileRuntimeStorage(); - $strategy = new PrivateCacheStrategy($storage); + $strategy = new PublicCacheStrategy($storage); $requests = [ new Request('GET', 'https://api.example.com/data', ['Authorization' => 'Bearer token']), @@ -218,7 +218,7 @@ public function testAuthorizationHeaderCaseSensitivity() public function testOtherCacheControlDirectivesStillWork() { $storage = new VolatileRuntimeStorage(); - $strategy = new PrivateCacheStrategy($storage); + $strategy = new PublicCacheStrategy($storage); // Test no-store still prevents caching even with authorization allowances $request = new Request('GET', 'https://api.example.com/data', [ @@ -235,4 +235,29 @@ public function testOtherCacheControlDirectivesStillWork() $cached = $strategy->fetch($request); $this->assertNull($cached, 'No cache entry should exist when no-store is present'); } + + /** + * Test that PrivateCacheStrategy still caches requests with Authorization header normally + */ + public function testPrivateCacheStrategyAllowsAuthorizationCaching() + { + $storage = new VolatileRuntimeStorage(); + $strategy = new PrivateCacheStrategy($storage); + + $request = new Request('GET', 'https://api.example.com/data', [ + 'Authorization' => 'Bearer secret-token' + ]); + + // Private cache should cache this even with just max-age + $response = new Response(200, [ + 'Cache-Control' => 'max-age=3600' + ], 'Private cache data'); + + $result = $strategy->cache($request, $response); + $this->assertTrue($result, 'Private cache should allow caching of authenticated requests'); + + $cached = $strategy->fetch($request); + $this->assertNotNull($cached, 'Cache entry should exist in private cache for authenticated request'); + $this->assertEquals('Private cache data', (string) $cached->getResponse()->getBody()); + } } \ No newline at end of file