Skip to content

Commit 1e0ad0b

Browse files
committed
adds jwt abstraction
1 parent f1e7f2b commit 1e0ad0b

10 files changed

Lines changed: 286 additions & 439 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ composer.lock
44
.cache
55
.docs
66
.gitmodules
7+
.phpunit.result.cache
78

89
# IntelliJ
910
.idea

UPGRADING.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,27 @@ $httpClient = new Google\Http\Client\GuzzleClient($guzzle);
6464
$auth = new GoogleAuth(['httpClient' => $httpClient]);
6565
```
6666

67+
#### Improved JWT handling
68+
69+
* Provides an abstraction from `firebase/jwt`, `phpseclib/phpseclib`, and `kelvinmo/simplejwt`
70+
* Using the composer "[replace](https://stackoverflow.com/questions/18882201/how-does-the-replace-property-work-with-composer)" keyword, users can ignore sub-dependencies such as Firebase JWT in favor of a separate JWT library
71+
* **TODO**: Provide documentation on how to use a different library
72+
* Adds `JwtClientInterface` and `FirebaseJwtClient`
73+
74+
**Example**
75+
76+
```php
77+
$jwt = new class implements Google\Auth\Jwt\JwtClientInterface {
78+
public function encode($payload, $signingKey, $signingAlg, $keyId) {
79+
// encode method
80+
}
81+
82+
// ... other JWT hander interface methods go here ...
83+
};
84+
$googleAuth = new GoogleAuth(['jwtClient' => $jwt]);
85+
$googleAuth->verify($someJwt);
86+
```
87+
6788
#### New `GoogleAuth` class
6889

6990
`GoogleAuth` replaces `ApplicationDefaultCredentials`, and provides a

src/Auth/GoogleAuth.php

Lines changed: 94 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,22 @@
2020
namespace Google\Auth;
2121

2222
use DomainException;
23+
use Firebase\JWT\JWT;
24+
use Firebase\JWT\JWK;
2325
use Google\Auth\Credentials\ComputeCredentials;
2426
use Google\Auth\Credentials\ServiceAccountCredentials;
2527
use Google\Auth\Credentials\ServiceAccountJwtAccessCredentials;
2628
use Google\Auth\Credentials\CredentialsInterface;
2729
use Google\Auth\Credentials\UserRefreshCredentials;
2830
use Google\Auth\Http\ClientFactory;
31+
use Google\Auth\Jwt\FirebaseJwtClient;
32+
use Google\Auth\Jwt\JwtClientInterface;
2933
use Google\Cache\MemoryCacheItemPool;
3034
use GuzzleHttp\Psr7\Request;
3135
use InvalidArgumentException;
3236
use Psr\Cache\CacheItemPoolInterface;
3337
use RuntimeException;
38+
use UnexpectedValueException;
3439

3540
/**
3641
* GoogleAuth obtains the default credentials for
@@ -73,20 +78,19 @@ class GoogleAuth
7378
{
7479
private const TOKEN_REVOKE_URI = 'https://oauth2.googleapis.com/revoke';
7580
private const OIDC_CERT_URI = 'https://www.googleapis.com/oauth2/v3/certs';
76-
private const OIDC_ISSUERS = ['accounts.google.com', 'https://accounts.google.com'];
81+
private const OIDC_ISSUERS = ['http://accounts.google.com', 'https://accounts.google.com'];
7782
private const IAP_JWK_URI = 'https://www.gstatic.com/iap/verify/public_key-jwk';
7883
private const IAP_ISSUERS = ['https://cloud.google.com/iap'];
7984

8085
private const ENV_VAR = 'GOOGLE_APPLICATION_CREDENTIALS';
8186
private const WELL_KNOWN_PATH = 'gcloud/application_default_credentials.json';
8287
private const NON_WINDOWS_WELL_KNOWN_PATH_BASE = '.config';
8388

84-
private const ON_COMPUTE_CACHE_KEY = 'google_auth_on_gce_cache';
85-
86-
private $httpClient;
8789
private $cache;
8890
private $cacheLifetime;
8991
private $cachePrefix;
92+
private $httpClient;
93+
private $jwtClient;
9094

9195
/**
9296
* Obtains an AuthTokenMiddleware which will fetch an access token to use in
@@ -98,6 +102,7 @@ class GoogleAuth
98102
*
99103
* @param array $options {
100104
* @type ClientInterface $httpClient client which delivers psr7 request
105+
* @type JwtClientInterface $jwtClient
101106
* @type CacheItemPoolInterface $cache A cache implementation, may be
102107
* provided if you have one already available for use.
103108
* @type int $cacheLifetime
@@ -107,15 +112,18 @@ class GoogleAuth
107112
public function __construct(array $options = [])
108113
{
109114
$options += [
110-
'httpClient' => null,
111115
'cache' => null,
112116
'cacheLifetime' => 1500,
113117
'cachePrefix' => '',
118+
'httpClient' => null,
119+
'jwtClient' => null,
114120
];
115-
$this->httpClient = $options['httpClient'] ?: ClientFactory::build();
116121
$this->cache = $options['cache'] ?: new MemoryCacheItemPool();
117122
$this->cacheLifetime = $options['cacheLifetime'];
118123
$this->cachePrefix = $options['cachePrefix'];
124+
$this->httpClient = $options['httpClient'] ?: ClientFactory::build();
125+
$this->jwtClient = $options['jwtClient']
126+
?: new FirebaseJwtClient(new JWT(), new JWK());
119127
}
120128

121129
/**
@@ -233,13 +241,18 @@ public function makeCredentials(array $options = []): CredentialsInterface
233241
* Determines if this a GCE instance, by accessing the expected metadata
234242
* host.
235243
*
244+
* @param array $options [optional] Configuration options.
245+
* @param string $options.cacheKey cache key used for caching the result
246+
*
236247
* @return bool
237248
*/
238-
public function onCompute(): bool
249+
public function onCompute(array $options = []): bool
239250
{
240-
$cacheItem = $this->cache->getItem(
241-
$this->cachePrefix . self::ON_COMPUTE_CACHE_KEY
242-
);
251+
$options += [
252+
'cacheKey' => null,
253+
];
254+
$cacheKey = $options['cacheKey'] ?: 'google_auth_on_gce_cache';
255+
$cacheItem = $this->cache->getItem($this->cachePrefix . $cacheKey);
243256

244257
if ($cacheItem->isHit()) {
245258
return $cacheItem->get();
@@ -257,23 +270,42 @@ public function onCompute(): bool
257270
* @param string $token The JSON Web Token to be verified.
258271
* @param array $options [optional] Configuration options.
259272
* @param string $options.audience The indended recipient of the token.
260-
* @param string $options.issuer The intended issuer of the token.
261-
* @param string $certsLocation URI for JSON certificate array conforming to
273+
* @param string $options.cacheKey cache key used for caching certs
274+
* @param string $options.certsLocation URI for JSON certificate array conforming to
262275
* the JWK spec (see https://tools.ietf.org/html/rfc7517).
276+
* @param array $options.issuers The intended issuers of the token.
263277
*/
264-
public function verify(string $token, array $options = []): array
278+
public function verify(string $token, array $options = []): bool
265279
{
266-
$location = isset($options['certsLocation'])
267-
? $options['certsLocation']
268-
: self::OIDC_CERT_URI;
269-
270-
$cacheKey = isset($options['cacheKey'])
271-
? $options['cacheKey']
272-
: $this->getCacheKeyFromCertLocation($location);
280+
$options += [
281+
'audience' => null,
282+
'certsLocation' => null,
283+
'cacheKey' => null,
284+
'issuers' => null,
285+
];
286+
$location = $options['certsLocation'] ?: self::OIDC_CERT_URI;
287+
$cacheKey = $options['cacheKey'] ?:
288+
sprintf('google_auth_certs_cache|%s', sha1($location));
273289

274290
$certs = $this->getCerts($location, $cacheKey);
275-
$oauth = new OAuth2();
276-
return $oauth->verify($token, $certs, $options);
291+
$alg = $this->determineAlg($certs);
292+
293+
$keys = $this->jwtClient->parseKeySet($certs);
294+
$payload = $this->jwtClient->decode($token, $keys, [$alg]);
295+
296+
$issuers = $options['issuers'] ?:
297+
['RS256' => self::OIDC_ISSUERS, 'ES256' => self::IAP_ISSUERS][$alg];
298+
299+
if (empty($payload['iss']) || !in_array($payload['iss'], $issuers)) {
300+
throw new UnexpectedValueException('Issuer does not match');
301+
}
302+
303+
$aud = $options['audience'] ?: null;
304+
if ($aud && isset($payload['aud']) && $payload['aud'] != $aud) {
305+
throw new UnexpectedValueException('Audience does not match');
306+
}
307+
308+
return true;
277309
}
278310

279311
/**
@@ -286,8 +318,9 @@ public function verify(string $token, array $options = []): array
286318
* @return array
287319
* @throws InvalidArgumentException If received certs are in an invalid format.
288320
*/
289-
private function getCerts(string $location, string $cacheKey): array {
290-
$cacheItem = $this->cache->getItem($cacheKey);
321+
private function getCerts(string $location, string $cacheKey): array
322+
{
323+
$cacheItem = $this->cache->getItem($this->cachePrefix . $cacheKey);
291324
$certs = $cacheItem ? $cacheItem->get() : null;
292325

293326
$gotNewCerts = false;
@@ -298,11 +331,6 @@ private function getCerts(string $location, string $cacheKey): array {
298331
}
299332

300333
if (!isset($certs['keys'])) {
301-
if ($location !== self::IAP_JWK_URI) {
302-
throw new InvalidArgumentException(
303-
'federated sign-on certs expects "keys" to be set'
304-
);
305-
}
306334
throw new InvalidArgumentException(
307335
'certs expects "keys" to be set'
308336
);
@@ -316,7 +344,40 @@ private function getCerts(string $location, string $cacheKey): array {
316344
$this->cache->save($cacheItem);
317345
}
318346

319-
return $certs['keys'];
347+
return $certs;
348+
}
349+
350+
/**
351+
* Identifies the expected algorithm to verify by looking at the "alg" key
352+
* of the provided certs.
353+
*
354+
* @param array $certs Certificate array according to the JWK spec (see
355+
* https://tools.ietf.org/html/rfc7517).
356+
* @return string The expected algorithm, such as "ES256" or "RS256".
357+
*/
358+
private function determineAlg(array $certs): string
359+
{
360+
$alg = null;
361+
foreach ($certs['keys'] as $cert) {
362+
if (empty($cert['alg'])) {
363+
throw new InvalidArgumentException(
364+
'certs expects "alg" to be set'
365+
);
366+
}
367+
$alg = $alg ?: $cert['alg'];
368+
369+
if ($alg != $cert['alg']) {
370+
throw new InvalidArgumentException(
371+
'More than one alg detected in certs'
372+
);
373+
}
374+
}
375+
if (!in_array($alg, ['RS256', 'ES256'])) {
376+
throw new InvalidArgumentException(
377+
'unrecognized "alg" in certs, expected ES256 or RS256'
378+
);
379+
}
380+
return $alg;
320381
}
321382

322383
/**
@@ -353,22 +414,6 @@ private function retrieveCertsFromLocation(string $url): array
353414
), $response->getStatusCode());
354415
}
355416

356-
/**
357-
* Generate a cache key based on the cert location using sha1 with the
358-
* exception of using "federated_signon_certs_v3" to preserve BC.
359-
*
360-
* @param string $certsLocation
361-
* @return string
362-
*/
363-
private function getCacheKeyFromCertLocation($certsLocation)
364-
{
365-
$key = $certsLocation === self::OIDC_CERT_URI
366-
? 'federated_signon_certs_v3'
367-
: sha1($certsLocation);
368-
369-
return 'google_auth_certs_cache|' . $key;
370-
}
371-
372417
/**
373418
* Revoke an OAuth2 access token or refresh token. This method will revoke the current access
374419
* token, if a token isn't provided.
@@ -378,11 +423,11 @@ private function getCacheKeyFromCertLocation($certsLocation)
378423
*/
379424
public function revoke($token): bool
380425
{
381-
$oauth = new OAuth2([
426+
$oauth2 = new OAuth2([
382427
'tokenRevokeUri' => self::TOKEN_REVOKE_URI,
428+
'httpClient' => $this->httpClient,
383429
]);
384-
385-
return $oauth->revoke($token);
430+
return $oauth2->revoke($token);
386431
}
387432

388433
/**

src/Auth/Jwt/FirebaseJwtClient.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
/*
3+
* Copyright 2020 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
declare(strict_types=1);
19+
20+
namespace Google\Auth\Jwt;
21+
22+
use Firebase\JWT\JWT;
23+
use Firebase\JWT\JWK;
24+
25+
class FirebaseJwtClient implements JwtClientInterface
26+
{
27+
private $jwt;
28+
private $jwk;
29+
30+
public function __construct(JWT $jwt, JWK $jwk)
31+
{
32+
$this->jwt = $jwt;
33+
$this->jwk = $jwk;
34+
}
35+
36+
public function encode(
37+
array $payload,
38+
string $signingKey,
39+
string $signingAlg,
40+
string $keyId
41+
): string {
42+
return $this->jwt->encode($payload, $signingKey, $signingAlg, $keyId);
43+
}
44+
45+
public function decode(string $jwt, array $keys, array $allowedAlgs): array
46+
{
47+
return (array) $this->jwt->decode($jwt, $keys, $allowedAlgs);
48+
}
49+
50+
public function parseKeySet(array $keySet): array
51+
{
52+
return $this->jwk->parseKeySet($keySet);
53+
}
54+
}
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,34 @@
11
<?php
2-
/**
2+
/*
33
* Copyright 2020 Google LLC
44
*
55
* Licensed under the Apache License, Version 2.0 (the "License");
66
* you may not use this file except in compliance with the License.
77
* You may obtain a copy of the License at
88
*
9-
* http://www.apache.org/licenses/LICENSE-2.0
9+
* http://www.apache.org/licenses/LICENSE-2.0
1010
*
1111
* Unless required by applicable law or agreed to in writing, software
1212
* distributed under the License is distributed on an "AS IS" BASIS,
1313
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1414
* See the License for the specific language governing permissions and
1515
* limitations under the License.
1616
*/
17-
namespace Google\Auth\HttpHandler;
1817

19-
class Guzzle7HttpHandler extends Guzzle6HttpHandler
18+
declare(strict_types=1);
19+
20+
namespace Google\Auth\Jwt;
21+
22+
interface JwtClientInterface
2023
{
21-
}
24+
public function encode(
25+
array $payload,
26+
string $signingKey,
27+
string $signingAlg,
28+
string $keyId
29+
): string;
30+
31+
public function decode(string $jwt, array $keys, array $allowedAlgs): array;
32+
33+
public function parseKeySet(array $keySey): array;
34+
}

0 commit comments

Comments
 (0)