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

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