From 070fc14c4ff273fe313c18f5bbb5c9f312847814 Mon Sep 17 00:00:00 2001 From: djmaze Date: Fri, 12 Nov 2021 10:29:58 +0100 Subject: [PATCH] OAuth2 draft code made with the old RainLoop code --- plugins/login-oauth2/LoginOAuth2.js | 19 ++ plugins/login-oauth2/index.php | 323 ++++++++++++++++++++++++++++ 2 files changed, 342 insertions(+) create mode 100644 plugins/login-oauth2/LoginOAuth2.js create mode 100644 plugins/login-oauth2/index.php diff --git a/plugins/login-oauth2/LoginOAuth2.js b/plugins/login-oauth2/LoginOAuth2.js new file mode 100644 index 000000000..f39bbf12e --- /dev/null +++ b/plugins/login-oauth2/LoginOAuth2.js @@ -0,0 +1,19 @@ +(rl => { + if (rl) { + addEventListener('rl-view-model', e => { + // instanceof LoginUserView + if (e.detail && 'Login' === e.detail.viewModelTemplateID) { + const LoginUserView = e.detail, + submitCommand = LoginUserView.submitCommand; + LoginUserView.submitCommand = (self, event) => { + if (LoginUserView.email().includes('@gmail.com')) { + // TODO: redirect to google + } else { + submitCommand.call(LoginUserView, self, event); + } + }; + } + }); + } + +})(window.rl); diff --git a/plugins/login-oauth2/index.php b/plugins/login-oauth2/index.php new file mode 100644 index 000000000..5480aca2c --- /dev/null +++ b/plugins/login-oauth2/index.php @@ -0,0 +1,323 @@ +UseLangs(true); + $this->addJs('LoginOAuth2.js'); +// $this->addHook('imap.before-connect', array($this, $oImapClient, &$aCredentials)); +// $this->addHook('imap.after-connect', array($this, $oImapClient, $aCredentials)); + $this->addHook('imap.before-login', 'clientLogin'); +// $this->addHook('imap.after-login', array($this, $oImapClient, $aCredentials)); +// $this->addHook('smtp.before-connect', array($this, $oSmtpClient, &$aCredentials)); +// $this->addHook('smtp.after-connect', array($this, $oSmtpClient, $aCredentials)); + $this->addHook('smtp.before-login', 'clientLogin'); +// $this->addHook('smtp.after-login', array($this, $oSmtpClient, $aCredentials)); +// $this->addHook('sieve.before-connect', array($this, $oSieveClient, &$aCredentials)); +// $this->addHook('sieve.after-connect', array($this, $oSieveClient, $aCredentials)); + $this->addHook('sieve.before-login', 'clientLogin'); +// $this->addHook('sieve.after-login', array($this, $oSieveClient, $aCredentials)); + $this->addHook('filter.account', 'filterAccount'); + } + + public function configMapping() : array + { + return [ + \RainLoop\Plugins\Property::NewInstance('client_id') + ->SetLabel('Client ID') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING), + \RainLoop\Plugins\Property::NewInstance('client_secret') + ->SetLabel('Client Secret') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING), + ]; + } + + public function clientLogin(\RainLoop\Model\Account $oAccount, \MailSo\Net\NetClient $oClient, array &$aCredentials) : void + { + $sPassword = $aCredentials['Password']; + $iGatLen = \strlen(static::GMAIL_TOKENS_PREFIX); + if ($sPassword && static::GMAIL_TOKENS_PREFIX === \substr($sPassword, 0, $iGatLen)) { + $aTokens = \json_decode(\substr($sPassword, $iGatLen)); + $sAccessToken = !empty($aTokens[0]) ? $aTokens[0] : ''; + $sRefreshToken = !empty($aTokens[1]) ? $aTokens[1] : ''; + } + if ($sAccessToken && $sRefreshToken) { + $aCredentials['Password'] = $this->gmailRefreshToken($sAccessToken, $sRefreshToken); + $aCredentials['UseAuthOAuth2IfSupported'] = true; + } + } + + public function filterAccount(\RainLoop\Model\Account $oAccount) : void + { + if ($oAccount instanceof \RainLoop\Model\MainAccount) { + /** + * TODO + * Because password rotates, so does the CryptKey. + * So we need to securely save a cryptkey. + * Encrypted using the old/new refresh token is an option: + * 1. decrypt cryptkey with the old refresh token + * 2. $oAccount->SetCryptKey('cryptkey') + * 3. encrypt cryptkey with the new refresh token + */ + } + } + + protected function loginProcess(&$oAccount, $sEmail, $sPassword) : int + { + $oActions = \RainLoop::Actions(); + $iErrorCode = \RainLoop\Notifications::UnknownError; + try + { + $oAccount = $oActions->LoginProcess($sEmail, $sPassword, '', '', false, true); + if ($oAccount instanceof \RainLoop\Model\Account) { + $oActions->AuthToken($oAccount); + $iErrorCode = 0; + } else { + $oAccount = null; + $iErrorCode = \RainLoop\Notifications::AuthError; + } + } + catch (\RainLoop\Exceptions\ClientException $oException) + { + $iErrorCode = $oException->getCode(); + } + catch (\Exception $oException) + { + unset($oException); + $iErrorCode = \RainLoop\Notifications::UnknownError; + } + + return $iErrorCode; + } + + /** + * GMail + */ + + protected static function gmailTokensPassword($sAccessToken, $sRefreshToken) : string + { + return static::GMAIL_TOKENS_PREFIX . \json_encode(array($sAccessToken, $sRefreshToken)); + } + + protected function gmailStoreTokens($oCache, $sAccessToken, $sRefreshToken) : void + { + $sCacheKey = 'tokens='.\sha1($sRefreshToken); + $oCache->Set($sCacheKey, $sAccessToken); + $oCache->SetTimer($sCacheKey); + } + + protected function gmailRefreshToken($sAccessToken, $sRefreshToken) : string + { + $oActions = \RainLoop::Actions(); + $oAccount = $oActions->getAccountFromToken(false); + $oDomain = $oAccount->Domain(); + $oLogger = $oImapClient->Logger(); + if ($oAccount && $oActions->GetIsJson()) { + $oCache = $oActions->Cacher($oAccount); + $sCacheKey = 'tokens='.\sha1($sRefreshToken); + + $sCachedAccessToken = $oCache->Get($sCacheKey); + $iTime = $oCache->GetTimer($sCacheKey); + + if (!$sCachedAccessToken || !$iTime) { + $this->gmailStoreTokens($oCache, $sAccessToken, $sRefreshToken); + } else if (\time() - 1200 > $iTime) { // 20min + $oGMail = $this->gmailConnector(); + if ($oGMail) { + $aRefreshTokenResponse = $oGMail->getAccessToken( + static::TOKEN_URI, + 'refresh_token', + array('refresh_token' => $sRefreshToken) + ); + if (!empty($aRefreshTokenResponse['result']['access_token'])) { + $sCachedAccessToken = $aRefreshTokenResponse['result']['access_token']; + $this->gmailStoreTokens($oCache, $sCachedAccessToken, $sRefreshToken); +/* + $oAccount->SetPassword(static::gmailTokensPassword($sCachedAccessToken, $sRefreshToken)); + $oActions->AuthToken($oAccount); +// $oActions->SetUpdateAuthToken($oActions->GetSpecAuthToken()); + $oActions->sUpdateAuthToken = $oActions->GetSpecAuthToken(); + $sUpdateToken = $oActions->GetUpdateAuthToken(); + if ($sUpdateToken) { + $aResponseItem['UpdateToken'] = $sUpdateToken; + } +*/ + } + } + } + + if ($sCachedAccessToken) { + return $sCachedAccessToken; + } + } + + return $sAccessToken; + } + + protected function gmailConnector() : ?\OAuth2\Client + { + $oGMail = null; + $oActions = \RainLoop::Actions(); + $client_id = \trim($this->Config()->Get('plugin', 'client_id', '')); + $client_secret = \trim($this->Config()->Get('plugin', 'client_secret', '')); + if ($client_id && $client_secret) { + include_once APP_VERSION_ROOT_PATH.'app/libraries/PHP-OAuth2/Client.php'; + include_once APP_VERSION_ROOT_PATH.'app/libraries/PHP-OAuth2/GrantType/IGrantType.php'; + include_once APP_VERSION_ROOT_PATH.'app/libraries/PHP-OAuth2/GrantType/AuthorizationCode.php'; + include_once APP_VERSION_ROOT_PATH.'app/libraries/PHP-OAuth2/GrantType/RefreshToken.php'; + + try + { + $oGMail = new \OAuth2\Client($client_id, $client_secret); + $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); + } + } + } + catch (\Exception $oException) + { + $oActions->Logger()->WriteException($oException, \MailSo\Log\Enumerations\Type::ERROR); + } + } + + return $oGMail; + } + + protected function gmailPopupService() : string + { + $sLoginUrl = ''; + $oAccount = null; + $oActions = \RainLoop::Actions(); + $oHttp = $oActions->Http(); + + $bLogin = false; + $iErrorCode = \RainLoop\Notifications::UnknownError; + + try + { + $oGMail = $this->gmailConnector(); + if ($oHttp->HasQuery('error')) { + $iErrorCode = ('access_denied' === $oHttp->GetQuery('error')) ? + \RainLoop\Notifications::SocialGMailLoginAccessDisable : \RainLoop\Notifications::UnknownError; + } else if ($oGMail) { + $oAccount = $oActions->GetAccount(); + $bLogin = !$oAccount; + + $sCheckToken = ''; + $sCheckAuth = ''; + $sState = $oHttp->GetQuery('state'); + if (!empty($sState)) { + $aParts = explode('|', $sState, 3); + $sCheckToken = !empty($aParts[1]) ? $aParts[1] : ''; + $sCheckAuth = !empty($aParts[2]) ? $aParts[2] : ''; + } + + $sRedirectUrl = $oHttp->GetFullUrl().'?SocialGMail'; + if (!$oHttp->HasQuery('code')) { + $aParams = array( + 'scope' => \trim(\implode(' ', array( + 'https://www.googleapis.com/auth/userinfo.email', + 'https://www.googleapis.com/auth/userinfo.profile', + 'https://mail.google.com/' + ))), + 'state' => '1|'.\RainLoop\Utils::GetConnectionToken().'|'.$oActions->GetSpecAuthToken(), + 'response_type' => 'code' + ); + + $aParams['access_type'] = 'offline'; + // $aParams['prompt'] = 'consent'; + + $sLoginUrl = $oGMail->getAuthenticationUrl(static::LOGIN_URI, $sRedirectUrl, $aParams); + } else if (!empty($sState) && $sCheckToken === \RainLoop\Utils::GetConnectionToken()) { + if (!empty($sCheckAuth)) { + $oActions->SetSpecAuthToken($sCheckAuth); + $oAccount = $oActions->GetAccount(); + $bLogin = !$oAccount; + } + + $aAuthorizationCodeResponse = $oGMail->getAccessToken( + static::TOKEN_URI, + 'authorization_code', + array( + 'code' => $oHttp->GetQuery('code'), + 'redirect_uri' => $sRedirectUrl + ) + ); + + $sAccessToken = !empty($aAuthorizationCodeResponse['result']['access_token']) ? $aAuthorizationCodeResponse['result']['access_token'] : ''; + $sRefreshToken = !empty($aAuthorizationCodeResponse['result']['refresh_token']) ? $aAuthorizationCodeResponse['result']['refresh_token'] : ''; + + if (!empty($sAccessToken)) { + $oGMail->setAccessToken($sAccessToken); + $aUserInfoResponse = $oGMail->fetch('https://www.googleapis.com/oauth2/v2/userinfo'); + + if (!empty($aUserInfoResponse['result']['id'])) { + if ($bLogin) { + $aUserData = null; + if (!empty($aUserInfoResponse['result']['email'])) { + $aUserData = array( + 'Email' => $aUserInfoResponse['result']['email'], + 'Password' => static::gmailTokensPassword($sAccessToken, $sRefreshToken) + ); + } + + if ($aUserData && \is_array($aUserData) && !empty($aUserData['Email']) && isset($aUserData['Password'])) { + $iErrorCode = $this->loginProcess($oAccount, $aUserData['Email'], $aUserData['Password']); + } else { + $iErrorCode = \RainLoop\Notifications::SocialGMailLoginAccessDisable; + } + } + } + } + } + } + } + catch (\Exception $oException) + { + $oActions->Logger()->WriteException($oException, \MailSo\Log\Enumerations\Type::ERROR); + } + + $oActions = \RainLoop::Actions(); + $oActions->Http()->ServerNoCache(); + \header('Content-Type: text/html; charset=utf-8'); + $sHtml = \file_get_contents(APP_VERSION_ROOT_PATH.'app/templates/Social.html'); + if ($sLoginUrl) { + $sHtml = \strtr($sHtml, array( + '{{RefreshMeta}}' => '', + '{{Script}}' => '' + )); + } else { + $sCallBackType = $bLogin ? '_login' : ''; + $sConnectionFunc = 'rl_'.\md5(\RainLoop\Utils::GetConnectionToken()).'_gmail'.$sCallBackType.'_service'; + $sHtml = \strtr($sHtml, array( + '{{RefreshMeta}}' => '', + '{{Script}}' => '' + )); + } + + $bAppCssDebug = $oActions->Config()->Get('labs', 'use_app_debug_css', false); + return \strtr($sHtml, array( + '{{Stylesheet}}' => $oActions->StaticPath('css/social'.($bAppCssDebug ? '' : '.min').'.css'), + '{{Icon}}' => 'gmail' + )); + } +}