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

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