Skip to content

Fix cert caching #5

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Dec 6, 2020
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/vendor/
composer.lock
.idea/
.phpunit.result.cache
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## 2.0.1 - 2020-12-06

**Fixed**

- Fixed certificates cached too long ([#3](https://github.com/stackkit/laravel-google-cloud-tasks-queue/issues/3))

## 2.0.0 - 2020-10-11

**Added**
Expand Down
1 change: 0 additions & 1 deletion src/CloudTasksQueue.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
use Illuminate\Contracts\Queue\Queue as QueueContract;
use Illuminate\Queue\Queue as LaravelQueue;
use Illuminate\Support\InteractsWithTime;
use Illuminate\Support\Str;

class CloudTasksQueue extends LaravelQueue implements QueueContract
{
Expand Down
10 changes: 0 additions & 10 deletions src/Errors.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,6 @@

class Errors
{
public static function invalidCredentials()
{
return 'Google Cloud credentials not provided. To fix this, in config/queue.php, connections.cloudtasks.credentials, provide the path to your credentials JSON file';
}

public static function credentialsFileDoesNotExist()
{
return 'Google Cloud credentials JSON file does not exist';
}

public static function invalidProject()
{
return 'Google Cloud project not provided. To fix this, set the STACKKIT_CLOUD_TASKS_PROJECT environment variable';
Expand Down
50 changes: 45 additions & 5 deletions src/OpenIdVerificator.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

namespace Stackkit\LaravelGoogleCloudTasksQueue;

use Carbon\Carbon;
use Firebase\JWT\JWT;
use Firebase\JWT\SignatureInvalidException;
use GuzzleHttp\Client;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Cache;
Expand All @@ -17,25 +19,50 @@ class OpenIdVerificator

private $guzzle;
private $rsa;
private $jwt;
private $maxAge = [];

public function __construct(Client $guzzle, RSA $rsa)
public function __construct(Client $guzzle, RSA $rsa, JWT $jwt)
{
$this->guzzle = $guzzle;
$this->rsa = $rsa;
$this->jwt = $jwt;
}

public function decodeOpenIdToken($openIdToken, $kid, $cache = true)
{
if (!$cache) {
$this->forgetFromCache();
}

$publicKey = $this->getPublicKey($kid);

try {
return $this->jwt->decode($openIdToken, $publicKey, ['RS256']);
} catch (SignatureInvalidException $e) {
if (!$cache) {
throw $e;
}

return $this->decodeOpenIdToken($openIdToken, $kid, false);
}
}

public function getPublicKey($kid = null)
{
$v3Certs = Cache::rememberForever(self::V3_CERTS, function () {
return $this->getv3Certs();
});
if (Cache::has(self::V3_CERTS)) {
$v3Certs = Cache::get(self::V3_CERTS);
} else {
$v3Certs = $this->getFreshCertificates();
Cache::put(self::V3_CERTS, $v3Certs, Carbon::now()->addSeconds($this->maxAge[self::URL_OPENID_CONFIG]));
}

$cert = $kid ? collect($v3Certs)->firstWhere('kid', '=', $kid) : $v3Certs[0];

return $this->extractPublicKeyFromCertificate($cert);
}

private function getv3Certs()
private function getFreshCertificates()
{
$jwksUri = $this->callApiAndReturnValue(self::URL_OPENID_CONFIG, 'jwks_uri');

Expand Down Expand Up @@ -63,11 +90,24 @@ private function callApiAndReturnValue($url, $value)

$data = json_decode($response->getBody(), true);

$maxAge = 0;
foreach ($response->getHeader('Cache-Control') as $line) {
preg_match('/max-age=(\d+)/', $line, $matches);
$maxAge = isset($matches[1]) ? (int) $matches[1] : 0;
}

$this->maxAge[$url] = $maxAge;

return Arr::get($data, $value);
}

public function isCached()
{
return Cache::has(self::V3_CERTS);
}

public function forgetFromCache()
{
Cache::forget(self::V3_CERTS);
}
}
3 changes: 1 addition & 2 deletions src/TaskHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,8 @@ public function authorizeRequest()

$openIdToken = $this->request->bearerToken();
$kid = $this->publicKey->getKidFromOpenIdToken($openIdToken);
$publicKey = $this->publicKey->getPublicKey($kid);

$decodedToken = $this->jwt->decode($openIdToken, $publicKey, ['RS256']);
$decodedToken = $this->publicKey->decodeOpenIdToken($openIdToken, $kid);

$this->validateToken($decodedToken);
}
Expand Down
35 changes: 34 additions & 1 deletion tests/GooglePublicKeyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@

namespace Tests;

use Carbon\Carbon;
use Firebase\JWT\JWT;
use GuzzleHttp\Client;
use Illuminate\Cache\Events\CacheHit;
use Illuminate\Cache\Events\CacheMissed;
use Illuminate\Cache\Events\KeyWritten;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Event;
use Mockery;
use phpseclib\Crypt\RSA;
use Stackkit\LaravelGoogleCloudTasksQueue\OpenIdVerificator;
Expand All @@ -26,7 +32,7 @@ protected function setUp(): void

$this->guzzle = Mockery::mock(new Client());

$this->publicKey = new OpenIdVerificator($this->guzzle, new RSA());
$this->publicKey = new OpenIdVerificator($this->guzzle, new RSA(), new JWT());
}

/** @test */
Expand All @@ -48,10 +54,37 @@ public function it_caches_the_gcloud_public_key()
/** @test */
public function it_will_return_the_cached_gcloud_public_key()
{
Event::fake();

$this->publicKey->getPublicKey();

Event::assertDispatched(CacheMissed::class);
Event::assertDispatched(KeyWritten::class);

$this->publicKey->getPublicKey();

Event::assertDispatched(CacheHit::class);

$this->guzzle->shouldHaveReceived('get')->twice();
}

/** @test */
public function public_key_is_cached_according_to_cache_control_headers()
{
Event::fake();

$this->publicKey->getPublicKey();

$this->publicKey->getPublicKey();

Carbon::setTestNow(Carbon::now()->addSeconds(3600));
$this->publicKey->getPublicKey();

Carbon::setTestNow(Carbon::now()->addSeconds(5));
$this->publicKey->getPublicKey();

Event::assertDispatched(CacheMissed::class, 2);
Event::assertDispatched(KeyWritten::class, 2);

}
}
22 changes: 19 additions & 3 deletions tests/TaskHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
namespace Tests;

use Firebase\JWT\JWT;
use Firebase\JWT\SignatureInvalidException;
use Google\Cloud\Tasks\V2\CloudTasksClient;
use GuzzleHttp\Client;
use Illuminate\Support\Facades\Cache;
use Illuminate\Cache\Events\CacheHit;
use Illuminate\Cache\Events\KeyWritten;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Mail;
use Mockery;
use phpseclib\Crypt\RSA;
use Stackkit\LaravelGoogleCloudTasksQueue\CloudTasksException;
use Stackkit\LaravelGoogleCloudTasksQueue\OpenIdVerificator;
use Stackkit\LaravelGoogleCloudTasksQueue\TaskHandler;
Expand Down Expand Up @@ -116,6 +117,21 @@ public function it_will_validate_the_token_expiration()
$this->handler->handle($this->simpleJob());
}

/** @test */
public function in_case_of_signature_verification_failure_it_will_retry()
{
Event::fake();

$this->jwt->shouldReceive('decode')->andThrow(SignatureInvalidException::class);

$this->expectException(SignatureInvalidException::class);

$this->handler->handle($this->simpleJob());

Event::assertDispatched(CacheHit::class);
Event::assertDispatched(KeyWritten::class);
}

/** @test */
public function it_runs_the_incoming_job()
{
Expand Down
7 changes: 7 additions & 0 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Tests;

use Illuminate\Support\Facades\Artisan;

class TestCase extends \Orchestra\Testbench\TestCase
{
/**
Expand Down Expand Up @@ -29,6 +31,11 @@ protected function getPackageProviders($app)
*/
protected function getEnvironmentSetUp($app)
{
foreach (glob(storage_path('framework/cache/data/*/*/*')) as $file) {
unlink($file);
}

$app['config']->set('cache.default', 'file');
$app['config']->set('queue.default', 'cloudtasks');
$app['config']->set('queue.connections.cloudtasks', [
'driver' => 'cloudtasks',
Expand Down