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 19namespace Google\AccessToken; 20 21use Firebase\JWT\ExpiredException as ExpiredExceptionV3; 22use Firebase\JWT\SignatureInvalidException; 23use Firebase\JWT\Key; 24use GuzzleHttp\Client; 25use GuzzleHttp\ClientInterface; 26use InvalidArgumentException; 27use phpseclib3\Crypt\PublicKeyLoader; 28use phpseclib3\Crypt\RSA\PublicKey; 29use Psr\Cache\CacheItemPoolInterface; 30use Google\Auth\Cache\MemoryCacheItemPool; 31use Google\Exception as GoogleException; 32use DateTime; 33use DomainException; 34use Exception; 35use ExpiredException; // Firebase v2 36use LogicException; 37 38/** 39 * Wrapper around Google Access Tokens which provides convenience functions 40 * 41 */ 42class Verify 43{ 44 const FEDERATED_SIGNON_CERT_URL = 'https://www.googleapis.com/oauth2/v3/certs'; 45 const OAUTH2_ISSUER = 'accounts.google.com'; 46 const OAUTH2_ISSUER_HTTPS = 'https://accounts.google.com'; 47 48 /** 49 * @var ClientInterface The http client 50 */ 51 private $http; 52 53 /** 54 * @var CacheItemPoolInterface cache class 55 */ 56 private $cache; 57 58 /** 59 * Instantiates the class, but does not initiate the login flow, leaving it 60 * to the discretion of the caller. 61 */ 62 public function __construct( 63 ClientInterface $http = null, 64 CacheItemPoolInterface $cache = null, 65 $jwt = null 66 ) { 67 if (null === $http) { 68 $http = new Client(); 69 } 70 71 if (null === $cache) { 72 $cache = new MemoryCacheItemPool; 73 } 74 75 $this->http = $http; 76 $this->cache = $cache; 77 $this->jwt = $jwt ?: $this->getJwtService(); 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 $idToken the ID token in JWT format 87 * @param string $audience Optional. The audience to verify against JWt "aud" 88 * @return array the token payload, if successful 89 */ 90 public function verifyIdToken($idToken, $audience = null) 91 { 92 if (empty($idToken)) { 93 throw new LogicException('id_token cannot be null'); 94 } 95 96 // set phpseclib constants if applicable 97 $this->setPhpsecConstants(); 98 99 // Check signature 100 $certs = $this->getFederatedSignOnCerts(); 101 foreach ($certs as $cert) { 102 try { 103 $args = [$idToken]; 104 $publicKey = $this->getPublicKey($cert); 105 if (class_exists(Key::class)) { 106 $args[] = new Key($publicKey, 'RS256'); 107 } else { 108 $args[] = $publicKey; 109 $args[] = ['RS256']; 110 } 111 $payload = \call_user_func_array([$this->jwt, 'decode'], $args); 112 113 if (property_exists($payload, 'aud')) { 114 if ($audience && $payload->aud != $audience) { 115 return false; 116 } 117 } 118 119 // support HTTP and HTTPS issuers 120 // @see https://developers.google.com/identity/sign-in/web/backend-auth 121 $issuers = array(self::OAUTH2_ISSUER, self::OAUTH2_ISSUER_HTTPS); 122 if (!isset($payload->iss) || !in_array($payload->iss, $issuers)) { 123 return false; 124 } 125 126 return (array) $payload; 127 } catch (ExpiredException $e) { 128 return false; 129 } catch (ExpiredExceptionV3 $e) { 130 return false; 131 } catch (SignatureInvalidException $e) { 132 // continue 133 } catch (DomainException $e) { 134 // continue 135 } 136 } 137 138 return false; 139 } 140 141 private function getCache() 142 { 143 return $this->cache; 144 } 145 146 /** 147 * Retrieve and cache a certificates file. 148 * 149 * @param $url string location 150 * @throws \Google\Exception 151 * @return array certificates 152 */ 153 private function retrieveCertsFromLocation($url) 154 { 155 // If we're retrieving a local file, just grab it. 156 if (0 !== strpos($url, 'http')) { 157 if (!$file = file_get_contents($url)) { 158 throw new GoogleException( 159 "Failed to retrieve verification certificates: '" . 160 $url . "'." 161 ); 162 } 163 164 return json_decode($file, true); 165 } 166 167 $response = $this->http->get($url); 168 169 if ($response->getStatusCode() == 200) { 170 return json_decode((string) $response->getBody(), true); 171 } 172 throw new GoogleException( 173 sprintf( 174 'Failed to retrieve verification certificates: "%s".', 175 $response->getBody()->getContents() 176 ), 177 $response->getStatusCode() 178 ); 179 } 180 181 // Gets federated sign-on certificates to use for verifying identity tokens. 182 // Returns certs as array structure, where keys are key ids, and values 183 // are PEM encoded certificates. 184 private function getFederatedSignOnCerts() 185 { 186 $certs = null; 187 if ($cache = $this->getCache()) { 188 $cacheItem = $cache->getItem('federated_signon_certs_v3'); 189 $certs = $cacheItem->get(); 190 } 191 192 193 if (!$certs) { 194 $certs = $this->retrieveCertsFromLocation( 195 self::FEDERATED_SIGNON_CERT_URL 196 ); 197 198 if ($cache) { 199 $cacheItem->expiresAt(new DateTime('+1 hour')); 200 $cacheItem->set($certs); 201 $cache->save($cacheItem); 202 } 203 } 204 205 if (!isset($certs['keys'])) { 206 throw new InvalidArgumentException( 207 'federated sign-on certs expects "keys" to be set' 208 ); 209 } 210 211 return $certs['keys']; 212 } 213 214 private function getJwtService() 215 { 216 $jwtClass = 'JWT'; 217 if (class_exists('\Firebase\JWT\JWT')) { 218 $jwtClass = 'Firebase\JWT\JWT'; 219 } 220 221 if (property_exists($jwtClass, 'leeway') && $jwtClass::$leeway < 1) { 222 // Ensures JWT leeway is at least 1 223 // @see https://github.com/google/google-api-php-client/issues/827 224 $jwtClass::$leeway = 1; 225 } 226 227 return new $jwtClass; 228 } 229 230 private function getPublicKey($cert) 231 { 232 $bigIntClass = $this->getBigIntClass(); 233 $modulus = new $bigIntClass($this->jwt->urlsafeB64Decode($cert['n']), 256); 234 $exponent = new $bigIntClass($this->jwt->urlsafeB64Decode($cert['e']), 256); 235 $component = array('n' => $modulus, 'e' => $exponent); 236 237 if (class_exists('phpseclib3\Crypt\RSA\PublicKey')) { 238 /** @var PublicKey $loader */ 239 $loader = PublicKeyLoader::load($component); 240 241 return $loader->toString('PKCS8'); 242 } 243 244 $rsaClass = $this->getRsaClass(); 245 $rsa = new $rsaClass(); 246 $rsa->loadKey($component); 247 248 return $rsa->getPublicKey(); 249 } 250 251 private function getRsaClass() 252 { 253 if (class_exists('phpseclib3\Crypt\RSA')) { 254 return 'phpseclib3\Crypt\RSA'; 255 } 256 257 if (class_exists('phpseclib\Crypt\RSA')) { 258 return 'phpseclib\Crypt\RSA'; 259 } 260 261 return 'Crypt_RSA'; 262 } 263 264 private function getBigIntClass() 265 { 266 if (class_exists('phpseclib3\Math\BigInteger')) { 267 return 'phpseclib3\Math\BigInteger'; 268 } 269 270 if (class_exists('phpseclib\Math\BigInteger')) { 271 return 'phpseclib\Math\BigInteger'; 272 } 273 274 return 'Math_BigInteger'; 275 } 276 277 private function getOpenSslConstant() 278 { 279 if (class_exists('phpseclib3\Crypt\AES')) { 280 return 'phpseclib3\Crypt\AES::ENGINE_OPENSSL'; 281 } 282 283 if (class_exists('phpseclib\Crypt\RSA')) { 284 return 'phpseclib\Crypt\RSA::MODE_OPENSSL'; 285 } 286 287 if (class_exists('Crypt_RSA')) { 288 return 'CRYPT_RSA_MODE_OPENSSL'; 289 } 290 291 throw new Exception('Cannot find RSA class'); 292 } 293 294 /** 295 * phpseclib calls "phpinfo" by default, which requires special 296 * whitelisting in the AppEngine VM environment. This function 297 * sets constants to bypass the need for phpseclib to check phpinfo 298 * 299 * @see phpseclib/Math/BigInteger 300 * @see https://github.com/GoogleCloudPlatform/getting-started-php/issues/85 301 */ 302 private function setPhpsecConstants() 303 { 304 if (filter_var(getenv('GAE_VM'), FILTER_VALIDATE_BOOLEAN)) { 305 if (!defined('MATH_BIGINTEGER_OPENSSL_ENABLED')) { 306 define('MATH_BIGINTEGER_OPENSSL_ENABLED', true); 307 } 308 if (!defined('CRYPT_RSA_MODE')) { 309 define('CRYPT_RSA_MODE', constant($this->getOpenSslConstant())); 310 } 311 } 312 } 313} 314