Skip to content

Commit 17f7940

Browse files
Merge pull request #4 from stackkit/bugfix/cert-cache
Fix cert caching
2 parents 4a02e28 + b07247c commit 17f7940

File tree

8 files changed

+203
-28
lines changed

8 files changed

+203
-28
lines changed

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+
## 1.0.1 - 2020-12-07
8+
9+
**Fixed**
10+
11+
- Fixed certificates cached too long
12+
713
## 1.0.0 - 2020-10-11
814

915
**Added**

composer.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@
1111
"ext-json": "*",
1212
"google/cloud-scheduler": "^1.4",
1313
"firebase/php-jwt": "^5.2",
14-
"phpseclib/phpseclib": "~2.0",
15-
"laravel/framework": "8.*"
14+
"phpseclib/phpseclib": "~2.0"
1615
},
1716
"require-dev": {
1817
"mockery/mockery": "^1.2",

phpunit.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
<env name="APP_DEBUG" value="1"/>
1919
<env name="APP_ENV" value="testing"/>
2020
<env name="APP_KEY" value="AckfSECXIvnK5r28GVIWUAxmbBSjTsmF"/>
21-
<env name="CACHE_DRIVER" value="array"/>
21+
<env name="CACHE_DRIVER" value="file"/>
2222
<env name="SESSION_DRIVER" value="array"/>
2323
<env name="MAIL_DRIVER" value="log"/>
2424
</php>

src/OpenIdVerificator.php

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

33
namespace Stackkit\LaravelGoogleCloudScheduler;
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;
@@ -19,6 +21,7 @@ class OpenIdVerificator
1921
private $guzzle;
2022
private $rsa;
2123
private $jwt;
24+
private $maxAge = [];
2225

2326
public function __construct(Client $guzzle, RSA $rsa, JWT $jwt)
2427
{
@@ -32,7 +35,6 @@ public function guardAgainstInvalidOpenIdToken($decodedToken)
3235
/**
3336
* https://developers.google.com/identity/protocols/oauth2/openid-connect#validatinganidtoken
3437
*/
35-
3638
if (!in_array($decodedToken->iss, ['https://accounts.google.com', 'accounts.google.com'])) {
3739
throw new CloudSchedulerException('The given OpenID token is not valid');
3840
}
@@ -46,30 +48,40 @@ public function guardAgainstInvalidOpenIdToken($decodedToken)
4648
}
4749
}
4850

49-
public function decodeToken($token)
51+
public function decodeOpenIdToken($openIdToken, $kid, $cache = true)
5052
{
53+
if (!$cache) {
54+
$this->forgetFromCache();
55+
}
56+
57+
$publicKey = $this->getPublicKey($kid);
58+
5159
try {
52-
$kid = $this->getKidFromOpenIdToken($token);
53-
$publicKey = $this->getPublicKey($kid);
60+
return $this->jwt->decode($openIdToken, $publicKey, ['RS256']);
61+
} catch (SignatureInvalidException $e) {
62+
if (!$cache) {
63+
throw $e;
64+
}
5465

55-
return $this->jwt->decode($token, $publicKey, ['RS256']);
56-
} catch (Throwable $e) {
57-
throw new CloudSchedulerException('Could not decode token');
66+
return $this->decodeOpenIdToken($openIdToken, $kid, false);
5867
}
5968
}
6069

6170
public function getPublicKey($kid = null)
6271
{
63-
$v3Certs = Cache::rememberForever(self::V3_CERTS, function () {
64-
return $this->getv3Certs();
65-
});
72+
if (Cache::has(self::V3_CERTS)) {
73+
$v3Certs = Cache::get(self::V3_CERTS);
74+
} else {
75+
$v3Certs = $this->getFreshCertificates();
76+
Cache::put(self::V3_CERTS, $v3Certs, Carbon::now()->addSeconds($this->maxAge[self::URL_OPENID_CONFIG]));
77+
}
6678

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

6981
return $this->extractPublicKeyFromCertificate($cert);
7082
}
7183

