PluginProbe
Authorizer / 3.9.1
Authorizer v3.9.1
3.15.3 3.15.2 3.15.1 3.15.0 3.14.3 3.14.4 3.14.2 3.14.1 2.8.1 2.8.2 2.8.3 2.8.4 2.8.5 2.8.6 2.8.7 2.8.8 2.9.0 2.9.1 2.9.10 2.9.11 2.9.12 2.9.13 2.9.2 2.9.3 2.9.6 All 126 releases
authorizer / vendor / google / auth / src / AccessToken.php

AccessToken.php in Authorizer 3.9.1, at vendor/google/auth/src/AccessToken.php

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