0

I got google api-php-client and I can't make a login as I always get error:

[02-Nov-2017 15:30:52 Europe/Helsinki] ***START***
scripto\CustomException: [0: E_CUSTOM: Suppressed or Custom error] cURL error 6: Could not resolve host: www.googleapis.com (see http://curl.haxx.se/libcurl/c/libcurl-errors.html).
in /home/suexec-test/server/GuzzleHttp/Handler/CurlFactory.php on line 186
[Trace]:
#0 /home/suexec-test/server/ExceptionFactory.php(117): scripto\CustomException::buildFromException(Object(GuzzleHttp\Exception\ConnectException))
#1 [internal function]: scripto\ExceptionFactory::exception_handler(Object(GuzzleHttp\Exception\ConnectException))
#2 {main}
[Previous]:
scripto\CustomException: [0: E_CUSTOM: Suppressed or Custom error] cURL error 6: Could not resolve host: www.googleapis.com (see http://curl.haxx.se/libcurl/c/libcurl-errors.html).
in /home/suexec-test/server/GuzzleHttp/Handler/CurlFactory.php on line 186
[Trace]:
#0 /home/suexec-test/server/GuzzleHttp/Handler/CurlFactory.php(150): GuzzleHttp\Handler\CurlFactory::createRejection(Object(GuzzleHttp\Handler\EasyHandle), Array)
#1 /home/suexec-test/server/GuzzleHttp/Handler/CurlFactory.php(103): GuzzleHttp\Handler\CurlFactory::finishError(Object(GuzzleHttp\Handler\CurlHandler), Object(GuzzleHttp\Handler\EasyHandle), Object(GuzzleHttp\Handler\CurlFactory))
#2 /home/suexec-test/server/GuzzleHttp/Handler/CurlHandler.php(43): GuzzleHttp\Handler\CurlFactory::finish(Object(GuzzleHttp\Handler\CurlHandler), Object(GuzzleHttp\Handler\EasyHandle), Object(GuzzleHttp\Handler\CurlFactory))
#3 /home/suexec-test/server/GuzzleHttp/Handler/Proxy.php(28): GuzzleHttp\Handler\CurlHandler->__invoke(Object(GuzzleHttp\Psr7\Request), Array)
#4 /home/suexec-test/server/GuzzleHttp/Handler/Proxy.php(51): GuzzleHttp\Handler\Proxy::GuzzleHttp\Handler\{closure}(Object(GuzzleHttp\Psr7\Request), Array)
#5 /home/suexec-test/server/GuzzleHttp/PrepareBodyMiddleware.php(66): GuzzleHttp\Handler\Proxy::GuzzleHttp\Handler\{closure}(Object(GuzzleHttp\Psr7\Request), Array)
#6 /home/suexec-test/server/GuzzleHttp/Middleware.php(30): GuzzleHttp\PrepareBodyMiddleware->__invoke(Object(GuzzleHttp\Psr7\Request), Array)
#7 /home/suexec-test/server/GuzzleHttp/RedirectMiddleware.php(70): GuzzleHttp\Middleware::GuzzleHttp\{closure}(Object(GuzzleHttp\Psr7\Request), Array)
#8 /home/suexec-test/server/GuzzleHttp/Middleware.php(57): GuzzleHttp\RedirectMiddleware->__invoke(Object(GuzzleHttp\Psr7\Request), Array)
#9 /home/suexec-test/server/GuzzleHttp/HandlerStack.php(67): GuzzleHttp\Middleware::GuzzleHttp\{closure}(Object(GuzzleHttp\Psr7\Request), Array)
#10 /home/suexec-test/server/GuzzleHttp/Client.php(281): GuzzleHttp\HandlerStack->__invoke(Object(GuzzleHttp\Psr7\Request), Array)
#11 /home/suexec-test/server/GuzzleHttp/Client.php(103): GuzzleHttp\Client->transfer(Object(GuzzleHttp\Psr7\Request), Array)
#12 /home/suexec-test/server/GuzzleHttp/Client.php(110): GuzzleHttp\Client->sendAsync(Object(GuzzleHttp\Psr7\Request), Array)
#13 /home/suexec-test/server/Google/Auth/HttpHandler/Guzzle6HttpHandler.php(34): GuzzleHttp\Client->send(Object(GuzzleHttp\Psr7\Request), Array)
#14 /home/suexec-test/server/Google/Auth/OAuth2.php(501): Google\Auth\HttpHandler\Guzzle6HttpHandler->__invoke(Object(GuzzleHttp\Psr7\Request))
#15 /home/suexec-test/server/Google/Google_Client.php(195): Google\Auth\OAuth2->fetchAuthToken(Object(Google\Auth\HttpHandler\Guzzle6HttpHandler))
#16 /home/suexec-test/server/Google/Google_Client.php(174): Google_Client->fetchAccessTokenWithAuthCode('4/4BhYCtvi43DGf...')
#17 /home/suexec-test/tests/testGoogle-4.php(56): Google_Client->authenticate('4/4BhYCtvi43DGf...')
#18 /home/suexec-test/server/Dispatcher.php(96): require_once('/home/suexec-te...')
#19 /home/suexec-test/server/Controller.php(68): scripto\Dispatcher->run()
#20 /home/suexec-test/public_html/index.php(7): scripto\Controller->run()
#21 {main}
***END***

I created a web application, Credentials

defined the redirect url, Redirect url

and enabled G+, G+ service

The problem is that even if all classes are loaded my code can't pass $client->authenticate($_GET['code']);.

I prepared a test file for you: at http://localhost/auth/google/login and http://localhost/auth/google/logout you should call the same file to observe the error thrown.

The test file is a modification from this post:

<?php
if (\session_status() != PHP_SESSION_ACTIVE) {
   Session_start();
}

$whoami = $_SERVER['REQUEST_URI'];
$login = $logout = false;
if (\strpos($whoami, '/login') !== false)
   $login = true;
if (\strpos($whoami, '/logout') !== false)
   $logout = true;

/*
 * Configuration and setup Google SDK
 */
$appId              = '519650546062-XXXXXX.apps.googleusercontent.com';
$appSecret          = 'XXXXXXX';
// CALLED BY US & GOOGLE!
$loginUri           = 'http://localhost/auth/google/login';
// CALLED BY US!
$logoutUri          = 'http://localhost/auth/google/logout';
$access             = 'online';
$scopes             = "openid profile email";
$incremental_scopes = false;  // what if for true?
//$state = 123;

//Create Google Client
$client = new Google_Client();
$client->setApplicationName("PHP Google OAuth Login Example");
$client->setClientId($appId);
$client->setClientSecret($appSecret);
$client->setRedirectUri($loginUri);
$client->setAccessType($access);
$client->setIncludeGrantedScopes($incremental_scopes);
$client->addScope($scopes);
//$client->setState($state);

//Send Client Request?
$objOAuthService = new Google_Service_Oauth2($client);

//CALLED BY US
if ($logout) {
  unset($_SESSION['access_token']);
  $client->revokeToken();
  // Back to login!
  header('Location: ' . filter_var($loginUri, FILTER_SANITIZE_URL));
}

//CALLED BY GOOGLE
if (isset($_GET['code'])) {
error_log('1....I\'M GOING TO BREAK...');
   $client->authenticate($_GET['code']);
   $_SESSION['access_token'] = $client->getAccessToken();
   // Back to this file!
   header('Location: ' . filter_var($loginUri, FILTER_SANITIZE_URL));
}

//CALLED BY US 2ND TIME (after previous header)
if (isset($_SESSION['access_token']) && $_SESSION['access_token']) {
error_log('2....session access token='.$_SESSION['access_token']);
   $client->setAccessToken($_SESSION['access_token']);
}

//ONLY DURING 2ND CALL (from previous if...)
if ($client->getAccessToken()) {
   $userData = $objOAuthService->userinfo->get();
   if(!empty($userData)) {
      // do sth with data!
   }
   $_SESSION['access_token'] = $client->getAccessToken();
error_log('3.....user data:'.print_r($userData, true));
} else {
   $authUrl = $client->createAuthUrl();
}

   $out = <<<EOT
<html>
   <head> 
      <title>Google OAuth v.2.0 Login test</title>
   </head>
   <body>

EOT;
if (isset($authUrl)) {
   $d = \urldecode($authUrl);
   $out .= <<<EOT
   <p>decoded authUrl = '{$d}'</p>
EOT;
   }
$out .= <<<EOT
   <h2>PHP Google OAuth 2.0 Login</h2>
EOT;
if (isset($authUrl)) {
   $out .= <<<EOT
      <p><a href='{$authUrl}'>Login with Google API</a></p>
EOT;
} else {
   $out .= <<<EOT
      <p>Welcome <a href="{$userData['link']}">{$userData['name']}</a>.</p>
      <p>Your email: {$userData['email']}</p>
      <p><a href={$logoutUri}>Logout</a></p>

EOT;
}
$e = \nl2br(\htmlspecialchars(\print_r($_SESSION, true)));
$out .= <<<EOT
      <p><h3>SESSION:</h3></p>
      <p>{$e}</p>
   </body>
</html>
EOT;

echo $out;

The code breaks just after error_log('1....I\'M GOING TO BREAK...');. The funny thing is that I managed to bypass the whole library with curl and get the access token which has the form:

[02-Nov-2017 14:06:38 Europe/Helsinki] Array
(
    [access_token] => ya29.Glv3B...xlSKIL_N67PE4...PYGDjZo-jD8v...ITFTnU
    [expires_in] => 2017-11-02 15:06:37
    [id_token] => eyJhbGciOiJImt...tlf_-Y1hXoCVSp...Ve_bK8F-jTCt...zg
    [token_type] => Bearer
)

but I can't find the endpoint to get user's data: sth like this [occasionally] breaks [I counted as much as 90% failure]

public function userinfo() {
   $access_token = $this->access['access_token'];
   $url = 'https://www.googleapis.com/plus/v1/people/me';
   $ch = curl_init();      
   curl_setopt($ch, CURLOPT_URL, $url);      
   curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
   curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
   curl_setopt($ch, CURLOPT_HTTPHEADER, array('Authorization: Bearer '. $access_token));
   $data = json_decode(curl_exec($ch), true);
   $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);      
   if ($http_code != 200)
      throw new \ErrorException(__CLASS__ . "::userinfo() is called but failed to get user information!", 0, 1);
   $this->data = $data;
}

thanks to this excellent post.

Any idea what is wrong with the official php library? Is anything I'm missing from the console?

Thank you.

centurian
  • 1,168
  • 13
  • 25
  • I should mention that I don't use composer but I can see all classes and needed files from the dependencies are loaded on demand using my own class loader. If someone experienced with composer can cross test the issue would be interesting! – centurian Nov 03 '17 at 19:47

1 Answers1

0

After a lot of searching,

as google documentation says:

You do not need to install any libraries to be able to directly call the OAuth 2.0 endpoints. enter image description here

Also at OpenId:

This document describes our OAuth 2.0 implementation for authentication, which conforms to the OpenID Connect specification, and is OpenID Certified. The documentation found in Using OAuth 2.0 to Access Google APIs also applies to this service.

So, I sent all libraries to my recycle bin and I use REST with cURL to make my server requests. Let's define a class GoogleToken that contains properties related with our requests to Google servers like url, client id, client secret etc:

file GoogleToken.php

<?php
namespace test;

/**
 * Google authentication Config class
 */
class GoogleToken {
   /** See the list: https://developers.google.com/identity/protocols/OAuth2WebServer#creatingclient */
   /** the application/client id */
   const appId = '';
   /** the application/client secret */
   const appSecret = '';
   /** the application/client redirect uri after login */
   const loginUri = 'http://localhost/auth/google/login';
   /** the application/client redirect uri after logout */
   const logoutUri = 'http://localhost/auth/google/logout';
   /** use 'online' to get the access token!
    * use 'offline' to get a refresh token along with the access token, 
    * see also 'prompt'!
    */
   const access = 'offline';
   /** the application/client approval prompt [auto|force]
    * use 'force' to get  a refresh token!
    */
   const prompt = 'force';
   /** the application/client access permissions
    *  [openid profile email|see https://developers.google.com/identity/protocols/googlescopes]
    */
   const scopes = 'openid profile email';

   public function __construct() {}
}

The above class should be seen as a programming token that helps other classes to achieve their goals (maybe it's a new paradigm, you can evaluate it). In reality, it's a more complex class that can serve our purposes with methods for data IO, ease management from user without touching the php files etc.

Let's build a class that make requests to Google servers and stores (temporarily) responses, the actual tokens that Google sends:

file GoogleHttpClient.php

A skeleton of this class:

public function authenticate($code)
public function verify($access_token)
public function validate($verify_token, $user)
public function refresh($refresh_token)
public function revokeToken($access_token)
public function userInfo($access_token)
public function createAuthUrl()

All methods accept arguments because with redirects what is stored internally is lost and this is the reason we need a storage mechanism to provide these arguments. File test.php provides the top layer that stores class properties and recall them if in need. The interesting part is that method authenticate() should make in order:

  1. a validation request

  2. a user info request

and not the other way around or, you will experience latency/errors from Google. The class GoogleHttpClient:

<?php
namespace test;

/**
 * The GoogleHttpClient is a helper class.
 * 
 * As this class is called between different calls, it needs access to a state
 * storage mechanism like sessions.
 * Even though the methods try to abstract away the complexity of the calls by 
 * a logical organisation of the actions involved, you are responsible for the 
 * order or context under which they are called.
 */
class GoogleHttpClient {
   /** @var GoogleToken $token The GoogleToken class object */
   protected $token;

   /** 
    * @var string[] $auth The authorization data returned by Google in the form:
    * Array(
    *    [access_token] => xxx
    *    [expires_in] => [unix timestamp]
    *    [id_token] => xxx.xxx.xxx
    *    [refresh_token] => xxx
    *    [token_type] => Bearer
    * )
    * 
    * This property should be stored in session (per user) in case we need a 
    * verification or refresh.
    */
   protected $auth;

   /** 
    * @var string[] $verify The response data returned by Google in the form:
    * Array(
    *    [issued_to] => xxx.apps.googleusercontent.com (@see GoogleToken['values']['appId'])
    *    [audience] => xxx.apps.googleusercontent.com (@see GoogleToken['values']['appId'])
    *    [user_id] => xxx
    *    [scope] => xxx (@see createAuthUrl())
    *    [expires_in] => xxx (<= 3600)
    *    [email] => xxx
    *    [verified_email] => 1
    *    [access_type] => xxx (@see createAuthUrl())
    * )
    * 
    * You shouldn't store this property in session.
    */
   protected $verify;

   /** 
    * @var string[] $user The user data returned by Google.
    * 
    * You should verify this property with your native authorization system and
    * integrate with it finally.
    */
   protected $user;

   /** @var string[] $error The error returned by Google */
   protected $error;

   /** 
    * @var boolean $log If you should log errors and assigned properties for debuging
    */
   protected $log;

   const INVALID_TOKEN = 1;

   /**
    * Constructor.
    * 
    * preconditions: a GoogleToken should be passed.
    *
    * postconditions: 
    * 
    * @param GoogleToken $token The GoogleToken that contains initialization values
    * @param boolean $log True, if we want to log errors and assigned properties
    * 
    * @return GoogleHttpClient A GoogleHttpClient object
    */
   public function __construct(GoogleToken $token, $log = false) {
      $this->token = $token;
      $this->log = $log;
   }

   /**
    * It makes a authentication request to Google.
    * 
    * preconditions: after the call to Google at this link, @see createAuthUrl(), 
    *    Google responds by making a call back with a unique id that should be
    *    passed as argument.
    *
    * postconditions: it sets properties $auth, $user, $verify and $error. 
    *    On error, $auth, $verify and $error contain the responses returned  by 
    *    Google depending at the method where error appeared, @see verify(), @see validate().
    *    If the authentication succeeds then, $error is null.
    * 
    * @param string $code A unique request id issued by Google
    * 
    * @return boolean True on success
    */
   public function authenticate($code) {
      $client_id = $this->token::appId;
      $client_secret  = $this->token::appSecret;
      $redirect_uri = $this->token::loginUri;
      $url = 'https://accounts.google.com/o/oauth2/token';
      $curlPost = 'client_id=' . $client_id . '&client_secret=' . $client_secret . '&redirect_uri=' . $redirect_uri . '&code='. $code . '&grant_type=authorization_code';
      $ch = curl_init();
      curl_setopt($ch, CURLOPT_URL, $url);      
      curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);    
      curl_setopt($ch, CURLOPT_POST, 1);     
      curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
      curl_setopt($ch, CURLOPT_POSTFIELDS, $curlPost);   
      $data = json_decode(curl_exec($ch), true);
      $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);    
      if ($http_code != 200) {
         if ($this->log)
            error_log(__CLASS__ . '::authenticate() error: http code not 200. Responded: '.print_r($data, true));
         $this->error = $data;
      } else {
         if ($this->log)
            error_log(__CLASS__ . '::auth='.print_r($data, true));
         $this->verify($data['access_token']);
         $this->user = $this->userInfo($data['access_token']);
         $this->validate($this->verify, $this->user);
      }
      $this->auth = $data;
      if ($this->error)
         return false;
      else
         return true;
   }

   /**
    * It requests for a verification token from Google.
    * 
    * preconditions: it is called from @see authenticate().
    *    Calling this method from redirects means all properties are null and we 
    *    pass session data.
    *
    * postconditions: it sets property $verify. 
    *    On error, $verify and $error contain the responses returned  by Google.
    *    If the request succeeds then, $error is null.
    * 
    * @param string $access_token Google's authorization response under key 'access_token'
    * 
    * @return boolean True on success
    */
   public function verify($access_token) {
      $client_id = $this->token::appId;
      $client_secret  = $this->token::appSecret; 
      $url = 'https://www.googleapis.com/oauth2/v2/tokeninfo';
      $curlPost = 'access_token='. $access_token;
      $ch = curl_init();
      curl_setopt($ch, CURLOPT_URL, $url);      
      curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);    
      curl_setopt($ch, CURLOPT_POST, 1);     
      curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
      curl_setopt($ch, CURLOPT_POSTFIELDS, $curlPost);   
      $data = json_decode(curl_exec($ch), true);
      $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);    
      if ($http_code != 200) {
         if ($this->log)
            error_log(__CLASS__ . '::verify() error: http code not 200. Responded: '.print_r($data, true));
         $this->error = $data;
      } else {
         if ($this->log)
            error_log(__CLASS__ . '::verify='.print_r($data, true));
      }
      $this->verify = $data;
      if ($this->error)
         return false;
      else
         return true;
   }

   /**
    * It compares properties $verify with $user and GoogleToken.
    * 
    * preconditions: it is called from @see authenticate().
    *    Calling this method from redirects means all properties are null and we 
    *    pass session data.
    *
    * postconditions: on failed validation, $error contains a custom error with fields:
    *    -error_id: a number
    *    -error_description: a message.
    *    If the verification succeeds then, $error is null.
    * 
    * @param string[] $verify_token Google's verification response token
    * @param string $user Google's user info response
    * 
    * @return boolean True on success
    */
   public function validate($verify_token, $user) {
      if (($verify_token['user_id'] != $user['id']) || ($verify_token['email'] != $user['email']) || ($verify_token['issued_to'] != $this->token::appId)) {
         $this->error = array('error_id' => self::INVALID_TOKEN, 'error_description' => 'Access token does not pass validation!');
         if ($this->log)
            error_log(__CLASS__ . '::error='.print_r($this->error, true));
      }
      if ($this->error)
         return false;
      else
         return true;
   }

   /**
    * It is called when property $auth has expired.
    * 
    * preconditions: authentication has been run, @see authenticate().
    *    Calling this method from redirects means all properties are null and we 
    *    pass session data.
    *
    * postconditions: it re-sets property $auth. 
    *    On error, $auth and $error contain the responses returned  by Google.
    *    If the refresh succeeds then, $error is null.
    * 
    * @param string $refresh_token Google's authorization response under key 'refresh_token'
    * 
    * @return boolean True on success
    */
   public function refresh($refresh_token) {
      $client_id = $this->token::appId;
      $client_secret  = $this->token::appSecret; 
      $url = 'https://accounts.google.com/o/oauth2/token';
      $curlPost = 'client_id=' . $client_id . '&client_secret=' . $client_secret . '&refresh_token='. $refresh_token . '&grant_type=refresh_token';
      $ch = curl_init();
      curl_setopt($ch, CURLOPT_URL, $url);      
      curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);    
      curl_setopt($ch, CURLOPT_POST, 1);     
      curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
      curl_setopt($ch, CURLOPT_POSTFIELDS, $curlPost);   
      $data = json_decode(curl_exec($ch), true);
      $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);    
      if ($http_code != 200) {
         if ($this->log)
            error_log(__CLASS__ . '::refresh() error: http code not 200. Responded: '.print_r($data, true));
         $this->error = $data;
      } else {
         if ($this->log)
            error_log(__CLASS__ . '::auth='.print_r($data, true));
      }
      $this->auth = $data;
      if ($this->error)
         return false;
      else
         return true;
   }

   /**
    * It un-registers application from user's account but id does not (and cannot)
    * logout user from Google.
    * 
    * preconditions: authentication has been run, @see authenticate().
    *    Calling this method from redirects means all properties are null and we 
    *    pass session data.
    *
    * postconditions: it un-registers application from user's account. 
    *    On error, $error contains the response returned  by Google.
    *    If the revoke succeeds then, all properties are nullified.
    * 
    * @param string $access_token Google's authorization response under key 'access_token'
    * 
    * @return boolean True on success
    */
   public function revokeToken($access_token) {
      $client_id = $this->token::appId;
      $client_secret  = $this->token::appSecret; 
      $url = 'https://accounts.google.com/o/oauth2/revoke';
      $curlPost = 'token=' . $access_token;
      $ch = curl_init();
      curl_setopt($ch, CURLOPT_URL, $url);      
      curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);    
      curl_setopt($ch, CURLOPT_POST, 1);     
      curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
      curl_setopt($ch, CURLOPT_POSTFIELDS, $curlPost);   
      $data = json_decode(curl_exec($ch), true);
      $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);    
      if ($http_code != 200) {
         if ($this->log)
            error_log(__CLASS__ . '::revokeToken() error: http code not 200. Responded: '.print_r($data, true));
         $this->error = $data;
      } else {
         $this->auth = null;
         $this->user = null;
         $this->verify = null;
         $this->error = null;
         if ($this->log)
            error_log(__CLASS__ . '::revokeToken() run and erased all properties!');
      }
      if ($this->error)
         return false;
      else
         return true;
   }

   /**
    * It is called from @see authenticate() or you can call it independently at 
    * a later time.
    * 
    * preconditions: authentication has been run, @see authenticate().
    *    Calling this method from redirects means all properties are null and we 
    *    pass session data.
    *
    * postconditions: it sets property $user. 
    *    On error, $user and $error contain the responses returned  by Google.
    *    If the request succeeds then, $error is null.
    * 
    * @param string $access_token Google's authorization response under key 'access_token'
    * 
    * @return string[] A hash array of user's data
    */
   public function userInfo($access_token) {
      /** format of data:
       * Array(
       * [id] => xxx
       * [email] => xxx@gmail.com
       * [verified_email] => 1
       * [name] => xxx xxx
       * [given_name] => xxx
       * [family_name] => xxx
       * [picture] => https://lh6.googleusercontent.com/.../photo.jpg?sz=50
       * [locale] => en
       * )
       */
      $url = 'https://www.googleapis.com/userinfo/v2/me';
      $ch = curl_init();      
      curl_setopt($ch, CURLOPT_URL, $url);      
      curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
      curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
      curl_setopt($ch, CURLOPT_HTTPHEADER, array('Authorization: Bearer '. $access_token));
      $data = json_decode(curl_exec($ch), true);
      $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);      
      if ($http_code != 200) {
         if ($this->log)
            error_log(__CLASS__ . '::userInfo() error: http code not 200. Responded: '.print_r($data, true));
         $this->error = $data;
      } else {
         if ($this->log)
            error_log(__CLASS__ . '::user='.print_r($data, true));
      }
      $this->user = $data;
      return $this->user;
   }

   /**
    * Creates a authentication link to Google servers.
    */
   public function createAuthUrl() {
      $scopes = \urlencode($this->token::scopes);
      $redirect = \urlencode($this->token::loginUri);
      $appId = $this->token::appId;
      $access = $this->token::access;
      $prompt = $this->token::prompt;
      return "https://accounts.google.com/o/oauth2/auth?scope={$scopes}&redirect_uri={$redirect}&response_type=code&client_id={$appId}&access_type={$access}&approval_prompt={$prompt}";
   }

   /**
    * Returns property $token
    */
   public function getToken() {
      return $this->token;
   }

   /**
    * Returns property $auth
    */
   public function getAuthToken() {
      return $this->auth;
   }

   /**
    * Returns property $verify
    */
   public function getVerifyToken() {
      return $this->verify;
   }

   /**
    * Returns property $user
    */
   public function getUser() {
      return $this->user;
   }

   /**
    * Returns property $error
    */
   public function getError() {
      return $this->error;
   }
}

