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;
+ }
+}