AutorÃa | Ultima modificación | Ver Log |
<?php/** Copyright 2008 Google Inc.** Licensed under the Apache License, Version 2.0 (the "License");* you may not use this file except in compliance with the License.* You may obtain a copy of the License at** http://www.apache.org/licenses/LICENSE-2.0** Unless required by applicable law or agreed to in writing, software* distributed under the License is distributed on an "AS IS" BASIS,* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.* See the License for the specific language governing permissions and* limitations under the License.*/if (!class_exists('Google_Client')) {require_once dirname(__FILE__) . '/../autoload.php';}/*** Authentication class that deals with the OAuth 2 web-server authentication flow**/#[AllowDynamicProperties]class Google_Auth_OAuth2 extends Google_Auth_Abstract{const OAUTH2_REVOKE_URI = 'https://accounts.google.com/o/oauth2/revoke';const OAUTH2_TOKEN_URI = 'https://accounts.google.com/o/oauth2/token';const OAUTH2_AUTH_URL = 'https://accounts.google.com/o/oauth2/auth';const CLOCK_SKEW_SECS = 300; // five minutes in secondsconst AUTH_TOKEN_LIFETIME_SECS = 300; // five minutes in secondsconst MAX_TOKEN_LIFETIME_SECS = 86400; // one day in secondsconst OAUTH2_ISSUER = 'accounts.google.com';const OAUTH2_ISSUER_HTTPS = 'https://accounts.google.com';/** @var Google_Auth_AssertionCredentials $assertionCredentials */private $assertionCredentials;/*** @var string The state parameters for CSRF and other forgery protection.*/private $state;/*** @var array The token bundle.*/private $token = array();/*** @var Google_Client the base client*/private $client;/*** Instantiates the class, but does not initiate the login flow, leaving it* to the discretion of the caller.*/public function __construct(Google_Client $client){$this->client = $client;}/*** Perform an authenticated / signed apiHttpRequest.* This function takes the apiHttpRequest, calls apiAuth->sign on it* (which can modify the request in what ever way fits the auth mechanism)* and then calls apiCurlIO::makeRequest on the signed request** @param Google_Http_Request $request* @return Google_Http_Request The resulting HTTP response including the* responseHttpCode, responseHeaders and responseBody.*/public function authenticatedRequest(Google_Http_Request $request){$request = $this->sign($request);return $this->client->getIo()->makeRequest($request);}/*** @param string $code* @param boolean $crossClient* @throws Google_Auth_Exception* @return string*/public function authenticate($code, $crossClient = false){if (strlen($code) == 0) {throw new Google_Auth_Exception("Invalid code");}$arguments = array('code' => $code,'grant_type' => 'authorization_code','client_id' => $this->client->getClassConfig($this, 'client_id'),'client_secret' => $this->client->getClassConfig($this, 'client_secret'));if ($crossClient !== true) {$arguments['redirect_uri'] = $this->client->getClassConfig($this, 'redirect_uri');}// We got here from the redirect from a successful authorization grant,// fetch the access token$request = new Google_Http_Request(self::OAUTH2_TOKEN_URI,'POST',array(),$arguments);$request->disableGzip();$response = $this->client->getIo()->makeRequest($request);if ($response->getResponseHttpCode() == 200) {$this->setAccessToken($response->getResponseBody());$this->token['created'] = time();return $this->getAccessToken();} else {$decodedResponse = json_decode($response->getResponseBody(), true);if ($decodedResponse != null && $decodedResponse['error']) {$errorText = $decodedResponse['error'];if (isset($decodedResponse['error_description'])) {$errorText .= ": " . $decodedResponse['error_description'];}}throw new Google_Auth_Exception(sprintf("Error fetching OAuth2 access token, message: '%s'",$errorText),$response->getResponseHttpCode());}}/*** Create a URL to obtain user authorization.* The authorization endpoint allows the user to first* authenticate, and then grant/deny the access request.* @param string $scope The scope is expressed as a list of space-delimited strings.* @return string*/public function createAuthUrl($scope){$params = array('response_type' => 'code','redirect_uri' => $this->client->getClassConfig($this, 'redirect_uri'),'client_id' => $this->client->getClassConfig($this, 'client_id'),'scope' => $scope,'access_type' => $this->client->getClassConfig($this, 'access_type'),);// Prefer prompt to approval prompt.if ($this->client->getClassConfig($this, 'prompt')) {$params = $this->maybeAddParam($params, 'prompt');} else {$params = $this->maybeAddParam($params, 'approval_prompt');}$params = $this->maybeAddParam($params, 'login_hint');$params = $this->maybeAddParam($params, 'hd');$params = $this->maybeAddParam($params, 'openid.realm');$params = $this->maybeAddParam($params, 'include_granted_scopes');// If the list of scopes contains plus.login, add request_visible_actions// to auth URL.$rva = $this->client->getClassConfig($this, 'request_visible_actions');if (strpos($scope, 'plus.login') && strlen($rva) > 0) {$params['request_visible_actions'] = $rva;}if (isset($this->state)) {$params['state'] = $this->state;}return self::OAUTH2_AUTH_URL . "?" . http_build_query($params, '', '&');}/*** @param string $token* @throws Google_Auth_Exception*/public function setAccessToken($token){$token = json_decode($token, true);if ($token == null) {throw new Google_Auth_Exception('Could not json decode the token');}if (! isset($token['access_token'])) {throw new Google_Auth_Exception("Invalid token format");}$this->token = $token;}public function getAccessToken(){return json_encode($this->token);}public function getRefreshToken(){if (array_key_exists('refresh_token', $this->token)) {return $this->token['refresh_token'];} else {return null;}}public function setState($state){$this->state = $state;}public function setAssertionCredentials(Google_Auth_AssertionCredentials $creds){$this->assertionCredentials = $creds;}/*** Include an accessToken in a given apiHttpRequest.* @param Google_Http_Request $request* @return Google_Http_Request* @throws Google_Auth_Exception*/public function sign(Google_Http_Request $request){// add the developer key to the request before signing itif ($this->client->getClassConfig($this, 'developer_key')) {$request->setQueryParam('key', $this->client->getClassConfig($this, 'developer_key'));}// Cannot sign the request without an OAuth access token.if (null == $this->token && null == $this->assertionCredentials) {return $request;}// Check if the token is set to expire in the next 30 seconds// (or has already expired).if ($this->isAccessTokenExpired()) {if ($this->assertionCredentials) {$this->refreshTokenWithAssertion();} else {$this->client->getLogger()->debug('OAuth2 access token expired');if (! array_key_exists('refresh_token', $this->token)) {$error = "The OAuth 2.0 access token has expired,"." and a refresh token is not available. Refresh tokens"." are not returned for responses that were auto-approved.";$this->client->getLogger()->error($error);throw new Google_Auth_Exception($error);}$this->refreshToken($this->token['refresh_token']);}}$this->client->getLogger()->debug('OAuth2 authentication');// Add the OAuth2 header to the request$request->setRequestHeaders(array('Authorization' => 'Bearer ' . $this->token['access_token']));return $request;}/*** Fetches a fresh access token with the given refresh token.* @param string $refreshToken* @return void*/public function refreshToken($refreshToken){$this->refreshTokenRequest(array('client_id' => $this->client->getClassConfig($this, 'client_id'),'client_secret' => $this->client->getClassConfig($this, 'client_secret'),'refresh_token' => $refreshToken,'grant_type' => 'refresh_token'));}/*** Fetches a fresh access token with a given assertion token.* @param Google_Auth_AssertionCredentials $assertionCredentials optional.* @return void*/public function refreshTokenWithAssertion($assertionCredentials = null){if (!$assertionCredentials) {$assertionCredentials = $this->assertionCredentials;}$cacheKey = $assertionCredentials->getCacheKey();if ($cacheKey) {// We can check whether we have a token available in the// cache. If it is expired, we can retrieve a new one from// the assertion.$token = $this->client->getCache()->get($cacheKey);if ($token) {$this->setAccessToken($token);}if (!$this->isAccessTokenExpired()) {return;}}$this->client->getLogger()->debug('OAuth2 access token expired');$this->refreshTokenRequest(array('grant_type' => 'assertion','assertion_type' => $assertionCredentials->assertionType,'assertion' => $assertionCredentials->generateAssertion(),));if ($cacheKey) {// Attempt to cache the token.$this->client->getCache()->set($cacheKey,$this->getAccessToken());}}private function refreshTokenRequest($params){if (isset($params['assertion'])) {$this->client->getLogger()->info('OAuth2 access token refresh with Signed JWT assertion grants.');} else {$this->client->getLogger()->info('OAuth2 access token refresh');}$http = new Google_Http_Request(self::OAUTH2_TOKEN_URI,'POST',array(),$params);$http->disableGzip();$request = $this->client->getIo()->makeRequest($http);$code = $request->getResponseHttpCode();$body = $request->getResponseBody();if (200 == $code) {$token = json_decode($body, true);if ($token == null) {throw new Google_Auth_Exception("Could not json decode the access token");}if (! isset($token['access_token']) || ! isset($token['expires_in'])) {throw new Google_Auth_Exception("Invalid token format");}if (isset($token['id_token'])) {$this->token['id_token'] = $token['id_token'];}$this->token['access_token'] = $token['access_token'];$this->token['expires_in'] = $token['expires_in'];$this->token['created'] = time();} else {throw new Google_Auth_Exception("Error refreshing the OAuth2 token, message: '$body'", $code);}}/*** Revoke an OAuth2 access token or refresh token. This method will revoke the current access* token, if a token isn't provided.* @throws Google_Auth_Exception* @param string|null $token The token (access token or a refresh token) that should be revoked.* @return boolean Returns True if the revocation was successful, otherwise False.*/public function revokeToken($token = null){if (!$token) {if (!$this->token) {// Not initialized, no token to actually revokereturn false;} elseif (array_key_exists('refresh_token', $this->token)) {$token = $this->token['refresh_token'];} else {$token = $this->token['access_token'];}}$request = new Google_Http_Request(self::OAUTH2_REVOKE_URI,'POST',array(),"token=$token");$request->disableGzip();$response = $this->client->getIo()->makeRequest($request);$code = $response->getResponseHttpCode();if ($code == 200) {$this->token = null;return true;}return false;}/*** Returns if the access_token is expired.* @return bool Returns True if the access_token is expired.*/public function isAccessTokenExpired(){if (!$this->token || !isset($this->token['created'])) {return true;}// If the token is set to expire in the next 30 seconds.$expired = ($this->token['created']+ ($this->token['expires_in'] - 30)) < time();return $expired;}// Gets federated sign-on certificates to use for verifying identity tokens.// Returns certs as array structure, where keys are key ids, and values// are PEM encoded certificates.private function getFederatedSignOnCerts(){return $this->retrieveCertsFromLocation($this->client->getClassConfig($this, 'federated_signon_certs_url'));}/*** Retrieve and cache a certificates file.** @param $url string location* @throws Google_Auth_Exception* @return array certificates*/public function retrieveCertsFromLocation($url){// If we're retrieving a local file, just grab it.if ("http" != substr($url, 0, 4)) {$file = file_get_contents($url);if ($file) {return json_decode($file, true);} else {throw new Google_Auth_Exception("Failed to retrieve verification certificates: '" .$url . "'.");}}// This relies on makeRequest caching certificate responses.$request = $this->client->getIo()->makeRequest(new Google_Http_Request($url));if ($request->getResponseHttpCode() == 200) {$certs = json_decode($request->getResponseBody(), true);if ($certs) {return $certs;}}throw new Google_Auth_Exception("Failed to retrieve verification certificates: '" .$request->getResponseBody() . "'.",$request->getResponseHttpCode());}/*** Verifies an id token and returns the authenticated apiLoginTicket.* Throws an exception if the id token is not valid.* The audience parameter can be used to control which id tokens are* accepted. By default, the id token must have been issued to this OAuth2 client.** @param $id_token* @param $audience* @return Google_Auth_LoginTicket*/public function verifyIdToken($id_token = null, $audience = null){if (!$id_token) {$id_token = $this->token['id_token'];}$certs = $this->getFederatedSignonCerts();if (!$audience) {$audience = $this->client->getClassConfig($this, 'client_id');}return $this->verifySignedJwtWithCerts($id_token,$certs,$audience,array(self::OAUTH2_ISSUER, self::OAUTH2_ISSUER_HTTPS));}/*** Verifies the id token, returns the verified token contents.** @param $jwt string the token* @param $certs array of certificates* @param $required_audience string the expected consumer of the token* @param [$issuer] the expected issues, defaults to Google* @param [$max_expiry] the max lifetime of a token, defaults to MAX_TOKEN_LIFETIME_SECS* @throws Google_Auth_Exception* @return mixed token information if valid, false if not*/public function verifySignedJwtWithCerts($jwt,$certs,$required_audience,$issuer = null,$max_expiry = null) {if (!$max_expiry) {// Set the maximum time we will accept a token for.$max_expiry = self::MAX_TOKEN_LIFETIME_SECS;}$segments = explode(".", $jwt);if (count($segments) != 3) {throw new Google_Auth_Exception("Wrong number of segments in token: $jwt");}$signed = $segments[0] . "." . $segments[1];$signature = Google_Utils::urlSafeB64Decode($segments[2]);// Parse envelope.$envelope = json_decode(Google_Utils::urlSafeB64Decode($segments[0]), true);if (!$envelope) {throw new Google_Auth_Exception("Can't parse token envelope: " . $segments[0]);}// Parse token$json_body = Google_Utils::urlSafeB64Decode($segments[1]);$payload = json_decode($json_body, true);if (!$payload) {throw new Google_Auth_Exception("Can't parse token payload: " . $segments[1]);}// Check signature$verified = false;foreach ($certs as $keyName => $pem) {$public_key = new Google_Verifier_Pem($pem);if ($public_key->verify($signed, $signature)) {$verified = true;break;}}if (!$verified) {throw new Google_Auth_Exception("Invalid token signature: $jwt");}// Check issued-at timestamp$iat = 0;if (array_key_exists("iat", $payload)) {$iat = $payload["iat"];}if (!$iat) {throw new Google_Auth_Exception("No issue time in token: $json_body");}$earliest = $iat - self::CLOCK_SKEW_SECS;// Check expiration timestamp$now = time();$exp = 0;if (array_key_exists("exp", $payload)) {$exp = $payload["exp"];}if (!$exp) {throw new Google_Auth_Exception("No expiration time in token: $json_body");}if ($exp >= $now + $max_expiry) {throw new Google_Auth_Exception(sprintf("Expiration time too far in future: %s", $json_body));}$latest = $exp + self::CLOCK_SKEW_SECS;if ($now < $earliest) {throw new Google_Auth_Exception(sprintf("Token used too early, %s < %s: %s",$now,$earliest,$json_body));}if ($now > $latest) {throw new Google_Auth_Exception(sprintf("Token used too late, %s > %s: %s",$now,$latest,$json_body));}// support HTTP and HTTPS issuers// @see https://developers.google.com/identity/sign-in/web/backend-auth$iss = $payload['iss'];if ($issuer && !in_array($iss, (array) $issuer)) {throw new Google_Auth_Exception(sprintf("Invalid issuer, %s not in %s: %s",$iss,"[".implode(",", (array) $issuer)."]",$json_body));}// Check audience$aud = $payload["aud"];if ($aud != $required_audience) {throw new Google_Auth_Exception(sprintf("Wrong recipient, %s != %s:",$aud,$required_audience,$json_body));}// All good.return new Google_Auth_LoginTicket($envelope, $payload);}/*** Add a parameter to the auth params if not empty string.*/private function maybeAddParam($params, $name){$param = $this->client->getClassConfig($this, $name);if ($param != '') {$params[$name] = $param;}return $params;}}