Now we can call Google and make a login app:

file test.php

<?php
namespace test;

require_once __DIR__ . '/GoogleToken.php';
require_once __DIR__ . '/GoogleHttpClient.php';

if (\session_status() != PHP_SESSION_ACTIVE) {
   session_start();
}

$whoami = $_SERVER['REQUEST_URI'];
$login = $logout = false;
if (\strpos($whoami, '/login') !== false)
   $login = true;
if (\strpos($whoami, '/logout') !== false)
   $logout = true;

$token = new GoogleToken();
$client = new GoogleHttpClient($token, true);   // enable log
$loginUri = GoogleToken::loginUri;
$logoutUri = GoogleToken::logoutUri;

/* emulates...
 * LOGIN endpoint
 * --------------
 */
if ($login) {
  if (isset($_GET['code'])) {
      if ($client->authenticate($_GET['code']) {
         $_SESSION['google']['auth'] = $auth;
         $_SESSION['google']['auth']['expires_in'] = \date("Y-m-d H:i:s", \time() + $_SESSION['google']['auth']['expires_in']);
         $_SESSION['google']['user'] = $user;
      }
   }
   header('Location: ' . filter_var('http://localhost', FILTER_SANITIZE_URL));
}

/* emulates...
 * LOGOUT endpoint
 * ---------------
 */
if ($logout) {
  $client->revokeToken($_SESSION['google']['auth']['access_token']);
  unset($_SESSION['google']);
  header('Location: ' . filter_var('http://localhost', FILTER_SANITIZE_URL));
}

$userData = '';

/* emulates...
 * Redirect from:
 * --------------
 * - LOGIN endpoint
 */
if (isset($_SESSION['google'])) {
   if(\time() > \strtotime($_SESSION['google']['auth']['expires_in'])) {
      if(!$client->refresh($_SESSION['google']['auth']['access_token'])) {
         $client->revokeToken();
         unset($_SESSION['google']);
         header('Location: ' . filter_var('http://localhost', FILTER_SANITIZE_URL));
      }
   }
   $userData = $_SESSION['google']['user'];

/* emulates...
 * INITIAL endpoint
 * ----------------
 * or,
 * Redirect from:
 * --------------
 * - LOGOUT endpoint or,
 * - failed REFRESH
 */
} else {
   $authUrl = $client->createAuthUrl();
}

$out0 = <<<EOT
<html>
   <head> 
      <title>Google REST OAuth v.2.0 Login test</title>
   </head>
   <body>
EOT;
if (isset($authUrl)) {
   $d = \urldecode($authUrl);
   $out0 .= <<<EOT
      <p>decoded authUrl = '{$d}'</p>
EOT;
}
$out0 .= <<<EOT
      <h2>PHP Google OAuth 2.0 Login</h2>
EOT;
if (isset($authUrl)) {
   $out0 .= <<<EOT
      <p><a href="{$authUrl}">Login with Google API</a></p>
EOT;
} else {
    $out0 .= <<<EOT
      <p>Welcome <img src="{$userData['picture']}" style="width:50px;height:50px;"></img> {$userData['name']}.</p>
      <p>Your email: {$userData['email']}</p>
      <p><a href={$logoutUri}>Logout</a></p>
EOT;
}
$e = \nl2br(\htmlspecialchars(\print_r($_SESSION, true)));
$out0 .= <<<EOT
      <p><h3>SESSION:</h3></p>
      <p>{$e}</p>
   </body>
</html>
EOT;
echo $out0;

As we can see class GoogleHttpClient makes all the work but file test.php in reality should be replaced by another layer that handles our endpoints/requests on top of GoogleHttpClient.

Have fun!

centurian
  • 1,168
  • 13
  • 25
  • Google endpoints there: https://accounts.google.com/.well-known/openid-configuration – centurian Nov 07 '17 at 15:18
  • boolshit by Google, don't use it!!!: https://accounts.google.com/.well-known/openid-configuration – centurian Nov 13 '17 at 17:30
  • Google's return user info has key `sub` instead `id`. Should change `$user['id']` with `$user['sub']` at method `GoogleHttpClient.php::verify()` – centurian Nov 13 '17 at 19:03