diff --git a/Api/Data/ServiceInterface.php b/Api/Data/ServiceInterface.php index 3e011b5..1b7ba41 100644 --- a/Api/Data/ServiceInterface.php +++ b/Api/Data/ServiceInterface.php @@ -16,7 +16,7 @@ interface ServiceInterface const PORT = 'port'; const SECURE = 'secure'; const AUTH = 'auth'; - const TOKEN = 'token'; + const TOKEN_ID = 'token_id'; const KEY = 'key'; const REMOVE = 'remove'; @@ -120,7 +120,7 @@ public function getAuth(); /** * @return mixed */ - public function getToken(); + public function getTokenId(); /** * Get key @@ -225,10 +225,10 @@ public function setSecure($secure); public function setAuth($auth); /** - * @param string $token + * @param int $tokenId * @return \Swissup\Email\Api\Data\ServiceInterface */ - public function setToken($token); + public function setTokenId($tokenId); /** * Set key diff --git a/Controller/Adminhtml/Email/Service/Save.php b/Controller/Adminhtml/Email/Service/Save.php index 162217d..444aa14 100644 --- a/Controller/Adminhtml/Email/Service/Save.php +++ b/Controller/Adminhtml/Email/Service/Save.php @@ -76,8 +76,8 @@ public function execute() $this->session->setFormData(false); if ($model->hasData('callback_url')) { - $flowUrl = $model->getData('callback_url'); - return $resultRedirect->setUrl($flowUrl); + $callbackUrl = $model->getData('callback_url'); + return $resultRedirect->setUrl($callbackUrl); } if ($this->getRequest()->getParam('back')) { diff --git a/Controller/Gmail/GetOAuth2Token.php b/Controller/Gmail/GetOAuth2Token.php deleted file mode 100644 index 5b4ae28..0000000 --- a/Controller/Gmail/GetOAuth2Token.php +++ /dev/null @@ -1,155 +0,0 @@ -session = $session; - $this->serviceRepository = $serviceRepository; - $this->urlDecoder = $urldecoder; - $this->tokenValidator = $tokenValidator; - } - - private function isLoggedIn() - { - $request = $this->getRequest(); - $serviceId = $request->getParam('id', false); - $sessionIdKey = self::SESSION_ID_KEY; - - if (!empty($serviceId)) { - if ($this->tokenValidator->validateRequest($request)) { - $this->session->setData($sessionIdKey, $serviceId); - } else { - $this->session->setData($sessionIdKey, null); - } - } - - return (bool) $this->session->getData($sessionIdKey); - } - - private function getServiceId() - { - $sessionIdKey = self::SESSION_ID_KEY; - return (int) $this->session->getData($sessionIdKey); - } - - private function redirectReferer() - { - $refererUrl = $this->session->getData('referer'); - $refererUrl = empty($refererUrl) ? $this->_redirect->getRedirectUrl() : $refererUrl; - return $this->_redirect($refererUrl); - } - - /** - * Post user question - * - * @inherit - */ - public function execute() - { - if (!$this->isLoggedIn()) { - return $this->redirectReferer(); - } - - $id = $this->getServiceId(); - $service = $this->serviceRepository->getById($id); - - $request = $this->getRequest(); - $refererUrl = $request->getParam('referer'); - $refererUrl = $this->urlDecoder->decode($refererUrl); - if (!empty($refererUrl)) { - $this->session->setData('referer', $refererUrl); - } - - /* @var \League\OAuth2\Client\Provider\Google $provider */ - $provider = new \League\OAuth2\Client\Provider\Google([ - 'clientId' => $service->getUser(), - 'clientSecret' => $service->getPassword(), - 'redirectUri' => $this->_url->getUrl('*/*/*'), -// 'hostedDomain' => 'example.com', // optional; used to restrict access to users on your G Suite/Google Apps for Business accounts - 'scopes' => ['https://mail.google.com/'], - 'accessType' => 'offline' - ]); - - $errorParam = $request->getParam('error'); - $codeParam = $request->getParam('code'); - $stateParam = $request->getParam('state'); - - if (!empty($errorParam)) { - $this->messageManager->addErrorMessage( - htmlspecialchars($errorParam, ENT_QUOTES, 'UTF-8') - ); - return $this->redirectReferer(); - } elseif (empty($codeParam)) { - - // If we don't have an authorization code then get one - $authUrl = $provider->getAuthorizationUrl(/*['scope' => ['https://mail.google.com/']]*/); - $this->session->setData(self::FLOW_STATE_KEY, $provider->getState()); - - $resultRedirect = $this->resultRedirectFactory->create(); - $resultRedirect->setUrl($authUrl); - - return $resultRedirect; - // Check given state against previously stored one to mitigate CSRF attack - } elseif (empty($stateParam) || ($stateParam !== $this->session->getData(self::FLOW_STATE_KEY))) { - $this->session->setData(self::FLOW_STATE_KEY, null); - $this->messageManager->addErrorMessage( - __('Invalid state') - ); - return $this->redirectReferer(); - } else { - // Try to get an access token (using the authorization code grant) - $token = $provider->getAccessToken('authorization_code', [ - 'code' => $codeParam - ]); - - $refreshToken = $token->getRefreshToken(); - if (empty($refreshToken)) { - $authUrl = $provider->getAuthorizationUrl(['prompt' => 'consent', 'access_type' => 'offline']); - $this->session->setData(self::FLOW_STATE_KEY, $provider->getState()); - - $resultRedirect = $this->resultRedirectFactory->create(); - $resultRedirect->setUrl($authUrl); - return $resultRedirect; - } - $this->session->setData(self::FLOW_STATE_KEY, null); - - $tokenOptions = array_merge($token->jsonSerialize(), ['refresh_token' => $refreshToken]); - $service->setToken($tokenOptions); - $this->serviceRepository->save($service); - } - - return $this->redirectReferer(); - } -} diff --git a/Mail/Transport/GmailOAuth2.php b/Mail/Transport/GmailOAuth2.php index d4ac96e..3702268 100644 --- a/Mail/Transport/GmailOAuth2.php +++ b/Mail/Transport/GmailOAuth2.php @@ -24,8 +24,8 @@ public function __construct( SmtpOptions $options = null ) { if (! $options instanceof SmtpOptions) { - $host = 'smtp.gmail.com'; - $port = 587; + $host = isset($config['host']) ? $config['host'] : 'smtp.gmail.com'; + $port = isset($config['port']) ? (int) $config['port'] : 587; if (!isset($config['token']) || !isset($config['token']['access_token'])) { $phrase = new \Magento\Framework\Phrase('token is broken'); diff --git a/Model/Service.php b/Model/Service.php index e93fdae..4040708 100644 --- a/Model/Service.php +++ b/Model/Service.php @@ -161,11 +161,11 @@ public function getAuth() } /** - * @return string + * @return int */ - public function getToken() + public function getTokenId() { - return $this->getData(self::TOKEN); + return $this->getData(self::TOKEN_ID); } /** @@ -309,9 +309,9 @@ public function setAuth($auth) return $this->setData(self::AUTH, $auth); } - public function setToken($token) + public function setTokenId($tokenId) { - return $this->setData(self::TOKEN, $token); + return $this->setData(self::TOKEN_ID, $tokenId); } /** diff --git a/Model/Service/Encryptor.php b/Model/Service/Encryptor.php index 46c7db7..ad30a20 100644 --- a/Model/Service/Encryptor.php +++ b/Model/Service/Encryptor.php @@ -13,18 +13,12 @@ class Encryptor implements ServiceEncryptorInterface */ private $encryptor; - /** - * @var SerializerInterface - */ - private $serializer; - /** * @param EncryptorInterface $encryptor */ - public function __construct(EncryptorInterface $encryptor, SerializerInterface $serializer) + public function __construct(EncryptorInterface $encryptor) { $this->encryptor = $encryptor; - $this->serializer = $serializer; } /** @@ -32,24 +26,7 @@ public function __construct(EncryptorInterface $encryptor, SerializerInterface $ */ private function getAttributes() { - return ['password', 'token']; - } - - /** - * @return string[] - */ - private function getSerializableAttributes() - { - return ['token']; - } - - /** - * @param $attribute - * @return bool - */ - private function isSerializableAttribute($attribute) - { - return in_array($attribute, $this->getSerializableAttributes()); + return ['password']; } /** @@ -60,9 +37,6 @@ public function encrypt(ServiceInterface $object): void { foreach ($this->getAttributes() as $attributeCode) { $value = $object->getData($attributeCode); - if ($this->isSerializableAttribute($attributeCode)) { - $value = $this->serializer->serialize($value); - } if ($value) { $object->setData($attributeCode, $this->encryptor->encrypt($value)); } @@ -80,9 +54,6 @@ public function decrypt(ServiceInterface $object): void if ($value) { try { $value = $this->encryptor->decrypt($value); - if ($this->isSerializableAttribute($attributeCode)) { - $value = $this->serializer->unserialize($value); - } $object->setData($attributeCode, $value); } catch (\Exception $e) { // value is not encrypted or something wrong with encrypted data diff --git a/Plugin/Model/ServiceOAuth2TokenPlugin.php b/Plugin/Model/ServiceOAuth2TokenPlugin.php index 99001b9..72031ba 100644 --- a/Plugin/Model/ServiceOAuth2TokenPlugin.php +++ b/Plugin/Model/ServiceOAuth2TokenPlugin.php @@ -3,8 +3,8 @@ namespace Swissup\Email\Plugin\Model; -use Magento\Framework\DataObject; use Swissup\Email\Model\Service; +use Swissup\OAuth2Client\Model\AccessTokenRepository; class ServiceOAuth2TokenPlugin { @@ -15,19 +15,9 @@ class ServiceOAuth2TokenPlugin private $serviceRepository; /** - * @var \Magento\Framework\Url + * @var AccessTokenRepository */ - private $urlBuilder; - - /** - * @var \Magento\Framework\Url\EncoderInterface - */ - private $urlEncoder; - - /** - * @var \Swissup\OAuth2Client\Model\Data\FlowToken - */ - private $flowToken; + private $accessTokenRepository; /** * @var \Psr\Log\LoggerInterface $logger @@ -36,81 +26,78 @@ class ServiceOAuth2TokenPlugin public function __construct( \Swissup\Email\Model\ServiceRepository $serviceRepository, - \Magento\Framework\Url $urlBuilder, - \Magento\Framework\Url\EncoderInterface $urlEncoder, - \Swissup\OAuth2Client\Model\Data\FlowToken $flowToken, + \Swissup\OAuth2Client\Model\AccessTokenRepository $accessTokenRepository, \Psr\Log\LoggerInterface $logger ) { $this->serviceRepository = $serviceRepository; - $this->urlBuilder = $urlBuilder; - $this->urlEncoder = $urlEncoder; - $this->flowToken = $flowToken; + $this->accessTokenRepository = $accessTokenRepository; $this->logger = $logger; } + private function isGoogleOAuth2(Service $service): bool + { + return $service->getAuth() === Service::AUTH_TYPE_XOAUTH2 && $service->getType() === Service::TYPE_GMAILOAUTH2; + } + public function afterAfterCommitCallback(Service $subject): void { - if ($subject->getAuth() !== Service::AUTH_TYPE_XOAUTH2 || - $subject->getType() !== Service::TYPE_GMAILOAUTH2) { + if (!$this->isGoogleOAuth2($subject)) { return; } + $tokenId = $subject->getTokenId(); + /** @var $accessToken \Swissup\OAuth2Client\Model\AccessToken */ + $accessToken = $this->accessTokenRepository->getById($tokenId); - $tokenOptions = $subject->getToken(); - if (empty($tokenOptions)) { - $urlBuilder = $this->urlBuilder; - $refererUrl = $urlBuilder->getCurrentUrl(); - $refererUrl = $this->urlEncoder->encode($refererUrl); - $callbackUrl = $urlBuilder->getUrl( - 'swissup_email/gmail/getOAuth2Token', - [ - '_nosid' => true, - '_query' => [ - 'id' => $subject->getId(), - 'referer' => $refererUrl, - 'token' => $this->flowToken->getToken(), - ] - ] - ); + if (!$accessToken->isInitialized()) { + $callbackUrl = $accessToken->getCallbackUrl(); $subject->setData('callback_url', $callbackUrl); } } public function afterAfterLoad(Service $subject): void { - if ($subject->getAuth() !== Service::AUTH_TYPE_XOAUTH2 || - $subject->getType() !== Service::TYPE_GMAILOAUTH2) { + if (!$this->isGoogleOAuth2($subject)) { return; } - $tokenOptions = $subject->getToken(); - if (empty($tokenOptions)) { - return; + // create new access token record + $tokenId = $subject->getTokenId(); + if (empty($tokenId)) { + /** @var \Swissup\OAuth2Client\Model\AccessToken $accessToken */ + $accessToken = $this->accessTokenRepository->create(); + $accessToken->setHasDataChanges(true); + $accessToken = $this->accessTokenRepository->save($accessToken); + $tokenId = $accessToken->getId(); + + $subject->setTokenId($tokenId); + $this->serviceRepository->save($subject); } - $storedToken = new \League\OAuth2\Client\Token\AccessToken($tokenOptions); - $refreshToken = $storedToken->getRefreshToken(); - if ($storedToken->hasExpired() && !empty($refreshToken)) { - $urlBuilder = $this->urlBuilder; - $redirectUri = $urlBuilder->getUrl('swissup_email/gmail/getOAuth2Token'); - /* @var \League\OAuth2\Client\Provider\Google $provider */ - $provider = new \League\OAuth2\Client\Provider\Google([ - 'clientId' => $subject->getUser(), - 'clientSecret' => $subject->getPassword(), - 'redirectUri' => $redirectUri, -// 'hostedDomain' => 'example.com', // optional; used to restrict access to users on your G Suite/Google Apps for Business accounts - 'scopes' => ['https://mail.google.com/'], - 'accessType' => 'offline' - ]); - try { - $token = $provider->getAccessToken('refresh_token', [ - 'refresh_token' => $refreshToken - ]); - $tokenOptions = array_merge($token->jsonSerialize(), ['refresh_token' => $refreshToken]); - $subject->setToken($tokenOptions); - $this->serviceRepository->save($subject); - } catch (\Exception $e) { - $this->logger->critical($e->getMessage()); - } + // refresh creadential + $tokenId = $subject->getTokenId(); + /** @var \Swissup\OAuth2Client\Model\AccessToken $accessToken */ + $accessToken = $this->accessTokenRepository->getById($tokenId); + $storedCredentialHash = $accessToken->getCredentialHash(); + $clientId = $subject->getUser(); + $clientSecret = $subject->getPassword(); + $scope = implode(' ', ['https://mail.google.com/']); + $credential = $accessToken->getCredential(); + $credential->setClientId($clientId) + ->setClientSecret($clientSecret) + ->setScope($scope); + $hash = $credential->getHash(); + if ($storedCredentialHash !== $hash || $credential->isExpired()) { + $credential->save(); + $accessToken->setCredentialHash($hash); + $this->accessTokenRepository->save($accessToken); } + + // refresh access_token is expired + $accessToken = $accessToken->runRefreshToken(); + if ($accessToken) { + $this->accessTokenRepository->save($accessToken); + } + + $subject->setData('token', $accessToken->getData()); } } diff --git a/README.md b/README.md index 0a96149..2534f6c 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ If `Type` selects `Gmail`. Use an [App Password](https://security.google.com/set > > - [Transition from less secure apps to OAuth](https://support.google.com/a/answer/14114704?hl=en) -If the `Type` field is set to `Gmail OAuth 2.0`, please follow the [Google instructions](https://developers.google.com/identity/openid-connect/openid-connect#registeringyourapp) to create the required credentials. In your credentials, you need to add `Authorized redirect URIs` with at least one URI, such as `https://localhost/swissup_email/gmail/getOAuth2Token/` (replace `localhost` with your Magento store URL). +If the `Type` field is set to `Gmail OAuth 2.0`, please follow the [Google instructions](https://developers.google.com/identity/openid-connect/openid-connect#registeringyourapp) to create the required credentials. In your credentials, you need to add `Authorized redirect URIs` with at least one URI, such as `https://localhost/swissup_oauth2client/google/getToken/` (replace `localhost` with your Magento store URL). ![Gmail OAuth2 Credential](https://github.com/swissup/module-email/assets/412612/47802486-2725-4642-91e2-8ff8ead58389) ###### Customize the User Consent Screen diff --git a/composer.json b/composer.json index 264ab13..250ed36 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ "magento/module-config": "^101.1", "laminas/laminas-mail": "^2.9.0", "laminas/laminas-mime": "^2.5.0", - "swissup/module-oauth2-client": "*" + "swissup/module-oauth2-client": "^1.0" }, "require-dev": { "slm/mail": "~1.5", diff --git a/etc/db_schema.xml b/etc/db_schema.xml index 88269c9..e7f9e74 100644 --- a/etc/db_schema.xml +++ b/etc/db_schema.xml @@ -12,7 +12,6 @@ - diff --git a/etc/frontend/routes.xml b/etc/frontend/routes.xml deleted file mode 100644 index e12f1c6..0000000 --- a/etc/frontend/routes.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/etc/module.xml b/etc/module.xml index 55520b0..f39f469 100644 --- a/etc/module.xml +++ b/etc/module.xml @@ -1,6 +1,6 @@ - +