Skip to content

Commit b636c6e

Browse files
committed
Added more tests
1 parent e07a6d4 commit b636c6e

File tree

4 files changed

+220
-79
lines changed

4 files changed

+220
-79
lines changed

src/GooglePublicKey.php

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
namespace Stackkit\LaravelGoogleCloudTasksQueue;
4+
5+
use Firebase\JWT\JWT;
6+
use GuzzleHttp\Client;
7+
use Illuminate\Support\Arr;
8+
use Illuminate\Support\Facades\Cache;
9+
use phpseclib\Crypt\RSA;
10+
use phpseclib\Math\BigInteger;
11+
12+
class GooglePublicKey
13+
{
14+
private const CACHE_KEY = 'GooglePublicKey';
15+
16+
private $guzzle;
17+
18+
public function __construct(Client $guzzle)
19+
{
20+
$this->guzzle = $guzzle;
21+
}
22+
23+
public function get()
24+
{
25+
return Cache::rememberForever(self::CACHE_KEY, function () {
26+
return $this->fetch();
27+
});
28+
}
29+
30+
private function fetch()
31+
{
32+
$jwksUri = $this->getJwksUri();
33+
34+
$keys = $this->getCertificateKeys($jwksUri);
35+
36+
$firstKey = $keys[1];
37+
38+
$modulus = $firstKey['n'];
39+
$exponent = $firstKey['e'];
40+
41+
$rsa = app(RSA::class);
42+
43+
$modulus = new BigInteger(JWT::urlsafeB64Decode($modulus), 256);
44+
$exponent = new BigInteger(JWT::urlsafeB64Decode($exponent), 256);
45+
46+
$rsa->loadKey([
47+
'n' => $modulus,
48+
'e' => $exponent
49+
]);
50+
$rsa->setPublicKey();
51+
52+
return $rsa->getPublicKey();
53+
}
54+
55+
private function getJwksUri()
56+
{
57+
$discoveryEndpoint = 'https://accounts.google.com/.well-known/openid-configuration';
58+
59+
$configurationJson = $this->guzzle->get($discoveryEndpoint);
60+
61+
$configurations = json_decode($configurationJson->getBody(), true);
62+
63+
return Arr::get($configurations, 'jwks_uri');
64+
}
65+
66+
private function getCertificateKeys($jwksUri)
67+
{
68+
$json = $this->guzzle->get($jwksUri);
69+
70+
$certificates = json_decode($json->getBody(), true);
71+
72+
return Arr::get($certificates, 'keys');
73+
}
74+
75+
public function isCached()
76+
{
77+
return Cache::has(self::CACHE_KEY);
78+
}
79+
}

src/TaskHandler.php

Lines changed: 5 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,24 @@
33
namespace Stackkit\LaravelGoogleCloudTasksQueue;
44

55
use Google\Cloud\Tasks\V2\CloudTasksClient;
6-
use GuzzleHttp\Client;
76
use Illuminate\Http\Request;
87
use Illuminate\Queue\Worker;
98
use Illuminate\Queue\WorkerOptions;
10-
use Illuminate\Support\Arr;
11-
use phpseclib\Crypt\RSA;
12-
use phpseclib\Math\BigInteger;
13-
use Throwable;
149
use Firebase\JWT\JWT;
1510

