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).

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