1 | <?php
|
---|
2 |
|
---|
3 | /*
|
---|
4 | * Copyright 2008 Google Inc.
|
---|
5 | *
|
---|
6 | * Licensed under the Apache License, Version 2.0 (the "License");
|
---|
7 | * you may not use this file except in compliance with the License.
|
---|
8 | * You may obtain a copy of the License at
|
---|
9 | *
|
---|
10 | * http://www.apache.org/licenses/LICENSE-2.0
|
---|
11 | *
|
---|
12 | * Unless required by applicable law or agreed to in writing, software
|
---|
13 | * distributed under the License is distributed on an "AS IS" BASIS,
|
---|
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
---|
15 | * See the License for the specific language governing permissions and
|
---|
16 | * limitations under the License.
|
---|
17 | */
|
---|
18 |
|
---|
19 | namespace Google\AccessToken;
|
---|
20 |
|
---|
21 | use DateTime;
|
---|
22 | use DomainException;
|
---|
23 | use Exception;
|
---|
24 | use ExpiredException;
|
---|
25 | use Firebase\JWT\ExpiredException as ExpiredExceptionV3;
|
---|
26 | use Firebase\JWT\JWT;
|
---|
27 | use Firebase\JWT\Key;
|
---|
28 | use Firebase\JWT\SignatureInvalidException;
|
---|
29 | use Google\Auth\Cache\MemoryCacheItemPool;
|
---|
30 | use Google\Exception as GoogleException;
|
---|
31 | use GuzzleHttp\Client;
|
---|
32 | use GuzzleHttp\ClientInterface;
|
---|
33 | use InvalidArgumentException;
|
---|
34 | use LogicException;
|
---|
35 | use phpseclib3\Crypt\AES;
|
---|
36 | use phpseclib3\Crypt\PublicKeyLoader;
|
---|
37 | use phpseclib3\Math\BigInteger;
|
---|
38 | use Psr\Cache\CacheItemPoolInterface;
|
---|
39 |
|
---|
40 | /**
|
---|
41 | * Wrapper around Google Access Tokens which provides convenience functions
|
---|
42 | *
|
---|
43 | */
|
---|
44 | class Verify
|
---|
45 | {
|
---|
46 | const FEDERATED_SIGNON_CERT_URL = 'https://www.googleapis.com/oauth2/v3/certs';
|
---|
47 | const OAUTH2_ISSUER = 'accounts.google.com';
|
---|
48 | const OAUTH2_ISSUER_HTTPS = 'https://accounts.google.com';
|
---|
49 |
|
---|
50 | /**
|
---|
51 | * @var ClientInterface The http client
|
---|
52 | */
|
---|
53 | private $http;
|
---|
54 |
|
---|
55 | /**
|
---|
56 | * @var CacheItemPoolInterface cache class
|
---|
57 | */
|
---|
58 | private $cache;
|
---|
59 |
|
---|
60 | /**
|
---|
61 | * @var \Firebase\JWT\JWT
|
---|
62 | */
|
---|
63 | public $jwt;
|
---|
64 |
|
---|
65 | /**
|
---|
66 | * Instantiates the class, but does not initiate the login flow, leaving it
|
---|
67 | * to the discretion of the caller.
|
---|
68 | */
|
---|
69 | public function __construct(
|
---|
70 | ?ClientInterface $http = null,
|
---|
71 | ?CacheItemPoolInterface $cache = null,
|
---|
72 | ?string $jwt = null
|
---|
73 | ) {
|
---|
74 | if (null === $http) {
|
---|
75 | $http = new Client();
|
---|
76 | }
|
---|
77 |
|
---|
78 | if (null === $cache) {
|
---|
79 | $cache = new MemoryCacheItemPool();
|
---|
80 | }
|
---|
81 |
|
---|
82 | $this->http = $http;
|
---|
83 | $this->cache = $cache;
|
---|
84 | $this->jwt = $jwt ?: $this->getJwtService();
|
---|
85 | }
|
---|
86 |
|
---|
87 | /**
|
---|
88 | * Verifies an id token and returns the authenticated apiLoginTicket.
|
---|
89 | * Throws an exception if the id token is not valid.
|
---|
90 | * The audience parameter can be used to control which id tokens are
|
---|
91 | * accepted. By default, the id token must have been issued to this OAuth2 client.
|
---|
92 | *
|
---|
93 | * @param string $idToken the ID token in JWT format
|
---|
94 | * @param string $audience Optional. The audience to verify against JWt "aud"
|
---|
95 | * @return array|false the token payload, if successful
|
---|
96 | */
|
---|
97 | public function verifyIdToken($idToken, $audience = null)
|
---|
98 | {
|
---|
99 | if (empty($idToken)) {
|
---|
100 | throw new LogicException('id_token cannot be null');
|
---|
101 | }
|
---|
102 |
|
---|
103 | // set phpseclib constants if applicable
|
---|
104 | $this->setPhpsecConstants();
|
---|
105 |
|
---|
106 | // 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();
|
---|
198 | }
|
---|
199 |
|
---|
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);
|
---|
210 | }
|
---|
211 | }
|
---|
212 |
|
---|
213 | if (!isset($certs['keys'])) {
|
---|
214 | throw new InvalidArgumentException(
|
---|
215 | 'federated sign-on certs expects "keys" to be set'
|
---|
216 | );
|
---|
217 | }
|
---|
218 |
|
---|
219 | return $certs['keys'];
|
---|
220 | }
|
---|
221 |
|
---|
222 | private function getJwtService()
|
---|
223 | {
|
---|
224 | $jwt = new JWT();
|
---|
225 | if ($jwt::$leeway < 1) {
|
---|
226 | // Ensures JWT leeway is at least 1
|
---|
227 | // @see https://github.com/google/google-api-php-client/issues/827
|
---|
228 | $jwt::$leeway = 1;
|
---|
229 | }
|
---|
230 |
|
---|
231 | return $jwt;
|
---|
232 | }
|
---|
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 | }
|
---|
264 | }
|
---|