1611
class TaskHandler
1712
{
18-
private $client;
1913
private $request;
2014
private $guzzle;
2115
private $jwt;
16+
private $publicKey;
2217

23-
public function __construct(CloudTasksClient $client, Request $request, Client $guzzle, JWT $jwt)
18+
public function __construct(CloudTasksClient $client, Request $request, JWT $jwt, GooglePublicKey $publicKey)
2419
{
2520
$this->client = $client;
2621
$this->request = $request;
27-
$this->guzzle = $guzzle;
2822
$this->jwt = $jwt;
23+
$this->publicKey = $publicKey;
2924
}
3025

3126
/**
@@ -50,64 +45,14 @@ public function authorizeRequest()
5045
throw new CloudTasksException('Missing [Authorization] header');
5146
}
5247

53-
// @todo - kill this check with a Mock
54-
if (app()->environment('testing')) {
55-
return;
56-
}
57-
5848
$openIdToken = $this->request->bearerToken();
59-
$pubKey = $this->getGooglePublicKey();
49+
$publicKey = $this->publicKey->get();
6050

61-
$decodedToken = $this->jwt->decode($openIdToken, $pubKey, ['RS256']);
51+
$decodedToken = $this->jwt->decode($openIdToken, $publicKey, ['RS256']);
6252

6353
$this->validateToken($decodedToken);
6454
}
6555

66-
private function getGooglePublicKey()
67-
{
68-
$jwksUri = $this->getJwksUri();
69-
70-
$keys = $this->getCertificateKeys($jwksUri);
71-
72-
$firstKey = $keys[1];
73-
74-
$modulus = $firstKey['n'];
75-
$exponent = $firstKey['e'];
76-
77-
$rsa = new RSA();
78-
79-
$modulus = new BigInteger(JWT::urlsafeB64Decode($modulus), 256);
80-
$exponent = new BigInteger(JWT::urlsafeB64Decode($exponent), 256);
81-
82-
$rsa->loadKey([
83-
'n' => $modulus,
84-
'e' => $exponent
85-
]);
86-
$rsa->setPublicKey();
87-
88-
return $rsa->getPublicKey();
89-
}
90-
91-
private function getJwksUri()
92-
{
93-
$discoveryEndpoint = 'https://accounts.google.com/.well-known/openid-configuration';
94-
95-
$configurationJson = $this->guzzle->get($discoveryEndpoint);
96-
97-
$configurations = json_decode($configurationJson->getBody(), true);
98-
99-
return Arr::get($configurations, 'jwks_uri');
100-
}
101-
102-
private function getCertificateKeys($jwksUri)
103-
{
104-
$json = $this->guzzle->get($jwksUri);
105-
106-
$certificates = json_decode($json->getBody(), true);
107-
108-
return Arr::get($certificates, 'keys');
109-
}
110-
11156
/**
11257
* https://developers.google.com/identity/protocols/oauth2/openid-connect#validatinganidtoken
11358
*

tests/GooglePublicKeyTest.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
namespace Tests;
4+
5+
use Illuminate\Support\Facades\Cache;
6+
use Mockery;
7+
use phpseclib\Crypt\RSA;
8+
use Stackkit\LaravelGoogleCloudTasksQueue\GooglePublicKey;
9+
10+
class GooglePublicKeyTest extends TestCase
11+
{
12+
/**
13+
* @var GooglePublicKey
14+
*/
15+
private $publicKey;
16+
17+
protected function setUp(): void
18+
{
19+
parent::setUp();
20+
21+
$this->publicKey = app(GooglePublicKey::class);
22+
}
23+
24+
/** @test */
25+
public function it_fetches_the_gcloud_public_key()
26+
{
27+
$this->assertStringContainsString('-----BEGIN PUBLIC KEY-----', $this->publicKey->get());
28+
}
29+
30+
/** @test */
31+
public function it_caches_the_gcloud_public_key()
32+
{
33+
$this->assertFalse($this->publicKey->isCached());
34+
35+
$this->publicKey->get();
36+
37+
$this->assertTrue($this->publicKey->isCached());
38+
}
39+
40+
/** @test */
41+
public function it_will_return_the_cached_gcloud_public_key()
42+
{
43+
$this->app->instance(RSA::class, ($rsa = Mockery::mock(new RSA())->byDefault()));
44+
45+
$this->publicKey->get();
46+
47+
$this->publicKey->get();
48+
49+
$rsa->shouldHaveReceived('loadKey')->once();
50+
}
51+
}

tests/TaskHandlerTest.php

Lines changed: 85 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,12 @@
55
use Firebase\JWT\JWT;
66
use Google\Cloud\Tasks\V2\CloudTasksClient;
77
use GuzzleHttp\Client;
8+
use Illuminate\Support\Facades\Cache;
89
use Illuminate\Support\Facades\Mail;
10+
use Mockery;
11+
use phpseclib\Crypt\RSA;
912
use Stackkit\LaravelGoogleCloudTasksQueue\CloudTasksException;
13+
use Stackkit\LaravelGoogleCloudTasksQueue\GooglePublicKey;
1014
use Stackkit\LaravelGoogleCloudTasksQueue\TaskHandler;
1115
use Tests\Support\TestMailable;
1216

@@ -17,54 +21,116 @@ class TaskHandlerTest extends TestCase
1721
*/
1822
private $handler;
1923

20-
private $client;
21-
2224
private $jwt;
2325

