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

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