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

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