Skip to content

Commit 7b050e2

Browse files
Merge pull request #5 from stackkit/bugfix/cert-cache
Fix cert caching
2 parents ba5557d + 21d0db2 commit 7b050e2

File tree

9 files changed

+113
-22
lines changed

9 files changed

+113
-22
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
/vendor/
22
composer.lock
33
.idea/
4+
.phpunit.result.cache

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
55
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
66

7+
## 2.0.1 - 2020-12-06
8+
9+
**Fixed**
10+
11+
- Fixed certificates cached too long ([#3](https://github.com/stackkit/laravel-google-cloud-tasks-queue/issues/3))
12+
713
## 2.0.0 - 2020-10-11
814

915
**Added**

src/CloudTasksQueue.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
use Illuminate\Contracts\Queue\Queue as QueueContract;
1212
use Illuminate\Queue\Queue as LaravelQueue;
1313
use Illuminate\Support\InteractsWithTime;
14-
use Illuminate\Support\Str;
1514

1615
class CloudTasksQueue extends LaravelQueue implements QueueContract
1716
{

src/Errors.php

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,6 @@
44

55
class Errors
66
{
7-
public static function invalidCredentials()
8-
{
9-
return 'Google Cloud credentials not provided. To fix this, in config/queue.php, connections.cloudtasks.credentials, provide the path to your credentials JSON file';
10-
}
11-
12-
public static function credentialsFileDoesNotExist()
13-
{
14-
return 'Google Cloud credentials JSON file does not exist';
15-
}
16-
177
public static function invalidProject()
188
{
199
return 'Google Cloud project not provided. To fix this, set the STACKKIT_CLOUD_TASKS_PROJECT environment variable';

src/OpenIdVerificator.php

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
namespace Stackkit\LaravelGoogleCloudTasksQueue;
44

5+
use Carbon\Carbon;
56
use Firebase\JWT\JWT;
7+
use Firebase\JWT\SignatureInvalidException;
68
use GuzzleHttp\Client;
79
use Illuminate\Support\Arr;
810
use Illuminate\Support\Facades\Cache;
@@ -17,25 +19,50 @@ class OpenIdVerificator
1719

1820
private $guzzle;
1921
private $rsa;
22+
private $jwt;
23+
private $maxAge = [];
2024

21-
public function __construct(Client $guzzle, RSA $rsa)
25+
public function __construct(Client $guzzle, RSA $rsa, JWT $jwt)
2226
{
2327
$this->guzzle = $guzzle;
2428
$this->rsa = $rsa;
29+
$this->jwt = $jwt;
30+
}
31+
32+
public function decodeOpenIdToken($openIdToken, $kid, $cache = true)
33+
{
34+
if (!$cache) {
35+
$this->forgetFromCache();
36+
}
37+
38+
$publicKey = $this->getPublicKey($kid);
39+
40+
try {
41+
return $this->jwt->decode($openIdToken, $publicKey, ['RS256']);
42+
} catch (SignatureInvalidException $e) {
43+
if (!$cache) {
44+
throw $e;
45+
}
46+
47+
return $this->decodeOpenIdToken($openIdToken, $kid, false);
48+
}
2549
}
2650

2751
public function getPublicKey($kid = null)
2852
{
29-
$v3Certs = Cache::rememberForever(self::V3_CERTS, function () {
30-
return $this->getv3Certs();
31-
});
53+
if (Cache::has(self::V3_CERTS)) {
54+
$v3Certs = Cache::get(self::V3_CERTS);
55+
} else {
56+
$v3Certs = $this->getFreshCertificates();
57+
Cache::put(self::V3_CERTS, $v3Certs, Carbon::now()->addSeconds($this->maxAge[self::URL_OPENID_CONFIG]));
58+
}
3259

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

3562
return $this->extractPublicKeyFromCertificate($cert);
3663
}
3764

38-
private function getv3Certs()
65+
private function getFreshCertificates()
3966
{
4067
$jwksUri = $this->callApiAndReturnValue(self::URL_OPENID_CONFIG, 'jwks_uri');
4168

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

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

93+
$maxAge = 0;
94+
foreach ($response->getHeader('Cache-Control') as $line) {
95+
preg_match('/max-age=(\d+)/', $line, $matches);
96+
$maxAge = isset($matches[1]) ? (int) $matches[1] : 0;
97+
}
98+
99+
$this->maxAge[$url] = $maxAge;
100+
66101
return Arr::get($data, $value);
67102
}
68103

69104
public function isCached()
70105
{
71106
return Cache::has(self::V3_CERTS);
72107
}
108+
109+
public function forgetFromCache()
110+
{
111+
Cache::forget(self::V3_CERTS);
112+
}
73113
}

src/TaskHandler.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,8 @@ public function authorizeRequest()
4747

4848
$openIdToken = $this->request->bearerToken();
4949
$kid = $this->publicKey->getKidFromOpenIdToken($openIdToken);
50-
$publicKey = $this->publicKey->getPublicKey($kid);
5150

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

5453
$this->validateToken($decodedToken);
5554
}

tests/GooglePublicKeyTest.php

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@
22

33
namespace Tests;
44

5+
use Carbon\Carbon;
6+
use Firebase\JWT\JWT;
57
use GuzzleHttp\Client;
8+
use Illuminate\Cache\Events\CacheHit;
9+
use Illuminate\Cache\Events\CacheMissed;
10+
use Illuminate\Cache\Events\KeyWritten;
611
use Illuminate\Support\Facades\Cache;
12+
use Illuminate\Support\Facades\Event;
713
use Mockery;
814
use phpseclib\Crypt\RSA;
915
use Stackkit\LaravelGoogleCloudTasksQueue\OpenIdVerificator;
@@ -26,7 +32,7 @@ protected function setUp(): void
2632

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

29-
$this->publicKey = new OpenIdVerificator($this->guzzle, new RSA());
35+
$this->publicKey = new OpenIdVerificator($this->guzzle, new RSA(), new JWT());
3036
}
3137

3238
/** @test */
@@ -48,10 +54,37 @@ public function it_caches_the_gcloud_public_key()
4854
/** @test */
4955
public function it_will_return_the_cached_gcloud_public_key()
5056
{
57+
Event::fake();
58+
5159
$this->publicKey->getPublicKey();
5260

61+
Event::assertDispatched(CacheMissed::class);
62+
Event::assertDispatched(KeyWritten::class);
63+
5364
$this->publicKey->getPublicKey();
5465

66+
Event::assertDispatched(CacheHit::class);
67+
5568
$this->guzzle->shouldHaveReceived('get')->twice();
5669
}
70+
71+
/** @test */
72+
public function public_key_is_cached_according_to_cache_control_headers()
73+
{
74+
Event::fake();
75+
76+
$this->publicKey->getPublicKey();
77+
78+
$this->publicKey->getPublicKey();
79+
80+
Carbon::setTestNow(Carbon::now()->addSeconds(3600));
81+
$this->publicKey->getPublicKey();
82+
83+
Carbon::setTestNow(Carbon::now()->addSeconds(5));
84+
$this->publicKey->getPublicKey();
85+
86+
Event::assertDispatched(CacheMissed::class, 2);
87+
Event::assertDispatched(KeyWritten::class, 2);
88+
89+
}
5790
}

tests/TaskHandlerTest.php

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
namespace Tests;
44

55
use Firebase\JWT\JWT;
6+
use Firebase\JWT\SignatureInvalidException;
67
use Google\Cloud\Tasks\V2\CloudTasksClient;
7-
use GuzzleHttp\Client;
8-
use Illuminate\Support\Facades\Cache;
8+
use Illuminate\Cache\Events\CacheHit;
9+
use Illuminate\Cache\Events\KeyWritten;
10+
use Illuminate\Support\Facades\Event;
911
use Illuminate\Support\Facades\Mail;
1012
use Mockery;
11-
use phpseclib\Crypt\RSA;
1213
use Stackkit\LaravelGoogleCloudTasksQueue\CloudTasksException;
1314
use Stackkit\LaravelGoogleCloudTasksQueue\OpenIdVerificator;
1415
use Stackkit\LaravelGoogleCloudTasksQueue\TaskHandler;
@@ -116,6 +117,21 @@ public function it_will_validate_the_token_expiration()
116117
$this->handler->handle($this->simpleJob());
117118
}
118119