26+
private $request;
27+
2428
protected function setUp(): void
2529
{
2630
parent::setUp();
2731

28-
$this->client = \Mockery::mock(CloudTasksClient::class)->makePartial();
32+
$this->request = request();
2933

30-
$this->jwt = \Mockery::mock(JWT::class)->makePartial();
34+
// We don't have a valid token to test with, so for now act as if its always valid
35+
$this->app->instance(JWT::class, ($this->jwt = Mockery::mock(new JWT())->byDefault()->makePartial()));
36+
$this->jwt->shouldReceive('decode')->andReturn((object) [
37+
'iss' => 'accounts.google.com',
38+
'aud' => 'https://localhost/my-handler',
39+
'exp' => time() + 10
40+
])->byDefault();
3141

32-
$this->handler = \Mockery::mock(new TaskHandler(
33-
$this->client,
42+
// Ensure we don't fetch the Google public key each test...
43+
$googlePublicKey = Mockery::mock(app(GooglePublicKey::class));
44+
$googlePublicKey->shouldReceive('get')->andReturnNull();
45+
46+
$this->handler = new TaskHandler(
47+
new CloudTasksClient([
48+
'credentials' => __DIR__ . '/Support/gcloud-key-valid.json'
49+
]),
3450
request(),
35-
new Client(),
36-
$this->jwt
37-
))->shouldAllowMockingProtectedMethods();
38-
$this->app->instance(TaskHandler::class, $this->handler);
51+
$this->jwt,
52+
$googlePublicKey
53+
);
3954

40-
$this->jwt->shouldReceive('decode')->andReturnNull();
41-
$this->handler->shouldReceive('authorizeRequest')->andReturnNull();
55+
$this->request->headers->add(['Authorization' => 'Bearer 123']);
4256
}
4357

4458
/** @test */
4559
public function it_needs_an_authorization_header()
4660
{
61+
$this->request->headers->remove('Authorization');
62+
4763
$this->expectException(CloudTasksException::class);
4864
$this->expectExceptionMessage('Missing [Authorization] header');
4965

50-
request()->headers->add(['X-Cloudtasks-Taskname' => 'test']);
51-
request()->headers->add(['X-Cloudtasks-Queuename' => 'test']);
5266
$this->handler->handle();
5367
}
5468

69+
/** @test */
70+
public function the_authorization_header_must_contain_a_valid_gcloud_token()
71+
{
72+
request()->headers->add([
73+
'Authorization' => 'Bearer 123',
74+
]);
75+
76+
$this->expectException(CloudTasksException::class);
77+
$this->expectExceptionMessage('Could not decode incoming task');
78+
79+
$this->handler->handle();
80+
81+
// @todo - test with a valid token, not sure how to do that right now
82+
}
83+
84+
/** @test */
85+
public function it_will_validate_the_token_iss()
86+
{
87+
$this->jwt->shouldReceive('decode')->andReturn((object) [
88+
'iss' => 'test',
89+
]);
90+
$this->expectException(CloudTasksException::class);
91+
$this->expectExceptionMessage('The given OpenID token is not valid');
92+
$this->handler->handle($this->simpleJob());
93+
}
94+
95+
/** @test */
96+
public function it_will_validate_the_token_handler()
97+
{
98+
$this->jwt->shouldReceive('decode')->andReturn((object) [
99+
'iss' => 'accounts.google.com',
100+
'aud' => '__incorrect_aud__'
101+
]);
102+
$this->expectException(CloudTasksException::class);
103+
$this->expectExceptionMessage('The given OpenID token is not valid');
104+
$this->handler->handle($this->simpleJob());
105+
}
106+
107+
/** @test */
108+
public function it_will_validate_the_token_expiration()
109+
{
110+
$this->jwt->shouldReceive('decode')->andReturn((object) [
111+
'iss' => 'accounts.google.com',
112+
'aud' => 'https://localhost/my-handler',
113+
'exp' => time() - 1
114+
]);
115+
$this->expectException(CloudTasksException::class);
116+
$this->expectExceptionMessage('The given OpenID token has expired');
117+
$this->handler->handle($this->simpleJob());
118+
}
119+
55120
/** @test */
56121
public function it_runs_the_incoming_job()
57122
{
58123
Mail::fake();
59124

60-
request()->headers->add(['X-Cloudtasks-Taskname' => 'test']);
61-
request()->headers->add(['X-Cloudtasks-Queuename' => 'test']);
62125
request()->headers->add(['Authorization' => 'Bearer 123']);
63126

64-
$this->client->shouldReceive('getTask')->andReturnNull();
65-
66-
$this->handler->handle(json_decode(file_get_contents(__DIR__ . '/Support/test-job-payload.json'), true));
127+
$this->handler->handle($this->simpleJob());
67128

68129
Mail::assertSent(TestMailable::class);
69130
}
131+
132+
private function simpleJob()
133+
{
134+
return json_decode(file_get_contents(__DIR__ . '/Support/test-job-payload.json'), true);
135+
}
70136
}

0 commit comments

Comments
 (0)