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