Skip to content

Commit

Permalink
feat: respect cache control for access token certs (#479)
Browse files Browse the repository at this point in the history
  • Loading branch information
bshaffer authored Oct 10, 2023
1 parent 22209fd commit 6d426b5
Show file tree
Hide file tree
Showing 2 changed files with 82 additions and 13 deletions.
31 changes: 22 additions & 9 deletions src/AccessToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -311,11 +311,9 @@ private function getCerts($location, $cacheKey, array $options = [])
$cacheItem = $this->cache->getItem($cacheKey);
$certs = $cacheItem ? $cacheItem->get() : null;

$gotNewCerts = false;
$expireTime = null;
if (!$certs) {
$certs = $this->retrieveCertsFromLocation($location, $options);

$gotNewCerts = true;
list($certs, $expireTime) = $this->retrieveCertsFromLocation($location, $options);
}

if (!isset($certs['keys'])) {
Expand All @@ -331,8 +329,8 @@ private function getCerts($location, $cacheKey, array $options = [])

// Push caching off until after verifying certs are in a valid format.
// Don't want to cache bad data.
if ($gotNewCerts) {
$cacheItem->expiresAt(new DateTime('+1 hour'));
if ($expireTime) {
$cacheItem->expiresAt(new DateTime($expireTime));
$cacheItem->set($certs);
$this->cache->save($cacheItem);
}
Expand All @@ -345,13 +343,14 @@ private function getCerts($location, $cacheKey, array $options = [])
*
* @param string $url location
* @param array<mixed> $options [optional] Configuration options.
* @return array<mixed> certificates
* @return array{array<mixed>, string}
* @throws InvalidArgumentException If certs could not be retrieved from a local file.
* @throws RuntimeException If certs could not be retrieved from a remote location.
*/
private function retrieveCertsFromLocation($url, array $options = [])
{
// If we're retrieving a local file, just grab it.
$expireTime = '+1 hour';
if (strpos($url, 'http') !== 0) {
if (!file_exists($url)) {
throw new InvalidArgumentException(sprintf(
Expand All @@ -360,14 +359,28 @@ private function retrieveCertsFromLocation($url, array $options = [])
));
}

return json_decode((string) file_get_contents($url), true);
return [
json_decode((string) file_get_contents($url), true),
$expireTime
];
}

$httpHandler = $this->httpHandler;
$response = $httpHandler(new Request('GET', $url), $options);

if ($response->getStatusCode() == 200) {
return json_decode((string) $response->getBody(), true);
if ($cacheControl = $response->getHeaderLine('Cache-Control')) {
array_map(function ($value) use (&$expireTime) {
list($key, $value) = explode('=', $value) + [null, null];
if (trim($key) == 'max-age') {
$expireTime = '+' . $value . ' seconds';
}
}, explode(',', $cacheControl));
}
return [
json_decode((string) $response->getBody(), true),
$expireTime
];
}

throw new RuntimeException(sprintf(
Expand Down
64 changes: 60 additions & 4 deletions tests/AccessTokenTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -264,22 +264,33 @@ public function testEsVerifyEndToEnd()
$this->assertEquals('https://cloud.google.com/iap', $payload['iss']);
}

public function testGetCertsForIap()
/**
* @dataProvider provideCertsFromUrl
*/
public function testGetCertsFromUrl($certUrl)
{
$token = new AccessToken();
$reflector = new \ReflectionObject($token);
$cacheKeyMethod = $reflector->getMethod('getCacheKeyFromCertLocation');
$cacheKeyMethod->setAccessible(true);
$getCertsMethod = $reflector->getMethod('getCerts');
$getCertsMethod->setAccessible(true);
$cacheKey = $cacheKeyMethod->invoke($token, AccessToken::IAP_CERT_URL);
$cacheKey = $cacheKeyMethod->invoke($token, $certUrl);
$certs = $getCertsMethod->invoke(
$token,
AccessToken::IAP_CERT_URL,
$certUrl,
$cacheKey
);
$this->assertTrue(is_array($certs));
$this->assertEquals(5, count($certs));
$this->assertGreaterThanOrEqual(2, count($certs));
}

public function provideCertsFromUrl()
{
return [
[AccessToken::IAP_CERT_URL],
[AccessToken::FEDERATED_SIGNON_CERT_URL],
];
}

public function testRetrieveCertsFromLocationLocalFile()
Expand Down Expand Up @@ -398,6 +409,51 @@ public function testRetrieveCertsFromLocationLocalFileInvalidFileData()
]);
}

public function testRetrieveCertsFromLocationRespectsCacheControl()
{
$certsLocation = __DIR__ . '/fixtures/federated-certs.json';
$certsJson = file_get_contents($certsLocation);
$certsData = json_decode($certsJson, true);

$httpHandler = function (RequestInterface $request) use ($certsJson) {
return new Response(200, [
'cache-control' => 'public, max-age=1000',
], $certsJson);
};

$phpunit = $this;

$item = $this->prophesize('Psr\Cache\CacheItemInterface');
$item->get()
->shouldBeCalledTimes(1)
->willReturn(null);
$item->set($certsData)
->shouldBeCalledTimes(1)
->willReturn($item->reveal());

// Assert date-time is set with difference of 1000 (the max-age in the Cache-Control header)
$item->expiresAt(Argument::type('\DateTime'))
->shouldBeCalledTimes(1)
->will(function ($value) use ($phpunit) {
$phpunit->assertEqualsWithDelta(1000, $value[0]->getTimestamp() - time(), 1);
return $this;
});

$this->cache->getItem('google_auth_certs_cache|federated_signon_certs_v3')
->shouldBeCalledTimes(1)
->willReturn($item->reveal());

$this->cache->save(Argument::type('Psr\Cache\CacheItemInterface'))
->shouldBeCalledTimes(1);

$token = new AccessTokenStub(
$httpHandler,
$this->cache->reveal()
);

$token->verify($this->token);
}

public function testRetrieveCertsFromLocationRemote()
{
$certsLocation = __DIR__ . '/fixtures/federated-certs.json';
Expand Down

0 comments on commit 6d426b5

Please sign in to comment.