PluginProbe ʕ •ᴥ•ʔ
ShareThis Dashboard for Google Analytics / 3.2.4
ShareThis Dashboard for Google Analytics v3.2.4
3.3.2 trunk 1.0.7 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 2.1 2.1.2 2.1.3 2.1.4 2.1.5 2.2.5 2.3.5 2.3.6 2.3.7 2.3.8 2.4.0 2.4.1 2.5.0 2.5.1 2.5.2 2.5.3 2.5.4 2.5.5 3.0.0 3.1.0 3.1.1 3.1.2 3.1.3 3.1.4 3.1.5 3.1.6 3.1.7 3.2.0 3.2.1 3.2.2 3.2.3 3.2.4 3.3.0 3.3.1
googleanalytics / lib / analytics-admin / vendor / google / auth / src / AccessToken.php
googleanalytics / lib / analytics-admin / vendor / google / auth / src Last commit date
Cache 3 years ago Credentials 3 years ago HttpHandler 3 years ago Middleware 3 years ago AccessToken.php 3 years ago ApplicationDefaultCredentials.php 3 years ago CacheTrait.php 3 years ago CredentialsLoader.php 3 years ago FetchAuthTokenCache.php 3 years ago FetchAuthTokenInterface.php 3 years ago GCECache.php 3 years ago GetQuotaProjectInterface.php 3 years ago Iam.php 3 years ago OAuth2.php 3 years ago ProjectIdProviderInterface.php 3 years ago ServiceAccountSignerTrait.php 3 years ago SignBlobInterface.php 3 years ago UpdateMetadataInterface.php 3 years ago
AccessToken.php
485 lines
1 <?php
2 /*
3 * Copyright 2019 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 namespace Google\Auth;
19
20 use DateTime;
21 use Exception;
22 use Firebase\JWT\ExpiredException;
23 use Firebase\JWT\JWT;
24 use Firebase\JWT\Key;
25 use Firebase\JWT\SignatureInvalidException;
26 use Google\Auth\Cache\MemoryCacheItemPool;
27 use Google\Auth\HttpHandler\HttpClientCache;
28 use Google\Auth\HttpHandler\HttpHandlerFactory;
29 use GuzzleHttp\Psr7\Request;
30 use GuzzleHttp\Psr7\Utils;
31 use InvalidArgumentException;
32 use phpseclib\Crypt\RSA;
33 use phpseclib\Math\BigInteger;
34 use Psr\Cache\CacheItemPoolInterface;
35 use RuntimeException;
36 use SimpleJWT\InvalidTokenException;
37 use SimpleJWT\JWT as SimpleJWT;
38 use SimpleJWT\Keys\KeyFactory;
39 use SimpleJWT\Keys\KeySet;
40 use UnexpectedValueException;
41
42 /**
43 * Wrapper around Google Access Tokens which provides convenience functions.
44 *
45 * @experimental
46 */
47 class AccessToken
48 {
49 const FEDERATED_SIGNON_CERT_URL = 'https://www.googleapis.com/oauth2/v3/certs';
50 const IAP_CERT_URL = 'https://www.gstatic.com/iap/verify/public_key-jwk';
51 const IAP_ISSUER = 'https://cloud.google.com/iap';
52 const OAUTH2_ISSUER = 'accounts.google.com';
53 const OAUTH2_ISSUER_HTTPS = 'https://accounts.google.com';
54 const OAUTH2_REVOKE_URI = 'https://oauth2.googleapis.com/revoke';
55
56 /**
57 * @var callable
58 */
59 private $httpHandler;
60
61 /**
62 * @var CacheItemPoolInterface
63 */
64 private $cache;
65
66 /**
67 * @param callable $httpHandler [optional] An HTTP Handler to deliver PSR-7 requests.
68 * @param CacheItemPoolInterface $cache [optional] A PSR-6 compatible cache implementation.
69 */
70 public function __construct(
71 callable $httpHandler = null,
72 CacheItemPoolInterface $cache = null
73 ) {
74 $this->httpHandler = $httpHandler
75 ?: HttpHandlerFactory::build(HttpClientCache::getHttpClient());
76 $this->cache = $cache ?: new MemoryCacheItemPool();
77 }
78
79 /**
80 * Verifies an id token and returns the authenticated apiLoginTicket.
81 * Throws an exception if the id token is not valid.
82 * The audience parameter can be used to control which id tokens are
83 * accepted. By default, the id token must have been issued to this OAuth2 client.
84 *
85 * @param string $token The JSON Web Token to be verified.
86 * @param array<mixed> $options [optional] {
87 * Configuration options.
88 * @type string $audience The indended recipient of the token.
89 * @type string $issuer The intended issuer of the token.
90 * @type string $cacheKey The cache key of the cached certs. Defaults to
91 * the sha1 of $certsLocation if provided, otherwise is set to
92 * "federated_signon_certs_v3".
93 * @type string $certsLocation The location (remote or local) from which
94 * to retrieve certificates, if not cached. This value should only be
95 * provided in limited circumstances in which you are sure of the
96 * behavior.
97 * @type bool $throwException Whether the function should throw an
98 * exception if the verification fails. This is useful for
99 * determining the reason verification failed.
100 * }
101 * @return array<mixed>|false the token payload, if successful, or false if not.
102 * @throws InvalidArgumentException If certs could not be retrieved from a local file.
103 * @throws InvalidArgumentException If received certs are in an invalid format.
104 * @throws InvalidArgumentException If the cert alg is not supported.
105 * @throws RuntimeException If certs could not be retrieved from a remote location.
106 * @throws UnexpectedValueException If the token issuer does not match.
107 * @throws UnexpectedValueException If the token audience does not match.
108 */
109 public function verify($token, array $options = [])
110 {
111 $audience = isset($options['audience'])
112 ? $options['audience']
113 : null;
114 $issuer = isset($options['issuer'])
115 ? $options['issuer']
116 : null;
117 $certsLocation = isset($options['certsLocation'])
118 ? $options['certsLocation']
119 : self::FEDERATED_SIGNON_CERT_URL;
120 $cacheKey = isset($options['cacheKey'])
121 ? $options['cacheKey']
122 : $this->getCacheKeyFromCertLocation($certsLocation);
123 $throwException = isset($options['throwException'])
124 ? $options['throwException']
125 : false; // for backwards compatibility
126
127 // Check signature against each available cert.
128 $certs = $this->getCerts($certsLocation, $cacheKey, $options);
129 $alg = $this->determineAlg($certs);
130 if (!in_array($alg, ['RS256', 'ES256'])) {
131 throw new InvalidArgumentException(
132 'unrecognized "alg" in certs, expected ES256 or RS256'
133 );
134 }
135 try {
136 if ($alg == 'RS256') {
137 return $this->verifyRs256($token, $certs, $audience, $issuer);
138 }
139 return $this->verifyEs256($token, $certs, $audience, $issuer);
140 } catch (ExpiredException $e) { // firebase/php-jwt 5+
141 } catch (SignatureInvalidException $e) { // firebase/php-jwt 5+
142 } catch (InvalidTokenException $e) { // simplejwt
143 } catch (DomainException $e) { // @phpstan-ignore-line
144 } catch (InvalidArgumentException $e) {
145 } catch (UnexpectedValueException $e) {
146 }
147
148 if ($throwException) {
149 throw $e;
150 }
151
152 return false;
153 }
154
155 /**
156 * Identifies the expected algorithm to verify by looking at the "alg" key
157 * of the provided certs.
158 *
159 * @param array<mixed> $certs Certificate array according to the JWK spec (see
160 * https://tools.ietf.org/html/rfc7517).
161 * @return string The expected algorithm, such as "ES256" or "RS256".
162 */
163 private function determineAlg(array $certs)
164 {
165 $alg = null;
166 foreach ($certs as $cert) {
167 if (empty($cert['alg'])) {
168 throw new InvalidArgumentException(
169 'certs expects "alg" to be set'
170 );
171 }
172 $alg = $alg ?: $cert['alg'];
173
174 if ($alg != $cert['alg']) {
175 throw new InvalidArgumentException(
176 'More than one alg detected in certs'
177 );
178 }
179 }
180 return $alg;
181 }
182
183 /**
184 * Verifies an ES256-signed JWT.
185 *
186 * @param string $token The JSON Web Token to be verified.
187 * @param array<mixed> $certs Certificate array according to the JWK spec (see
188 * https://tools.ietf.org/html/rfc7517).
189 * @param string|null $audience If set, returns false if the provided
190 * audience does not match the "aud" claim on the JWT.
191 * @param string|null $issuer If set, returns false if the provided
192 * issuer does not match the "iss" claim on the JWT.
193 * @return array<mixed> the token payload, if successful, or false if not.
194 */
195 private function verifyEs256($token, array $certs, $audience = null, $issuer = null)
196 {
197 $this->checkSimpleJwt();
198
199 $jwkset = new KeySet();
200 foreach ($certs as $cert) {
201 $jwkset->add(KeyFactory::create($cert, 'php'));
202 }
203
204 // Validate the signature using the key set and ES256 algorithm.
205 $jwt = $this->callSimpleJwtDecode([$token, $jwkset, 'ES256']);
206 $payload = $jwt->getClaims();
207
208 if ($audience) {
209 if (!isset($payload['aud']) || $payload['aud'] != $audience) {
210 throw new UnexpectedValueException('Audience does not match');
211 }
212 }
213
214 // @see https://cloud.google.com/iap/docs/signed-headers-howto#verifying_the_jwt_payload
215 $issuer = $issuer ?: self::IAP_ISSUER;
216 if (!isset($payload['iss']) || $payload['iss'] !== $issuer) {
217 throw new UnexpectedValueException('Issuer does not match');
218 }
219
220 return $payload;
221 }
222
223 /**
224 * Verifies an RS256-signed JWT.
225 *
226 * @param string $token The JSON Web Token to be verified.
227 * @param array<mixed> $certs Certificate array according to the JWK spec (see
228 * https://tools.ietf.org/html/rfc7517).
229 * @param string|null $audience If set, returns false if the provided
230 * audience does not match the "aud" claim on the JWT.
231 * @param string|null $issuer If set, returns false if the provided
232 * issuer does not match the "iss" claim on the JWT.
233 * @return array<mixed> the token payload, if successful, or false if not.
234 */
235 private function verifyRs256($token, array $certs, $audience = null, $issuer = null)
236 {
237 $this->checkAndInitializePhpsec();
238 $keys = [];
239 foreach ($certs as $cert) {
240 if (empty($cert['kid'])) {
241 throw new InvalidArgumentException(
242 'certs expects "kid" to be set'
243 );
244 }
245 if (empty($cert['n']) || empty($cert['e'])) {
246 throw new InvalidArgumentException(
247 'RSA certs expects "n" and "e" to be set'
248 );
249 }
250 $rsa = new RSA();
251 $rsa->loadKey([
252 'n' => new BigInteger($this->callJwtStatic('urlsafeB64Decode', [
253 $cert['n'],
254 ]), 256),
255 'e' => new BigInteger($this->callJwtStatic('urlsafeB64Decode', [
256 $cert['e']
257 ]), 256),
258 ]);
259
260 // create an array of key IDs to certs for the JWT library
261 $keys[$cert['kid']] = new Key($rsa->getPublicKey(), 'RS256');
262 }
263
264 $payload = $this->callJwtStatic('decode', [
265 $token,
266 $keys,
267 ]);
268
269 if ($audience) {
270 if (!property_exists($payload, 'aud') || $payload->aud != $audience) {
271 throw new UnexpectedValueException('Audience does not match');
272 }
273 }
274
275 // support HTTP and HTTPS issuers
276 // @see https://developers.google.com/identity/sign-in/web/backend-auth
277 $issuers = $issuer ? [$issuer] : [self::OAUTH2_ISSUER, self::OAUTH2_ISSUER_HTTPS];
278 if (!isset($payload->iss) || !in_array($payload->iss, $issuers)) {
279 throw new UnexpectedValueException('Issuer does not match');
280 }
281
282 return (array) $payload;
283 }
284
285 /**
286 * Revoke an OAuth2 access token or refresh token. This method will revoke the current access
287 * token, if a token isn't provided.
288 *
289 * @param string|array<mixed> $token The token (access token or a refresh token) that should be revoked.
290 * @param array<mixed> $options [optional] Configuration options.
291 * @return bool Returns True if the revocation was successful, otherwise False.
292 */
293 public function revoke($token, array $options = [])
294 {
295 if (is_array($token)) {
296 if (isset($token['refresh_token'])) {
297 $token = $token['refresh_token'];
298 } else {
299 $token = $token['access_token'];
300 }
301 }
302
303 $body = Utils::streamFor(http_build_query(['token' => $token]));
304 $request = new Request('POST', self::OAUTH2_REVOKE_URI, [
305 'Cache-Control' => 'no-store',
306 'Content-Type' => 'application/x-www-form-urlencoded',
307 ], $body);
308
309 $httpHandler = $this->httpHandler;
310
311 $response = $httpHandler($request, $options);
312
313 return $response->getStatusCode() == 200;
314 }
315
316 /**
317 * Gets federated sign-on certificates to use for verifying identity tokens.
318 * Returns certs as array structure, where keys are key ids, and values
319 * are PEM encoded certificates.
320 *
321 * @param string $location The location from which to retrieve certs.
322 * @param string $cacheKey The key under which to cache the retrieved certs.
323 * @param array<mixed> $options [optional] Configuration options.
324 * @return array<mixed>
325 * @throws InvalidArgumentException If received certs are in an invalid format.
326 */
327 private function getCerts($location, $cacheKey, array $options = [])
328 {
329 $cacheItem = $this->cache->getItem($cacheKey);
330 $certs = $cacheItem ? $cacheItem->get() : null; // @phpstan-ignore-line
331
332 $gotNewCerts = false;
333 if (!$certs) {
334 $certs = $this->retrieveCertsFromLocation($location, $options);
335
336 $gotNewCerts = true;
337 }
338
339 if (!isset($certs['keys'])) {
340 if ($location !== self::IAP_CERT_URL) {
341 throw new InvalidArgumentException(
342 'federated sign-on certs expects "keys" to be set'
343 );
344 }
345 throw new InvalidArgumentException(
346 'certs expects "keys" to be set'
347 );
348 }
349
350 // Push caching off until after verifying certs are in a valid format.
351 // Don't want to cache bad data.
352 if ($gotNewCerts) {
353 $cacheItem->expiresAt(new DateTime('+1 hour'));
354 $cacheItem->set($certs);
355 $this->cache->save($cacheItem);
356 }
357
358 return $certs['keys'];
359 }
360
361 /**
362 * Retrieve and cache a certificates file.
363 *
364 * @param string $url location
365 * @param array<mixed> $options [optional] Configuration options.
366 * @return array<mixed> certificates
367 * @throws InvalidArgumentException If certs could not be retrieved from a local file.
368 * @throws RuntimeException If certs could not be retrieved from a remote location.
369 */
370 private function retrieveCertsFromLocation($url, array $options = [])
371 {
372 // If we're retrieving a local file, just grab it.
373 if (strpos($url, 'http') !== 0) {
374 if (!file_exists($url)) {
375 throw new InvalidArgumentException(sprintf(
376 'Failed to retrieve verification certificates from path: %s.',
377 $url
378 ));
379 }
380
381 return json_decode((string) file_get_contents($url), true);
382 }
383
384 $httpHandler = $this->httpHandler;
385 $response = $httpHandler(new Request('GET', $url), $options);
386
387 if ($response->getStatusCode() == 200) {
388 return json_decode((string) $response->getBody(), true);
389 }
390
391 throw new RuntimeException(sprintf(
392 'Failed to retrieve verification certificates: "%s".',
393 $response->getBody()->getContents()
394 ), $response->getStatusCode());
395 }
396
397 /**
398 * @return void
399 */
400 private function checkAndInitializePhpsec()
401 {
402 // @codeCoverageIgnoreStart
403 if (!class_exists('phpseclib\Crypt\RSA')) {
404 throw new RuntimeException('Please require phpseclib/phpseclib v2 to use this utility.');
405 }
406 // @codeCoverageIgnoreEnd
407
408 $this->setPhpsecConstants();
409 }
410
411 /**
412 * @return void
413 */
414 private function checkSimpleJwt()
415 {
416 // @codeCoverageIgnoreStart
417 if (!class_exists(SimpleJwt::class)) {
418 throw new RuntimeException('Please require kelvinmo/simplejwt ^0.2 to use this utility.');
419 }
420 // @codeCoverageIgnoreEnd
421 }
422
423 /**
424 * phpseclib calls "phpinfo" by default, which requires special
425 * whitelisting in the AppEngine VM environment. This function
426 * sets constants to bypass the need for phpseclib to check phpinfo
427 *
428 * @see phpseclib/Math/BigInteger
429 * @see https://github.com/GoogleCloudPlatform/getting-started-php/issues/85
430 * @codeCoverageIgnore
431 *
432 * @return void
433 */
434 private function setPhpsecConstants()
435 {
436 if (filter_var(getenv('GAE_VM'), FILTER_VALIDATE_BOOLEAN)) {
437 if (!defined('MATH_BIGINTEGER_OPENSSL_ENABLED')) {
438 define('MATH_BIGINTEGER_OPENSSL_ENABLED', true);
439 }
440 if (!defined('CRYPT_RSA_MODE')) {
441 define('CRYPT_RSA_MODE', RSA::MODE_OPENSSL);
442 }
443 }
444 }
445
446 /**
447 * Provide a hook to mock calls to the JWT static methods.
448 *
449 * @param string $method
450 * @param array<mixed> $args
451 * @return mixed
452 */
453 protected function callJwtStatic($method, array $args = [])
454 {
455 return call_user_func_array([JWT::class, $method], $args); // @phpstan-ignore-line
456 }
457
458 /**
459 * Provide a hook to mock calls to the JWT static methods.
460 *
461 * @param array<mixed> $args
462 * @return mixed
463 */
464 protected function callSimpleJwtDecode(array $args = [])
465 {
466 return call_user_func_array([SimpleJwt::class, 'decode'], $args);
467 }
468
469 /**
470 * Generate a cache key based on the cert location using sha1 with the
471 * exception of using "federated_signon_certs_v3" to preserve BC.
472 *
473 * @param string $certsLocation
474 * @return string
475 */
476 private function getCacheKeyFromCertLocation($certsLocation)
477 {
478 $key = $certsLocation === self::FEDERATED_SIGNON_CERT_URL
479 ? 'federated_signon_certs_v3'
480 : sha1($certsLocation);
481
482 return 'google_auth_certs_cache|' . $key;
483 }
484 }
485