[ Index ]

PHP Cross Reference of moodle-2.8

title

Body

[close]

/lib/google/Google/Auth/ -> OAuth2.php (source)

   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  }


Generated: Fri Nov 28 20:29:05 2014 Cross-referenced by PHPXref 0.7.1