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