diff --git a/application/Espo/Controllers/ExternalAccount.php b/application/Espo/Controllers/ExternalAccount.php index a331bacc51..1e4d41b478 100644 --- a/application/Espo/Controllers/ExternalAccount.php +++ b/application/Espo/Controllers/ExternalAccount.php @@ -93,12 +93,7 @@ class ExternalAccount extends \Espo\Core\Controllers\Record { list($integration, $userId) = explode('__', $params['id']); - if ($this->getUser()->id != $userId && !$this->getUser()->isAdmin()) { - throw new Forbidden(); - } - - $entity = $this->getEntityManager()->getEntity('ExternalAccount', $params['id']); - return $entity->toArray(); + return $this->getRecordService()->read($params['id'])->getValueMap(); } public function actionUpdate($params, $data, $request) diff --git a/application/Espo/Core/ExternalAccount/ClientManager.php b/application/Espo/Core/ExternalAccount/ClientManager.php index 454f1bcce5..f2266d857b 100644 --- a/application/Espo/Core/ExternalAccount/ClientManager.php +++ b/application/Espo/Core/ExternalAccount/ClientManager.php @@ -69,11 +69,13 @@ class ClientManager $externalAccountEntity = $this->clientMap[$hash]['externalAccountEntity']; $externalAccountEntity->set('accessToken', $data['accessToken']); $externalAccountEntity->set('tokenType', $data['tokenType']); + $externalAccountEntity->set('expiresAt', $data['expiresAt'] ?? null); $copy = $this->getEntityManager()->getEntity('ExternalAccount', $externalAccountEntity->id); if ($copy) { $copy->set('accessToken', $data['accessToken']); $copy->set('tokenType', $data['tokenType']); + $copy->set('expiresAt', $data['expiresAt'] ?? null); $this->getEntityManager()->saveEntity($copy, ['isTokenRenewal' => true]); } } diff --git a/application/Espo/Core/ExternalAccount/Clients/OAuth2Abstract.php b/application/Espo/Core/ExternalAccount/Clients/OAuth2Abstract.php index 8f51c7f957..4f73abdf74 100644 --- a/application/Espo/Core/ExternalAccount/Clients/OAuth2Abstract.php +++ b/application/Espo/Core/ExternalAccount/Clients/OAuth2Abstract.php @@ -29,9 +29,9 @@ namespace Espo\Core\ExternalAccount\Clients; -use \Espo\Core\Exceptions\Error; +use Espo\Core\Exceptions\Error; -use \Espo\Core\ExternalAccount\OAuth2\Client; +use Espo\Core\ExternalAccount\OAuth2\Client; abstract class OAuth2Abstract implements IClient { @@ -39,7 +39,7 @@ abstract class OAuth2Abstract implements IClient protected $manager = null; - protected $paramList = array( + protected $paramList = [ 'endpoint', 'tokenEndpoint', 'clientId', @@ -48,7 +48,8 @@ abstract class OAuth2Abstract implements IClient 'accessToken', 'refreshToken', 'redirectUri', - ); + 'expiresAt', + ]; protected $clientId = null; @@ -60,7 +61,9 @@ abstract class OAuth2Abstract implements IClient protected $redirectUri = null; - public function __construct($client, array $params = array(), $manager = null) + protected $expiresAt = null; + + public function __construct($client, array $params = [], $manager = null) { $this->client = $client; @@ -103,6 +106,25 @@ abstract class OAuth2Abstract implements IClient } } + protected function getAccessTokenDataFromResponseResult($result) + { + $data = []; + + $data['accessToken'] = $result['access_token']; + $data['tokenType'] = $result['token_type']; + + $data['expiresAt'] = null; + + if (isset($result['expires_in']) && is_numeric($result['expires_in'])) { + $data['expiresAt'] = (new \DateTime()) + ->modify('+' . $result['expires_in'] . ' seconds') + ->modify('-1 seconds') + ->format('Y-m-d H:i:s'); + } + + return $data; + } + public function getAccessTokenFromAuthorizationCode($code) { $r = $this->client->getAccessToken($this->getParam('tokenEndpoint'), Client::GRANT_TYPE_AUTHORIZATION_CODE, [ @@ -111,15 +133,16 @@ abstract class OAuth2Abstract implements IClient ]); if ($r['code'] == 200) { - $data = []; if (!empty($r['result'])) { - $data['accessToken'] = $r['result']['access_token']; - $data['tokenType'] = $r['result']['token_type']; + $data = $this->getAccessTokenDataFromResponseResult($r['result']); + $data['refreshToken'] = $r['result']['refresh_token']; + + return $data; } else { $GLOBALS['log']->debug("OAuth getAccessTokenFromAuthorizationCode; Response: " . json_encode($r)); + return null; } - return $data; } else { $GLOBALS['log']->debug("OAuth getAccessTokenFromAuthorizationCode; Response: " . json_encode($r)); } @@ -193,15 +216,13 @@ abstract class OAuth2Abstract implements IClient protected function refreshToken() { if (!empty($this->refreshToken)) { - $r = $this->client->getAccessToken($this->getParam('tokenEndpoint'), Client::GRANT_TYPE_REFRESH_TOKEN, array( + $r = $this->client->getAccessToken($this->getParam('tokenEndpoint'), Client::GRANT_TYPE_REFRESH_TOKEN, [ 'refresh_token' => $this->refreshToken, - )); + ]); if ($r['code'] == 200) { if (is_array($r['result'])) { if (!empty($r['result']['access_token'])) { - $data = array(); - $data['accessToken'] = $r['result']['access_token']; - $data['tokenType'] = $r['result']['token_type']; + $data = $this->getAccessTokenDataFromResponseResult($r['result']); $this->setParams($data); $this->afterTokenRefreshed($data); @@ -209,6 +230,11 @@ abstract class OAuth2Abstract implements IClient } } } + $GLOBALS['log']->notice("Oauth: Refreshing token failed for client {$this->clientId}: " . json_encode($r)); + } else { + $GLOBALS['log']->notice( + "Oauth: Could not refresh token for client {$this->clientId}, because refreshToken is empty."); + } } @@ -217,21 +243,20 @@ abstract class OAuth2Abstract implements IClient if ($r['code'] == 401 && !empty($r['result'])) { $result = $r['result']; if (strpos($r['header'], 'error=invalid_token') !== false) { - return array( + return [ 'action' => 'refreshToken' - ); + ]; } else { - return array( + return [ 'action' => 'renew' - ); + ]; } } else if ($r['code'] == 400 && !empty($r['result'])) { if ($r['result']['error'] == 'invalid_token') { - return array( + return [ 'action' => 'refreshToken' - ); + ]; } } } } - diff --git a/application/Espo/Core/ExternalAccount/OAuth2/Client.php b/application/Espo/Core/ExternalAccount/OAuth2/Client.php index 8e841362cf..a2f9f38357 100644 --- a/application/Espo/Core/ExternalAccount/OAuth2/Client.php +++ b/application/Espo/Core/ExternalAccount/OAuth2/Client.php @@ -62,6 +62,8 @@ class Client protected $accessToken = null; + protected $expiresAt = null; + protected $authType = self::AUTH_TYPE_URI; protected $tokenType = self::TOKEN_TYPE_URI; @@ -72,9 +74,9 @@ class Client protected $certificateFile = null; - protected $curlOptions = array(); + protected $curlOptions = []; - public function __construct(array $params = array()) + public function __construct(array $params = []) { if (!extension_loaded('curl')) { throw new \Exception('CURL extension not found.'); @@ -121,12 +123,17 @@ class Client $this->tokenType = $tokenType; } + public function setExpiresAt($value) + { + $this->expiresAt = $value; + } + public function setAccessTokenSecret($accessTokenSecret) { $this->accessTokenSecret = $accessTokenSecret; } - public function request($url, $params = null, $httpMethod = self::HTTP_METHOD_GET, array $httpHeaders = array()) + public function request($url, $params = null, $httpMethod = self::HTTP_METHOD_GET, array $httpHeaders = []) { if ($this->accessToken) { switch ($this->tokenType) { @@ -148,7 +155,7 @@ class Client return $this->execute($url, $params, $httpMethod, $httpHeaders); } - private function execute($url, $params = null, $httpMethod, array $httpHeaders = array()) + private function execute($url, $params = null, $httpMethod, array $httpHeaders = []) { $curlOptions = array( CURLOPT_RETURNTRANSFER => true, @@ -243,7 +250,7 @@ class Client { $params['grant_type'] = $grantType; - $httpHeaders = array(); + $httpHeaders = []; switch ($this->tokenType) { case self::AUTH_TYPE_URI: case self::AUTH_TYPE_FORM: @@ -261,4 +268,3 @@ class Client return $this->execute($url, $params, self::HTTP_METHOD_POST, $httpHeaders); } } - diff --git a/application/Espo/Services/ExternalAccount.php b/application/Espo/Services/ExternalAccount.php index 98e953b7d0..e15ab46bc3 100644 --- a/application/Espo/Services/ExternalAccount.php +++ b/application/Espo/Services/ExternalAccount.php @@ -29,11 +29,11 @@ namespace Espo\Services; -use \Espo\ORM\Entity; +use Espo\ORM\Entity; -use \Espo\Core\Exceptions\Error; -use \Espo\Core\Exceptions\Forbidden; -use \Espo\Core\Exceptions\NotFound; +use Espo\Core\Exceptions\Error; +use Espo\Core\Exceptions\NotFound; +use Espo\Core\Exceptions\Forbidden; class ExternalAccount extends Record { @@ -92,6 +92,7 @@ class ExternalAccount extends Record $entity->clear('accessToken'); $entity->clear('refreshToken'); $entity->clear('tokenType'); + $entity->clear('expiresAt'); foreach ($result as $name => $value) { $entity->set($name, $value); } @@ -109,4 +110,29 @@ class ExternalAccount extends Record throw new Error("Could not load client for {$integration}."); } } + + public function read($id) + { + list($integration, $userId) = explode('__', $id); + + if ($this->getUser()->id != $userId && !$this->getUser()->isAdmin()) { + throw new Forbidden(); + } + + $entity = $this->getEntityManager()->getEntity('ExternalAccount', $id); + + if (!$entity) throw new NotFoundSilent("Record does not exist."); + + list($integration, $id) = explode('__', $entity->id); + + $externalAccountSecretAttributeList = $this->getMetadata()->get( + ['integrations', $integration, 'externalAccountSecretAttributeList']) ?? []; + + foreach ($externalAccountSecretAttributeList as $a) { + $entity->clear($a); + } + + return $entity; + + } }