Skip to content

Commit 9ab8a91

Browse files
authored
chore: refactor verify to use firebase/jwt CachedKeySet (#2596)
1 parent 703ba9a commit 9ab8a91

7 files changed

Lines changed: 54 additions & 223 deletions

File tree

composer.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
"google/apiclient-services": "~0.350",
1414
"firebase/php-jwt": "^6.0||^7.0",
1515
"monolog/monolog": "^2.9||^3.0",
16-
"phpseclib/phpseclib": "^3.0.50",
1716
"guzzlehttp/guzzle": "^7.4.5",
1817
"guzzlehttp/psr7": "^2.6"
1918
},

src/AccessToken/Verify.php

Lines changed: 34 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -18,24 +18,20 @@
1818

1919
namespace Google\AccessToken;
2020

21-
use DateTime;
2221
use DomainException;
2322
use Exception;
24-
use ExpiredException;
25-
use Firebase\JWT\ExpiredException as ExpiredExceptionV3;
23+
use Firebase\JWT\CachedKeySet;
24+
use Firebase\JWT\ExpiredException;
2625
use Firebase\JWT\JWT;
27-
use Firebase\JWT\Key;
2826
use Firebase\JWT\SignatureInvalidException;
2927
use Google\Auth\Cache\MemoryCacheItemPool;
30-
use Google\Exception as GoogleException;
3128
use GuzzleHttp\Client;
32-
use GuzzleHttp\ClientInterface;
29+
use GuzzleHttp\ClientInterface as GuzzleClientInterface;
30+
use GuzzleHttp\Psr7\HttpFactory;
3331
use InvalidArgumentException;
3432
use LogicException;
35-
use phpseclib3\Crypt\AES;
36-
use phpseclib3\Crypt\PublicKeyLoader;
37-
use phpseclib3\Math\BigInteger;
3833
use Psr\Cache\CacheItemPoolInterface;
34+
use Psr\Http\Client\ClientInterface;
3935

4036
/**
4137
* Wrapper around Google Access Tokens which provides convenience functions
@@ -48,26 +44,21 @@ class Verify
4844
const OAUTH2_ISSUER_HTTPS = 'https://accounts.google.com';
4945

5046
/**
51-
* @var ClientInterface The http client
52-
*/
53-
private $http;
54-
55-
/**
56-
* @var CacheItemPoolInterface cache class
57-
*/
58-
private $cache;
47+
* @var \Firebase\JWT\JWT
48+
*/
49+
public JWT $jwt;
5950

6051
/**
61-
* @var \Firebase\JWT\JWT
52+
* @var \Firebase\JWT\CachedKeySet
6253
*/
63-
public $jwt;
54+
private CachedKeySet $keySet;
6455

6556
/**
6657
* Instantiates the class, but does not initiate the login flow, leaving it
6758
* to the discretion of the caller.
6859
*/
6960
public function __construct(
70-
?ClientInterface $http = null,
61+
?GuzzleClientInterface $http = null,
7162
?CacheItemPoolInterface $cache = null,
7263
?JWT $jwt = null
7364
) {
@@ -79,9 +70,17 @@ public function __construct(
7970
$cache = new MemoryCacheItemPool();
8071
}
8172

82-
$this->http = $http;
83-
$this->cache = $cache;
73+
if (!$http instanceof ClientInterface) {
74+
throw new InvalidArgumentException('http client must implement ' . ClientInterface::class);
75+
}
76+
8477
$this->jwt = $jwt ?: $this->getJwtService();
78+
$this->keySet = new CachedKeySet(
79+
self::FEDERATED_SIGNON_CERT_URL,
80+
$http,
81+
new HttpFactory(),
82+
$cache
83+
);
8584
}
8685

