Google_OAuth2

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

Defined (1)

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

/gdwpm-api/auth/Google_OAuth2.php  
  1. class Google_OAuth2 extends Google_Auth { 
  2. public $clientId; 
  3. public $clientSecret; 
  4. public $developerKey; 
  5. public $token; 
  6. public $redirectUri; 
  7. public $state; 
  8. public $accessType = 'offline'; 
  9. public $approvalPrompt = 'force'; 
  10. public $requestVisibleActions; 
  11.  
  12. /** @var Google_AssertionCredentials $assertionCredentials */ 
  13. public $assertionCredentials; 
  14.  
  15. const OAUTH2_REVOKE_URI = 'https://accounts.google.com/o/oauth2/revoke'; 
  16. const OAUTH2_TOKEN_URI = 'https://accounts.google.com/o/oauth2/token'; 
  17. const OAUTH2_AUTH_URL = 'https://accounts.google.com/o/oauth2/auth'; 
  18. const OAUTH2_FEDERATED_SIGNON_CERTS_URL = 'https://www.googleapis.com/oauth2/v1/certs'; 
  19. const CLOCK_SKEW_SECS = 300; // five minutes in seconds 
  20. const AUTH_TOKEN_LIFETIME_SECS = 300; // five minutes in seconds 
  21. const MAX_TOKEN_LIFETIME_SECS = 86400; // one day in seconds 
  22.  
  23. /** 
  24. * Instantiates the class, but does not initiate the login flow, leaving it 
  25. * to the discretion of the caller (which is done by calling authenticate()). 
  26. */ 
  27. public function __construct() { 
  28. global $gdwpm_apiConfig; 
  29.  
  30. if (! empty($gdwpm_apiConfig['developer_key'])) { 
  31. $this->developerKey = $gdwpm_apiConfig['developer_key']; 
  32.  
  33. if (! empty($gdwpm_apiConfig['oauth2_client_id'])) { 
  34. $this->clientId = $gdwpm_apiConfig['oauth2_client_id']; 
  35.  
  36. if (! empty($gdwpm_apiConfig['oauth2_client_secret'])) { 
  37. $this->clientSecret = $gdwpm_apiConfig['oauth2_client_secret']; 
  38.  
  39. if (! empty($gdwpm_apiConfig['oauth2_redirect_uri'])) { 
  40. $this->redirectUri = $gdwpm_apiConfig['oauth2_redirect_uri']; 
  41.  
  42. if (! empty($gdwpm_apiConfig['oauth2_access_type'])) { 
  43. $this->accessType = $gdwpm_apiConfig['oauth2_access_type']; 
  44.  
  45. if (! empty($gdwpm_apiConfig['oauth2_approval_prompt'])) { 
  46. $this->approvalPrompt = $gdwpm_apiConfig['oauth2_approval_prompt']; 
  47.  
  48.  
  49. /** 
  50. * @param $service 
  51. * @param string|null $code 
  52. * @throws Google_AuthException 
  53. * @return string 
  54. */ 
  55. public function authenticate($service, $code = null) { 
  56. if (!$code && isset($_GET['code'])) { 
  57. $code = $_GET['code']; 
  58.  
  59. if ($code) { 
  60. // We got here from the redirect from a successful authorization grant, fetch the access token 
  61. $request = Google_Client::$io->makeRequest(new Google_HttpRequest(self::OAUTH2_TOKEN_URI, 'POST', array(), array( 
  62. 'code' => $code,  
  63. 'grant_type' => 'authorization_code',  
  64. 'redirect_uri' => $this->redirectUri,  
  65. 'client_id' => $this->clientId,  
  66. 'client_secret' => $this->clientSecret 
  67. ))); 
  68.  
  69. if ($request->getResponseHttpCode() == 200) { 
  70. $this->setAccessToken($request->getResponseBody()); 
  71. $this->token['created'] = time(); 
  72. return $this->getAccessToken(); 
  73. } else { 
  74. $response = $request->getResponseBody(); 
  75. $decodedResponse = json_decode($response, true); 
  76. if ($decodedResponse != null && $decodedResponse['error']) { 
  77. $response = $decodedResponse['error']; 
  78. throw new Google_AuthException("Error fetching OAuth2 access token, message: '$response'", $request->getResponseHttpCode()); 
  79.  
  80. $authUrl = $this->createAuthUrl($service['scope']); 
  81. header('Location: ' . $authUrl); 
  82. return true; 
  83.  
  84. /** 
  85. * Create a URL to obtain user authorization. 
  86. * The authorization endpoint allows the user to first 
  87. * authenticate, and then grant/deny the access request. 
  88. * @param string $scope The scope is expressed as a list of space-delimited strings. 
  89. * @return string 
  90. */ 
  91. public function createAuthUrl($scope) { 
  92. $params = array( 
  93. 'response_type=code',  
  94. 'redirect_uri=' . urlencode($this->redirectUri),  
  95. 'client_id=' . urlencode($this->clientId),  
  96. 'scope=' . urlencode($scope),  
  97. 'access_type=' . urlencode($this->accessType),  
  98. 'approval_prompt=' . urlencode($this->approvalPrompt),  
  99. ); 
  100.  
  101. // if the list of scopes contains plus.login, add request_visible_actions 
  102. // to auth URL 
  103. if(strpos($scope, 'plus.login') && count($this->requestVisibleActions) > 0) { 
  104. $params[] = 'request_visible_actions=' . 
  105. urlencode($this->requestVisibleActions); 
  106.  
  107. if (isset($this->state)) { 
  108. $params[] = 'state=' . urlencode($this->state); 
  109. $params = implode('&', $params); 
  110. return self::OAUTH2_AUTH_URL . "?$params"; 
  111.  
  112. /** 
  113. * @param string $token 
  114. * @throws Google_AuthException 
  115. */ 
  116. public function setAccessToken($token) { 
  117. $token = json_decode($token, true); 
  118. if ($token == null) { 
  119. throw new Google_AuthException('Could not json decode the token'); 
  120. if (! isset($token['access_token'])) { 
  121. throw new Google_AuthException("Invalid token format"); 
  122. $this->token = $token; 
  123.  
  124. public function getAccessToken() { 
  125. return json_encode($this->token); 
  126.  
  127. public function setDeveloperKey($developerKey) { 
  128. $this->developerKey = $developerKey; 
  129.  
  130. public function setState($state) { 
  131. $this->state = $state; 
  132.  
  133. public function setAccessType($accessType) { 
  134. $this->accessType = $accessType; 
  135.  
  136. public function setApprovalPrompt($approvalPrompt) { 
  137. $this->approvalPrompt = $approvalPrompt; 
  138.  
  139. public function setAssertionCredentials(Google_AssertionCredentials $creds) { 
  140. $this->assertionCredentials = $creds; 
  141.  
  142. /** 
  143. * Include an accessToken in a given apiHttpRequest. 
  144. * @param Google_HttpRequest $request 
  145. * @return Google_HttpRequest 
  146. * @throws Google_AuthException 
  147. */ 
  148. public function sign(Google_HttpRequest $request) { 
  149. // add the developer key to the request before signing it 
  150. if ($this->developerKey) { 
  151. $requestUrl = $request->getUrl(); 
  152. $requestUrl .= (strpos($request->getUrl(), '?') === false) ? '?' : '&'; 
  153. $requestUrl .= 'key=' . urlencode($this->developerKey); 
  154. $request->setUrl($requestUrl); 
  155.  
  156. // Cannot sign the request without an OAuth access token. 
  157. if (null == $this->token && null == $this->assertionCredentials) { 
  158. return $request; 
  159.  
  160. // Check if the token is set to expire in the next 30 seconds 
  161. // (or has already expired). 
  162. if ($this->isAccessTokenExpired()) { 
  163. if ($this->assertionCredentials) { 
  164. $this->refreshTokenWithAssertion(); 
  165. } else { 
  166. if (! array_key_exists('refresh_token', $this->token)) { 
  167. throw new Google_AuthException("The OAuth 2.0 access token has expired, " 
  168. . "and a refresh token is not available. Refresh tokens are not " 
  169. . "returned for responses that were auto-approved."); 
  170. $this->refreshToken($this->token['refresh_token']); 
  171.  
  172. // Add the OAuth2 header to the request 
  173. $request->setRequestHeaders( 
  174. array('Authorization' => 'Bearer ' . $this->token['access_token']) 
  175. ); 
  176.  
  177. return $request; 
  178.  
  179. /** 
  180. * Fetches a fresh access token with the given refresh token. 
  181. * @param string $refreshToken 
  182. * @return void 
  183. */ 
  184. public function refreshToken($refreshToken) { 
  185. $this->refreshTokenRequest(array( 
  186. 'client_id' => $this->clientId,  
  187. 'client_secret' => $this->clientSecret,  
  188. 'refresh_token' => $refreshToken,  
  189. 'grant_type' => 'refresh_token' 
  190. )); 
  191.  
  192. /** 
  193. * Fetches a fresh access token with a given assertion token. 
  194. * @param Google_AssertionCredentials $assertionCredentials optional. 
  195. * @return void 
  196. */ 
  197. public function refreshTokenWithAssertion($assertionCredentials = null) { 
  198. if (!$assertionCredentials) { 
  199. $assertionCredentials = $this->assertionCredentials; 
  200.  
  201. $this->refreshTokenRequest(array( 
  202. 'grant_type' => 'assertion',  
  203. 'assertion_type' => $assertionCredentials->assertionType,  
  204. 'assertion' => $assertionCredentials->generateAssertion(),  
  205. )); 
  206.  
  207. private function refreshTokenRequest($params) { 
  208. $http = new Google_HttpRequest(self::OAUTH2_TOKEN_URI, 'POST', array(), $params); 
  209. $request = Google_Client::$io->makeRequest($http); 
  210.  
  211. $code = $request->getResponseHttpCode(); 
  212. $body = $request->getResponseBody(); 
  213. if (200 == $code) { 
  214. $token = json_decode($body, true); 
  215. if ($token == null) { 
  216. throw new Google_AuthException("Could not json decode the access token"); 
  217.  
  218. if (! isset($token['access_token']) || ! isset($token['expires_in'])) { 
  219. throw new Google_AuthException("Invalid token format"); 
  220.  
  221. $this->token['access_token'] = $token['access_token']; 
  222. $this->token['expires_in'] = $token['expires_in']; 
  223. $this->token['created'] = time(); 
  224. } else { 
  225. throw new Google_AuthException("Error refreshing the OAuth2 token, message: '$body'", $code); 
  226.  
  227. /** 
  228. * Revoke an OAuth2 access token or refresh token. This method will revoke the current access 
  229. * token, if a token isn't provided. 
  230. * @throws Google_AuthException 
  231. * @param string|null $token The token (access token or a refresh token) that should be revoked. 
  232. * @return boolean Returns True if the revocation was successful, otherwise False. 
  233. */ 
  234. public function revokeToken($token = null) { 
  235. if (!$token) { 
  236. $token = $this->token['access_token']; 
  237. $request = new Google_HttpRequest(self::OAUTH2_REVOKE_URI, 'POST', array(), "token=$token"); 
  238. $response = Google_Client::$io->makeRequest($request); 
  239. $code = $response->getResponseHttpCode(); 
  240. if ($code == 200) { 
  241. $this->token = null; 
  242. return true; 
  243.  
  244. return false; 
  245.  
  246. /** 
  247. * Returns if the access_token is expired. 
  248. * @return bool Returns True if the access_token is expired. 
  249. */ 
  250. public function isAccessTokenExpired() { 
  251. if (null == $this->token) { 
  252. return true; 
  253.  
  254. // If the token is set to expire in the next 30 seconds. 
  255. $expired = ($this->token['created'] 
  256. + ($this->token['expires_in'] - 30)) < time(); 
  257.  
  258. return $expired; 
  259.  
  260. // Gets federated sign-on certificates to use for verifying identity tokens. 
  261. // Returns certs as array structure, where keys are key ids, and values 
  262. // are PEM encoded certificates. 
  263. private function getFederatedSignOnCerts() { 
  264. // This relies on makeRequest caching certificate responses. 
  265. $request = Google_Client::$io->makeRequest(new Google_HttpRequest( 
  266. self::OAUTH2_FEDERATED_SIGNON_CERTS_URL)); 
  267. if ($request->getResponseHttpCode() == 200) { 
  268. $certs = json_decode($request->getResponseBody(), true); 
  269. if ($certs) { 
  270. return $certs; 
  271. throw new Google_AuthException( 
  272. "Failed to retrieve verification certificates: '" . 
  273. $request->getResponseBody() . "'.",  
  274. $request->getResponseHttpCode()); 
  275.  
  276. /** 
  277. * Verifies an id token and returns the authenticated apiLoginTicket. 
  278. * Throws an exception if the id token is not valid. 
  279. * The audience parameter can be used to control which id tokens are 
  280. * accepted. By default, the id token must have been issued to this OAuth2 client. 
  281. * @param $id_token 
  282. * @param $audience 
  283. * @return Google_LoginTicket 
  284. */ 
  285. public function verifyIdToken($id_token = null, $audience = null) { 
  286. if (!$id_token) { 
  287. $id_token = $this->token['id_token']; 
  288.  
  289. $certs = $this->getFederatedSignonCerts(); 
  290. if (!$audience) { 
  291. $audience = $this->clientId; 
  292. return $this->verifySignedJwtWithCerts($id_token, $certs, $audience); 
  293.  
  294. // Verifies the id token, returns the verified token contents. 
  295. // Visible for testing. 
  296. function verifySignedJwtWithCerts($jwt, $certs, $required_audience) { 
  297. $segments = explode(".", $jwt); 
  298. if (count($segments) != 3) { 
  299. throw new Google_AuthException("Wrong number of segments in token: $jwt"); 
  300. $signed = $segments[0] . "." . $segments[1]; 
  301. $signature = Google_Utils::urlSafeB64Decode($segments[2]); 
  302.  
  303. // Parse envelope. 
  304. $envelope = json_decode(Google_Utils::urlSafeB64Decode($segments[0]), true); 
  305. if (!$envelope) { 
  306. throw new Google_AuthException("Can't parse token envelope: " . $segments[0]); 
  307.  
  308. // Parse token 
  309. $json_body = Google_Utils::urlSafeB64Decode($segments[1]); 
  310. $payload = json_decode($json_body, true); 
  311. if (!$payload) { 
  312. throw new Google_AuthException("Can't parse token payload: " . $segments[1]); 
  313.  
  314. // Check signature 
  315. $verified = false; 
  316. foreach ($certs as $keyName => $pem) { 
  317. $public_key = new Google_PemVerifier($pem); 
  318. if ($public_key->verify($signed, $signature)) { 
  319. $verified = true; 
  320. break; 
  321.  
  322. if (!$verified) { 
  323. throw new Google_AuthException("Invalid token signature: $jwt"); 
  324.  
  325. // Check issued-at timestamp 
  326. $iat = 0; 
  327. if (array_key_exists("iat", $payload)) { 
  328. $iat = $payload["iat"]; 
  329. if (!$iat) { 
  330. throw new Google_AuthException("No issue time in token: $json_body"); 
  331. $earliest = $iat - self::CLOCK_SKEW_SECS; 
  332.  
  333. // Check expiration timestamp 
  334. $now = time(); 
  335. $exp = 0; 
  336. if (array_key_exists("exp", $payload)) { 
  337. $exp = $payload["exp"]; 
  338. if (!$exp) { 
  339. throw new Google_AuthException("No expiration time in token: $json_body"); 
  340. if ($exp >= $now + self::MAX_TOKEN_LIFETIME_SECS) { 
  341. throw new Google_AuthException( 
  342. "Expiration time too far in future: $json_body"); 
  343.  
  344. $latest = $exp + self::CLOCK_SKEW_SECS; 
  345. if ($now < $earliest) { 
  346. throw new Google_AuthException( 
  347. "Token used too early, $now < $earliest: $json_body"); 
  348. if ($now > $latest) { 
  349. throw new Google_AuthException( 
  350. "Token used too late, $now > $latest: $json_body"); 
  351.  
  352. // TODO(beaton): check issuer field? 
  353.  
  354. // Check audience 
  355. $aud = $payload["aud"]; 
  356. if ($aud != $required_audience) { 
  357. throw new Google_AuthException("Wrong recipient, $aud != $required_audience: $json_body"); 
  358.  
  359. // All good. 
  360. return new Google_LoginTicket($envelope, $payload);