source: vendor/google/auth/src/AccessToken.php

Last change on this file was e3d4e0a, checked in by Vlado 222039 <vlado.popovski@…>, 7 days ago

Upload project files

  • Property mode set to 100644
File size: 17.1 KB
RevLine 
[e3d4e0a]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
18namespace Google\Auth;
19
20use DateTime;
21use Firebase\JWT\ExpiredException;
22use Firebase\JWT\JWT;
23use Firebase\JWT\Key;
24use Firebase\JWT\SignatureInvalidException;
25use Google\Auth\Cache\MemoryCacheItemPool;
26use Google\Auth\HttpHandler\HttpClientCache;
27use Google\Auth\HttpHandler\HttpHandlerFactory;
28use GuzzleHttp\Psr7\Request;
29use GuzzleHttp\Psr7\Utils;
30use InvalidArgumentException;
31use phpseclib3\Crypt\PublicKeyLoader;
32use phpseclib3\Crypt\RSA;
33use phpseclib3\Math\BigInteger;
34use Psr\Cache\CacheItemPoolInterface;
35use RuntimeException;
36use SimpleJWT\InvalidTokenException;
37use SimpleJWT\JWT as SimpleJWT;
38use SimpleJWT\Keys\KeyFactory;
39use SimpleJWT\Keys\KeySet;
40use TypeError;
41use UnexpectedValueException;
42
43/**
44 * Wrapper around Google Access Tokens which provides convenience functions.
45 *
46 * @experimental
47 */
48class 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|null $httpHandler [optional] An HTTP Handler to deliver PSR-7 requests.
69 * @param CacheItemPoolInterface|null $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}
Note: See TracBrowser for help on using the repository browser.