120+
/** @test */
121+
public function in_case_of_signature_verification_failure_it_will_retry()
122+
{
123+
Event::fake();
124+
125+
$this->jwt->shouldReceive('decode')->andThrow(SignatureInvalidException::class);
126+
127+
$this->expectException(SignatureInvalidException::class);
128+
129+
$this->handler->handle($this->simpleJob());
130+
131+
Event::assertDispatched(CacheHit::class);
132+
Event::assertDispatched(KeyWritten::class);
133+
}
134+
119135
/** @test */
120136
public function it_runs_the_incoming_job()
121137
{

tests/TestCase.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace Tests;
44

5+
use Illuminate\Support\Facades\Artisan;
6+
57
class TestCase extends \Orchestra\Testbench\TestCase
68
{
79
/**
@@ -29,6 +31,11 @@ protected function getPackageProviders($app)
2931
*/
3032
protected function getEnvironmentSetUp($app)
3133
{
34+
foreach (glob(storage_path('framework/cache/data/*/*/*')) as $file) {
35+
unlink($file);
36+
}
37+
38+
$app['config']->set('cache.default', 'file');
3239
$app['config']->set('queue.default', 'cloudtasks');
3340
$app['config']->set('queue.connections.cloudtasks', [
3441
'driver' => 'cloudtasks',

0 commit comments

Comments
 (0)