From f73456c462805a7bb78c4a44396513467a96a94e Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Wed, 5 Jun 2024 00:13:31 +0200 Subject: [PATCH] Test to add GMail as AdditionalAccount but it fails due to missing cookies --- plugins/login-gmail/LoginOAuth2.js | 13 +- plugins/login-gmail/index.php | 285 +++++++++++++++++++---------- 2 files changed, 200 insertions(+), 98 deletions(-) diff --git a/plugins/login-gmail/LoginOAuth2.js b/plugins/login-gmail/LoginOAuth2.js index efee7ad60..d1781c2a4 100644 --- a/plugins/login-gmail/LoginOAuth2.js +++ b/plugins/login-gmail/LoginOAuth2.js @@ -1,10 +1,10 @@ (rl => { const client_id = rl.pluginSettingsGet('login-gmail', 'client_id'), - login = () => { + login = mode => { document.location = 'https://accounts.google.com/o/oauth2/auth?' + (new URLSearchParams({ response_type: 'code', client_id: client_id, - redirect_uri: document.location.href + '?LoginGMail', + redirect_uri: document.location.href + '?' + mode, scope: [ // Primary Google Account email address 'https://www.googleapis.com/auth/userinfo.email', @@ -36,10 +36,17 @@ container = e.detail.viewModelDom.querySelector('#plugin-Login-BottomControlGroup'), btn = Element.fromHTML(''), div = Element.fromHTML('
'); - btn.onclick = login; + btn.onclick = ()=>login('LoginGMail'); div.append(btn); container && container.append(div); } + if ('PopupsAccount' === e.detail.viewModelTemplateID) { + const + container = e.detail.viewModelDom.querySelector('footer'), + btn = Element.fromHTML(''); + btn.onclick = ()=>login('AddGMail'); + container && container.append(btn); + } }); } diff --git a/plugins/login-gmail/index.php b/plugins/login-gmail/index.php index ccbd6fe2d..9ce96fe3d 100644 --- a/plugins/login-gmail/index.php +++ b/plugins/login-gmail/index.php @@ -34,6 +34,7 @@ class LoginGMailPlugin extends \RainLoop\Plugins\AbstractPlugin $this->addHook('sieve.before-login', 'clientLogin'); $this->addPartHook('LoginGMail', 'ServiceLoginGMail'); + $this->addPartHook('AddGMail', 'ServiceAddGMail'); // Prevent Disallowed Sec-Fetch Dest: document Mode: navigate Site: cross-site User: true $this->addHook('filter.http-paths', 'httpPaths'); @@ -47,81 +48,20 @@ class LoginGMailPlugin extends \RainLoop\Plugins\AbstractPlugin \trim($oConfig->Get('security', 'secfetch_allow', '') . ';site=cross-site', ';') ); } + if (!empty($aPaths[0]) && 'AddGMail' === $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 - ]; - + $aUserInfo = $this->gmailResponse('LoginGMail'); $oPassword = new \SnappyMail\SensitiveString($aUserInfo['id']); $oAccount = $oActions->LoginProcess($aUserInfo['email'], $oPassword); // $oAccount = MainAccount::NewInstanceFromCredentials($oActions, $aUserInfo['email'], $aUserInfo['email'], $oPassword, true); @@ -141,6 +81,50 @@ class LoginGMailPlugin extends \RainLoop\Plugins\AbstractPlugin exit; } + /** + * Add as additional account + */ + public function ServiceAddGMail() : string + { + $oActions = \RainLoop\Api::Actions(); + try + { + if (empty($_COOKIE)) { + // Failed in Firefox + exit; + } + + $oMainAccount = $oActions->getMainAccountFromToken(); + $aAccounts = $oActions->GetAccounts($oMainAccount); + + $aUserInfo = $this->gmailResponse('AddGMail'); + $sEmail = $aUserInfo['email']; + if ($oMainAccount->Email() === $sEmail || isset($aAccounts[$sEmail])) { + throw new ClientException(Notifications::AccountAlreadyExists); + } + + $oPassword = new \SnappyMail\SensitiveString('oauth2'); + $oAccount = $oActions->LoginProcess($aUserInfo['email'], $oPassword, false); + if ($oAccount) { + $aAccount = $oAccount->asTokenArray($oMainAccount); + $aAccount['name'] = ''; + $oActions->LocalStorageProvider()->Put($oAccount, StorageType::CONFIG, 'oauth2', + \SnappyMail\Crypt::EncryptToJSON(static::$auth, $oAccount->CryptKey()) + ); + $aAccounts[$sEmail] = $aAccount; + $this->SetAccounts($oMainAccount, $aAccounts); + } + } + catch (\Exception $oException) + { + print_r($oException); + exit($oException->getMessage()); + $oActions->Logger()->WriteException($oException, \LOG_ERR); + } + exit; + $oActions->Location(\RainLoop\Utils::WebPath()); + } + public function configMapping() : array { return [ @@ -158,38 +142,73 @@ class LoginGMailPlugin extends \RainLoop\Plugins\AbstractPlugin 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')) { + if (\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()) + if ($oAccount instanceof MainAccount) { + 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'); + } + } else if ('oauth2' == $oSettings->passphrase) { + // \RainLoop\Model\AdditionalAccount + try { + $aData = static::$auth ?: \SnappyMail\Crypt::DecryptFromJSON( + $oActions->LocalStorageProvider()->Get($oAccount, StorageType::CONFIG, 'oauth2'), + $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->LocalStorageProvider()->Put($oAccount, StorageType::CONFIG, 'oauth2', + \SnappyMail\Crypt::EncryptToJSON($aData, $oAccount->CryptKey()) + ); + } + } + } + $oSettings->passphrase = $aData['access_token']; + \array_unshift($oSettings->SASLMechanisms, 'OAUTHBEARER', 'XOAUTH2'); } - $oSettings->passphrase = $aData['access_token']; - \array_unshift($oSettings->SASLMechanisms, 'OAUTHBEARER', 'XOAUTH2'); } } } @@ -220,4 +239,80 @@ class LoginGMailPlugin extends \RainLoop\Plugins\AbstractPlugin } return null; } + + + protected function gmailResponse(string $query) : array + { + $oActions = \RainLoop\Api::Actions(); + $oHttp = $oActions->Http(); + $oHttp->ServerNoCache(); + + 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().'?'.$query + ) + ); + 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 + ]; + + return $aUserInfo; + } + }