See Release Notes
Long Term Support Release
Differences Between: [Versions 401 and 402] [Versions 401 and 403]
1 <?php 2 /* 3 * Copyright 2008 Google Inc. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 if (!class_exists('Google_Client')) { 19 require_once dirname(__FILE__) . '/../autoload.php'; 20 } 21 22 /** 23 * Authentication class that deals with the OAuth 2 web-server authentication flow 24 * 25 */ 26 class Google_Auth_OAuth2 extends Google_Auth_Abstract 27 { 28 const OAUTH2_REVOKE_URI = 'https://accounts.google.com/o/oauth2/revoke'; 29 const OAUTH2_TOKEN_URI = 'https://accounts.google.com/o/oauth2/token'; 30 const OAUTH2_AUTH_URL = 'https://accounts.google.com/o/oauth2/auth'; 31 const CLOCK_SKEW_SECS = 300; // five minutes in seconds 32 const AUTH_TOKEN_LIFETIME_SECS = 300; // five minutes in seconds 33 const MAX_TOKEN_LIFETIME_SECS = 86400; // one day in seconds 34 const OAUTH2_ISSUER = 'accounts.google.com'; 35 const OAUTH2_ISSUER_HTTPS = 'https://accounts.google.com'; 36 37 /** @var Google_Auth_AssertionCredentials $assertionCredentials */ 38 private $assertionCredentials; 39 40 /** 41 * @var string The state parameters for CSRF and other forgery protection. 42 */ 43 private $state; 44 45 /** 46 * @var array The token bundle. 47 */ 48 private $token = array(); 49 50 /** 51 * @var Google_Client the base client 52 */ 53 private $client; 54 55 /** 56 * Instantiates the class, but does not initiate the login flow, leaving it 57 * to the discretion of the caller. 58 */ 59 public function __construct(Google_Client $client) 60 { 61 $this->client = $client; 62 } 63 64 /** 65 * Perform an authenticated / signed apiHttpRequest. 66 * This function takes the apiHttpRequest, calls apiAuth->sign on it 67 * (which can modify the request in what ever way fits the auth mechanism) 68 * and then calls apiCurlIO::makeRequest on the signed request 69 * 70 * @param Google_Http_Request $request 71 * @return Google_Http_Request The resulting HTTP response including the 72 * responseHttpCode, responseHeaders and responseBody. 73 */ 74 public function authenticatedRequest(Google_Http_Request $request) 75 { 76 $request = $this->sign($request); 77 return $this->client->getIo()->makeRequest($request); 78 } 79 80 /** 81 * @param string $code 82 * @param boolean $crossClient 83 * @throws Google_Auth_Exception 84 * @return string 85 */ 86 public function authenticate($code, $crossClient = false) 87 { 88 if (strlen($code) == 0) { 89 throw new Google_Auth_Exception("Invalid code"); 90 } 91 92 $arguments = array( 93 'code' => $code, 94 'grant_type' => 'authorization_code', 95 'client_id' => $this->client->getClassConfig($this, 'client_id'), 96 'client_secret' => $this->client->getClassConfig($this, 'client_secret') 97 ); 98 99 if ($crossClient !== true) { 100 $arguments['redirect_uri'] = $this->client->getClassConfig($this, 'redirect_uri'); 101 } 102 103 // We got here from the redirect from a successful authorization grant, 104 // fetch the access token 105 $request = new Google_Http_Request( 106 self::OAUTH2_TOKEN_URI, 107 'POST', 108 array(), 109 $arguments 110 ); 111 $request->disableGzip(); 112 $response = $this->client->getIo()->makeRequest($request); 113 114 if ($response->getResponseHttpCode() == 200) { 115 $this->setAccessToken($response->getResponseBody()); 116 $this->token['created'] = time(); 117 return $this->getAccessToken(); 118 } else { 119 $decodedResponse = json_decode($response->getResponseBody(), true); 120 if ($decodedResponse != null && $decodedResponse['error']) { 121 $errorText = $decodedResponse['error']; 122 if (isset($decodedResponse['error_description'])) { 123 $errorText .= ": " . $decodedResponse['error_description']; 124 } 125 } 126 throw new Google_Auth_Exception( 127 sprintf( 128 "Error fetching OAuth2 access token, message: '%s'", 129 $errorText 130 ), 131 $response->getResponseHttpCode() 132 ); 133 } 134 } 135 136 /** 137 * Create a URL to obtain user authorization. 138 * The authorization endpoint allows the user to first 139 * authenticate, and then grant/deny the access request. 140 * @param string $scope The scope is expressed as a list of space-delimited strings. 141 * @return string 142 */ 143 public function createAuthUrl($scope) 144 { 145 $params = array( 146 'response_type' => 'code', 147 'redirect_uri' => $this->client->getClassConfig($this, 'redirect_uri'), 148 'client_id' => $this->client->getClassConfig($this, 'client_id'), 149 'scope' => $scope, 150 'access_type' => $this->client->getClassConfig($this, 'access_type'), 151 ); 152 153 // Prefer prompt to approval prompt. 154 if ($this->client->getClassConfig($this, 'prompt')) { 155 $params = $this->maybeAddParam($params, 'prompt'); 156 } else { 157 $params = $this->maybeAddParam($params, 'approval_prompt'); 158 } 159 $params = $this->maybeAddParam($params, 'login_hint'); 160 $params = $this->maybeAddParam($params, 'hd'); 161 $params = $this->maybeAddParam($params, 'openid.realm'); 162 $params = $this->maybeAddParam($params, 'include_granted_scopes'); 163 164 // If the list of scopes contains plus.login, add request_visible_actions 165 // to auth URL. 166 $rva = $this->client->getClassConfig($this, 'request_visible_actions'); 167 if (strpos($scope, 'plus.login') && strlen($rva) > 0) { 168 $params['request_visible_actions'] = $rva; 169 } 170 171 if (isset($this->state)) { 172 $params['state'] = $this->state; 173 } 174 175 return self::OAUTH2_AUTH_URL . "?" . http_build_query($params, '', '&'); 176 } 177 178 /** 179 * @param string $token 180 * @throws Google_Auth_Exception 181 */ 182 public function setAccessToken($token) 183 { 184 $token = json_decode($token, true); 185 if ($token == null) { 186 throw new Google_Auth_Exception('Could not json decode the token'); 187 } 188 if (! isset($token['access_token'])) { 189 throw new Google_Auth_Exception("Invalid token format"); 190 } 191 $this->token = $token; 192 } 193 194 public function getAccessToken() 195 { 196 return json_encode($this->token); 197 } 198 199 public function getRefreshToken() 200 { 201 if (array_key_exists('refresh_token', $this->token)) { 202 return $this->token['refresh_token']; 203 } else { 204 return null; 205 } 206 } 207 208 public function setState($state) 209 { 210 $this->state = $state; 211 } 212 213 public function setAssertionCredentials(Google_Auth_AssertionCredentials $creds) 214 { 215 $this->assertionCredentials = $creds; 216 } 217 218 /** 219 * Include an accessToken in a given apiHttpRequest. 220 * @param Google_Http_Request $request 221 * @return Google_Http_Request 222 * @throws Google_Auth_Exception 223 */ 224 public function sign(Google_Http_Request $request) 225 { 226 // add the developer key to the request before signing it 227 if ($this->client->getClassConfig($this, 'developer_key')) { 228 $request->setQueryParam('key', $this->client->getClassConfig($this, 'developer_key')); 229 } 230 231 // Cannot sign the request without an OAuth access token. 232 if (null == $this->token && null == $this->assertionCredentials) { 233 return $request; 234 } 235 236 // Check if the token is set to expire in the next 30 seconds 237 // (or has already expired). 238 if ($this->isAccessTokenExpired()) { 239 if ($this->assertionCredentials) { 240 $this->refreshTokenWithAssertion(); 241 } else { 242 $this->client->getLogger()->debug('OAuth2 access token expired'); 243 if (! array_key_exists('refresh_token', $this->token)) { 244 $error = "The OAuth 2.0 access token has expired," 245 ." and a refresh token is not available. Refresh tokens" 246 ." are not returned for responses that were auto-approved."; 247 248 $this->client->getLogger()->error($error); 249 throw new Google_Auth_Exception($error); 250 } 251 $this->refreshToken($this->token['refresh_token']); 252 } 253 } 254 255 $this->client->getLogger()->debug('OAuth2 authentication'); 256 257 // Add the OAuth2 header to the request 258 $request->setRequestHeaders( 259 array('Authorization' => 'Bearer ' . $this->token['access_token']) 260 ); 261 262 return $request; 263 } 264 265 /** 266 * Fetches a fresh access token with the given refresh token. 267 * @param string $refreshToken 268 * @return void 269 */ 270 public function refreshToken($refreshToken) 271 { 272 $this->refreshTokenRequest( 273 array( 274 'client_id' => $this->client->getClassConfig($this, 'client_id'), 275 'client_secret' => $this->client->getClassConfig($this, 'client_secret'), 276 'refresh_token' => $refreshToken, 277 'grant_type' => 'refresh_token' 278 ) 279 ); 280 } 281 282 /** 283 * Fetches a fresh access token with a given assertion token. 284 * @param Google_Auth_AssertionCredentials $assertionCredentials optional. 285 * @return void 286 */ 287 public function refreshTokenWithAssertion($assertionCredentials = null) 288 { 289 if (!$assertionCredentials) { 290 $assertionCredentials = $this->assertionCredentials; 291 } 292 293 $cacheKey = $assertionCredentials->getCacheKey(); 294 295 if ($cacheKey) { 296 // We can check whether we have a token available in the 297 // cache. If it is expired, we can retrieve a new one from 298 // the assertion. 299 $token = $this->client->getCache()->get($cacheKey); 300 if ($token) { 301 $this->setAccessToken($token); 302 } 303 if (!$this->isAccessTokenExpired()) { 304 return; 305 } 306 } 307 308 $this->client->getLogger()->debug('OAuth2 access token expired'); 309 $this->refreshTokenRequest( 310 array( 311 'grant_type' => 'assertion', 312 'assertion_type' => $assertionCredentials->assertionType, 313 'assertion' => $assertionCredentials->generateAssertion(), 314 ) 315 ); 316 317 if ($cacheKey) { 318 // Attempt to cache the token. 319 $this->client->getCache()->set( 320 $cacheKey, 321 $this->getAccessToken() 322 ); 323 } 324 } 325 326 private function refreshTokenRequest($params) 327 { 328 if (isset($params['assertion'])) { 329 $this->client->getLogger()->info( 330 'OAuth2 access token refresh with Signed JWT assertion grants.' 331 ); 332 } else { 333 $this->client->getLogger()->info('OAuth2 access token refresh'); 334 } 335 336 $http = new Google_Http_Request( 337 self::OAUTH2_TOKEN_URI, 338 'POST', 339 array(), 340 $params 341 ); 342 $http->disableGzip(); 343 $request = $this->client->getIo()->makeRequest($http); 344 345 $code = $request->getResponseHttpCode(); 346 $body = $request->getResponseBody(); 347 if (200 == $code) { 348 $token = json_decode($body, true); 349 if ($token == null) { 350 throw new Google_Auth_Exception("Could not json decode the access token"); 351 } 352 353 if (! isset($token['access_token']) || ! isset($token['expires_in'])) { 354 throw new Google_Auth_Exception("Invalid token format"); 355 } 356 357 if (isset($token['id_token'])) { 358 $this->token['id_token'] = $token['id_token']; 359 } 360 $this->token['access_token'] = $token['access_token']; 361 $this->token['expires_in'] = $token['expires_in']; 362 $this->token['created'] = time(); 363 } else { 364 throw new Google_Auth_Exception("Error refreshing the OAuth2 token, message: '$body'", $code); 365 } 366 } 367 368 /** 369 * Revoke an OAuth2 access token or refresh token. This method will revoke the current access 370 * token, if a token isn't provided. 371 * @throws Google_Auth_Exception 372 * @param string|null $token The token (access token or a refresh token) that should be revoked. 373 * @return boolean Returns True if the revocation was successful, otherwise False. 374 */ 375 public function revokeToken($token = null) 376 { 377 if (!$token) { 378 if (!$this->token) { 379 // Not initialized, no token to actually revoke 380 return false; 381 } elseif (array_key_exists('refresh_token', $this->token)) { 382 $token = $this->token['refresh_token']; 383 } else { 384 $token = $this->token['access_token']; 385 } 386 } 387 $request = new Google_Http_Request( 388 self::OAUTH2_REVOKE_URI, 389 'POST', 390 array(), 391 "token=$token" 392 ); 393 $request->disableGzip(); 394 $response = $this->client->getIo()->makeRequest($request); 395 $code = $response->getResponseHttpCode(); 396 if ($code == 200) { 397 $this->token = null; 398 return true; 399 } 400 401 return false; 402 } 403 404 /** 405 * Returns if the access_token is expired. 406 * @return bool Returns True if the access_token is expired. 407 */ 408 public function isAccessTokenExpired() 409 { 410 if (!$this->token || !isset($this->token['created'])) { 411 return true; 412 } 413 414 // If the token is set to expire in the next 30 seconds. 415 $expired = ($this->token['created'] 416 + ($this->token['expires_in'] - 30)) < time(); 417 418 return $expired; 419 } 420 421 // Gets federated sign-on certificates to use for verifying identity tokens. 422 // Returns certs as array structure, where keys are key ids, and values 423 // are PEM encoded certificates. 424 private function getFederatedSignOnCerts() 425 { 426 return $this->retrieveCertsFromLocation( 427 $this->client->getClassConfig($this, 'federated_signon_certs_url') 428 ); 429 } 430 431 /** 432 * Retrieve and cache a certificates file. 433 * 434 * @param $url string location 435 * @throws Google_Auth_Exception 436 * @return array certificates 437 */ 438 public function retrieveCertsFromLocation($url) 439 { 440 // If we're retrieving a local file, just grab it. 441 if ("http" != substr($url, 0, 4)) { 442 $file = file_get_contents($url); 443 if ($file) { 444 return json_decode($file, true); 445 } else { 446 throw new Google_Auth_Exception( 447 "Failed to retrieve verification certificates: '" . 448 $url . "'." 449 ); 450 } 451 } 452 453 // This relies on makeRequest caching certificate responses. 454 $request = $this->client->getIo()->makeRequest( 455 new Google_Http_Request( 456 $url 457 ) 458 ); 459 if ($request->getResponseHttpCode() == 200) { 460 $certs = json_decode($request->getResponseBody(), true); 461 if ($certs) { 462 return $certs; 463 } 464 } 465 throw new Google_Auth_Exception( 466 "Failed to retrieve verification certificates: '" . 467 $request->getResponseBody() . "'.", 468 $request->getResponseHttpCode() 469 ); 470 } 471 472 /** 473 * Verifies an id token and returns the authenticated apiLoginTicket. 474 * Throws an exception if the id token is not valid. 475 * The audience parameter can be used to control which id tokens are 476 * accepted. By default, the id token must have been issued to this OAuth2 client. 477 * 478 * @param $id_token 479 * @param $audience 480 * @return Google_Auth_LoginTicket 481 */ 482 public function verifyIdToken($id_token = null, $audience = null) 483 { 484 if (!$id_token) { 485 $id_token = $this->token['id_token']; 486 } 487 $certs = $this->getFederatedSignonCerts(); 488 if (!$audience) { 489 $audience = $this->client->getClassConfig($this, 'client_id'); 490 } 491 492 return $this->verifySignedJwtWithCerts( 493 $id_token, 494 $certs, 495 $audience, 496 array(self::OAUTH2_ISSUER, self::OAUTH2_ISSUER_HTTPS) 497 ); 498 } 499 500 /** 501 * Verifies the id token, returns the verified token contents. 502 * 503 * @param $jwt string the token 504 * @param $certs array of certificates 505 * @param $required_audience string the expected consumer of the token 506 * @param [$issuer] the expected issues, defaults to Google 507 * @param [$max_expiry] the max lifetime of a token, defaults to MAX_TOKEN_LIFETIME_SECS 508 * @throws Google_Auth_Exception 509 * @return mixed token information if valid, false if not 510 */ 511 public function verifySignedJwtWithCerts( 512 $jwt, 513 $certs, 514 $required_audience, 515 $issuer = null, 516 $max_expiry = null 517 ) { 518 if (!$max_expiry) { 519 // Set the maximum time we will accept a token for. 520 $max_expiry = self::MAX_TOKEN_LIFETIME_SECS; 521 } 522 523 $segments = explode(".", $jwt); 524 if (count($segments) != 3) { 525 throw new Google_Auth_Exception("Wrong number of segments in token: $jwt"); 526 } 527 $signed = $segments[0] . "." . $segments[1]; 528 $signature = Google_Utils::urlSafeB64Decode($segments[2]); 529 530 // Parse envelope. 531 $envelope = json_decode(Google_Utils::urlSafeB64Decode($segments[0]), true); 532 if (!$envelope) { 533 throw new Google_Auth_Exception("Can't parse token envelope: " . $segments[0]); 534 } 535 536 // Parse token 537 $json_body = Google_Utils::urlSafeB64Decode($segments[1]); 538 $payload = json_decode($json_body, true); 539 if (!$payload) { 540 throw new Google_Auth_Exception("Can't parse token payload: " . $segments[1]); 541 } 542 543 // Check signature 544 $verified = false; 545 foreach ($certs as $keyName => $pem) { 546 $public_key = new Google_Verifier_Pem($pem); 547 if ($public_key->verify($signed, $signature)) { 548 $verified = true; 549 break; 550 } 551 } 552 553 if (!$verified) { 554 throw new Google_Auth_Exception("Invalid token signature: $jwt"); 555 } 556 557 // Check issued-at timestamp 558 $iat = 0; 559 if (array_key_exists("iat", $payload)) { 560 $iat = $payload["iat"]; 561 } 562 if (!$iat) { 563 throw new Google_Auth_Exception("No issue time in token: $json_body"); 564 } 565 $earliest = $iat - self::CLOCK_SKEW_SECS; 566 567 // Check expiration timestamp 568 $now = time(); 569 $exp = 0; 570 if (array_key_exists("exp", $payload)) { 571 $exp = $payload["exp"]; 572 } 573 if (!$exp) { 574 throw new Google_Auth_Exception("No expiration time in token: $json_body"); 575 } 576 if ($exp >= $now + $max_expiry) { 577 throw new Google_Auth_Exception( 578 sprintf("Expiration time too far in future: %s", $json_body) 579 ); 580 } 581 582 $latest = $exp + self::CLOCK_SKEW_SECS; 583 if ($now < $earliest) { 584 throw new Google_Auth_Exception( 585 sprintf( 586 "Token used too early, %s < %s: %s", 587 $now, 588 $earliest, 589 $json_body 590 ) 591 ); 592 } 593 if ($now > $latest) { 594 throw new Google_Auth_Exception( 595 sprintf( 596 "Token used too late, %s > %s: %s", 597 $now, 598 $latest, 599 $json_body 600 ) 601 ); 602 } 603 604 // support HTTP and HTTPS issuers 605 // @see https://developers.google.com/identity/sign-in/web/backend-auth 606 $iss = $payload['iss']; 607 if ($issuer && !in_array($iss, (array) $issuer)) { 608 throw new Google_Auth_Exception( 609 sprintf( 610 "Invalid issuer, %s not in %s: %s", 611 $iss, 612 "[".implode(",", (array) $issuer)."]", 613 $json_body 614 ) 615 ); 616 } 617 618 // Check audience 619 $aud = $payload["aud"]; 620 if ($aud != $required_audience) { 621 throw new Google_Auth_Exception( 622 sprintf( 623 "Wrong recipient, %s != %s:", 624 $aud, 625 $required_audience, 626 $json_body 627 ) 628 ); 629 } 630 631 // All good. 632 return new Google_Auth_LoginTicket($envelope, $payload); 633 } 634 635 /** 636 * Add a parameter to the auth params if not empty string. 637 */ 638 private function maybeAddParam($params, $name) 639 { 640 $param = $this->client->getClassConfig($this, $name); 641 if ($param != '') { 642 $params[$name] = $param; 643 } 644 return $params; 645 } 646 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body