mirror of
https://github.com/the-djmaze/snappymail.git
synced 2025-01-02 21:12:02 +08:00
OAuth2 draft code made with the old RainLoop code
This commit is contained in:
parent
f14ebc6578
commit
070fc14c4f
2 changed files with 342 additions and 0 deletions
19
plugins/login-oauth2/LoginOAuth2.js
Normal file
19
plugins/login-oauth2/LoginOAuth2.js
Normal file
|
@ -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);
|
323
plugins/login-oauth2/index.php
Normal file
323
plugins/login-oauth2/index.php
Normal file
|
@ -0,0 +1,323 @@
|
|||
<?php
|
||||
|
||||
class LoginOAuth2Plugin extends \RainLoop\Plugins\AbstractPlugin
|
||||
{
|
||||
const
|
||||
NAME = 'OAuth2',
|
||||
VERSION = '1.0',
|
||||
RELEASE = '2021-11-12',
|
||||
REQUIRED = '2.9.1',
|
||||
CATEGORY = 'Login',
|
||||
DESCRIPTION = 'IMAP, Sieve & SMTP login using OAuth2';
|
||||
|
||||
const
|
||||
LOGIN_URI = 'https://accounts.google.com/o/oauth2/auth',
|
||||
TOKEN_URI = 'https://accounts.google.com/o/oauth2/token',
|
||||
GMAIL_TOKENS_PREFIX = ':GAT:';
|
||||
|
||||
public function Init() : void
|
||||
{
|
||||
$this->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}}' => '<meta http-equiv="refresh" content="0; URL='.$sLoginUrl.'" />',
|
||||
'{{Script}}' => ''
|
||||
));
|
||||
} else {
|
||||
$sCallBackType = $bLogin ? '_login' : '';
|
||||
$sConnectionFunc = 'rl_'.\md5(\RainLoop\Utils::GetConnectionToken()).'_gmail'.$sCallBackType.'_service';
|
||||
$sHtml = \strtr($sHtml, array(
|
||||
'{{RefreshMeta}}' => '',
|
||||
'{{Script}}' => '<script data-cfasync="false">opener && opener.'.$sConnectionFunc.' && opener.'.
|
||||
$sConnectionFunc.'('.$iErrorCode.'); self && self.close && self.close();</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'
|
||||
));
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue