From 143050176b73ba742f9d5de7c8d89ab96da9fd7b Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Thu, 28 Mar 2024 02:44:38 +0100 Subject: [PATCH] Resolve #1520 --- plugins/login-gmail/LoginOAuth2.js | 46 ++ plugins/login-gmail/OAuth2/Client.php | 526 ++++++++++++++++++ .../OAuth2/GrantType/AuthorizationCode.php | 41 ++ .../OAuth2/GrantType/ClientCredentials.php | 25 + .../OAuth2/GrantType/IGrantType.php | 15 + .../login-gmail/OAuth2/GrantType/Password.php | 41 ++ .../OAuth2/GrantType/RefreshToken.php | 34 ++ plugins/login-gmail/OAuth2/LICENSE | 504 +++++++++++++++++ plugins/login-gmail/OAuth2/README.md | 105 ++++ plugins/login-gmail/index.php | 229 ++++++++ 10 files changed, 1566 insertions(+) create mode 100644 plugins/login-gmail/LoginOAuth2.js create mode 100644 plugins/login-gmail/OAuth2/Client.php create mode 100644 plugins/login-gmail/OAuth2/GrantType/AuthorizationCode.php create mode 100644 plugins/login-gmail/OAuth2/GrantType/ClientCredentials.php create mode 100644 plugins/login-gmail/OAuth2/GrantType/IGrantType.php create mode 100644 plugins/login-gmail/OAuth2/GrantType/Password.php create mode 100644 plugins/login-gmail/OAuth2/GrantType/RefreshToken.php create mode 100644 plugins/login-gmail/OAuth2/LICENSE create mode 100644 plugins/login-gmail/OAuth2/README.md create mode 100644 plugins/login-gmail/index.php diff --git a/plugins/login-gmail/LoginOAuth2.js b/plugins/login-gmail/LoginOAuth2.js new file mode 100644 index 000000000..efee7ad60 --- /dev/null +++ b/plugins/login-gmail/LoginOAuth2.js @@ -0,0 +1,46 @@ +(rl => { + const client_id = rl.pluginSettingsGet('login-gmail', 'client_id'), + login = () => { + document.location = 'https://accounts.google.com/o/oauth2/auth?' + (new URLSearchParams({ + response_type: 'code', + client_id: client_id, + redirect_uri: document.location.href + '?LoginGMail', + scope: [ + // Primary Google Account email address + 'https://www.googleapis.com/auth/userinfo.email', + // Personal info + 'https://www.googleapis.com/auth/userinfo.profile', + // Associate personal info + 'openid', + // Access IMAP and SMTP through OAUTH + 'https://mail.google.com/' + ].join(' '), + state: 'gmail', // + rl.settings.app('token') + localStorage.getItem('smctoken') + // Force authorize screen, so we always get a refresh_token + access_type: 'offline', + prompt: 'consent' + })); + }; + + if (client_id) { + addEventListener('sm-user-login', e => { + if (event.detail.get('Email').includes('@gmail.com')) { + e.preventDefault(); + login(); + } + }); + + addEventListener('rl-view-model', e => { + if ('Login' === e.detail.viewModelTemplateID) { + const + container = e.detail.viewModelDom.querySelector('#plugin-Login-BottomControlGroup'), + btn = Element.fromHTML(''), + div = Element.fromHTML('
'); + btn.onclick = login; + div.append(btn); + container && container.append(div); + } + }); + } + +})(window.rl); diff --git a/plugins/login-gmail/OAuth2/Client.php b/plugins/login-gmail/OAuth2/Client.php new file mode 100644 index 000000000..46f34c42f --- /dev/null +++ b/plugins/login-gmail/OAuth2/Client.php @@ -0,0 +1,526 @@ + + * @author Anis Berejeb + * @version 1.3.1-dev + */ +namespace OAuth2; + +class Client +{ + /** + * Different AUTH method + */ + const AUTH_TYPE_URI = 0; + const AUTH_TYPE_AUTHORIZATION_BASIC = 1; + const AUTH_TYPE_FORM = 2; + + /** + * Different Access token type + */ + const ACCESS_TOKEN_URI = 0; + const ACCESS_TOKEN_BEARER = 1; + const ACCESS_TOKEN_OAUTH = 2; + const ACCESS_TOKEN_MAC = 3; + + /** + * Different Grant types + */ + const GRANT_TYPE_AUTH_CODE = 'authorization_code'; + const GRANT_TYPE_PASSWORD = 'password'; + const GRANT_TYPE_CLIENT_CREDENTIALS = 'client_credentials'; + const GRANT_TYPE_REFRESH_TOKEN = 'refresh_token'; + + /** + * HTTP Methods + */ + const HTTP_METHOD_GET = 'GET'; + const HTTP_METHOD_POST = 'POST'; + const HTTP_METHOD_PUT = 'PUT'; + const HTTP_METHOD_DELETE = 'DELETE'; + const HTTP_METHOD_HEAD = 'HEAD'; + const HTTP_METHOD_PATCH = 'PATCH'; + + /** + * HTTP Form content types + */ + const HTTP_FORM_CONTENT_TYPE_APPLICATION = 0; + const HTTP_FORM_CONTENT_TYPE_MULTIPART = 1; + + /** + * Client ID + * + * @var string + */ + protected $client_id = null; + + /** + * Client Secret + * + * @var string + */ + protected $client_secret = null; + + /** + * Client Authentication method + * + * @var int + */ + protected $client_auth = self::AUTH_TYPE_URI; + + /** + * Access Token + * + * @var string + */ + protected $access_token = null; + + /** + * Access Token Type + * + * @var int + */ + protected $access_token_type = self::ACCESS_TOKEN_URI; + + /** + * Access Token Secret + * + * @var string + */ + protected $access_token_secret = null; + + /** + * Access Token crypt algorithm + * + * @var string + */ + protected $access_token_algorithm = null; + + /** + * Access Token Parameter name + * + * @var string + */ + protected $access_token_param_name = 'access_token'; + + /** + * The path to the certificate file to use for https connections + * + * @var string Defaults to . + */ + protected $certificate_file = null; + + /** + * cURL options + * + * @var array + */ + protected $curl_options = array(); + + /** + * Construct + * + * @param string $client_id Client ID + * @param string $client_secret Client Secret + * @param int $client_auth (AUTH_TYPE_URI, AUTH_TYPE_AUTHORIZATION_BASIC, AUTH_TYPE_FORM) + * @param string $certificate_file Indicates if we want to use a certificate file to trust the server. Optional, defaults to null. + * @return void + */ + public function __construct($client_id, $client_secret, $client_auth = self::AUTH_TYPE_URI, $certificate_file = null) + { + if (!extension_loaded('curl')) { + throw new Exception('The PHP exention curl must be installed to use this library.', Exception::CURL_NOT_FOUND); + } + + $this->client_id = $client_id; + $this->client_secret = $client_secret; + $this->client_auth = $client_auth; + $this->certificate_file = $certificate_file; + if (!empty($this->certificate_file) && !is_file($this->certificate_file)) { + throw new InvalidArgumentException('The certificate file was not found', InvalidArgumentException::CERTIFICATE_NOT_FOUND); + } + } + + /** + * Get the client Id + * + * @return string Client ID + */ + public function getClientId() + { + return $this->client_id; + } + + /** + * Get the client Secret + * + * @return string Client Secret + */ + public function getClientSecret() + { + return $this->client_secret; + } + + /** + * getAuthenticationUrl + * + * @param string $auth_endpoint Url of the authentication endpoint + * @param string $redirect_uri Redirection URI + * @param array $extra_parameters Array of extra parameters like scope or state (Ex: array('scope' => null, 'state' => '')) + * @return string URL used for authentication + */ + public function getAuthenticationUrl($auth_endpoint, $redirect_uri, array $extra_parameters = array()) + { + $parameters = array_merge(array( + 'response_type' => 'code', + 'client_id' => $this->client_id, + 'redirect_uri' => $redirect_uri + ), $extra_parameters); + return $auth_endpoint . '?' . http_build_query($parameters, '', '&'); + } + + /** + * getAccessToken + * + * @param string $token_endpoint Url of the token endpoint + * @param string $grant_type Grant Type ('authorization_code', 'password', 'client_credentials', 'refresh_token', or a custom code (@see GrantType Classes) + * @param array $parameters Array sent to the server (depend on which grant type you're using) + * @param array $extra_headers Array of extra headers + * @return array Array of parameters required by the grant_type (CF SPEC) + */ + public function getAccessToken($token_endpoint, $grant_type, array $parameters, array $extra_headers = array()) + { + if (!$grant_type) { + throw new InvalidArgumentException('The grant_type is mandatory.', InvalidArgumentException::INVALID_GRANT_TYPE); + } + $grantTypeClassName = $this->convertToCamelCase($grant_type); + $grantTypeClass = __NAMESPACE__ . '\\GrantType\\' . $grantTypeClassName; + if (!class_exists($grantTypeClass)) { + throw new InvalidArgumentException('Unknown grant type \'' . $grant_type . '\'', InvalidArgumentException::INVALID_GRANT_TYPE); + } + $grantTypeObject = new $grantTypeClass(); + $grantTypeObject->validateParameters($parameters); + if (!defined($grantTypeClass . '::GRANT_TYPE')) { + throw new Exception('Unknown constant GRANT_TYPE for class ' . $grantTypeClassName, Exception::GRANT_TYPE_ERROR); + } + $parameters['grant_type'] = $grantTypeClass::GRANT_TYPE; + $http_headers = $extra_headers; + switch ($this->client_auth) { + case self::AUTH_TYPE_URI: + case self::AUTH_TYPE_FORM: + $parameters['client_id'] = $this->client_id; + $parameters['client_secret'] = $this->client_secret; + break; + case self::AUTH_TYPE_AUTHORIZATION_BASIC: + $parameters['client_id'] = $this->client_id; + $http_headers['Authorization'] = 'Basic ' . base64_encode($this->client_id . ':' . $this->client_secret); + break; + default: + throw new Exception('Unknown client auth type.', Exception::INVALID_CLIENT_AUTHENTICATION_TYPE); + break; + } + + return $this->executeRequest($token_endpoint, $parameters, self::HTTP_METHOD_POST, $http_headers, self::HTTP_FORM_CONTENT_TYPE_APPLICATION); + } + + /** + * setToken + * + * @param string $token Set the access token + * @return void + */ + public function setAccessToken($token) + { + $this->access_token = $token; + } + + /** + * Check if there is an access token present + * + * @return bool Whether the access token is present + */ + public function hasAccessToken() + { + return !!$this->access_token; + } + + /** + * Set the client authentication type + * + * @param string $client_auth (AUTH_TYPE_URI, AUTH_TYPE_AUTHORIZATION_BASIC, AUTH_TYPE_FORM) + * @return void + */ + public function setClientAuthType($client_auth) + { + $this->client_auth = $client_auth; + } + + /** + * Set an option for the curl transfer + * + * @param int $option The CURLOPT_XXX option to set + * @param mixed $value The value to be set on option + * @return void + */ + public function setCurlOption($option, $value) + { + $this->curl_options[$option] = $value; + } + + /** + * Set multiple options for a cURL transfer + * + * @param array $options An array specifying which options to set and their values + * @return void + */ + public function setCurlOptions($options) + { + $this->curl_options = array_merge($this->curl_options, $options); + } + + /** + * Set the access token type + * + * @param int $type Access token type (ACCESS_TOKEN_BEARER, ACCESS_TOKEN_MAC, ACCESS_TOKEN_URI) + * @param string $secret The secret key used to encrypt the MAC header + * @param string $algorithm Algorithm used to encrypt the signature + * @return void + */ + public function setAccessTokenType($type, $secret = null, $algorithm = null) + { + $this->access_token_type = $type; + $this->access_token_secret = $secret; + $this->access_token_algorithm = $algorithm; + } + + /** + * Fetch a protected ressource + * + * @param string $protected_ressource_url Protected resource URL + * @param array $parameters Array of parameters + * @param string $http_method HTTP Method to use (POST, PUT, GET, HEAD, DELETE) + * @param array $http_headers HTTP headers + * @param int $form_content_type HTTP form content type to use + * @return array + */ + public function fetch($protected_resource_url, $parameters = array(), $http_method = self::HTTP_METHOD_GET, array $http_headers = array(), $form_content_type = self::HTTP_FORM_CONTENT_TYPE_MULTIPART) + { + if ($this->access_token) { + switch ($this->access_token_type) { + case self::ACCESS_TOKEN_URI: + if (is_array($parameters)) { + $parameters[$this->access_token_param_name] = $this->access_token; + } else { + throw new InvalidArgumentException( + 'You need to give parameters as array if you want to give the token within the URI.', + InvalidArgumentException::REQUIRE_PARAMS_AS_ARRAY + ); + } + break; + case self::ACCESS_TOKEN_BEARER: + $http_headers['Authorization'] = 'Bearer ' . $this->access_token; + break; + case self::ACCESS_TOKEN_OAUTH: + $http_headers['Authorization'] = 'OAuth ' . $this->access_token; + break; + case self::ACCESS_TOKEN_MAC: + $http_headers['Authorization'] = 'MAC ' . $this->generateMACSignature($protected_resource_url, $parameters, $http_method); + break; + default: + throw new Exception('Unknown access token type.', Exception::INVALID_ACCESS_TOKEN_TYPE); + break; + } + } + return $this->executeRequest($protected_resource_url, $parameters, $http_method, $http_headers, $form_content_type); + } + + /** + * Generate the MAC signature + * + * @param string $url Called URL + * @param array $parameters Parameters + * @param string $http_method Http Method + * @return string + */ + private function generateMACSignature($url, $parameters, $http_method) + { + $timestamp = time(); + $nonce = uniqid(); + $parsed_url = parse_url($url); + if (!isset($parsed_url['port'])) + { + $parsed_url['port'] = ($parsed_url['scheme'] == 'https') ? 443 : 80; + } + if ($http_method == self::HTTP_METHOD_GET) { + if (is_array($parameters)) { + $parsed_url['path'] .= '?' . http_build_query($parameters, '', '&'); + } elseif ($parameters) { + $parsed_url['path'] .= '?' . $parameters; + } + } + + $signature = base64_encode(hash_hmac($this->access_token_algorithm, + $timestamp . "\n" + . $nonce . "\n" + . $http_method . "\n" + . $parsed_url['path'] . "\n" + . $parsed_url['host'] . "\n" + . $parsed_url['port'] . "\n\n" + , $this->access_token_secret, true)); + + return 'id="' . $this->access_token . '", ts="' . $timestamp . '", nonce="' . $nonce . '", mac="' . $signature . '"'; + } + + /** + * Execute a request (with curl) + * + * @param string $url URL + * @param mixed $parameters Array of parameters + * @param string $http_method HTTP Method + * @param array $http_headers HTTP Headers + * @param int $form_content_type HTTP form content type to use + * @return array + */ + private function executeRequest($url, $parameters = array(), $http_method = self::HTTP_METHOD_GET, array $http_headers = null, $form_content_type = self::HTTP_FORM_CONTENT_TYPE_MULTIPART) + { + $curl_options = array( + CURLOPT_RETURNTRANSFER => true, + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_CUSTOMREQUEST => $http_method + ); + + switch($http_method) { + case self::HTTP_METHOD_POST: + $curl_options[CURLOPT_POST] = true; + /* No break */ + case self::HTTP_METHOD_PUT: + case self::HTTP_METHOD_PATCH: + + /** + * Passing an array to CURLOPT_POSTFIELDS will encode the data as multipart/form-data, + * while passing a URL-encoded string will encode the data as application/x-www-form-urlencoded. + * http://php.net/manual/en/function.curl-setopt.php + */ + if(is_array($parameters) && self::HTTP_FORM_CONTENT_TYPE_APPLICATION === $form_content_type) { + $parameters = http_build_query($parameters, '', '&'); + } + $curl_options[CURLOPT_POSTFIELDS] = $parameters; + break; + case self::HTTP_METHOD_HEAD: + $curl_options[CURLOPT_NOBODY] = true; + /* No break */ + case self::HTTP_METHOD_DELETE: + case self::HTTP_METHOD_GET: + if (is_array($parameters) && count($parameters) > 0) { + $url .= '?' . http_build_query($parameters, '', '&'); + } elseif ($parameters) { + $url .= '?' . $parameters; + } + break; + default: + break; + } + + $curl_options[CURLOPT_URL] = $url; + + if (is_array($http_headers)) { + $header = array(); + foreach($http_headers as $key => $parsed_urlvalue) { + $header[] = "$key: $parsed_urlvalue"; + } + $curl_options[CURLOPT_HTTPHEADER] = $header; + } + + $ch = curl_init(); + curl_setopt_array($ch, $curl_options); + // https handling + if (!empty($this->certificate_file)) { + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); + curl_setopt($ch, CURLOPT_CAINFO, $this->certificate_file); + } else { + // bypass ssl verification + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); + } + if (!empty($this->curl_options)) { + curl_setopt_array($ch, $this->curl_options); + } + $result = curl_exec($ch); + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE); + if ($curl_error = curl_error($ch)) { + throw new Exception($curl_error, Exception::CURL_ERROR); + } else { + $json_decode = json_decode($result, true); + } + curl_close($ch); + + return array( + 'result' => (null === $json_decode) ? $result : $json_decode, + 'code' => $http_code, + 'content_type' => $content_type + ); + } + + /** + * Set the name of the parameter that carry the access token + * + * @param string $name Token parameter name + * @return void + */ + public function setAccessTokenParamName($name) + { + $this->access_token_param_name = $name; + } + + /** + * Converts the class name to camel case + * + * @param mixed $grant_type the grant type + * @return string + */ + private function convertToCamelCase($grant_type) + { + $parts = explode('_', $grant_type); + array_walk($parts, function(&$item) { $item = ucfirst($item);}); + return implode('', $parts); + } +} + +class Exception extends \Exception +{ + const CURL_NOT_FOUND = 0x01; + const CURL_ERROR = 0x02; + const GRANT_TYPE_ERROR = 0x03; + const INVALID_CLIENT_AUTHENTICATION_TYPE = 0x04; + const INVALID_ACCESS_TOKEN_TYPE = 0x05; +} + +class InvalidArgumentException extends \InvalidArgumentException +{ + const INVALID_GRANT_TYPE = 0x01; + const CERTIFICATE_NOT_FOUND = 0x02; + const REQUIRE_PARAMS_AS_ARRAY = 0x03; + const MISSING_PARAMETER = 0x04; +} diff --git a/plugins/login-gmail/OAuth2/GrantType/AuthorizationCode.php b/plugins/login-gmail/OAuth2/GrantType/AuthorizationCode.php new file mode 100644 index 000000000..f3436e4c5 --- /dev/null +++ b/plugins/login-gmail/OAuth2/GrantType/AuthorizationCode.php @@ -0,0 +1,41 @@ + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random + Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/plugins/login-gmail/OAuth2/README.md b/plugins/login-gmail/OAuth2/README.md new file mode 100644 index 000000000..9de3570e0 --- /dev/null +++ b/plugins/login-gmail/OAuth2/README.md @@ -0,0 +1,105 @@ +# Light PHP wrapper for the OAuth 2.0 + +[![Latest Stable Version](https://poser.pugx.org/adoy/fastcgi-client/v/stable)](https://packagist.org/packages/adoy/fastcgi-client) +[![GitHub](https://img.shields.io/github/license/adoy/PHP-OAuth2)](LICENSE) +[![Total Downloads](https://poser.pugx.org/adoy/fastcgi-client/downloads)](https://packagist.org/packages/adoy/fastcgi-client) + + +## How can I use it ? + +```php +getAuthenticationUrl(AUTHORIZATION_ENDPOINT, REDIRECT_URI); + header('Location: ' . $auth_url); + die('Redirect'); +} +else +{ + $params = array('code' => $_GET['code'], 'redirect_uri' => REDIRECT_URI); + $response = $client->getAccessToken(TOKEN_ENDPOINT, 'authorization_code', $params); + parse_str($response['result'], $info); + $client->setAccessToken($info['access_token']); + $response = $client->fetch('https://graph.facebook.com/me'); + var_dump($response, $response['result']); +} +``` + +## How can I add a new Grant Type ? + +Simply write a new class in the namespace OAuth2\GrantType. You can place the class file under GrantType. +Here is an example : + +```php +getAccessToken(TOKEN_ENDPOINT, 'my_custom_grant_type', $params); +``` + +## LICENSE + +This Code is released under the GNU LGPL + +Please do not change the header of the file(s). + +This library is free software; you can redistribute it and/or modify it +under the terms of the GNU Lesser General Public License as published +by the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This library is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +or FITNESS FOR A PARTICULAR PURPOSE. + +See the GNU Lesser General Public License for more details. diff --git a/plugins/login-gmail/index.php b/plugins/login-gmail/index.php new file mode 100644 index 000000000..706a8e738 --- /dev/null +++ b/plugins/login-gmail/index.php @@ -0,0 +1,229 @@ +UseLangs(true); + $this->addJs('LoginOAuth2.js'); + $this->addHook('imap.before-login', 'clientLogin'); + $this->addHook('smtp.before-login', 'clientLogin'); + $this->addHook('sieve.before-login', 'clientLogin'); + +// set_include_path(get_include_path() . PATH_SEPARATOR . __DIR__); + spl_autoload_register(function($classname){ + if (str_starts_with($classname, 'OAuth2\\')) { + include_once __DIR__ . strtr("\\{$classname}", '\\', DIRECTORY_SEPARATOR) . '.php'; + } + }); + + $this->addPartHook('LoginGMail', 'ServiceLoginGMail'); + + // Prevent Disallowed Sec-Fetch Dest: document Mode: navigate Site: cross-site User: true + $this->addHook('filter.http-paths', 'httpPaths'); + } + + public function httpPaths(array $aPaths) : void + { + if (!empty($aPaths[0]) && 'LoginGMail' === $aPaths[0]) { + $oConfig = \RainLoop\Api::Config(); + $oConfig->Set('security', 'secfetch_allow', + \trim($oConfig->Get('security', 'secfetch_allow', '') . ';site=cross-site', ';') + ); + } + } + + public function ServiceLoginGMail() : string + { + $oActions = \RainLoop\Api::Actions(); + $oHttp = $oActions->Http(); + $oHttp->ServerNoCache(); + + try + { + if (isset($_GET['error'])) { + throw new \RuntimeException($_GET['error']); + } + if (!isset($_GET['code']) || empty($_GET['state']) || 'gmail' !== $_GET['state']) { + $oActions->Location(\RainLoop\Utils::WebPath()); + exit; + } + $oGMail = $this->gmailConnector(); + if (!$oGMail) { + $oActions->Location(\RainLoop\Utils::WebPath()); + exit; + } + + $iExpires = \time(); + $aResponse = $oGMail->getAccessToken( + static::TOKEN_URI, + 'authorization_code', + array( + 'code' => $_GET['code'], + 'redirect_uri' => $oHttp->GetFullUrl().'?LoginGMail' + ) + ); + if (200 != $aResponse['code']) { + if (isset($aResponse['result']['error'])) { + throw new \RuntimeException( + $aResponse['code'] + . ': ' + . $aResponse['result']['error'] + . ' / ' + . $aResponse['result']['error_description'] + ); + } + throw new \RuntimeException("HTTP: {$aResponse['code']}"); + } + $aResponse = $aResponse['result']; + if (empty($aResponse['access_token'])) { + throw new \RuntimeException('access_token missing'); + } + if (empty($aResponse['refresh_token'])) { + throw new \RuntimeException('refresh_token missing'); + } + + $sAccessToken = $aResponse['access_token']; + $iExpires += $aResponse['expires_in']; + + $oGMail->setAccessToken($sAccessToken); + $aUserInfo = $oGMail->fetch('https://www.googleapis.com/oauth2/v2/userinfo'); + if (200 != $aUserInfo['code']) { + throw new \RuntimeException("HTTP: {$aResponse['code']}"); + } + $aUserInfo = $aUserInfo['result']; + if (empty($aUserInfo['id'])) { + throw new \RuntimeException('unknown id'); + } + if (empty($aUserInfo['email'])) { + throw new \RuntimeException('unknown email address'); + } + + static::$auth = [ + 'access_token' => $sAccessToken, + 'refresh_token' => $aResponse['refresh_token'], + 'expires_in' => $aResponse['expires_in'], + 'expires' => $iExpires + ]; + + $oPassword = new \SnappyMail\SensitiveString($aUserInfo['id']); + $oAccount = $oActions->LoginProcess($aUserInfo['email'], $oPassword); +// $oAccount = MainAccount::NewInstanceFromCredentials($oActions, $aUserInfo['email'], $aUserInfo['email'], $oPassword, true); + if ($oAccount) { +// $oActions->SetMainAuthAccount($oAccount); +// $oActions->SetAuthToken($oAccount); + $oActions->StorageProvider()->Put($oAccount, StorageType::SESSION, \RainLoop\Utils::GetSessionToken(), + \SnappyMail\Crypt::EncryptToJSON(static::$auth, $oAccount->CryptKey()) + ); + } + } + catch (\Exception $oException) + { + $oActions->Logger()->WriteException($oException, \LOG_ERR); + } + $oActions->Location(\RainLoop\Utils::WebPath()); + exit; + } + + public function configMapping() : array + { + return [ + \RainLoop\Plugins\Property::NewInstance('client_id') + ->SetLabel('Client ID') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING) + ->SetAllowedInJs(), + \RainLoop\Plugins\Property::NewInstance('client_secret') + ->SetLabel('Client Secret') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING) + ->SetEncrypted() + ]; + } + + public function clientLogin(\RainLoop\Model\Account $oAccount, \MailSo\Net\NetClient $oClient, \MailSo\Net\ConnectSettings $oSettings) : void + { + if ($oAccount instanceof MainAccount && \str_ends_with($oAccount->Email(), '@gmail.com')) { + $oActions = \RainLoop\Api::Actions(); + try { + $aData = static::$auth ?: \SnappyMail\Crypt::DecryptFromJSON( + $oActions->StorageProvider()->Get($oAccount, StorageType::SESSION, \RainLoop\Utils::GetSessionToken()), + $oAccount->CryptKey() + ); + } catch (\Throwable $oException) { +// $oActions->Logger()->WriteException($oException, \LOG_ERR); + return; + } + if (!empty($aData['expires']) && !empty($aData['access_token']) && !empty($aData['refresh_token'])) { + if (\time() >= $aData['expires']) { + $iExpires = \time(); + $oGMail = $this->gmailConnector(); + if ($oGMail) { + $aRefreshTokenResponse = $oGMail->getAccessToken( + static::TOKEN_URI, + 'refresh_token', + array('refresh_token' => $aData['refresh_token']) + ); + if (!empty($aRefreshTokenResponse['result']['access_token'])) { + $aData['access_token'] = $aRefreshTokenResponse['result']['access_token']; + $aResponse['expires'] = $iExpires + $aResponse['expires_in']; + $oActions->StorageProvider()->Put($oAccount, StorageType::SESSION, \RainLoop\Utils::GetSessionToken(), + \SnappyMail\Crypt::EncryptToJSON($aData, $oAccount->CryptKey()) + ); + } + } + } + $oSettings->passphrase = $aData['access_token']; + \array_unshift($oSettings->SASLMechanisms, 'OAUTHBEARER', 'XOAUTH2'); + } + } + } + + protected function gmailConnector() : ?\OAuth2\Client + { + $client_id = \trim($this->Config()->Get('plugin', 'client_id', '')); + $client_secret = \trim($this->Config()->getDecrypted('plugin', 'client_secret', '')); + if ($client_id && $client_secret) { + try + { + $oGMail = new \OAuth2\Client($client_id, $client_secret); + $oActions = \RainLoop\Api::Actions(); + $sProxy = $oActions->Config()->Get('labs', 'curl_proxy', ''); + if (\strlen($sProxy)) { + $oGMail->setCurlOption(CURLOPT_PROXY, $sProxy); + $sProxyAuth = $oActions->Config()->Get('labs', 'curl_proxy_auth', ''); + if (\strlen($sProxyAuth)) { + $oGMail->setCurlOption(CURLOPT_PROXYUSERPWD, $sProxyAuth); + } + } + return $oGMail; + } + catch (\Exception $oException) + { + $oActions->Logger()->WriteException($oException, \LOG_ERR); + } + } + return null; + } +}