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