From bfabc33ac2b982c097fece905d05f94a85bf59e9 Mon Sep 17 00:00:00 2001 From: aaronbauman <aaronbauman@384578.no-reply.drupal.org> Date: Tue, 15 Jun 2021 09:54:27 -0400 Subject: [PATCH] Issue #3102133 by AaronBauman: Refresh identity whenever token is refreshed --- .../SalesforceJWTPlugin.php | 5 +- src/IdentityNotFoundException.php | 12 +++ src/Rest/RestClient.php | 22 +++-- src/Rest/SalesforceIdentity.php | 58 ++++++++++++ src/Rest/SalesforceIdentityInterface.php | 20 ++++ src/SalesforceAuthProviderInterface.php | 21 ++++- src/SalesforceAuthProviderPluginBase.php | 93 +++++++++---------- src/Storage/SalesforceAuthTokenStorage.php | 11 ++- .../SalesforceAuthTokenStorageInterface.php | 5 +- 9 files changed, 184 insertions(+), 63 deletions(-) create mode 100644 src/IdentityNotFoundException.php create mode 100644 src/Rest/SalesforceIdentity.php create mode 100644 src/Rest/SalesforceIdentityInterface.php diff --git a/modules/salesforce_jwt/src/Plugin/SalesforceAuthProvider/SalesforceJWTPlugin.php b/modules/salesforce_jwt/src/Plugin/SalesforceAuthProvider/SalesforceJWTPlugin.php index b5eb3872..379c00b9 100644 --- a/modules/salesforce_jwt/src/Plugin/SalesforceAuthProvider/SalesforceJWTPlugin.php +++ b/modules/salesforce_jwt/src/Plugin/SalesforceAuthProvider/SalesforceJWTPlugin.php @@ -177,6 +177,7 @@ class SalesforceJWTPlugin extends SalesforceAuthProviderPluginBase { $response = $this->httpClient->retrieveResponse(new Uri($this->getLoginUrl() . static::AUTH_TOKEN_PATH), $data, ['Content-Type' => 'application/x-www-form-urlencoded']); $token = $this->parseAccessTokenResponse($response); $this->storage->storeAccessToken($this->service(), $token); + $this->refreshIdentity($token); return $token; } @@ -184,7 +185,9 @@ class SalesforceJWTPlugin extends SalesforceAuthProviderPluginBase { * {@inheritDoc} */ public function refreshAccessToken(TokenInterface $token) { - return $this->requestAccessToken($this->generateAssertion()); + $token = $this->requestAccessToken($this->generateAssertion()); + $this->refreshIdentity($token); + return $token; } /** diff --git a/src/IdentityNotFoundException.php b/src/IdentityNotFoundException.php new file mode 100644 index 00000000..e2426d5d --- /dev/null +++ b/src/IdentityNotFoundException.php @@ -0,0 +1,12 @@ +<?php + +namespace Drupal\salesforce; + +/** + * Class IdentityNotFoundException extends Runtime Exception. + * + * Thrown when an auth provider does not have a properly initialized identity. + */ +class IdentityNotFoundException extends \RuntimeException { + +} \ No newline at end of file diff --git a/src/Rest/RestClient.php b/src/Rest/RestClient.php index efe52b1d..781a884e 100644 --- a/src/Rest/RestClient.php +++ b/src/Rest/RestClient.php @@ -8,6 +8,7 @@ use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\State\StateInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\salesforce\IdentityNotFoundException; use Drupal\salesforce\SalesforceAuthProviderPluginManagerInterface; use Drupal\salesforce\SelectQueryInterface; use Drupal\salesforce\SFID; @@ -364,19 +365,20 @@ class RestClient implements RestClientInterface { if (!$this->authProvider) { return []; } - $id = $this->authProvider->getIdentity(); - if (!empty($id)) { - $url = str_replace('v{version}/', '', $id['urls']['rest']); - $response = new RestResponse($this->httpRequest($url)); - foreach ($response->data as $version) { - $versions[$version['version']] = $version; - } - $this->cache->set('salesforce:versions', $versions, $this->getRequestTime() + self::LONGTERM_CACHE_LIFETIME, ['salesforce']); - return $versions; + try { + $id = $this->authProvider->getIdentity(); } - else { + catch (IdentityNotFoundException $e) { return []; } + + $url = str_replace('v{version}/', '', $id->getUrl('rest')); + $response = new RestResponse($this->httpRequest($url)); + foreach ($response->data as $version) { + $versions[$version['version']] = $version; + } + $this->cache->set('salesforce:versions', $versions, $this->getRequestTime() + self::LONGTERM_CACHE_LIFETIME, ['salesforce']); + return $versions; } /** diff --git a/src/Rest/SalesforceIdentity.php b/src/Rest/SalesforceIdentity.php new file mode 100644 index 00000000..ad032fee --- /dev/null +++ b/src/Rest/SalesforceIdentity.php @@ -0,0 +1,58 @@ +<?php + +namespace Drupal\salesforce\Rest; + +use OAuth\Common\Http\Exception\TokenResponseException; + +class SalesforceIdentity implements SalesforceIdentityInterface { + + protected $data; + + /** + * Handle the identity response from Salesforce. + * + * @param string $responseBody + * JSON identity response from Salesforce. + * + * @throws \OAuth\Common\Http\Exception\TokenResponseException + * If responseBody cannot be parsed, or contains an error. + */ + public function __construct($responseBody) { + $data = json_decode($responseBody, TRUE); + + if (NULL === $data || !is_array($data)) { + throw new TokenResponseException('Unable to parse response.'); + } + elseif (isset($data['error'])) { + throw new TokenResponseException('Error in retrieving token: "' . $data['error'] . '"'); + } + $this->data = $data; + } + + /** + * Static creation method. + * + * @param array $data + * Data array. + * + * @return \Drupal\salesforce\Rest\SalesforceIdentity + * New identity. + * + * @throws \OAuth\Common\Http\Exception\TokenResponseException + */ + public static function create(array $data) { + return new static(json_encode($data)); + } + + /** + * {@inheritdoc} + */ + public function getUrl($api_type, $api_version = NULL) { + if (empty($this->data['urls'][$api_type])) { + return ''; + } + $url = $this->data['urls'][$api_type]; + return $api_version ? str_replace('{version}', $api_version, $url) : $url; + } + +} \ No newline at end of file diff --git a/src/Rest/SalesforceIdentityInterface.php b/src/Rest/SalesforceIdentityInterface.php new file mode 100644 index 00000000..061f8185 --- /dev/null +++ b/src/Rest/SalesforceIdentityInterface.php @@ -0,0 +1,20 @@ +<?php + +namespace Drupal\salesforce\Rest; + +interface SalesforceIdentityInterface { + + /** + * Given API type and optional API version, return the API url. + * + * @param string $api_type + * The api type, e.g. rest, partner, meta. + * @param string $api_version + * If given, replace {version} placeholder. Otherwise, return the raw URL. + * + * @return string + * The API url. + */ + public function getUrl($api_type, $api_version = NULL); + +} \ No newline at end of file diff --git a/src/SalesforceAuthProviderInterface.php b/src/SalesforceAuthProviderInterface.php index 776e3cee..cc1baaab 100644 --- a/src/SalesforceAuthProviderInterface.php +++ b/src/SalesforceAuthProviderInterface.php @@ -38,6 +38,8 @@ interface SalesforceAuthProviderInterface extends ServiceInterface, PluginFormIn /** * Perform a refresh of the given token. * + * NB: This method should also refresh any associated identity. + * * @param \OAuth\Common\Token\TokenInterface $token * The token. * @@ -49,6 +51,20 @@ interface SalesforceAuthProviderInterface extends ServiceInterface, PluginFormIn */ public function refreshAccessToken(TokenInterface $token); + /** + * Given a token, fetch the SF identity. + * + * @param \OAuth\Common\Token\TokenInterface $token + * The token. + * + * @return \Drupal\salesforce\Rest\SalesforceIdentityInterface + * The refreshed identity. + * + * @throws \OAuth\OAuth2\Service\Exception\MissingRefreshTokenException + * Comment. + */ + public function refreshIdentity(TokenInterface $token); + /** * Return the credentials configured for this auth provider instance. * @@ -72,8 +88,11 @@ interface SalesforceAuthProviderInterface extends ServiceInterface, PluginFormIn /** * Identify for this connection. * - * @return array + * @return \Drupal\salesforce\Rest\SalesforceIdentityInterface * Identity for this connection. + * + * @throws \Drupal\salesforce\IdentityNotFoundException + * If there is no identity. */ public function getIdentity(); diff --git a/src/SalesforceAuthProviderPluginBase.php b/src/SalesforceAuthProviderPluginBase.php index be042046..1a0e2a73 100644 --- a/src/SalesforceAuthProviderPluginBase.php +++ b/src/SalesforceAuthProviderPluginBase.php @@ -7,10 +7,11 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Messenger\MessengerTrait; use Drupal\Core\Routing\TrustedRedirectResponse; use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\salesforce\Rest\SalesforceIdentity; use Drupal\salesforce\Storage\SalesforceAuthTokenStorageInterface; use OAuth\Common\Http\Client\ClientInterface; -use OAuth\Common\Http\Exception\TokenResponseException; use OAuth\Common\Http\Uri\Uri; +use OAuth\Common\Token\TokenInterface; use OAuth\OAuth2\Service\Salesforce; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -184,24 +185,51 @@ abstract class SalesforceAuthProviderPluginBase extends Salesforce implements Sa return TRUE; } $token = $this->getAccessToken(); - $headers = [ - 'Authorization' => 'OAuth ' . $token->getAccessToken(), - 'Content-type' => 'application/json', - ]; - $data = $token->getExtraParams(); try { - $response = $this->httpClient->retrieveResponse(new Uri($data['id']), [], $headers); + $this->refreshIdentity($token); } catch (\Exception $e) { + watchdog_exception('salesforce', $e); $this->messenger()->addError($e->getMessage()); $form_state->disableRedirect(); return FALSE; } - $identity = $this->parseIdentityResponse($response); - $this->storage->storeIdentity($this->service(), $identity); return TRUE; } + /** + * {@inheritdoc} + */ + public function requestAccessToken($code, $state = NULL) { + $token = parent::requestAccessToken($code, $state); + $this->refreshIdentity($token); + return $token; + } + + /** + * {@inheritdoc} + */ + public function refreshAccessToken(TokenInterface $token) { + $token = parent::refreshAccessToken($token); + $this->refreshIdentity($token); + return $token; + } + + /** + * {@inheritdoc} + */ + public function refreshIdentity(TokenInterface $token) { + $headers = [ + 'Authorization' => 'OAuth ' . $token->getAccessToken(), + 'Content-type' => 'application/json', + ]; + $data = $token->getExtraParams(); + $response = $this->httpClient->retrieveResponse(new Uri($data['id']), [], $headers); + $identity = new SalesforceIdentity($response); + $this->storage->storeIdentity($this->service(), $identity); + return $identity; + } + /** * {@inheritdoc} */ @@ -259,21 +287,11 @@ abstract class SalesforceAuthProviderPluginBase extends Salesforce implements Sa * {@inheritdoc} */ public function getApiEndpoint($api_type = 'rest') { - $url = &drupal_static(self::CLASS . __FUNCTION__ . $api_type); - if (!isset($url)) { - $identity = $this->getIdentity(); - if (empty($identity)) { - return FALSE; - } - if (is_string($identity)) { - $url = $identity; - } - elseif (isset($identity['urls'][$api_type])) { - $url = $identity['urls'][$api_type]; - } - $url = str_replace('{version}', $this->getApiVersion(), $url); + $identity = $this->getIdentity(); + if (empty($identity)) { + throw new IdentityNotFoundException(); } - return $url; + return $identity->getUrl($api_type, $this->getApiVersion()); } /** @@ -291,7 +309,11 @@ abstract class SalesforceAuthProviderPluginBase extends Salesforce implements Sa * {@inheritdoc} */ public function getIdentity() { - return $this->storage->retrieveIdentity($this->id()); + $identity = $this->storage->retrieveIdentity($this->id()); + if (empty($identity)) { + throw new IdentityNotFoundException(); + } + return $identity; } /** @@ -301,29 +323,6 @@ abstract class SalesforceAuthProviderPluginBase extends Salesforce implements Sa return $this->id(); } - /** - * Handle the identity response from Salesforce. - * - * @param string $responseBody - * JSON identity response from Salesforce. - * - * @return array - * The identity. - * - * @throws \OAuth\Common\Http\Exception\TokenResponseException - */ - protected function parseIdentityResponse($responseBody) { - $data = json_decode($responseBody, TRUE); - - if (NULL === $data || !is_array($data)) { - throw new TokenResponseException('Unable to parse response.'); - } - elseif (isset($data['error'])) { - throw new TokenResponseException('Error in retrieving token: "' . $data['error'] . '"'); - } - return $data; - } - /** * Accessor to the storage adapter to be able to retrieve tokens. * diff --git a/src/Storage/SalesforceAuthTokenStorage.php b/src/Storage/SalesforceAuthTokenStorage.php index 7f91440b..4936f97e 100644 --- a/src/Storage/SalesforceAuthTokenStorage.php +++ b/src/Storage/SalesforceAuthTokenStorage.php @@ -3,6 +3,8 @@ namespace Drupal\salesforce\Storage; use Drupal\Core\State\StateInterface; +use Drupal\salesforce\Rest\SalesforceIdentity; +use Drupal\salesforce\Rest\SalesforceIdentityInterface; use OAuth\Common\Storage\Exception\TokenNotFoundException; use OAuth\Common\Token\TokenInterface; @@ -157,7 +159,7 @@ class SalesforceAuthTokenStorage implements SalesforceAuthTokenStorageInterface /** * {@inheritdoc} */ - public function storeIdentity($service, $identity) { + public function storeIdentity($service, SalesforceIdentityInterface $identity) { $this->state->set(static::getIdentityStorageId($service), $identity); return $this; } @@ -173,7 +175,12 @@ class SalesforceAuthTokenStorage implements SalesforceAuthTokenStorageInterface * {@inheritdoc} */ public function retrieveIdentity($service) { - return $this->state->get(static::getIdentityStorageId($service)); + // Backwards compatibility in case someone missed the hook_update. + $identity = $this->state->get(static::getIdentityStorageId($service)); + if (is_array($identity)) { + $identity = SalesforceIdentity::create($identity); + } + return $identity; } /** diff --git a/src/Storage/SalesforceAuthTokenStorageInterface.php b/src/Storage/SalesforceAuthTokenStorageInterface.php index 1a5bd542..be95bf74 100644 --- a/src/Storage/SalesforceAuthTokenStorageInterface.php +++ b/src/Storage/SalesforceAuthTokenStorageInterface.php @@ -2,6 +2,7 @@ namespace Drupal\salesforce\Storage; +use Drupal\salesforce\Rest\SalesforceIdentityInterface; use OAuth\Common\Storage\TokenStorageInterface; /** @@ -16,7 +17,7 @@ interface SalesforceAuthTokenStorageInterface extends TokenStorageInterface { * * @return $this */ - public function storeIdentity($service, $identity); + public function storeIdentity($service, SalesforceIdentityInterface $identity); /** * Return boolean indicating whether this service has an identity. @@ -29,7 +30,7 @@ interface SalesforceAuthTokenStorageInterface extends TokenStorageInterface { /** * Identity for the given service. * - * @return array + * @return \Drupal\salesforce\Rest\SalesforceIdentityInterface * Identity. */ public function retrieveIdentity($service); -- GitLab