diff options
Diffstat (limited to 'Postman/Postman-Mail/google-api-php-client-1.1.2/src/Google/Auth/OAuth2.php')
-rw-r--r-- | Postman/Postman-Mail/google-api-php-client-1.1.2/src/Google/Auth/OAuth2.php | 633 |
1 files changed, 633 insertions, 0 deletions
diff --git a/Postman/Postman-Mail/google-api-php-client-1.1.2/src/Google/Auth/OAuth2.php b/Postman/Postman-Mail/google-api-php-client-1.1.2/src/Google/Auth/OAuth2.php new file mode 100644 index 0000000..d34dbee --- /dev/null +++ b/Postman/Postman-Mail/google-api-php-client-1.1.2/src/Google/Auth/OAuth2.php @@ -0,0 +1,633 @@ +<?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. + */ + +require_once realpath(dirname(__FILE__) . '/../../../autoload.php'); + +/** + * Authentication class that deals with the OAuth 2 web-server authentication flow + * + * @author Chris Chabot <chabotc@google.com> + * @author Chirag Shah <chirags@google.com> + * + */ +class Postman_Google_Auth_OAuth2 extends Postman_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 seconds + const AUTH_TOKEN_LIFETIME_SECS = 300; // five minutes in seconds + const MAX_TOKEN_LIFETIME_SECS = 86400; // one day in seconds + const OAUTH2_ISSUER = 'accounts.google.com'; + + /** @var Postman_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 Postman_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(Postman_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 Postman_Google_Http_Request $request + * @return Postman_Google_Http_Request The resulting HTTP response including the + * responseHttpCode, responseHeaders and responseBody. + */ + public function authenticatedRequest(Postman_Google_Http_Request $request) + { + $request = $this->sign($request); + return $this->client->getIo()->makeRequest($request); + } + + /** + * @param string $code + * @throws Postman_Google_Auth_Exception + * @return string + */ + public function authenticate($code) + { + if (strlen($code) == 0) { + throw new Postman_Google_Auth_Exception("Invalid code"); + } + + // We got here from the redirect from a successful authorization grant, + // fetch the access token + $request = new Postman_Google_Http_Request( + self::OAUTH2_TOKEN_URI, + 'POST', + array(), + array( + 'code' => $code, + 'grant_type' => 'authorization_code', + 'redirect_uri' => $this->client->getClassConfig($this, 'redirect_uri'), + 'client_id' => $this->client->getClassConfig($this, 'client_id'), + 'client_secret' => $this->client->getClassConfig($this, 'client_secret') + ) + ); + $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 Postman_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 Postman_Google_Auth_Exception + */ + public function setAccessToken($token) + { + $token = json_decode($token, true); + if ($token == null) { + throw new Postman_Google_Auth_Exception('Could not json decode the token'); + } + if (! isset($token['access_token'])) { + throw new Postman_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(Postman_Google_Auth_AssertionCredentials $creds) + { + $this->assertionCredentials = $creds; + } + + /** + * Include an accessToken in a given apiHttpRequest. + * @param Postman_Google_Http_Request $request + * @return Postman_Google_Http_Request + * @throws Postman_Google_Auth_Exception + */ + public function sign(Postman_Google_Http_Request $request) + { + // add the developer key to the request before signing it + if ($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 Postman_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 Postman_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 Postman_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 Postman_Google_Auth_Exception("Could not json decode the access token"); + } + + if (! isset($token['access_token']) || ! isset($token['expires_in'])) { + throw new Postman_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 Postman_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 Postman_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 revoke + return false; + } elseif (array_key_exists('refresh_token', $this->token)) { + $token = $this->token['refresh_token']; + } else { + $token = $this->token['access_token']; + } + } + $request = new Postman_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 Postman_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 Postman_Google_Auth_Exception( + "Failed to retrieve verification certificates: '" . + $url . "'." + ); + } + } + + // This relies on makeRequest caching certificate responses. + $request = $this->client->getIo()->makeRequest( + new Postman_Google_Http_Request( + $url + ) + ); + if ($request->getResponseHttpCode() == 200) { + $certs = json_decode($request->getResponseBody(), true); + if ($certs) { + return $certs; + } + } + throw new Postman_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 Postman_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, self::OAUTH2_ISSUER); + } + + /** + * 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 Postman_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 Postman_Google_Auth_Exception("Wrong number of segments in token: $jwt"); + } + $signed = $segments[0] . "." . $segments[1]; + $signature = Postman_Google_Utils::urlSafeB64Decode($segments[2]); + + // Parse envelope. + $envelope = json_decode(Postman_Google_Utils::urlSafeB64Decode($segments[0]), true); + if (!$envelope) { + throw new Postman_Google_Auth_Exception("Can't parse token envelope: " . $segments[0]); + } + + // Parse token + $json_body = Postman_Google_Utils::urlSafeB64Decode($segments[1]); + $payload = json_decode($json_body, true); + if (!$payload) { + throw new Postman_Google_Auth_Exception("Can't parse token payload: " . $segments[1]); + } + + // Check signature + $verified = false; + foreach ($certs as $keyName => $pem) { + $public_key = new Postman_Google_Verifier_Pem($pem); + if ($public_key->verify($signed, $signature)) { + $verified = true; + break; + } + } + + if (!$verified) { + throw new Postman_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 Postman_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 Postman_Google_Auth_Exception("No expiration time in token: $json_body"); + } + if ($exp >= $now + $max_expiry) { + throw new Postman_Google_Auth_Exception( + sprintf("Expiration time too far in future: %s", $json_body) + ); + } + + $latest = $exp + self::CLOCK_SKEW_SECS; + if ($now < $earliest) { + throw new Postman_Google_Auth_Exception( + sprintf( + "Token used too early, %s < %s: %s", + $now, + $earliest, + $json_body + ) + ); + } + if ($now > $latest) { + throw new Postman_Google_Auth_Exception( + sprintf( + "Token used too late, %s > %s: %s", + $now, + $latest, + $json_body + ) + ); + } + + $iss = $payload['iss']; + if ($issuer && $iss != $issuer) { + throw new Postman_Google_Auth_Exception( + sprintf( + "Invalid issuer, %s != %s: %s", + $iss, + $issuer, + $json_body + ) + ); + } + + // Check audience + $aud = $payload["aud"]; + if ($aud != $required_audience) { + throw new Postman_Google_Auth_Exception( + sprintf( + "Wrong recipient, %s != %s:", + $aud, + $required_audience, + $json_body + ) + ); + } + + // All good. + return new Postman_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; + } +} |