Google_Auth_OAuth2

Authentication class that deals with the OAuth 2 web-server authentication flow.

Defined (1)

The class is defined in the following location(s).

/vendor/google/apiclient/src/Google/Auth/OAuth2.php  
  1. class Google_Auth_OAuth2 extends Google_Auth_Abstract 
  2. const OAUTH2_REVOKE_URI = 'https://accounts.google.com/o/oauth2/revoke'; 
  3. const OAUTH2_TOKEN_URI = 'https://accounts.google.com/o/oauth2/token'; 
  4. const OAUTH2_AUTH_URL = 'https://accounts.google.com/o/oauth2/auth'; 
  5. const CLOCK_SKEW_SECS = 300; // five minutes in seconds 
  6. const AUTH_TOKEN_LIFETIME_SECS = 300; // five minutes in seconds 
  7. const MAX_TOKEN_LIFETIME_SECS = 86400; // one day in seconds 
  8. const OAUTH2_ISSUER = 'accounts.google.com'; 
  9. const OAUTH2_ISSUER_HTTPS = 'https://accounts.google.com'; 
  10.  
  11. /** @var Google_Auth_AssertionCredentials $assertionCredentials */ 
  12. private $assertionCredentials; 
  13.  
  14. /** 
  15. * @var string The state parameters for CSRF and other forgery protection. 
  16. */ 
  17. private $state; 
  18.  
  19. /** 
  20. * @var array The token bundle. 
  21. */ 
  22. private $token = array(); 
  23.  
  24. /** 
  25. * @var Google_Client the base client 
  26. */ 
  27. private $client; 
  28.  
  29. /** 
  30. * Instantiates the class, but does not initiate the login flow, leaving it 
  31. * to the discretion of the caller. 
  32. */ 
  33. public function __construct(Google_Client $client) 
  34. $this->client = $client; 
  35.  
  36. /** 
  37. * Perform an authenticated / signed apiHttpRequest. 
  38. * This function takes the apiHttpRequest, calls apiAuth->sign on it 
  39. * (which can modify the request in what ever way fits the auth mechanism) 
  40. * and then calls apiCurlIO::makeRequest on the signed request 
  41. * @param Google_Http_Request $request 
  42. * @return Google_Http_Request The resulting HTTP response including the 
  43. * responseHttpCode, responseHeaders and responseBody. 
  44. */ 
  45. public function authenticatedRequest(Google_Http_Request $request) 
  46. $request = $this->sign($request); 
  47. return $this->client->getIo()->makeRequest($request); 
  48.  
  49. /** 
  50. * @param string $code 
  51. * @param boolean $crossClient 
  52. * @throws Google_Auth_Exception 
  53. * @return string 
  54. */ 
  55. public function authenticate($code, $crossClient = false) 
  56. if (strlen($code) == 0) { 
  57. throw new Google_Auth_Exception("Invalid code"); 
  58.  
  59. $arguments = array( 
  60. 'code' => $code,  
  61. 'grant_type' => 'authorization_code',  
  62. 'client_id' => $this->client->getClassConfig($this, 'client_id'),  
  63. 'client_secret' => $this->client->getClassConfig($this, 'client_secret') 
  64. ); 
  65.  
  66. if ($crossClient !== true) { 
  67. $arguments['redirect_uri'] = $this->client->getClassConfig($this, 'redirect_uri'); 
  68.  
  69. // We got here from the redirect from a successful authorization grant,  
  70. // fetch the access token 
  71. $request = new Google_Http_Request( 
  72. self::OAUTH2_TOKEN_URI,  
  73. 'POST',  
  74. array(),  
  75. $arguments 
  76. ); 
  77. $request->disableGzip(); 
  78. $response = $this->client->getIo()->makeRequest($request); 
  79.  
  80. if ($response->getResponseHttpCode() == 200) { 
  81. $this->setAccessToken($response->getResponseBody()); 
  82. $this->token['created'] = time(); 
  83. return $this->getAccessToken(); 
  84. } else { 
  85. $decodedResponse = json_decode($response->getResponseBody(), true); 
  86. if ($decodedResponse != null && $decodedResponse['error']) { 
  87. $errorText = $decodedResponse['error']; 
  88. if (isset($decodedResponse['error_description'])) { 
  89. $errorText .= ": " . $decodedResponse['error_description']; 
  90. throw new Google_Auth_Exception( 
  91. sprintf( 
  92. "Error fetching OAuth2 access token, message: '%s'",  
  93. $errorText 
  94. ),  
  95. $response->getResponseHttpCode() 
  96. ); 
  97.  
  98. /** 
  99. * Create a URL to obtain user authorization. 
  100. * The authorization endpoint allows the user to first 
  101. * authenticate, and then grant/deny the access request. 
  102. * @param string $scope The scope is expressed as a list of space-delimited strings. 
  103. * @return string 
  104. */ 
  105. public function createAuthUrl($scope) 
  106. $params = array( 
  107. 'response_type' => 'code',  
  108. 'redirect_uri' => $this->client->getClassConfig($this, 'redirect_uri'),  
  109. 'client_id' => $this->client->getClassConfig($this, 'client_id'),  
  110. 'scope' => $scope,  
  111. 'access_type' => $this->client->getClassConfig($this, 'access_type'),  
  112. ); 
  113.  
  114. // Prefer prompt to approval prompt. 
  115. if ($this->client->getClassConfig($this, 'prompt')) { 
  116. $params = $this->maybeAddParam($params, 'prompt'); 
  117. } else { 
  118. $params = $this->maybeAddParam($params, 'approval_prompt'); 
  119. $params = $this->maybeAddParam($params, 'login_hint'); 
  120. $params = $this->maybeAddParam($params, 'hd'); 
  121. $params = $this->maybeAddParam($params, 'openid.realm'); 
  122. $params = $this->maybeAddParam($params, 'include_granted_scopes'); 
  123.  
  124. // If the list of scopes contains plus.login, add request_visible_actions 
  125. // to auth URL. 
  126. $rva = $this->client->getClassConfig($this, 'request_visible_actions'); 
  127. if (strpos($scope, 'plus.login') && strlen($rva) > 0) { 
  128. $params['request_visible_actions'] = $rva; 
  129.  
  130. if (isset($this->state)) { 
  131. $params['state'] = $this->state; 
  132.  
  133. return self::OAUTH2_AUTH_URL . "?" . http_build_query($params, '', '&'); 
  134.  
  135. /** 
  136. * @param string $token 
  137. * @throws Google_Auth_Exception 
  138. */ 
  139. public function setAccessToken($token) 
  140. $token = json_decode($token, true); 
  141. if ($token == null) { 
  142. throw new Google_Auth_Exception('Could not json decode the token'); 
  143. if (! isset($token['access_token'])) { 
  144. throw new Google_Auth_Exception("Invalid token format"); 
  145. $this->token = $token; 
  146.  
  147. public function getAccessToken() 
  148. return json_encode($this->token); 
  149.  
  150. public function getRefreshToken() 
  151. if (array_key_exists('refresh_token', $this->token)) { 
  152. return $this->token['refresh_token']; 
  153. } else { 
  154. return null; 
  155.  
  156. public function setState($state) 
  157. $this->state = $state; 
  158.  
  159. public function setAssertionCredentials(Google_Auth_AssertionCredentials $creds) 
  160. $this->assertionCredentials = $creds; 
  161.  
  162. /** 
  163. * Include an accessToken in a given apiHttpRequest. 
  164. * @param Google_Http_Request $request 
  165. * @return Google_Http_Request 
  166. * @throws Google_Auth_Exception 
  167. */ 
  168. public function sign(Google_Http_Request $request) 
  169. // add the developer key to the request before signing it 
  170. if ($this->client->getClassConfig($this, 'developer_key')) { 
  171. $request->setQueryParam('key', $this->client->getClassConfig($this, 'developer_key')); 
  172.  
  173. // Cannot sign the request without an OAuth access token. 
  174. if (null == $this->token && null == $this->assertionCredentials) { 
  175. return $request; 
  176.  
  177. // Check if the token is set to expire in the next 30 seconds 
  178. // (or has already expired). 
  179. if ($this->isAccessTokenExpired()) { 
  180. if ($this->assertionCredentials) { 
  181. $this->refreshTokenWithAssertion(); 
  182. } else { 
  183. $this->client->getLogger()->debug('OAuth2 access token expired'); 
  184. if (! array_key_exists('refresh_token', $this->token)) { 
  185. $error = "The OAuth 2.0 access token has expired, " 
  186. ." and a refresh token is not available. Refresh tokens" 
  187. ." are not returned for responses that were auto-approved."; 
  188.  
  189. $this->client->getLogger()->error($error); 
  190. throw new Google_Auth_Exception($error); 
  191. $this->refreshToken($this->token['refresh_token']); 
  192.  
  193. $this->client->getLogger()->debug('OAuth2 authentication'); 
  194.  
  195. // Add the OAuth2 header to the request 
  196. $request->setRequestHeaders( 
  197. array('Authorization' => 'Bearer ' . $this->token['access_token']) 
  198. ); 
  199.  
  200. return $request; 
  201.  
  202. /** 
  203. * Fetches a fresh access token with the given refresh token. 
  204. * @param string $refreshToken 
  205. * @return void 
  206. */ 
  207. public function refreshToken($refreshToken) 
  208. $this->refreshTokenRequest( 
  209. array( 
  210. 'client_id' => $this->client->getClassConfig($this, 'client_id'),  
  211. 'client_secret' => $this->client->getClassConfig($this, 'client_secret'),  
  212. 'refresh_token' => $refreshToken,  
  213. 'grant_type' => 'refresh_token' 
  214. ); 
  215.  
  216. /** 
  217. * Fetches a fresh access token with a given assertion token. 
  218. * @param Google_Auth_AssertionCredentials $assertionCredentials optional. 
  219. * @return void 
  220. */ 
  221. public function refreshTokenWithAssertion($assertionCredentials = null) 
  222. if (!$assertionCredentials) { 
  223. $assertionCredentials = $this->assertionCredentials; 
  224.  
  225. $cacheKey = $assertionCredentials->getCacheKey(); 
  226.  
  227. if ($cacheKey) { 
  228. // We can check whether we have a token available in the 
  229. // cache. If it is expired, we can retrieve a new one from 
  230. // the assertion. 
  231. $token = $this->client->getCache()->get($cacheKey); 
  232. if ($token) { 
  233. $this->setAccessToken($token); 
  234. if (!$this->isAccessTokenExpired()) { 
  235. return; 
  236.  
  237. $this->client->getLogger()->debug('OAuth2 access token expired'); 
  238. $this->refreshTokenRequest( 
  239. array( 
  240. 'grant_type' => 'assertion',  
  241. 'assertion_type' => $assertionCredentials->assertionType,  
  242. 'assertion' => $assertionCredentials->generateAssertion(),  
  243. ); 
  244.  
  245. if ($cacheKey) { 
  246. // Attempt to cache the token. 
  247. $this->client->getCache()->set( 
  248. $cacheKey,  
  249. $this->getAccessToken() 
  250. ); 
  251.  
  252. private function refreshTokenRequest($params) 
  253. if (isset($params['assertion'])) { 
  254. $this->client->getLogger()->info( 
  255. 'OAuth2 access token refresh with Signed JWT assertion grants.' 
  256. ); 
  257. } else { 
  258. $this->client->getLogger()->info('OAuth2 access token refresh'); 
  259.  
  260. $http = new Google_Http_Request( 
  261. self::OAUTH2_TOKEN_URI,  
  262. 'POST',  
  263. array(),  
  264. $params 
  265. ); 
  266. $http->disableGzip(); 
  267. $request = $this->client->getIo()->makeRequest($http); 
  268.  
  269. $code = $request->getResponseHttpCode(); 
  270. $body = $request->getResponseBody(); 
  271. if (200 == $code) { 
  272. $token = json_decode($body, true); 
  273. if ($token == null) { 
  274. throw new Google_Auth_Exception("Could not json decode the access token"); 
  275.  
  276. if (! isset($token['access_token']) || ! isset($token['expires_in'])) { 
  277. throw new Google_Auth_Exception("Invalid token format"); 
  278.  
  279. if (isset($token['id_token'])) { 
  280. $this->token['id_token'] = $token['id_token']; 
  281. $this->token['access_token'] = $token['access_token']; 
  282. $this->token['expires_in'] = $token['expires_in']; 
  283. $this->token['created'] = time(); 
  284. } else { 
  285. throw new Google_Auth_Exception("Error refreshing the OAuth2 token, message: '$body'", $code); 
  286.  
  287. /** 
  288. * Revoke an OAuth2 access token or refresh token. This method will revoke the current access 
  289. * token, if a token isn't provided. 
  290. * @throws Google_Auth_Exception 
  291. * @param string|null $token The token (access token or a refresh token) that should be revoked. 
  292. * @return boolean Returns True if the revocation was successful, otherwise False. 
  293. */ 
  294. public function revokeToken($token = null) 
  295. if (!$token) { 
  296. if (!$this->token) { 
  297. // Not initialized, no token to actually revoke 
  298. return false; 
  299. } elseif (array_key_exists('refresh_token', $this->token)) { 
  300. $token = $this->token['refresh_token']; 
  301. } else { 
  302. $token = $this->token['access_token']; 
  303. $request = new Google_Http_Request( 
  304. self::OAUTH2_REVOKE_URI,  
  305. 'POST',  
  306. array(),  
  307. "token=$token" 
  308. ); 
  309. $request->disableGzip(); 
  310. $response = $this->client->getIo()->makeRequest($request); 
  311. $code = $response->getResponseHttpCode(); 
  312. if ($code == 200) { 
  313. $this->token = null; 
  314. return true; 
  315.  
  316. return false; 
  317.  
  318. /** 
  319. * Returns if the access_token is expired. 
  320. * @return bool Returns True if the access_token is expired. 
  321. */ 
  322. public function isAccessTokenExpired() 
  323. if (!$this->token || !isset($this->token['created'])) { 
  324. return true; 
  325.  
  326. // If the token is set to expire in the next 30 seconds. 
  327. $expired = ($this->token['created'] 
  328. + ($this->token['expires_in'] - 30)) < time(); 
  329.  
  330. return $expired; 
  331.  
  332. // Gets federated sign-on certificates to use for verifying identity tokens. 
  333. // Returns certs as array structure, where keys are key ids, and values 
  334. // are PEM encoded certificates. 
  335. private function getFederatedSignOnCerts() 
  336. return $this->retrieveCertsFromLocation( 
  337. $this->client->getClassConfig($this, 'federated_signon_certs_url') 
  338. ); 
  339.  
  340. /** 
  341. * Retrieve and cache a certificates file. 
  342. * @param $url string location 
  343. * @throws Google_Auth_Exception 
  344. * @return array certificates 
  345. */ 
  346. public function retrieveCertsFromLocation($url) 
  347. // If we're retrieving a local file, just grab it. 
  348. if ("http" != substr($url, 0, 4)) { 
  349. $file = file_get_contents($url); 
  350. if ($file) { 
  351. return json_decode($file, true); 
  352. } else { 
  353. throw new Google_Auth_Exception( 
  354. "Failed to retrieve verification certificates: '" . 
  355. $url . "'." 
  356. ); 
  357.  
  358. // This relies on makeRequest caching certificate responses. 
  359. $request = $this->client->getIo()->makeRequest( 
  360. new Google_Http_Request( 
  361. $url 
  362. ); 
  363. if ($request->getResponseHttpCode() == 200) { 
  364. $certs = json_decode($request->getResponseBody(), true); 
  365. if ($certs) { 
  366. return $certs; 
  367. throw new Google_Auth_Exception( 
  368. "Failed to retrieve verification certificates: '" . 
  369. $request->getResponseBody() . "'.",  
  370. $request->getResponseHttpCode() 
  371. ); 
  372.  
  373. /** 
  374. * Verifies an id token and returns the authenticated apiLoginTicket. 
  375. * Throws an exception if the id token is not valid. 
  376. * The audience parameter can be used to control which id tokens are 
  377. * accepted. By default, the id token must have been issued to this OAuth2 client. 
  378. * @param $id_token 
  379. * @param $audience 
  380. * @return Google_Auth_LoginTicket 
  381. */ 
  382. public function verifyIdToken($id_token = null, $audience = null) 
  383. if (!$id_token) { 
  384. $id_token = $this->token['id_token']; 
  385. $certs = $this->getFederatedSignonCerts(); 
  386. if (!$audience) { 
  387. $audience = $this->client->getClassConfig($this, 'client_id'); 
  388.  
  389. return $this->verifySignedJwtWithCerts( 
  390. $id_token,  
  391. $certs,  
  392. $audience,  
  393. array(self::OAUTH2_ISSUER, self::OAUTH2_ISSUER_HTTPS) 
  394. ); 
  395.  
  396. /** 
  397. * Verifies the id token, returns the verified token contents. 
  398. * @param $jwt string the token 
  399. * @param $certs array of certificates 
  400. * @param $required_audience string the expected consumer of the token 
  401. * @param [$issuer] the expected issues, defaults to Google 
  402. * @param [$max_expiry] the max lifetime of a token, defaults to MAX_TOKEN_LIFETIME_SECS 
  403. * @throws Google_Auth_Exception 
  404. * @return mixed token information if valid, false if not 
  405. */ 
  406. public function verifySignedJwtWithCerts( 
  407. $jwt,  
  408. $certs,  
  409. $required_audience,  
  410. $issuer = null,  
  411. $max_expiry = null 
  412. ) { 
  413. if (!$max_expiry) { 
  414. // Set the maximum time we will accept a token for. 
  415. $max_expiry = self::MAX_TOKEN_LIFETIME_SECS; 
  416.  
  417. $segments = explode(".", $jwt); 
  418. if (count($segments) != 3) { 
  419. throw new Google_Auth_Exception("Wrong number of segments in token: $jwt"); 
  420. $signed = $segments[0] . "." . $segments[1]; 
  421. $signature = Google_Utils::urlSafeB64Decode($segments[2]); 
  422.  
  423. // Parse envelope. 
  424. $envelope = json_decode(Google_Utils::urlSafeB64Decode($segments[0]), true); 
  425. if (!$envelope) { 
  426. throw new Google_Auth_Exception("Can't parse token envelope: " . $segments[0]); 
  427.  
  428. // Parse token 
  429. $json_body = Google_Utils::urlSafeB64Decode($segments[1]); 
  430. $payload = json_decode($json_body, true); 
  431. if (!$payload) { 
  432. throw new Google_Auth_Exception("Can't parse token payload: " . $segments[1]); 
  433.  
  434. // Check signature 
  435. $verified = false; 
  436. foreach ($certs as $keyName => $pem) { 
  437. $public_key = new Google_Verifier_Pem($pem); 
  438. if ($public_key->verify($signed, $signature)) { 
  439. $verified = true; 
  440. break; 
  441.  
  442. if (!$verified) { 
  443. throw new Google_Auth_Exception("Invalid token signature: $jwt"); 
  444.  
  445. // Check issued-at timestamp 
  446. $iat = 0; 
  447. if (array_key_exists("iat", $payload)) { 
  448. $iat = $payload["iat"]; 
  449. if (!$iat) { 
  450. throw new Google_Auth_Exception("No issue time in token: $json_body"); 
  451. $earliest = $iat - self::CLOCK_SKEW_SECS; 
  452.  
  453. // Check expiration timestamp 
  454. $now = time(); 
  455. $exp = 0; 
  456. if (array_key_exists("exp", $payload)) { 
  457. $exp = $payload["exp"]; 
  458. if (!$exp) { 
  459. throw new Google_Auth_Exception("No expiration time in token: $json_body"); 
  460. if ($exp >= $now + $max_expiry) { 
  461. throw new Google_Auth_Exception( 
  462. sprintf("Expiration time too far in future: %s", $json_body) 
  463. ); 
  464.  
  465. $latest = $exp + self::CLOCK_SKEW_SECS; 
  466. if ($now < $earliest) { 
  467. throw new Google_Auth_Exception( 
  468. sprintf( 
  469. "Token used too early, %s < %s: %s",  
  470. $now,  
  471. $earliest,  
  472. $json_body 
  473. ); 
  474. if ($now > $latest) { 
  475. throw new Google_Auth_Exception( 
  476. sprintf( 
  477. "Token used too late, %s > %s: %s",  
  478. $now,  
  479. $latest,  
  480. $json_body 
  481. ); 
  482.  
  483. // support HTTP and HTTPS issuers 
  484. // @see https://developers.google.com/identity/sign-in/web/backend-auth 
  485. $iss = $payload['iss']; 
  486. if ($issuer && !in_array($iss, (array) $issuer)) { 
  487. throw new Google_Auth_Exception( 
  488. sprintf( 
  489. "Invalid issuer, %s not in %s: %s",  
  490. $iss,  
  491. "[".implode(", ", (array) $issuer)."]",  
  492. $json_body 
  493. ); 
  494.  
  495. // Check audience 
  496. $aud = $payload["aud"]; 
  497. if ($aud != $required_audience) { 
  498. throw new Google_Auth_Exception( 
  499. sprintf( 
  500. "Wrong recipient, %s != %s:",  
  501. $aud,  
  502. $required_audience,  
  503. $json_body 
  504. ); 
  505.  
  506. // All good. 
  507. return new Google_Auth_LoginTicket($envelope, $payload); 
  508.  
  509. /** 
  510. * Add a parameter to the auth params if not empty string. 
  511. */ 
  512. private function maybeAddParam($params, $name) 
  513. $param = $this->client->getClassConfig($this, $name); 
  514. if ($param != '') { 
  515. $params[$name] = $param; 
  516. return $params;