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