8786
/**
@@ -100,123 +99,27 @@ public function verifyIdToken($idToken, $audience = null)
10099
throw new LogicException('id_token cannot be null');
101100
}
102101

103-
// set phpseclib constants if applicable
104-
$this->setPhpsecConstants();
105-
106102
// Check signature
107-
$certs = $this->getFederatedSignOnCerts();
108-
foreach ($certs as $cert) {
109-
try {
110-
$args = [$idToken];
111-
$publicKey = $this->getPublicKey($cert);
112-
if (class_exists(Key::class)) {
113-
$args[] = new Key($publicKey, 'RS256');
114-
} else {
115-
$args[] = $publicKey;
116-
$args[] = ['RS256'];
117-
}
118-
$payload = \call_user_func_array([$this->jwt, 'decode'], $args);
119-
120-
if (property_exists($payload, 'aud')) {
121-
if ($audience && $payload->aud != $audience) {
122-
return false;
123-
}
124-
}
125-
126-
// support HTTP and HTTPS issuers
127-
// @see https://developers.google.com/identity/sign-in/web/backend-auth
128-
$issuers = [self::OAUTH2_ISSUER, self::OAUTH2_ISSUER_HTTPS];
129-
if (!isset($payload->iss) || !in_array($payload->iss, $issuers)) {
130-
return false;
131-
}
132-
133-
return (array)$payload;
134-
} catch (ExpiredException $e) { // @phpstan-ignore-line
135-
return false;
136-
} catch (ExpiredExceptionV3 $e) {
137-
return false;
138-
} catch (SignatureInvalidException $e) {
139-
// continue
140-
} catch (DomainException $e) {
141-
// continue
142-
}
143-
}
144-
145-
return false;
146-
}
147-
148-
private function getCache()
149-
{
150-
return $this->cache;
151-
}
152-
153-
/**
154-
* Retrieve and cache a certificates file.
155-
*
156-
* @param string $url location
157-
* @return array certificates
158-
* @throws \Google\Exception
159-
*/
160-
private function retrieveCertsFromLocation($url)
161-
{
162-
// If we're retrieving a local file, just grab it.
163-
if (0 !== strpos($url, 'http')) {
164-
if (!$file = file_get_contents($url)) {
165-
throw new GoogleException(
166-
"Failed to retrieve verification certificates: '".
167-
$url."'."
168-
);
169-
}
170-
171-
return json_decode($file, true);
172-
}
173-
174-
// @phpstan-ignore-next-line
175-
$response = $this->http->get($url);
176-
177-
if ($response->getStatusCode() == 200) {
178-
return json_decode((string)$response->getBody(), true);
179-
}
180-
throw new GoogleException(
181-
sprintf(
182-
'Failed to retrieve verification certificates: "%s".',
183-
$response->getBody()->getContents()
184-
),
185-
$response->getStatusCode()
186-
);
187-
}
188-
189-
// Gets federated sign-on certificates to use for verifying identity tokens.
190-
// Returns certs as array structure, where keys are key ids, and values
191-
// are PEM encoded certificates.
192-
private function getFederatedSignOnCerts()
193-
{
194-
$certs = null;
195-
if ($cache = $this->getCache()) {
196-
$cacheItem = $cache->getItem('federated_signon_certs_v3');
197-
$certs = $cacheItem->get();
103+
try {
104+
$payload = ($this->jwt)->decode($idToken, $this->keySet);
105+
} catch (ExpiredException | SignatureInvalidException | DomainException) {
106+
return false;
198107
}
199108

200-
201-
if (!$certs) {
202-
$certs = $this->retrieveCertsFromLocation(
203-
self::FEDERATED_SIGNON_CERT_URL
204-
);
205-
206-
if ($cache) {
207-
$cacheItem->expiresAt(new DateTime('+1 hour'));
208-
$cacheItem->set($certs);
209-
$cache->save($cacheItem);
109+
if (property_exists($payload, 'aud')) {
110+
if ($audience && $payload->aud != $audience) {
111+
return false;
210112
}
211113
}
212114

213-
if (!isset($certs['keys'])) {
214-
throw new InvalidArgumentException(
215-
'federated sign-on certs expects "keys" to be set'
216-
);
115+
// support HTTP and HTTPS issuers
116+
// @see https://developers.google.com/identity/sign-in/web/backend-auth
117+
$issuers = [self::OAUTH2_ISSUER, self::OAUTH2_ISSUER_HTTPS];
118+
if (!isset($payload->iss) || !in_array($payload->iss, $issuers)) {
119+
return false;
217120
}
218121

219-
return $certs['keys'];
122+
return (array) $payload;
220123
}
221124

222125
private function getJwtService()
@@ -230,35 +133,4 @@ private function getJwtService()
230133

231134
return $jwt;
232135
}
233-
234-
private function getPublicKey($cert)
235-
{
236-
$modulus = new BigInteger($this->jwt->urlsafeB64Decode($cert['n']), 256);
237-
$exponent = new BigInteger($this->jwt->urlsafeB64Decode($cert['e']), 256);
238-
$component = ['n' => $modulus, 'e' => $exponent];
239-
240-
$loader = PublicKeyLoader::load($component);
241-
242-
return $loader->toString('PKCS8');
243-
}
244-
245-
/**
246-
* phpseclib calls "phpinfo" by default, which requires special
247-
* whitelisting in the AppEngine VM environment. This function
248-
* sets constants to bypass the need for phpseclib to check phpinfo
249-
*
250-
* @see phpseclib/Math/BigInteger
251-
* @see https://github.com/GoogleCloudPlatform/getting-started-php/issues/85
252-
*/
253-
private function setPhpsecConstants()
254-
{
255-
if (filter_var(getenv('GAE_VM'), FILTER_VALIDATE_BOOLEAN)) {
256-
if (!defined('MATH_BIGINTEGER_OPENSSL_ENABLED')) {
257-
define('MATH_BIGINTEGER_OPENSSL_ENABLED', true);
258-
}
259-
if (!defined('CRYPT_RSA_MODE')) {
260-
define('CRYPT_RSA_MODE', AES::ENGINE_OPENSSL);
261-
}
262-
}
263-
}
264136
}

tests/BaseTest.php

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class BaseTest extends TestCase
3232
use ProphecyTrait;
3333

3434
private $key;
35-
private $client;
35+
protected $client;
3636

3737
public function getClient()
3838
{
@@ -45,14 +45,14 @@ public function getClient()
4545

4646
public function getCache($path = null)
4747
{
48-
$path = $path ?: sys_get_temp_dir().'/google-api-php-client-tests/';
48+
$path = sys_get_temp_dir() . '/google-api-php-client-tests/' . ($path ?: '');
4949
$filesystemAdapter = new Local($path);
5050
$filesystem = new Filesystem($filesystemAdapter);
5151

5252
return new FilesystemCachePool($filesystem);
5353
}
5454

55-
private function createClient()
55+
protected function createClient(array $scopes = null)
5656
{
5757
$options = [
5858
'auth' => 'google_auth',
@@ -69,14 +69,14 @@ private function createClient()
6969
$client = new Client();
7070
$client->setApplicationName('google-api-php-client-tests');
7171
$client->setHttpClient($httpClient);
72-
$client->setScopes(
73-
[
74-
"https://www.googleapis.com/auth/tasks",
75-
"https://www.googleapis.com/auth/adsense",
76-
"https://www.googleapis.com/auth/youtube",
77-
"https://www.googleapis.com/auth/drive",
78-
]
79-
);
72+
73+
$scopes = $scopes ?? [
74+
'https://www.googleapis.com/auth/tasks',
75+
'https://www.googleapis.com/auth/adsense',
76+
'https://www.googleapis.com/auth/youtube',
77+
'https://www.googleapis.com/auth/drive',
78+
];
79+
$client->setScopes($scopes);
8080

8181
if ($this->key) {
8282
$client->setDeveloperKey($this->key);
@@ -85,9 +85,7 @@ private function createClient()
8585
list($clientId, $clientSecret) = $this->getClientIdAndSecret();
8686
$client->setClientId($clientId);
8787
$client->setClientSecret($clientSecret);
88-
if (version_compare(PHP_VERSION, '5.5', '>=')) {
89-
$client->setCache($this->getCache());
90-
}
88+
$client->setCache($this->getCache(sha1(implode('', $scopes))));
9189

9290
return $client;
9391
}

0 commit comments

Comments
 (0)