72-
private function getv3Certs()
84+
private function getFreshCertificates()
7385
{
7486
$jwksUri = $this->callApiAndReturnValue(self::URL_OPENID_CONFIG, 'jwks_uri');
7587

@@ -97,11 +109,24 @@ private function callApiAndReturnValue($url, $value)
97109

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

112+
$maxAge = 0;
113+
foreach ($response->getHeader('Cache-Control') as $line) {
114+
preg_match('/max-age=(\d+)/', $line, $matches);
115+
$maxAge = isset($matches[1]) ? (int) $matches[1] : 0;
116+
}
117+
118+
$this->maxAge[$url] = $maxAge;
119+
100120
return Arr::get($data, $value);
101121
}
102122

103123
public function isCached()
104124
{
105125
return Cache::has(self::V3_CERTS);
106126
}
127+
128+
public function forgetFromCache()
129+
{
130+
Cache::forget(self::V3_CERTS);
131+
}
107132
}

src/TaskHandler.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ private function authorizeRequest()
5858

5959
$openIdToken = $this->request->bearerToken();
6060

61-
$decodedToken = $this->openId->decodeToken($openIdToken);
61+
$kid = $this->openId->getKidFromOpenIdToken($openIdToken);
62+
63+
$decodedToken = $this->openId->decodeOpenIdToken($openIdToken, $kid);
6264

6365
$this->openId->guardAgainstInvalidOpenIdToken($decodedToken);
6466
}

tests/GooglePublicKeyTest.php

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
namespace Tests;
4+
5+
use Carbon\Carbon;
6+
use Firebase\JWT\JWT;
7+
use GuzzleHttp\Client;
8+
use Illuminate\Cache\Events\CacheHit;
9+
use Illuminate\Cache\Events\CacheMissed;
10+
use Illuminate\Cache\Events\KeyWritten;
11+
use Illuminate\Support\Facades\Cache;
12+
use Illuminate\Support\Facades\Event;
13+
use Mockery;
14+
use phpseclib\Crypt\RSA;
15+
use Stackkit\LaravelGoogleCloudScheduler\OpenIdVerificator;
16+
17+
class GooglePublicKeyTest extends TestCase
18+
{
19+
/**
20+
* @var OpenIdVerificator
21+
*/
22+
private $publicKey;
23+
24+
/**
25+
* @var Client
26+
*/
27+
private $guzzle;
28+
29+
protected function setUp(): void
30+
{
31+
parent::setUp();
32+
33+
$this->guzzle = Mockery::mock(new Client());
34+
35+
$this->publicKey = new OpenIdVerificator($this->guzzle, new RSA(), new JWT());
36+
}
37+
38+
/** @test */
39+
public function it_fetches_the_gcloud_public_key()
40+
{
41+
$this->assertStringContainsString('-----BEGIN PUBLIC KEY-----', $this->publicKey->getPublicKey());
42+
}
43+
44+
/** @test */
45+
public function it_caches_the_gcloud_public_key()
46+
{
47+
$this->assertFalse($this->publicKey->isCached());
48+
49+
$this->publicKey->getPublicKey();
50+
51+
$this->assertTrue($this->publicKey->isCached());
52+
}
53+
54+
/** @test */
55+
public function it_will_return_the_cached_gcloud_public_key()
56+
{
57+
Event::fake();
58+
59+
$this->publicKey->getPublicKey();
60+
61+
Event::assertDispatched(CacheMissed::class);
62+
Event::assertDispatched(KeyWritten::class);
63+
64+
$this->publicKey->getPublicKey();
65+
66+
Event::assertDispatched(CacheHit::class);
67+
68+
$this->guzzle->shouldHaveReceived('get')->twice();
69+
}
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+
}
90+
}

0 commit comments

Comments
 (0)