diff --git a/composer.json b/composer.json
index b02b218338cbe6c585d21dac1d9eb9c057d19486..0fc9be263ee0e0794138ec9784550d74f9bcafb8 100644
--- a/composer.json
+++ b/composer.json
@@ -29,7 +29,9 @@
   "require": {
     "consolidation/output-formatters": "^3.2.0",
     "drupal/dynamic_entity_reference": "^2.0@alpha",
+    "drupal/key": "^1.7",
     "drupal/encrypt": "^3.0@rc",
+    "firebase/php-jwt": "^5.0",
     "lusitanian/oauth": "^0.8.11",
     "messageagency/force.com-toolkit-for-php": "^1.0.0"
   }
diff --git a/config/schema/salesforce.schema.yml b/config/schema/salesforce.schema.yml
index ac83bb9277c18346201dfbbaf48330bce4f17415..a05925dce747b3b421ac72766dec0e3c634ecae7 100644
--- a/config/schema/salesforce.schema.yml
+++ b/config/schema/salesforce.schema.yml
@@ -74,17 +74,3 @@ salesforce.salesforce_auth.*:
     provider_settings:
       type: salesforce.auth_provider_settings.[%parent.provider]
       label: 'Provider Plugin Settings'
-
-salesforce.auth_provider_settings.oauth:
-  type: mapping
-  label: 'Salesforce OAuth Provider Settings'
-  mapping:
-    consumer_key:
-      type: string
-      label: 'Consumer Key'
-    consumer_secret:
-      type: string
-      label: 'Consumer Secret'
-    login_url:
-      type: uri
-      label: 'Login URL'
diff --git a/modules/salesforce_encrypt/config/schema/salesforce_encrypt.schema.yml b/modules/salesforce_encrypt/config/schema/salesforce_encrypt.schema.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5fdb2e46ce00e0ecd5943dd6e1b05f2c13b92c5f
--- /dev/null
+++ b/modules/salesforce_encrypt/config/schema/salesforce_encrypt.schema.yml
@@ -0,0 +1,16 @@
+salesforce.auth_provider_settings.oauth_encrypt:
+  type: mapping
+  label: 'Salesforce OAuth, Encrypted, Provider Settings'
+  mapping:
+    consumer_key:
+      type: string
+      label: 'Consumer Key'
+    consumer_secret:
+      type: string
+      label: 'Consumer Secret'
+    login_url:
+      type: uri
+      label: 'Login URL'
+    encryption_profile:
+      type: encrypt.profile.[%key]
+      label: 'Encryption Profile ID'
diff --git a/modules/salesforce_encrypt/salesforce_encrypt.info.yml b/modules/salesforce_encrypt/salesforce_encrypt.info.yml
index 74f83be13dfa5e1c7636118857a99abcc330e226..521059d445a8a40a84db344f4da71a4c2a8cbcac 100644
--- a/modules/salesforce_encrypt/salesforce_encrypt.info.yml
+++ b/modules/salesforce_encrypt/salesforce_encrypt.info.yml
@@ -1,6 +1,6 @@
 name: Salesforce Encrypted Keys
 type: module
-description: Supplants Salesforce RestClient service to provide encrypted stateful data.
+description: Adds encryption support for auth providers.
 package: Salesforce
 core: 8.x
 dependencies:
diff --git a/modules/salesforce_encrypt/salesforce_encrypt.install b/modules/salesforce_encrypt/salesforce_encrypt.install
deleted file mode 100644
index 9f84836ed48954bc00196b295aac1c462e72410b..0000000000000000000000000000000000000000
--- a/modules/salesforce_encrypt/salesforce_encrypt.install
+++ /dev/null
@@ -1,54 +0,0 @@
-<?php
-
-/**
- * @file
- * Requirements and uninstall hooks.
- */
-
-use Drupal\Core\Url;
-use Drupal\salesforce\EntityNotFoundException;
-
-/**
- * Throw a runtime error if Salesforce encryption profile is not selected.
- *
- * Implements hook_requirements().
- */
-function salesforce_encrypt_requirements($phase) {
-  $requirements = [];
-  if ($phase == 'runtime') {
-    $profile_id = NULL;
-    try {
-      $profile = \Drupal::service('salesforce.client')->getEncryptionProfile();
-    }
-    catch (EntityNotFoundException $e) {
-      // Noop.
-    }
-    $requirements['salesforce_encrypt'] = [
-      'title' => t('Salesforce Encrypt'),
-      'value' => t('Encryption Profile'),
-    ];
-    if (empty($profile)) {
-      $requirements['salesforce_encrypt'] += [
-        'severity' => REQUIREMENT_ERROR,
-        'description' => t('You need to <a href="@url">select an encryption profile</a> in order to fully enable Salesforce Encrypt and protect sensitive information.', ['@url' => Url::fromRoute('salesforce_encrypt.settings')->toString()]),
-      ];
-    }
-    else {
-      $requirements['salesforce_encrypt'] += [
-        'severity' => REQUIREMENT_OK,
-        'description' => t('Profile id: <a href=":url">%profile</a>', ['%profile' => $profile->id(), ':url' => $profile->url()]),
-      ];
-    }
-  }
-  return $requirements;
-}
-
-/**
- * Implements hook_uninstall().
- *
- * Decrypt and purge our data.
- */
-function salesforce_encrypt_uninstall() {
-  \Drupal::service('salesforce.client')->disableEncryption();
-  \Drupal::state()->delete('salesforce_encrypt.profile');
-}
diff --git a/modules/salesforce_encrypt/salesforce_encrypt.links.menu.yml b/modules/salesforce_encrypt/salesforce_encrypt.links.menu.yml
deleted file mode 100644
index 7825f4241c443285956314e6c9a5f494e33ee884..0000000000000000000000000000000000000000
--- a/modules/salesforce_encrypt/salesforce_encrypt.links.menu.yml
+++ /dev/null
@@ -1,6 +0,0 @@
-salesforce_encrypt.settings:
-  route_name: salesforce_encrypt.settings
-  parent: salesforce.admin_config_salesforce
-  title: Salesforce Encrypt
-  description: 'Encrypt sensitive Salesforce OAuth credentials and identity.'
-  weight: 10
diff --git a/modules/salesforce_encrypt/salesforce_encrypt.routing.yml b/modules/salesforce_encrypt/salesforce_encrypt.routing.yml
deleted file mode 100644
index ad30e490962eb9b23ac5a07b6192734404a72924..0000000000000000000000000000000000000000
--- a/modules/salesforce_encrypt/salesforce_encrypt.routing.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-salesforce_encrypt.settings:
-  path: '/admin/config/salesforce/encrypt'
-  defaults:
-    _form: '\Drupal\salesforce_encrypt\Form\SettingsForm'
-    _title: 'Salesforce Encryption'
-    _description: 'Encrypt sensitive Salesforce OAuth credentials and identity.'
-  requirements:
-    _permission: 'administer salesforce encryption'
diff --git a/modules/salesforce_encrypt/src/Consumer/OAuthEncryptedCredentials.php b/modules/salesforce_encrypt/src/Consumer/OAuthEncryptedCredentials.php
new file mode 100644
index 0000000000000000000000000000000000000000..5c0fbded9fb7bc7af2bcf5bb4c1b12bbe7e3fed5
--- /dev/null
+++ b/modules/salesforce_encrypt/src/Consumer/OAuthEncryptedCredentials.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Drupal\salesforce_encrypt\Consumer;
+
+use Drupal\salesforce\Consumer\SalesforceCredentials;
+
+/**
+ * OAuth encrypted creds.
+ */
+class OAuthEncryptedCredentials extends SalesforceCredentials {
+
+  /**
+   * Encryption profile id.
+   *
+   * @var string
+   */
+  protected $encryptionProfileId;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct($consumerKey, $loginUrl, $consumerSecret, $encryptionProfileId) {
+    parent::__construct($consumerKey, $loginUrl, $consumerSecret);
+    $this->encryptionProfileId = $encryptionProfileId;
+  }
+
+  /**
+   * Getter.
+   *
+   * @return string
+   *   The encryption profile id.
+   */
+  public function getEncryptionProfileId() {
+    return $this->encryptionProfileId;
+  }
+
+}
diff --git a/modules/salesforce_encrypt/src/Form/SettingsForm.php b/modules/salesforce_encrypt/src/Form/SettingsForm.php
deleted file mode 100644
index 69e2aa10f39356f668a1052d2f5d74acc1de1982..0000000000000000000000000000000000000000
--- a/modules/salesforce_encrypt/src/Form/SettingsForm.php
+++ /dev/null
@@ -1,135 +0,0 @@
-<?php
-
-namespace Drupal\salesforce_encrypt\Form;
-
-use Drupal\Core\Form\FormBase;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\State\StateInterface;
-use Drupal\encrypt\EncryptionProfileManagerInterface;
-use Drupal\salesforce\EntityNotFoundException;
-use Drupal\salesforce_encrypt\Rest\EncryptedRestClientInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Drupal\Core\Url;
-
-/**
- * Base form for key add and edit forms.
- *
- * @deprecated will be removed in 8.x-4.0 release.
- */
-class SettingsForm extends FormBase {
-
-  /**
-   * Profile manager.
-   *
-   * @var \Drupal\encrypt\EncryptionProfileManagerInterface
-   */
-  protected $encryptionProfileManager;
-
-  /**
-   * SettingsForm constructor.
-   *
-   * @param \Drupal\Core\State\StateInterface $state
-   *   State service.
-   * @param \Drupal\encrypt\EncryptionProfileManagerInterface $encryptionProfileManager
-   *   Encryption profile manager service.
-   * @param \Drupal\salesforce_encrypt\Rest\EncryptedRestClientInterface $client
-   *   Rest client service.
-   */
-  public function __construct(StateInterface $state, EncryptionProfileManagerInterface $encryptionProfileManager, EncryptedRestClientInterface $client) {
-    $this->encryptionProfileManager = $encryptionProfileManager;
-    $this->state = $state;
-    $this->client = $client;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    return new static(
-      $container->get('state'),
-      $container->get('encrypt.encryption_profile.manager'),
-      $container->get('salesforce.client')
-    );
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getFormId() {
-    return 'salesforce_encrypt_config';
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function buildForm(array $form, FormStateInterface $form_state) {
-    $options = $this
-      ->encryptionProfileManager
-      ->getEncryptionProfileNamesAsOptions();
-    $default = NULL;
-    try {
-      $profile = $this->client->getEncryptionProfile();
-      if (!empty($profile)) {
-        $default = $profile->id();
-      }
-    }
-    catch (EntityNotFoundException $e) {
-      drupal_set_message($e->getFormattableMessage(), 'error');
-      drupal_set_message($this->t('Error while loading encryption profile. You will need to <a href=":encrypt">assign a new encryption profile</a>, then <a href=":oauth">re-authenticate to Salesforce</a>.', [':encrypt' => Url::fromRoute('salesforce_encrypt.settings')->toString(), ':oauth' => Url::fromRoute('salesforce.authorize')->toString()]), 'error');
-    }
-
-    $form['profile'] = [
-      '#type' => 'select',
-      '#title' => $this->t('Encryption Profile'),
-      '#description' => $this->t('Choose an encryption profile with which to encrypt Salesforce information.'),
-      '#options' => $options,
-      '#default_value' => $default,
-      '#empty_option' => $this->t('Do not use encryption'),
-    ];
-
-    $form['actions']['#type'] = 'actions';
-    $form['actions']['submit'] = [
-      '#type' => 'submit',
-      '#value' => $this->t('Save configuration'),
-      '#button_type' => 'primary',
-    ];
-
-    // By default, render the form using system-config-form.html.twig.
-    $form['#theme'] = 'system_config_form';
-
-    return $form;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function submitForm(array &$form, FormStateInterface $form_state) {
-    $old_profile_id = $this->state->get('salesforce_encrypt.profile');
-    $profile_id = $form_state->getValue('profile');
-
-    if ($old_profile_id == $profile_id) {
-      // No change to encryption profile. Do nothing.
-      return;
-    }
-
-    $profile = $this
-      ->encryptionProfileManager
-      ->getEncryptionProfile($profile_id);
-    if (empty($profile_id)) {
-      // New profile id empty: disable encryption.
-      $this->client->disableEncryption();
-    }
-    elseif (empty($old_profile_id)) {
-      // Old profile id empty: enable encryption anew.
-      $this->client->enableEncryption($profile);
-    }
-    else {
-      // Changing encryption profiles: disable, then re-enable.
-      $this->client->disableEncryption();
-      $this->client->enableEncryption($profile);
-    }
-    $this->state->resetCache();
-    drupal_set_message($this->t('The configuration options have been saved.'));
-  }
-
-}
diff --git a/modules/salesforce_encrypt/src/Plugin/SalesforceAuthProvider/SalesforceEncryptedOAuthPlugin.php b/modules/salesforce_encrypt/src/Plugin/SalesforceAuthProvider/SalesforceEncryptedOAuthPlugin.php
new file mode 100644
index 0000000000000000000000000000000000000000..4869e2bfe1f6ee138de1382820830fb8aa2a7f19
--- /dev/null
+++ b/modules/salesforce_encrypt/src/Plugin/SalesforceAuthProvider/SalesforceEncryptedOAuthPlugin.php
@@ -0,0 +1,293 @@
+<?php
+
+namespace Drupal\salesforce_encrypt\Plugin\SalesforceAuthProvider;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Routing\TrustedRedirectResponse;
+use Drupal\Core\Url;
+use Drupal\encrypt\EncryptionProfileInterface;
+use Drupal\encrypt\EncryptionProfileManagerInterface;
+use Drupal\encrypt\EncryptServiceInterface;
+use Drupal\salesforce\EntityNotFoundException;
+use Drupal\salesforce\SalesforceAuthProviderPluginBase;
+use Drupal\salesforce\SalesforceAuthProviderInterface;
+use Drupal\salesforce_encrypt\Consumer\OAuthEncryptedCredentials;
+use Drupal\salesforce_encrypt\SalesforceEncryptedAuthTokenStorageInterface;
+use OAuth\Common\Http\Client\ClientInterface;
+use OAuth\Common\Http\Uri\Uri;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * OAuth provider with encrypted credentials.
+ *
+ * @Plugin(
+ *   id = "oauth_encrypted",
+ *   label = @Translation("Salesforce OAuth User-Agent, Encrypted")
+ * )
+ */
+class SalesforceEncryptedOAuthPlugin extends SalesforceAuthProviderPluginBase {
+
+  /**
+   * OAuth credentials.
+   *
+   * @var \Drupal\salesforce\Consumer\SalesforceCredentials
+   */
+  protected $credentials;
+
+  /**
+   * Encryption profile manager.
+   *
+   * @var \Drupal\encrypt\EncryptionProfileManagerInterface
+   */
+  protected $encryptionProfileManager;
+
+  /**
+   * Encryption service.
+   *
+   * @var \Drupal\encrypt\EncryptServiceInterface
+   */
+  protected $encryption;
+
+  /**
+   * Encryption profile.
+   *
+   * @var \Drupal\encrypt\EncryptionProfileInterface
+   */
+  protected $encryptionProfile;
+
+  /**
+   * Encryption profile id.
+   *
+   * @var string
+   */
+  protected $encryptionProfileId;
+
+  /**
+   * {@inheritdoc}
+   */
+  const SERVICE_TYPE = 'oauth_encrypted';
+
+  /**
+   * {@inheritdoc}
+   */
+  const LABEL = 'OAuth Encrypted';
+
+  /**
+   * Token storage;.
+   *
+   * @var \Drupal\salesforce_encrypt\SalesforceEncryptedAuthTokenStorageInterface
+   */
+  protected $storage;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct($id, OAuthEncryptedCredentials $credentials, ClientInterface $httpClient, SalesforceEncryptedAuthTokenStorageInterface $storage, EncryptionProfileManagerInterface $encryptionProfileManager, EncryptServiceInterface $encrypt) {
+    parent::__construct($credentials, $httpClient, $storage, [], new Uri($credentials->getLoginUrl()));
+    $this->id = $id;
+    $this->encryptionProfileManager = $encryptionProfileManager;
+    $this->encryption = $encrypt;
+    $this->encryptionProfileId = $credentials->getEncryptionProfileId();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    $configuration = array_merge(self::defaultConfiguration(), $configuration);
+    $storage = $container->get('salesforce.auth_token_storage');
+    /** @var \Drupal\encrypt\EncryptServiceInterface $encrypt */
+    $encrypt = $container->get('encryption');
+    $encryptProfileMan = $container->get('encrypt.encryption_profile.manager');
+    if ($configuration['encryption_profile']) {
+      try {
+        $profile = $encryptProfileMan->getEncryptionProfile($configuration['encryption_profile']);
+        $configuration['consumer_key'] = $encrypt->decrypt($configuration['consumer_key'], $profile);
+        $configuration['consumer_secret'] = $encrypt->decrypt($configuration['consumer_secret'], $profile);
+      }
+      catch (\Exception $e) {
+        // Any exception here may cause WSOD, don't allow that to happen.
+        watchdog_exception('SFOAuthEncrypted', $e);
+      }
+    }
+    $cred = new OAuthEncryptedCredentials($configuration['consumer_key'], $configuration['login_url'], $configuration['consumer_secret'], $configuration['encryption_profile']);
+    return new static($configuration['id'], $cred, $container->get('salesforce.http_client_wrapper'), $storage, $encryptProfileMan, $encrypt);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function defaultConfiguration() {
+    $defaults = parent::defaultConfiguration();
+    return array_merge($defaults, [
+      'encryption_profile' => NULL,
+    ]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hookEncryptionProfileDelete(EncryptionProfileInterface $profile) {
+    if ($this->encryptionProfile()->id() == $profile->id()) {
+      // @todo decrypt identity, access token, refresh token, consumer secret, consumer key and re-save
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function encryptionProfile() {
+    if ($this->encryptionProfile) {
+      return $this->encryptionProfile;
+    }
+    elseif (empty($this->encryptionProfileId)) {
+      return NULL;
+    }
+    else {
+      $this->encryptionProfile = $this->encryptionProfileManager
+        ->getEncryptionProfile($this->encryptionProfileId);
+      if (empty($this->encryptionProfile)) {
+        throw new EntityNotFoundException(['id' => $this->encryptionProfileId], 'encryption_profile');
+      }
+      return $this->encryptionProfile;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    $options = $this
+      ->encryptionProfileManager
+      ->getEncryptionProfileNamesAsOptions();
+    $default = NULL;
+    try {
+      $profile = $this->encryptionProfile();
+      if (!empty($profile)) {
+        $default = $profile->id();
+      }
+    }
+    catch (EntityNotFoundException $e) {
+      $this->messenger()->addError($e->getFormattableMessage());
+      $this->messenger()->addError($this->t('Error while loading encryption profile. You will need to assign a new encryption profile and re-authenticate to Salesforce.'));
+    }
+
+    if (empty($options)) {
+      $this->messenger()->addError($this->t('Please <a href="@href">create an encryption profile</a> before adding an OAuth Encrypted provider.', ['@href' => Url::fromRoute('entity.encryption_profile.add_form')->toString()]));
+    }
+
+    $form['consumer_key'] = [
+      '#title' => t('Salesforce consumer key'),
+      '#type' => 'textfield',
+      '#description' => t('Consumer key of the Salesforce remote application you want to grant access to. VALUE WILL BE ENCRYPTED ON FORM SUBMISSION.'),
+      '#required' => TRUE,
+      '#default_value' => $this->credentials->getConsumerKey(),
+    ];
+
+    $form['consumer_secret'] = [
+      '#title' => $this->t('Salesforce consumer secret'),
+      '#type' => 'textfield',
+      '#description' => $this->t('Consumer secret of the Salesforce remote application. VALUE WILL BE ENCRYPTED ON FORM SUBMISSION.'),
+      '#required' => TRUE,
+      '#default_value' => $this->credentials->getConsumerSecret(),
+    ];
+
+    $form['login_url'] = [
+      '#title' => t('Login URL'),
+      '#type' => 'textfield',
+      '#default_value' => $this->credentials->getLoginUrl(),
+      '#description' => t('Enter a login URL, either https://login.salesforce.com or https://test.salesforce.com.'),
+      '#required' => TRUE,
+    ];
+    $form['encryption_profile'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Encryption Profile'),
+      '#description' => $this->t('Choose an encryption profile with which to encrypt Salesforce information.'),
+      '#options' => $options,
+      '#default_value' => $default,
+      '#required' => TRUE,
+    ];
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+    $this->setConfiguration($form_state->getValues());
+    $settings = $form_state->getValue('provider_settings');
+    $this->encryptionProfileId = $settings['encryption_profile'];
+    $consumer_key = $settings['consumer_key'];
+    $settings['consumer_key'] = $this->encrypt($settings['consumer_key']);
+    $settings['consumer_secret'] = $this->encrypt($settings['consumer_secret']);
+    $form_state->setValue('provider_settings', $settings);
+    parent::submitConfigurationForm($form, $form_state);
+
+    // Write the config id to private temp store, so that we can use the same
+    // callback URL for all OAuth applications in Salesforce.
+    /** @var \Drupal\Core\TempStore\PrivateTempStore $tempstore */
+    $tempstore = \Drupal::service('user.private_tempstore')->get('salesforce_oauth');
+    $tempstore->set('config_id', $form_state->getValue('id'));
+
+    try {
+      $path = $this->getAuthorizationEndpoint();
+      $query = [
+        'redirect_uri' => $this->credentials->getCallbackUrl(),
+        'response_type' => 'code',
+        'client_id' => $consumer_key,
+      ];
+
+      // Send the user along to the Salesforce OAuth login form. If successful,
+      // the user will be redirected to {redirect_uri} to complete the OAuth
+      // handshake, and thence to the entity listing. Upon failure, the user
+      // redirect URI will send the user back to the edit form.
+      $response = new TrustedRedirectResponse($path . '?' . http_build_query($query), 302);
+      $response->send();
+      return;
+    }
+    catch (\Exception $e) {
+      $this->messenger()->addError(t("Error during authorization: %message", ['%message' => $e->getMessage()]));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function decrypt($value) {
+    return $this->encryption->decrypt($value, $this->encryptionProfile());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function encrypt($value) {
+    return $this->encryption->encrypt($value, $this->encryptionProfile());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConsumerSecret() {
+    return $this->credentials->getConsumerSecret();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function finalizeOauth() {
+    $this->requestAccessToken(\Drupal::request()->get('code'));
+    $token = $this->getAccessToken();
+
+    // Initialize identity.
+    $headers = [
+      'Authorization' => 'OAuth ' . $token->getAccessToken(),
+      'Content-type' => 'application/json',
+    ];
+    $data = $token->getExtraParams();
+    $response = $this->httpClient->retrieveResponse(new Uri($data['id']), [], $headers);
+    $identity = $this->parseIdentityResponse($response);
+    $this->storage->storeIdentity($this->service(), $identity);
+    return TRUE;
+  }
+
+}
diff --git a/modules/salesforce_encrypt/src/Rest/EncryptedRestClientInterface.php b/modules/salesforce_encrypt/src/Rest/EncryptedRestClientInterface.php
deleted file mode 100644
index 1ee3b9e95d563b43f9740cb5d6e491c994748ca8..0000000000000000000000000000000000000000
--- a/modules/salesforce_encrypt/src/Rest/EncryptedRestClientInterface.php
+++ /dev/null
@@ -1,87 +0,0 @@
-<?php
-
-namespace Drupal\salesforce_encrypt\Rest;
-
-use Drupal\encrypt\EncryptionProfileInterface;
-use Drupal\salesforce\Rest\RestClientInterface;
-
-/**
- * Objects, properties, and methods to communicate with the Salesforce REST API.
- *
- * @deprecated will be removed in 8.x-4.0 release.
- */
-interface EncryptedRestClientInterface extends RestClientInterface {
-
-  /**
-   * Encrypts all sensitive salesforce config values.
-   *
-   * @param \Drupal\encrypt\EncryptionProfileInterface $profile
-   *   Id of the Encrypt Profile to use for encryption.
-   *
-   * @return bool
-   *   TRUE if encryption was enabled or FALSE if it is already enabled
-   *
-   * @throws RuntimeException
-   *   If Salesforce encryption profile hasn't been selected.
-   */
-  public function enableEncryption(EncryptionProfileInterface $profile);
-
-  /**
-   * Decrypt and re-save sensitive salesforce config values.
-   *
-   * Inverse of ::enableEncryption.
-   *
-   * @return bool
-   *   TRUE if encryption was disabled or FALSE if it is already disabled
-   *
-   * @throws RuntimeException
-   *   If Salesforce encryption profile hasn't been selected.
-   */
-  public function disableEncryption();
-
-  /**
-   * Returns the EncryptionProfileInterface assigned to Salesforce Encrypt.
-   *
-   * @return \Drupal\encrypt\EncryptionProfileInterface|null
-   *   The assigned profile, or null if none has been assigned.
-   *
-   * @throws \Drupal\salesforce\EntityNotFoundException
-   *   If a profile is assigned, but cannot be loaded.
-   */
-  public function getEncryptionProfile();
-
-  /**
-   * If the given profile is our active one, disable encryption.
-   *
-   * Since we rely on a specific encryption profile, we need to respond in case
-   * it gets deleted. Check to see if the profile being deleted is the one
-   * assigned for encryption; if so, decrypt our config and disable encryption.
-   *
-   * @param \Drupal\encrypt\EncryptionProfileInterface $profile
-   *   The encryption profile being deleted.
-   */
-  public function hookEncryptionProfileDelete(EncryptionProfileInterface $profile);
-
-  /**
-   * Encrypts a value using the active encryption profile, or return plaintext.
-   *
-   * @param string $value
-   *   The value to encrypt.
-   *
-   * @return string
-   *   The encrypted value, or plaintext if no active profile.
-   */
-  public function encrypt($value);
-
-  /**
-   * Decrypts a value using active encryption profile, or return the same value.
-   *
-   * @param string $value
-   *   The value to decrypt.
-   *
-   * @return string
-   *   The decrypted value, or the unchanged value if no active profile.
-   */
-  public function decrypt($value);
-
-}
diff --git a/modules/salesforce_encrypt/src/Rest/RestClient.php b/modules/salesforce_encrypt/src/Rest/RestClient.php
deleted file mode 100644
index 6d8461f19f77ca2a75276fb7c9095ba61531b504..0000000000000000000000000000000000000000
--- a/modules/salesforce_encrypt/src/Rest/RestClient.php
+++ /dev/null
@@ -1,301 +0,0 @@
-<?php
-
-namespace Drupal\salesforce_encrypt\Rest;
-
-use Drupal\Component\Serialization\Json;
-use Drupal\Core\Cache\CacheBackendInterface;
-use Drupal\Core\Config\ConfigFactoryInterface;
-use Drupal\Core\Lock\LockBackendInterface;
-use Drupal\Core\State\StateInterface;
-use Drupal\Core\StringTranslation\StringTranslationTrait;
-use Drupal\Core\Url;
-use Drupal\encrypt\EncryptServiceInterface;
-use Drupal\encrypt\EncryptionProfileInterface;
-use Drupal\encrypt\EncryptionProfileManagerInterface;
-use Drupal\salesforce\EntityNotFoundException;
-use Drupal\salesforce\Rest\RestClient as SalesforceRestClient;
-use GuzzleHttp\ClientInterface;
-use Drupal\Component\Datetime\TimeInterface;
-
-/**
- * Objects, properties, and methods to communicate with the Salesforce REST API.
- *
- * @deprecated will be removed in 8.x-4.0 release.
- */
-class RestClient extends SalesforceRestClient implements EncryptedRestClientInterface {
-
-  use StringTranslationTrait;
-
-  /**
-   * Encryption service.
-   *
-   * @var \Drupal\encrypt\EncryptServiceInterface
-   */
-  protected $encryption;
-
-  /**
-   * Encryption profile manager.
-   *
-   * @var \Drupal\encrypt\EncryptionProfileManagerInterface
-   */
-  protected $encryptionProfileManager;
-
-  /**
-   * The active encryption profile id.
-   *
-   * @var string
-   */
-  protected $encryptionProfileId;
-
-  /**
-   * The encryption profile to use when encrypting and decrypting data.
-   *
-   * @var \Drupal\encrypt\EncryptionProfileInterface
-   */
-  protected $encryptionProfile;
-
-  /**
-   * Construct a new Encrypted Rest Client.
-   *
-   * @param \GuzzleHttp\ClientInterface $http_client
-   *   The GuzzleHttp Client.
-   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
-   *   The config factory service.
-   * @param \Drupal\Core\State\StateInterface $state
-   *   The state service.
-   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
-   *   The cache service.
-   * @param \Drupal\Component\Serialization\Json $json
-   *   The JSON serializer service.
-   * @param \Drupal\Component\Datetime\TimeInterface $time
-   *   The Time service.
-   * @param \Drupal\encrypt\EncryptServiceInterface $encryption
-   *   The encryption service.
-   * @param \Drupal\encrypt\EncryptionProfileManagerInterface $encryptionProfileManager
-   *   The Encryption profile manager service.
-   * @param \Drupal\Core\Lock\LockBackendInterface $lock
-   *   The lock backend service.
-   */
-  public function __construct(ClientInterface $http_client, ConfigFactoryInterface $config_factory, StateInterface $state, CacheBackendInterface $cache, Json $json, TimeInterface $time, EncryptServiceInterface $encryption, EncryptionProfileManagerInterface $encryptionProfileManager, LockBackendInterface $lock) {
-    parent::__construct($http_client, $config_factory, $state, $cache, $json, $time);
-    $this->encryption = $encryption;
-    $this->encryptionProfileId = $this->state->get('salesforce_encrypt.profile');
-    $this->encryptionProfileManager = $encryptionProfileManager;
-    $this->encryptionProfile = NULL;
-    $this->lock = $lock;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function enableEncryption(EncryptionProfileInterface $profile) {
-    if ($ret = $this->setEncryption($profile)) {
-      $this->state->resetCache();
-    }
-    return $ret;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function disableEncryption() {
-    if ($ret = $this->setEncryption()) {
-      $this->state->resetCache();
-    }
-    return $ret;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function hookEncryptionProfileDelete(EncryptionProfileInterface $profile) {
-    if ($this->encryptionProfileId == $profile->id()) {
-      $this->disableEncryption();
-    }
-  }
-
-  /**
-   * Set the given encryption profile as active.
-   *
-   * If given profile is null, decrypt and disable encryption.
-   *
-   * @param \Drupal\encrypt\EncryptionProfileInterface|null $profile
-   *   The encryption profile. If null, encryption will be disabled.
-   */
-  protected function setEncryption(EncryptionProfileInterface $profile = NULL) {
-    if (!$this->lock->acquire('salesforce_encrypt')) {
-      throw new \RuntimeException('Unable to acquire lock.');
-    }
-
-    $access_token = $this->getAccessToken();
-    $refresh_token = $this->getRefreshToken();
-    $identity = $this->getIdentity();
-    $consumerKey = $this->getConsumerKey();
-    $consumerSecret = $this->getConsumerSecret();
-
-    $this->encryptionProfileId = $profile == NULL ? NULL : $profile->id();
-    $this->encryptionProfile = $profile;
-    $this->state->set('salesforce_encrypt.profile', $this->encryptionProfileId);
-
-    $this->setAccessToken($access_token);
-    $this->setRefreshToken($refresh_token);
-    $this->setIdentity($identity);
-    $this->setConsumerKey($consumerKey);
-    $this->setConsumerSecret($consumerSecret);
-
-    $this->lock->release('salesforce_encrypt');
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getEncryptionProfile() {
-    if ($this->encryptionProfile) {
-      return $this->encryptionProfile;
-    }
-    elseif (empty($this->encryptionProfileId)) {
-      return NULL;
-    }
-    else {
-      $this->encryptionProfile = $this->encryptionProfileManager
-        ->getEncryptionProfile($this->encryptionProfileId);
-      if (empty($this->encryptionProfile)) {
-        throw new EntityNotFoundException(['id' => $this->encryptionProfileId], 'encryption_profile');
-      }
-      return $this->encryptionProfile;
-    }
-  }
-
-  /**
-   * Deprecated, use doGetEncryptionProfile.
-   *
-   * @deprecated use ::doGetEncryptionProfile().
-   */
-  protected function _getEncryptionProfile() {
-    return $this->doGetEncryptionProfile();
-  }
-
-  /**
-   * Exception-handling wrapper around getEncryptionProfile().
-   *
-   * GetEncryptionProfile() will throw an EntityNotFoundException exception
-   * if it has an encryption profile ID but cannot load it.  In this wrapper
-   * we handle that exception by setting a helpful error message and allow
-   * execution to proceed.
-   *
-   * @return \Drupal\encrypt\EncryptionProfileInterface|null
-   *   The encryption profile if it can be loaded, otherwise NULL.
-   */
-  protected function doGetEncryptionProfile() {
-    try {
-      $profile = $this->getEncryptionProfile();
-    }
-    catch (EntityNotFoundException $e) {
-      drupal_set_message($this->t('Error while loading encryption profile. You will need to <a href=":encrypt">assign a new encryption profile</a>, then <a href=":oauth">re-authenticate to Salesforce</a>.', [':encrypt' => Url::fromRoute('salesforce_encrypt.settings')->toString(), ':oauth' => Url::fromRoute('salesforce.authorize')->toString()]), 'error');
-    }
-
-    return $profile;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function encrypt($value) {
-    if (empty($this->doGetEncryptionProfile())) {
-      return $value;
-    }
-    else {
-      return $this->encryption->encrypt($value, $this->doGetEncryptionProfile());
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function decrypt($value) {
-    if (empty($this->doGetEncryptionProfile()) || empty($value) || mb_strlen($value) === 0) {
-      return $value;
-    }
-    else {
-      return $this->encryption->decrypt($value, $this->doGetEncryptionProfile());
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getAccessToken() {
-    return $this->decrypt(parent::getAccessToken());
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setAccessToken($token) {
-    return parent::setAccessToken($this->encrypt($token));
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getRefreshToken() {
-    return $this->decrypt(parent::getRefreshToken());
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setRefreshToken($token) {
-    return parent::setRefreshToken($this->encrypt($token));
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setIdentity($data) {
-    if (is_array($data)) {
-      $data = serialize($data);
-    }
-    return parent::setIdentity($this->encrypt($data));
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getIdentity() {
-    $data = $this->decrypt(parent::getIdentity());
-    if (!empty($data) && !is_array($data)) {
-      $data = unserialize($data);
-    }
-    return $data;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getConsumerKey() {
-    return $this->decrypt(parent::getConsumerKey());
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setConsumerKey($value) {
-    return parent::setConsumerKey($this->encrypt($value));
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getConsumerSecret() {
-    return $this->decrypt(parent::getConsumerSecret());
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setConsumerSecret($value) {
-    return parent::setConsumerSecret($this->encrypt($value));
-  }
-
-}
diff --git a/modules/salesforce_encrypt/src/SalesforceEncryptServiceProvider.php b/modules/salesforce_encrypt/src/SalesforceEncryptServiceProvider.php
deleted file mode 100644
index 21f6ca86421cae7156f71810495b27b2eb7a0856..0000000000000000000000000000000000000000
--- a/modules/salesforce_encrypt/src/SalesforceEncryptServiceProvider.php
+++ /dev/null
@@ -1,29 +0,0 @@
-<?php
-
-namespace Drupal\salesforce_encrypt;
-
-use Drupal\Core\DependencyInjection\ContainerBuilder;
-use Drupal\Core\DependencyInjection\ServiceProviderBase;
-use Drupal\salesforce_encrypt\Rest\RestClient;
-use Symfony\Component\DependencyInjection\Reference;
-
-/**
- * Modifies the salesforce client service.
- *
- * @deprecated will be removed in 8.x-4.0 release.
- */
-class SalesforceEncryptServiceProvider extends ServiceProviderBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function alter(ContainerBuilder $container) {
-    // Overrides salesforce.client class with our EncryptedRestClientInterface.
-    $container->getDefinition('salesforce.client')
-      ->setClass(RestClient::class)
-      ->addArgument(new Reference('encryption'))
-      ->addArgument(new Reference('encrypt.encryption_profile.manager'))
-      ->addArgument(new Reference('lock'));
-  }
-
-}
diff --git a/modules/salesforce_encrypt/src/SalesforceEncryptedAuthTokenStorage.php b/modules/salesforce_encrypt/src/SalesforceEncryptedAuthTokenStorage.php
new file mode 100644
index 0000000000000000000000000000000000000000..53252719adc918ff7dfd6e44b19e9cf9565b78c8
--- /dev/null
+++ b/modules/salesforce_encrypt/src/SalesforceEncryptedAuthTokenStorage.php
@@ -0,0 +1,91 @@
+<?php
+
+namespace Drupal\salesforce_encrypt;
+
+use Drupal\salesforce\Entity\SalesforceAuthConfig;
+use Drupal\salesforce\Storage\SalesforceAuthTokenStorage;
+use Drupal\salesforce_encrypt\Plugin\SalesforceAuthProvider\SalesforceEncryptedOAuthPlugin;
+use OAuth\Common\Token\TokenInterface;
+
+/**
+ * Auth token storage, using encryption.
+ */
+class SalesforceEncryptedAuthTokenStorage extends SalesforceAuthTokenStorage implements SalesforceEncryptedAuthTokenStorageInterface {
+
+  /**
+   * Auth plugin manager.
+   *
+   * @var \Drupal\salesforce\SalesforceAuthProviderPluginManager
+   */
+  protected $authPluginManager;
+
+  /**
+   * Given a service id, return the instantiated auth provider plugin.
+   *
+   * @param string $service_id
+   *   The service id.
+   *
+   * @return \Drupal\salesforce\SalesforceAuthProviderInterface
+   *   The plugin.
+   */
+  protected function service($service_id) {
+    if (!$this->authPluginManager) {
+      $this->authPluginManager = \Drupal::service('plugin.manager.salesforce.auth_providers');
+    }
+    $auth = SalesforceAuthConfig::load($service_id);
+    return $auth->getPlugin();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function retrieveAccessToken($service_id) {
+    $token = parent::retrieveAccessToken($service_id);
+    if ($token instanceof TokenInterface || !$this->service($service_id) instanceof SalesforceEncryptedOAuthPlugin) {
+      return $token;
+    }
+    $token = unserialize($this->service($service_id)->decrypt($token));
+    return $token;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function storeAccessToken($service_id, TokenInterface $token) {
+    if ($this->service($service_id) instanceof SalesforceEncryptedOAuthPlugin) {
+      $token = $this->service($service_id)->encrypt(serialize($token));
+    }
+    $this->state->set(self::getTokenStorageId($service_id), $token);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function storeIdentity($service_id, $identity) {
+    if ($this->service($service_id) instanceof SalesforceEncryptedOAuthPlugin) {
+      if (is_array($identity)) {
+        $identity = serialize($identity);
+      }
+      $identity = $this->service($service_id)->encrypt($identity);
+    }
+    $this->state->set(self::getIdentityStorageId($service_id), $identity);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function retrieveIdentity($service_id) {
+    $identity = parent::retrieveIdentity($service_id);
+    if (!$this->service($service_id) instanceof SalesforceEncryptedOAuthPlugin) {
+      return $identity;
+    }
+    $identity = $this->service($service_id)->decrypt($identity);
+    if (!empty($identity) && !is_array($identity)) {
+      $identity = unserialize($identity);
+    }
+    return $identity;
+  }
+
+}
diff --git a/modules/salesforce_encrypt/src/SalesforceEncryptedAuthTokenStorageInterface.php b/modules/salesforce_encrypt/src/SalesforceEncryptedAuthTokenStorageInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..6ff4be197a6c375408d9161a9976cbe9d19d51f2
--- /dev/null
+++ b/modules/salesforce_encrypt/src/SalesforceEncryptedAuthTokenStorageInterface.php
@@ -0,0 +1,12 @@
+<?php
+
+namespace Drupal\salesforce_encrypt;
+
+use Drupal\salesforce\Storage\SalesforceAuthTokenStorageInterface;
+
+/**
+ * Encrypted token storage interface.
+ */
+interface SalesforceEncryptedAuthTokenStorageInterface extends SalesforceAuthTokenStorageInterface {
+
+}
diff --git a/modules/salesforce_encrypt/src/SalesforceEncryptedOAuthPluginInterface.php b/modules/salesforce_encrypt/src/SalesforceEncryptedOAuthPluginInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..27a94f22ca80fdde2fd123674b14f3a7c4595b4b
--- /dev/null
+++ b/modules/salesforce_encrypt/src/SalesforceEncryptedOAuthPluginInterface.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace Drupal\salesforce_encrypt;
+
+use Drupal\encrypt\EncryptionProfileInterface;
+use Drupal\salesforce\SalesforceOAuthPluginInterface;
+
+/**
+ * Encrypted oauth provider interface.
+ */
+interface SalesforceEncryptedOAuthPluginInterface extends SalesforceOAuthPluginInterface {
+
+  /**
+   * Callback for hook_encryption_profile_predelete().
+   *
+   * @param \Drupal\encrypt\EncryptionProfileInterface $profile
+   *   The encryption profile being deleted.
+   */
+  public function hookEncryptionProfileDelete(EncryptionProfileInterface $profile);
+
+  /**
+   * Get the encryption profile assigned to this auth plugin.
+   *
+   * @return \Drupal\encrypt\EncryptionProfileInterface|null
+   *   Profile.
+   */
+  public function encryptionProfile();
+
+  /**
+   * Decrypt a given value, using the assigned encryption profile.
+   *
+   * @param string $value
+   *   The encrypted value.
+   *
+   * @return string
+   *   The plain text value.
+   *
+   * @throws \Drupal\encrypt\Exception\EncryptException
+   *   On decryption error.
+   */
+  public function decrypt($value);
+
+  /**
+   * Encrypt a value, using the assigned encryption profile.
+   *
+   * @param string $value
+   *   The plain text value.
+   *
+   * @return string
+   *   The encrypted value.
+   *
+   * @throws \Drupal\encrypt\Exception\EncryptException
+   *   On error.
+   */
+  public function encrypt($value);
+
+}
diff --git a/modules/salesforce_encrypt/tests/src/Unit/RestClientTest.php b/modules/salesforce_encrypt/tests/src/Unit/RestClientTest.php
deleted file mode 100644
index 84ccc59744e2c8c9436957019e9c331964d3dee3..0000000000000000000000000000000000000000
--- a/modules/salesforce_encrypt/tests/src/Unit/RestClientTest.php
+++ /dev/null
@@ -1,121 +0,0 @@
-<?php
-
-namespace Drupal\Tests\salesforce_encrypt\Unit;
-
-use Drupal\Component\Serialization\Json;
-use Drupal\Core\Cache\CacheBackendInterface;
-use Drupal\Core\Config\ConfigFactory;
-use Drupal\Core\Lock\LockBackendInterface;
-use Drupal\Core\State\State;
-use Drupal\Tests\UnitTestCase;
-use Drupal\encrypt\EncryptServiceInterface;
-use Drupal\encrypt\EncryptionProfileInterface;
-use Drupal\encrypt\EncryptionProfileManagerInterface;
-use Drupal\salesforce_encrypt\Rest\RestClient;
-use GuzzleHttp\Client;
-use Drupal\Component\Datetime\TimeInterface;
-
-/**
- * @coversDefaultClass \Drupal\salesforce_encrypt\Rest\RestClient
- * @group salesforce
- *
- * @deprecated will be removed in 8.x-4.0 release.
- */
-class RestClientTest extends UnitTestCase {
-
-  static public $modules = [
-    'key',
-    'encrypt',
-    'salesforce',
-    'salesforce_encrypt',
-  ];
-
-  protected $httpClient;
-  protected $configFactory;
-  protected $state;
-  protected $cache;
-  protected $json;
-  protected $time;
-  protected $encryption;
-  protected $profileManager;
-  protected $lock;
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setUp() {
-    parent::setUp();
-    $this->accessToken = 'foo';
-    $this->refreshToken = 'bar';
-    $this->identity = ['zee' => 'bang'];
-
-    $this->httpClient = $this->getMock(Client::CLASS);
-    $this->configFactory =
-      $this->getMockBuilder(ConfigFactory::CLASS)
-        ->disableOriginalConstructor()
-        ->getMock();
-    $this->state =
-      $this->getMockBuilder(State::CLASS)
-        ->disableOriginalConstructor()
-        ->getMock();
-    $this->cache = $this->createMock(CacheBackendInterface::CLASS);
-    $this->json = $this->createMock('Drupal\Component\Serialization\Json');
-    $this->encryption = $this->createMock(EncryptServiceInterface::CLASS);
-    $this->profileManager = $this->createMock(EncryptionProfileManagerInterface::CLASS);
-    $this->lock = $this->createMock(LockBackendInterface::CLASS);
-    $this->encryptionProfile = $this->createMock(EncryptionProfileInterface::CLASS);
-    $this->json = $this->createMock(Json::CLASS);
-    $this->time = $this->createMock(TimeInterface::CLASS);
-    $this->client = $this->getMockBuilder(RestClient::CLASS)
-      ->setMethods(['doGetEncryptionProfile'])
-      ->setConstructorArgs([
-        $this->httpClient,
-        $this->configFactory,
-        $this->state,
-        $this->cache,
-        $this->json,
-        $this->time,
-        $this->encryption,
-        $this->profileManager,
-        $this->lock,
-      ])
-      ->getMock();
-  }
-
-  /**
-   * @covers ::encrypt
-   *
-   * encrypt is protected, so we get at it through ::getAccessToken
-   * This test covers the case where access token is NULL.
-   */
-  public function testEncryptNull() {
-    // Test unencrypted.
-    $this->state->expects($this->any())
-      ->method('get')
-      ->willReturn(NULL);
-    $this->client->expects($this->any())
-      ->method('doGetEncryptionProfile')
-      ->willReturn(NULL);
-    $this->assertFalse($this->client->getAccessToken());
-  }
-
-  /**
-   * @covers ::encrypt
-   *
-   * This test covers the case where access token is not NULL.
-   */
-  public function testEncryptNotNull() {
-    // Test unencrypted.
-    $this->state->expects($this->any())
-      ->method('get')
-      ->willReturn('not null');
-    $this->client->expects($this->any())
-      ->method('doGetEncryptionProfile')
-      ->willReturn($this->encryptionProfile);
-    $this->encryption->expects($this->any())
-      ->method('decrypt')
-      ->willReturn($this->accessToken);
-    $this->assertEquals($this->accessToken, $this->client->getAccessToken());
-  }
-
-}
diff --git a/modules/salesforce_jwt/config/schema/salesforce_jwt.schema.yml b/modules/salesforce_jwt/config/schema/salesforce_jwt.schema.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1bef35d46118dafd25b3a0fafe721cd3e0e5dc7a
--- /dev/null
+++ b/modules/salesforce_jwt/config/schema/salesforce_jwt.schema.yml
@@ -0,0 +1,16 @@
+salesforce.auth_provider_settings.jwt:
+  type: mapping
+  label: 'Salesforce JWT Provider Settings'
+  mapping:
+    consumer_key:
+      type: string
+      label: 'Consumer Key'
+    login_user:
+      type: string
+      label: 'Login User'
+    login_url:
+      type: uri
+      label: 'Login URL'
+    encrypt_key:
+      type: key.key.[%key]
+      label: 'Encryption Key ID'
diff --git a/modules/salesforce_jwt/salesforce_jwt.info.yml b/modules/salesforce_jwt/salesforce_jwt.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..040a7403776d9e8109fe4d06e7a6f16f3095f976
--- /dev/null
+++ b/modules/salesforce_jwt/salesforce_jwt.info.yml
@@ -0,0 +1,8 @@
+name: Salesforce JWT Auth Provider
+type: module
+description: Provides key-based Salesforce authentication.
+core: 8.x
+package: Salesforce
+dependencies:
+  - salesforce
+  - key
diff --git a/modules/salesforce_jwt/src/Consumer/JWTCredentials.php b/modules/salesforce_jwt/src/Consumer/JWTCredentials.php
new file mode 100644
index 0000000000000000000000000000000000000000..c1b997e96a95244b1613ed9527b9a55e27405bb3
--- /dev/null
+++ b/modules/salesforce_jwt/src/Consumer/JWTCredentials.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace Drupal\salesforce_jwt\Consumer;
+
+use Drupal\salesforce\Consumer\SalesforceCredentials;
+
+/**
+ * JWT credentials.
+ */
+class JWTCredentials extends SalesforceCredentials {
+
+  /**
+   * Pre-authorized login user for JWT OAuth authentication.
+   *
+   * @var string
+   */
+  protected $loginUser;
+
+  /**
+   * Id of authorization key for this JWT Credential.
+   *
+   * @var string
+   */
+  protected $keyId;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct($consumerKey, $loginUrl, $loginUser, $keyId) {
+    parent::__construct($consumerKey, $loginUrl);
+    $this->loginUser = $loginUser;
+    $this->keyId = $keyId;
+  }
+
+  /**
+   * Login user getter.
+   *
+   * @return string
+   *   The login user.
+   */
+  public function getLoginUser() {
+    return $this->loginUser;
+  }
+
+  /**
+   * Authorization key getter.
+   *
+   * @return string
+   *   The key id.
+   */
+  public function getKeyId() {
+    return $this->keyId;
+  }
+
+}
diff --git a/modules/salesforce_jwt/src/Plugin/SalesforceAuthProvider/SalesforceJWTPlugin.php b/modules/salesforce_jwt/src/Plugin/SalesforceAuthProvider/SalesforceJWTPlugin.php
new file mode 100644
index 0000000000000000000000000000000000000000..24f5f6884a925272855a602613144455439c18d8
--- /dev/null
+++ b/modules/salesforce_jwt/src/Plugin/SalesforceAuthProvider/SalesforceJWTPlugin.php
@@ -0,0 +1,287 @@
+<?php
+
+namespace Drupal\salesforce_jwt\Plugin\SalesforceAuthProvider;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Drupal\salesforce_jwt\Consumer\JWTCredentials;
+use Drupal\salesforce\SalesforceAuthProviderPluginBase;
+use OAuth\Common\Http\Uri\Uri;
+use Drupal\salesforce\Storage\SalesforceAuthTokenStorageInterface;
+use OAuth\Common\Http\Client\ClientInterface;
+use OAuth\Common\Token\TokenInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Firebase\JWT\JWT;
+
+/**
+ * JWT Oauth plugin.
+ *
+ * @Plugin(
+ *   id = "jwt",
+ *   label = @Translation("Salesforce JWT OAuth")
+ * )
+ */
+class SalesforceJWTPlugin extends SalesforceAuthProviderPluginBase {
+
+  /**
+   * The credentials for this auth plugin.
+   *
+   * @var \Drupal\salesforce_jwt\Consumer\JWTCredentials
+   */
+  protected $credentials;
+
+  /**
+   * {@inheritdoc}
+   */
+  const SERVICE_TYPE = 'jwt';
+
+  /**
+   * {@inheritdoc}
+   */
+  const LABEL = 'JWT';
+
+  /**
+   * SalesforceAuthServiceBase constructor.
+   *
+   * @param string $id
+   *   The plugin / auth config id.
+   * @param \Drupal\salesforce_jwt\Consumer\JWTCredentials $credentials
+   *   The credentials.
+   * @param \OAuth\Common\Http\Client\ClientInterface $httpClient
+   *   Http client wrapper.
+   * @param \Drupal\salesforce\Storage\SalesforceAuthTokenStorageInterface $storage
+   *   Token storage.
+   *
+   * @throws \OAuth\OAuth2\Service\Exception\InvalidScopeException
+   *   On error.
+   */
+  public function __construct($id, JWTCredentials $credentials, ClientInterface $httpClient, SalesforceAuthTokenStorageInterface $storage) {
+    parent::__construct($credentials, $httpClient, $storage, [], new Uri($credentials->getLoginUrl()));
+    $this->id = $id;
+  }
+
+  public function getConsumerSecret() {
+    return parent::getConsumerSecret(); // TODO: Change the autogenerated stub
+  }
+
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    $configuration = array_merge(self::defaultConfiguration(), $configuration);
+    $cred = new JWTCredentials($configuration['consumer_key'], $configuration['login_url'], $configuration['login_user'], $configuration['encrypt_key']);
+    return new static($configuration['id'], $cred, $container->get('salesforce.http_client_wrapper'), $container->get('salesforce.auth_token_storage'));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function defaultConfiguration() {
+    $defaults = parent::defaultConfiguration();
+    return array_merge($defaults, [
+      'login_user' => '',
+      'encrypt_key' => '',
+    ]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLoginUrl() {
+    return $this->credentials->getLoginUrl();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    if (!$this->keyRepository()) {
+      $this->messenger()->addError($this->t('JWT Auth requires <a href="https://drupal.org/project/key">Key</a> module. Please install before adding a JWT Auth config.'));
+      return $form;
+    }
+    if (!$this->keyRepository()->getKeyNamesAsOptions(['type' => 'authentication'])) {
+      $this->messenger()->addError($this->t('Please <a href="@href">add an authentication key</a> before creating a JWT Auth provider.', ['@href' => Url::fromRoute('entity.key.add_form')->toString()]));
+      return $form;
+    }
+    $form['consumer_key'] = [
+      '#title' => t('Salesforce consumer key'),
+      '#type' => 'textfield',
+      '#description' => t('Consumer key of the Salesforce remote application you want to grant access to'),
+      '#required' => TRUE,
+      '#default_value' => $this->credentials->getConsumerKey(),
+    ];
+
+    $form['login_user'] = [
+      '#title' => $this->t('Salesforce login user'),
+      '#type' => 'textfield',
+      '#description' => $this->t('User account to issue token to'),
+      '#required' => TRUE,
+      '#default_value' => $this->credentials->getLoginUser(),
+    ];
+
+    $form['login_url'] = [
+      '#title' => t('Login URL'),
+      '#type' => 'textfield',
+      '#default_value' => $this->credentials->getLoginUrl(),
+      '#description' => t('Enter a login URL, either https://login.salesforce.com or https://test.salesforce.com.'),
+      '#required' => TRUE,
+    ];
+
+    // Can't use key-select input type here because its #process method doesn't
+    // fire on ajax, so the list is empty. DERP.
+    $form['encrypt_key'] = [
+      '#title' => 'Private Key',
+      '#type' => 'select',
+      '#options' => $this->keyRepository()->getKeyNamesAsOptions(['type' => 'authentication']),
+      '#required' => TRUE,
+      '#default_value' => $this->credentials->getKeyId(),
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
+    if (!$this->keyRepository()) {
+      $form_state->setError($form, $this->t('JWT Auth requires <a href="https://drupal.org/project/key">Key</a> module. Please install before adding a JWT Auth config.'));
+      return;
+    }
+    parent::validateConfigurationForm($form, $form_state);
+    $this->setConfiguration($form_state->getValues());
+    try {
+      $this->validateCredentials($this->getLoginUrl());
+    }
+    catch (\Exception $e) {
+      $form_state->setError($form, $e->getMessage());
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function save(array $form, FormStateInterface $form_state) {
+    parent::save($form, $form_state);
+    try {
+      $this->setConfiguration($form_state->getValues());
+      $this->getToken($this->getLoginUrl());
+      \Drupal::messenger()->addStatus(t('Successfully connected to Salesforce as user %name.', ['%name' => $this->getIdentity()['display_name']]));
+    }
+    catch (\Exception $e) {
+      $form_state->setError($form, $this->t('Failed to connect to Salesforce: %message', ['%message' => $e->getMessage()]));
+    }
+  }
+
+  /**
+   * Validate credentials prior to saving them.
+   *
+   * @param string $login_url
+   *   The login URL, from form input, against which to validate.
+   *
+   * @return \OAuth\Common\Token\TokenInterface|\OAuth\OAuth2\Token\StdOAuth2Token
+   *   On success.
+   *
+   * @throws \OAuth\Common\Http\Exception\TokenResponseException
+   *   On error.
+   */
+  protected function validateCredentials($login_url) {
+    // Initialize access token.
+    $assertion = $this->generateAssertion();
+    $data = [
+      'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
+      'assertion' => $assertion,
+    ];
+    $response = $this->httpClient->retrieveResponse(new Uri($login_url . static::AUTH_TOKEN_PATH), $data, ['Content-Type' => 'application/x-www-form-urlencoded']);
+    $token = $this->parseAccessTokenResponse($response);
+    return $token;
+  }
+
+  /**
+   * Gets a token from the given JWT OAuth endpoint.
+   */
+  protected function getToken($login_url) {
+    // Initialize access token.
+    $assertion = $this->generateAssertion();
+    $data = [
+      'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
+      'assertion' => $assertion,
+    ];
+    $response = $this->httpClient->retrieveResponse(new Uri($login_url . static::AUTH_TOKEN_PATH), $data, ['Content-Type' => 'application/x-www-form-urlencoded']);
+    $token = $this->parseAccessTokenResponse($response);
+    $this->storage->storeAccessToken($this->service(), $token);
+
+    // Initialize identity.
+    $headers = [
+      'Authorization' => 'OAuth ' . $token->getAccessToken(),
+      'Content-type' => 'application/json',
+    ];
+    $data = $token->getExtraParams();
+    $response = $this->httpClient->retrieveResponse(new Uri($data['id']), [], $headers);
+    $identity = $this->parseIdentityResponse($response);
+    $this->storage->storeIdentity($this->service(), $identity);
+
+    return $token;
+  }
+
+  /**
+   * Refreshes an OAuth2 access token.
+   *
+   * @param \OAuth\Common\Token\TokenInterface $token
+   *   The JWT OAuth token to refresh.
+   *
+   * @return \OAuth\Common\Token\TokenInterface
+   *   On success.
+   *
+   * @throws \OAuth\OAuth2\Service\Exception\MissingRefreshTokenException
+   *   On error.
+   * @throws \OAuth\Common\Http\Exception\TokenResponseException
+   *   On error.
+   */
+  public function refreshAccessToken(TokenInterface $token) {
+    $token = $this->getToken($this->getLoginUrl());
+    return $token;
+  }
+
+  /**
+   * Key repository wrapper.
+   *
+   * @return \Drupal\key\KeyRepository|false
+   *   The key repo.
+   */
+  protected function keyRepository() {
+    if (!\Drupal::hasService('key.repository')) {
+      return FALSE;
+    }
+    return \Drupal::service('key.repository');
+  }
+
+  /**
+   * Returns a JWT Assertion to authenticate.
+   *
+   * @return string
+   *   JWT Assertion.
+   */
+  protected function generateAssertion() {
+    $key = $this->keyRepository()->getKey($this->credentials->getKeyId())->getKeyValue();
+    $token = $this->generateAssertionClaim();
+    return JWT::encode($token, $key, 'RS256');
+  }
+
+  /**
+   * Returns a JSON encoded JWT Claim.
+   *
+   * @return array
+   *   The claim array.
+   */
+  protected function generateAssertionClaim() {
+    return [
+      'iss' => $this->credentials->getConsumerKey(),
+      'sub' => $this->credentials->getLoginUser(),
+      'aud' => $this->credentials->getLoginUrl(),
+      'exp' => \Drupal::time()->getCurrentTime() + 60,
+    ];
+  }
+
+}
diff --git a/modules/salesforce_mapping/src/Form/SalesforceMappingDeleteForm.php b/modules/salesforce_mapping/src/Form/SalesforceMappingDeleteForm.php
index 066fc61ac62ee29ca4c7866ad0197ea957b38809..ec011e9b4aa0fc2e5ba82a3e5f854ec8d3b59de7 100644
--- a/modules/salesforce_mapping/src/Form/SalesforceMappingDeleteForm.php
+++ b/modules/salesforce_mapping/src/Form/SalesforceMappingDeleteForm.php
@@ -7,7 +7,7 @@ use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Url;
 
 /**
- * Salesforce Mapping Delete Form .
+ * Salesforce Mapping Delete Form.
  */
 class SalesforceMappingDeleteForm extends EntityConfirmFormBase {
 
diff --git a/modules/salesforce_mapping/src/Form/SalesforceMappingFormCrudBase.php b/modules/salesforce_mapping/src/Form/SalesforceMappingFormCrudBase.php
index 7a3531c86f6d82a6e98039971af88edb22e064ee..a79db8b7815074a664f24484255c030a642a6b5e 100644
--- a/modules/salesforce_mapping/src/Form/SalesforceMappingFormCrudBase.php
+++ b/modules/salesforce_mapping/src/Form/SalesforceMappingFormCrudBase.php
@@ -30,7 +30,7 @@ abstract class SalesforceMappingFormCrudBase extends SalesforceMappingFormBase {
       $object_type_options = $this->getSalesforceObjectTypeOptions();
     }
     catch (\Exception $e) {
-      $href = new Url('salesforce.authorize');
+      $href = new Url('salesforce.admin_config_salesforce');
       drupal_set_message($this->t('Error when connecting to Salesforce. Please <a href="@href">check your credentials</a> and try again: %message', ['@href' => $href->toString(), '%message' => $e->getMessage()]), 'error');
       return $form;
     }
diff --git a/modules/salesforce_mapping/src/Form/SalesforceMappingFormCrudBase.php.orig b/modules/salesforce_mapping/src/Form/SalesforceMappingFormCrudBase.php.orig
new file mode 100644
index 0000000000000000000000000000000000000000..7a3531c86f6d82a6e98039971af88edb22e064ee
--- /dev/null
+++ b/modules/salesforce_mapping/src/Form/SalesforceMappingFormCrudBase.php.orig
@@ -0,0 +1,513 @@
+<?php
+
+namespace Drupal\salesforce_mapping\Form;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\salesforce_mapping\MappingConstants;
+use Drupal\Core\Url;
+use Drupal\salesforce\Event\SalesforceEvents;
+use Drupal\salesforce\Event\SalesforceErrorEvent;
+
+/**
+ * Salesforce Mapping Form base.
+ */
+abstract class SalesforceMappingFormCrudBase extends SalesforceMappingFormBase {
+
+  /**
+   * The storage controller.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $storageController;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    // Perform our salesforce queries first, so that if we can't connect we
+    // don't waste time on the rest of the form.
+    try {
+      $object_type_options = $this->getSalesforceObjectTypeOptions();
+    }
+    catch (\Exception $e) {
+      $href = new Url('salesforce.authorize');
+      drupal_set_message($this->t('Error when connecting to Salesforce. Please <a href="@href">check your credentials</a> and try again: %message', ['@href' => $href->toString(), '%message' => $e->getMessage()]), 'error');
+      return $form;
+    }
+    $form = parent::buildForm($form, $form_state);
+    $mapping = $this->entity;
+    $form['label'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Label'),
+      '#default_value' => $mapping->label(),
+      '#required' => TRUE,
+      '#weight' => -30,
+    ];
+    $form['id'] = [
+      '#type' => 'machine_name',
+      '#required' => TRUE,
+      '#default_value' => $mapping->id(),
+      '#maxlength' => 255,
+      '#machine_name' => [
+        'exists' => ['Drupal\salesforce_mapping\Entity\SalesforceMapping', 'load'],
+        'source' => ['label'],
+      ],
+      '#disabled' => !$mapping->isNew(),
+      '#weight' => -20,
+    ];
+
+    $form['drupal_entity'] = [
+      '#title' => $this->t('Drupal entity'),
+      '#type' => 'details',
+      '#attributes' => [
+        'id' => 'edit-drupal-entity',
+      ],
+      // Gently discourage admins from breaking existing fieldmaps:
+      '#open' => $mapping->isNew(),
+    ];
+
+    $entity_types = $this->getEntityTypeOptions();
+    $form['drupal_entity']['drupal_entity_type'] = [
+      '#title' => $this->t('Drupal Entity Type'),
+      '#id' => 'edit-drupal-entity-type',
+      '#type' => 'select',
+      '#description' => $this->t('Select a Drupal entity type to map to a Salesforce object.'),
+      '#options' => $entity_types,
+      '#default_value' => $mapping->drupal_entity_type,
+      '#required' => TRUE,
+      '#empty_option' => $this->t('- Select -'),
+      '#ajax' => [
+        'callback' => [$this, 'bundleCallback'],
+        'event' => 'change',
+        'wrapper' => 'drupal_bundle',
+      ],
+    ];
+
+    $form['drupal_entity']['drupal_bundle'] = [
+      '#title' => $this->t('Bundle'),
+      '#type' => 'select',
+      '#default_value' => $mapping->drupal_bundle,
+      '#empty_option' => $this->t('- Select -'),
+      // Bundle select options will get completely replaced after user selects
+      // an entity, but we include all possibilities here for js-free
+      // compatibility (for simpletest)
+      '#options' => $this->getBundleOptions(),
+      '#required' => TRUE,
+      '#prefix' => '<div id="drupal_bundle">',
+      '#suffix' => '</div>',
+      // Don't expose the bundle listing until user has selected an entity.
+      '#states' => [
+        'visible' => [
+          ':input[name="drupal_entity_type"]' => ['!value' => ''],
+        ],
+      ],
+    ];
+    $input = $form_state->getUserInput();
+    if (!empty($input) && !empty($input['drupal_entity_type'])) {
+      $entity_type = $input['drupal_entity_type'];
+    }
+    else {
+      $entity_type = $form['drupal_entity']['drupal_entity_type']['#default_value'];
+    }
+    $bundle_info = $this->entityManager->getBundleInfo($entity_type);
+
+    if (!empty($bundle_info)) {
+      $form['drupal_entity']['drupal_bundle']['#options'] = [];
+      $form['drupal_entity']['drupal_bundle']['#title'] = $this->t('@entity_type Bundle', ['@entity_type' => $entity_types[$entity_type]]);
+      foreach ($bundle_info as $key => $info) {
+        $form['drupal_entity']['drupal_bundle']['#options'][$key] = $info['label'];
+      }
+    }
+
+    $form['salesforce_object'] = [
+      '#title' => $this->t('Salesforce object'),
+      '#id' => 'edit-salesforce-object',
+      '#type' => 'details',
+      // Gently discourage admins from breaking existing fieldmaps:
+      '#open' => $mapping->isNew(),
+    ];
+
+    $salesforce_object_type = '';
+    if (!empty($form_state->getValues()) && !empty($form_state->getValue('salesforce_object_type'))) {
+      $salesforce_object_type = $form_state->getValue('salesforce_object_type');
+    }
+    elseif ($mapping->salesforce_object_type) {
+      $salesforce_object_type = $mapping->salesforce_object_type;
+    }
+    $form['salesforce_object']['salesforce_object_type'] = [
+      '#title' => $this->t('Salesforce Object'),
+      '#id' => 'edit-salesforce-object-type',
+      '#type' => 'select',
+      '#description' => $this->t('Select a Salesforce object to map.'),
+      '#default_value' => $salesforce_object_type,
+      '#options' => $object_type_options,
+      '#required' => TRUE,
+      '#empty_option' => $this->t('- Select -'),
+    ];
+
+    // @TODO either change sync_triggers to human readable values, or make them work as hex flags again.
+    $trigger_options = $this->getSyncTriggerOptions();
+    $form['sync_triggers'] = [
+      '#title' => t('Action triggers'),
+      '#type' => 'details',
+      '#open' => TRUE,
+      '#tree' => TRUE,
+      '#description' => t('Select which actions on Drupal entities and Salesforce
+        objects should trigger a synchronization. These settings are used by the
+        salesforce_push and salesforce_pull modules.'
+      ),
+    ];
+    if (empty($trigger_options)) {
+      $form['sync_triggers']['#description'] .= ' ' . $this->t('<br/><em>No trigger options are available when Salesforce Push and Pull modules are disabled. Enable one or both modules to allow Push or Pull processing.</em>');
+    }
+
+    foreach ($trigger_options as $option => $label) {
+      $form['sync_triggers'][$option] = [
+        '#title' => $label,
+        '#type' => 'checkbox',
+        '#default_value' => !empty($mapping->sync_triggers[$option]),
+      ];
+    }
+
+    if ($this->moduleHandler->moduleExists('salesforce_pull')) {
+      // @TODO should push and pull settings get moved into push and pull modules?
+      $form['pull'] = [
+        '#title' => t('Pull Settings'),
+        '#type' => 'details',
+        '#description' => '',
+        '#open' => TRUE,
+        '#tree' => FALSE,
+        '#states' => [
+          'visible' => [
+            ':input[name^="sync_triggers[pull"]' => ['checked' => TRUE],
+          ],
+        ],
+      ];
+
+      if (!$mapping->isNew()) {
+        $form['pull']['last_pull_date'] = [
+          '#type' => 'item',
+          '#title' => t('Last Pull Date: %last_pull', ['%last_pull' => $mapping->getLastPullTime() ? \Drupal::service('date.formatter')->format($mapping->getLastPullTime()) : 'never']),
+          '#markup' => t('Resetting last pull date will cause salesforce pull module to query for updated records without respect for the pull trigger date. This is useful, for example, to re-pull all records after a purge.'),
+        ];
+        $form['pull']['last_pull_reset'] = [
+          '#type' => 'button',
+          '#value' => t('Reset Last Pull Date'),
+          '#disabled' => $mapping->getLastPullTime() == NULL,
+          '#limit_validation_errors' => [],
+          '#validate' => ['::lastPullReset'],
+        ];
+
+        $form['pull']['last_delete_date'] = [
+          '#type' => 'item',
+          '#title' => t('Last Delete Date: %last_pull', ['%last_pull' => $mapping->getLastDeleteTime() ? \Drupal::service('date.formatter')->format($mapping->getLastDeleteTime()) : 'never']),
+          '#markup' => t('Resetting last delete date will cause salesforce pull module to query for deleted record without respect for the pull trigger date.'),
+        ];
+        $form['pull']['last_delete_reset'] = [
+          '#type' => 'button',
+          '#value' => t('Reset Last Delete Date'),
+          '#disabled' => $mapping->getLastDeleteTime() == NULL,
+          '#limit_validation_errors' => [],
+          '#validate' => ['::lastDeleteReset'],
+        ];
+
+        // This doesn't work until after mapping gets saved.
+        // @TODO figure out best way to alert admins about this, or AJAX-ify it.
+        $form['pull']['pull_trigger_date'] = [
+          '#type' => 'select',
+          '#title' => t('Date field to trigger pull'),
+          '#description' => t('Poll Salesforce for updated records based on the given date field. Defaults to "Last Modified Date".'),
+          '#required' => $mapping->salesforce_object_type,
+          '#default_value' => $mapping->pull_trigger_date,
+          '#options' => $this->getPullTriggerOptions(),
+        ];
+      }
+
+      $form['pull']['pull_where_clause'] = [
+        '#title' => t('Pull query SOQL "Where" clause'),
+        '#type' => 'textarea',
+        '#description' => t('Add a "where" SOQL condition clause to limit records pulled from Salesforce. e.g. Email != \'\' AND RecordType.DevelopName = \'ExampleRecordType\''),
+        '#default_value' => $mapping->pull_where_clause,
+      ];
+
+      $form['pull']['pull_where_clause'] = [
+        '#title' => t('Pull query SOQL "Where" clause'),
+        '#type' => 'textarea',
+        '#description' => t('Add a "where" SOQL condition clause to limit records pulled from Salesforce. e.g. Email != \'\' AND RecordType.DevelopName = \'ExampleRecordType\''),
+        '#default_value' => $mapping->pull_where_clause,
+      ];
+
+      $form['pull']['pull_frequency'] = [
+        '#title' => t('Pull Frequency'),
+        '#type' => 'number',
+        '#default_value' => $mapping->pull_frequency,
+        '#description' => t('Enter a frequency, in seconds, for how often this mapping should be used to pull data to Drupal. Enter 0 to pull as often as possible. FYI: 1 hour = 3600; 1 day = 86400. <em>NOTE: pull frequency is shared per-Salesforce Object. The setting is exposed here for convenience.</em>'),
+      ];
+    }
+
+    if ($this->moduleHandler->moduleExists('salesforce_push')) {
+      $form['push'] = [
+        '#title' => t('Push Settings'),
+        '#type' => 'details',
+        '#description' => t('The asynchronous push queue is always enabled in Drupal 8: real-time push fails are queued for async push. Alternatively, you can choose to disable real-time push and use async-only.'),
+        '#open' => TRUE,
+        '#tree' => FALSE,
+        '#states' => [
+          'visible' => [
+            ':input[name^="sync_triggers[push"]' => ['checked' => TRUE],
+          ],
+        ],
+      ];
+
+      $form['push']['async'] = [
+        '#title' => t('Disable real-time push'),
+        '#type' => 'checkbox',
+        '#description' => t('When real-time push is disabled, enqueue changes and push to Salesforce asynchronously during cron. When disabled, push changes immediately upon entity CRUD, and only enqueue failures for async push.'),
+        '#default_value' => $mapping->async,
+      ];
+
+      $form['push']['push_frequency'] = [
+        '#title' => t('Push Frequency'),
+        '#type' => 'number',
+        '#default_value' => $mapping->push_frequency,
+        '#description' => t('Enter a frequency, in seconds, for how often this mapping should be used to push data to Salesforce. Enter 0 to push as often as possible. FYI: 1 hour = 3600; 1 day = 86400.'),
+        '#min' => 0,
+      ];
+
+      $form['push']['push_limit'] = [
+        '#title' => t('Push Limit'),
+        '#type' => 'number',
+        '#default_value' => $mapping->push_limit,
+        '#description' => t('Enter the maximum number of records to be pushed to Salesforce during a single queue batch. Enter 0 to process as many records as possible, subject to the global push queue limit.'),
+        '#min' => 0,
+      ];
+
+      $form['push']['push_retries'] = [
+        '#title' => t('Push Retries'),
+        '#type' => 'number',
+        '#default_value' => $mapping->push_retries,
+        '#description' => t("Enter the maximum number of attempts to push a record to Salesforce before it's considered failed. Enter 0 for no limit."),
+        '#min' => 0,
+      ];
+
+      $form['push']['weight'] = [
+        '#title' => t('Weight'),
+        '#type' => 'select',
+        '#options' => array_combine(range(-50, 50), range(-50, 50)),
+        '#description' => t('Not yet in use. During cron, mapping weight determines in which order items will be pushed. Lesser weight items will be pushed before greater weight items.'),
+        '#default_value' => $mapping->weight,
+      ];
+
+      $description = t('Check this box to disable cron push processing for this mapping, and allow standalone processing. A URL will be generated after saving the mapping.');
+      if ($mapping->id()) {
+        $standalone_url = Url::fromRoute(
+          'salesforce_push.endpoint.salesforce_mapping',
+          [
+            'salesforce_mapping' => $mapping->id(),
+            'key' => \Drupal::state()->get('system.cron_key'),
+          ],
+          ['absolute' => TRUE])
+          ->toString();
+        $description = t('Check this box to disable cron push processing for this mapping, and allow standalone processing via this URL: <a href=":url">:url</a>', [':url' => $standalone_url]);
+      }
+
+      $form['push']['push_standalone'] = [
+        '#title' => t('Enable standalone push queue processing'),
+        '#type' => 'checkbox',
+        '#description' => $description,
+        '#default_value' => $mapping->push_standalone,
+      ];
+
+      // If global standalone is enabled, then we force this mapping's
+      // standalone property to true.
+      if ($this->config('salesforce.settings')->get('standalone')) {
+        $settings_url = Url::fromRoute('salesforce.global_settings');
+        $form['push']['push_standalone']['#default_value'] = TRUE;
+        $form['push']['push_standalone']['#disabled'] = TRUE;
+        $form['push']['push_standalone']['#description'] .= ' ' . t('See also <a href="@url">global standalone processing settings</a>.', ['@url' => $settings_url]);
+      }
+    }
+
+    $form['meta'] = [
+      '#type' => 'details',
+      '#open' => TRUE,
+      '#tree' => FALSE,
+      '#title' => t('Additional properties'),
+    ];
+
+    $form['meta']['weight'] = [
+      '#title' => t('Weight'),
+      '#type' => 'select',
+      '#options' => array_combine(range(-50, 50), range(-50, 50)),
+      '#description' => t('During cron, mapping weight determines in which order items will be pushed or pulled. Lesser weight items will be pushed or pulled before greater weight items.'),
+      '#default_value' => $mapping->weight,
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    $bundles = $this->entityManager->getBundleInfo($form_state->getValue('drupal_entity_type'));
+    if (empty($bundles[$form_state->getValue('drupal_bundle')])) {
+      $form_state->setErrorByName('drupal_bundle', $this->t('Invalid bundle for entity type.'));
+    }
+    $button = $form_state->getTriggeringElement();
+    if ($button['#id'] != $form['actions']['submit']['#id']) {
+      // Skip validation unless we hit the "save" button.
+      return;
+    }
+
+    parent::validateForm($form, $form_state);
+
+    if ($this->entity->doesPull()) {
+      try {
+        $this->client->query($this->entity->getPullQuery());
+      }
+      catch (\Exception $e) {
+        $form_state->setError($form['pull']['pull_where_clause'], $this->t('Test pull query returned an error. Please check logs for error details.'));
+        \Drupal::service('event_dispatcher')->dispatch(SalesforceEvents::ERROR, new SalesforceErrorEvent($e));
+      }
+    }
+  }
+
+  /**
+   * Submit handler for "reset pull timestamp" button.
+   */
+  public function lastPullReset(array $form, FormStateInterface $form_state) {
+    $mapping = $this->entity->setLastPullTime(NULL);
+    $this->entityTypeManager
+      ->getStorage('salesforce_mapped_object')
+      ->setForcePull($mapping);
+  }
+
+  /**
+   * Submit handler for "reset delete timestamp" button.
+   */
+  public function lastDeleteReset(array $form, FormStateInterface $form_state) {
+    $this->entity->setLastDeleteTime(NULL);
+  }
+
+  /**
+   * Ajax callback for salesforce_mapping_form() bundle selection.
+   */
+  public function bundleCallback($form, FormStateInterface $form_state) {
+    return $form['drupal_entity']['drupal_bundle'];
+  }
+
+  /**
+   * Return an array of all bundle options, for javascript-free fallback.
+   */
+  protected function getBundleOptions() {
+    $entities = $this->getEntityTypeOptions();
+    $bundles = $this->entityManager->getAllBundleInfo();
+    $options = [];
+    foreach ($bundles as $entity => $bundle_info) {
+      if (empty($entities[$entity])) {
+        continue;
+      }
+      foreach ($bundle_info as $bundle => $info) {
+        $entity_label = $entities[$entity];
+        $options[(string) $entity_label][$bundle] = (string) $info['label'];
+      }
+    }
+    return $options;
+  }
+
+  /**
+   * Return a list of Drupal entity types for mapping.
+   *
+   * @return array
+   *   An array of values keyed by machine name of the entity with the label as
+   *   the value, formatted to be appropriate as a value for #options.
+   */
+  protected function getEntityTypeOptions() {
+    $options = [];
+    $mappable_entity_types = $this->mappableEntityTypes
+      ->getMappableEntityTypes();
+    foreach ($mappable_entity_types as $entity_type_id => $info) {
+      $options[$info->id()] = $info->getLabel();
+    }
+    uasort($options, function ($a, $b) {
+      return strcmp($a->render(), $b->render());
+    });
+    return $options;
+  }
+
+  /**
+   * Helper to retreive a list of object type options.
+   *
+   * @return array
+   *   An array of values keyed by machine name of the object with the label as
+   *   the value, formatted to be appropriate as a value for #options.
+   */
+  protected function getSalesforceObjectTypeOptions() {
+    $sfobject_options = [];
+
+    // Note that we're filtering SF object types to a reasonable subset.
+    $config = $this->config('salesforce.settings');
+    $filter = $config->get('show_all_objects') ? [] : [
+      'updateable' => TRUE,
+      'triggerable' => TRUE,
+    ];
+    $sfobjects = $this->client->objects($filter);
+    foreach ($sfobjects as $object) {
+      $sfobject_options[$object['name']] = $object['label'] . ' (' . $object['name'] . ')';
+    }
+    asort($sfobject_options);
+    return $sfobject_options;
+  }
+
+  /**
+   * Return form options for available sync triggers.
+   *
+   * @return array
+   *   Array of sync trigger options keyed by their machine name with their
+   *   label as the value.
+   */
+  protected function getSyncTriggerOptions() {
+    $options = [];
+    if ($this->moduleHandler->moduleExists('salesforce_push')) {
+      $options += [
+        MappingConstants::SALESFORCE_MAPPING_SYNC_DRUPAL_CREATE => t('Drupal entity create (push)'),
+        MappingConstants::SALESFORCE_MAPPING_SYNC_DRUPAL_UPDATE => t('Drupal entity update (push)'),
+        MappingConstants::SALESFORCE_MAPPING_SYNC_DRUPAL_DELETE => t('Drupal entity delete (push)'),
+      ];
+    }
+    if ($this->moduleHandler->moduleExists('salesforce_pull')) {
+      $options += [
+        MappingConstants::SALESFORCE_MAPPING_SYNC_SF_CREATE => t('Salesforce object create (pull)'),
+        MappingConstants::SALESFORCE_MAPPING_SYNC_SF_UPDATE => t('Salesforce object update (pull)'),
+        MappingConstants::SALESFORCE_MAPPING_SYNC_SF_DELETE => t('Salesforce object delete (pull)'),
+      ];
+    }
+    return $options;
+  }
+
+  /**
+   * Return an array of Date fields suitable for use a pull trigger field.
+   *
+   * @return array
+   *   The options array.
+   */
+  private function getPullTriggerOptions() {
+    $options = [];
+    try {
+      $describe = $this->getSalesforceObject();
+    }
+    catch (\Exception $e) {
+      // No describe results means no datetime fields. We're done.
+      return [];
+    }
+
+    foreach ($describe->getFields() as $field) {
+      if ($field['type'] == 'datetime') {
+        $options[$field['name']] = $field['label'];
+      }
+    }
+    return $options;
+  }
+
+}
diff --git a/modules/salesforce_oauth/config/schema/salesforce_oauth.schema.yml b/modules/salesforce_oauth/config/schema/salesforce_oauth.schema.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d4ac5f53ec95d206c493ce9d37dbbcaefc71d8d1
--- /dev/null
+++ b/modules/salesforce_oauth/config/schema/salesforce_oauth.schema.yml
@@ -0,0 +1,13 @@
+salesforce.auth_provider_settings.oauth:
+  type: mapping
+  label: 'Salesforce OAuth Provider Settings'
+  mapping:
+    consumer_key:
+      type: string
+      label: 'Consumer Key'
+    consumer_secret:
+      type: string
+      label: 'Consumer Secret'
+    login_url:
+      type: uri
+      label: 'Login URL'
diff --git a/modules/salesforce_oauth/salesforce_oauth.info.yml b/modules/salesforce_oauth/salesforce_oauth.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7d97f8f70ed84d21088cb9291e1bf32d494dc917
--- /dev/null
+++ b/modules/salesforce_oauth/salesforce_oauth.info.yml
@@ -0,0 +1,7 @@
+name: Salesforce OAuth user-agent Provider
+type: module
+description: Provides user-agent-based Salesforce OAuth authentication.
+core: 8.x
+package: Salesforce
+dependencies:
+  - salesforce
diff --git a/src/Plugin/SalesforceAuthProvider/SalesforceOAuthPlugin.php b/modules/salesforce_oauth/src/Plugin/SalesforceAuthProvider/SalesforceOAuthPlugin.php
similarity index 89%
rename from src/Plugin/SalesforceAuthProvider/SalesforceOAuthPlugin.php
rename to modules/salesforce_oauth/src/Plugin/SalesforceAuthProvider/SalesforceOAuthPlugin.php
index f45134f82900b4573fc8bb0e37f81a58dfd8092f..b6f67cfae320550e4f5c82c38dcbdc4e1f0230a1 100644
--- a/src/Plugin/SalesforceAuthProvider/SalesforceOAuthPlugin.php
+++ b/modules/salesforce_oauth/src/Plugin/SalesforceAuthProvider/SalesforceOAuthPlugin.php
@@ -1,12 +1,12 @@
 <?php
 
-namespace Drupal\salesforce\Plugin\SalesforceAuthProvider;
+namespace Drupal\salesforce_oauth\Plugin\SalesforceAuthProvider;
 
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Routing\TrustedRedirectResponse;
-use Drupal\salesforce\Consumer\OAuthCredentials;
+use Drupal\salesforce\Consumer\SalesforceCredentials;
+use Drupal\salesforce\SalesforceAuthProviderInterface;
 use Drupal\salesforce\SalesforceAuthProviderPluginBase;
-use Drupal\salesforce\SalesforceOAuthPluginInterface;
 use Drupal\salesforce\Storage\SalesforceAuthTokenStorageInterface;
 use OAuth\Common\Http\Client\ClientInterface;
 use OAuth\Common\Http\Uri\Uri;
@@ -22,12 +22,12 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
  *
  * @deprecated BC legacy auth scheme only. will be removed in 8.x-4.0.
  */
-class SalesforceOAuthPlugin extends SalesforceAuthProviderPluginBase implements SalesforceOAuthPluginInterface {
+class SalesforceOAuthPlugin extends SalesforceAuthProviderPluginBase implements SalesforceAuthProviderInterface {
 
   /**
    * Credentials.
    *
-   * @var \Drupal\salesforce\Consumer\OAuthCredentials
+   * @var \Drupal\salesforce\Consumer\SalesforceCredentials
    */
   protected $credentials;
 
@@ -46,7 +46,7 @@ class SalesforceOAuthPlugin extends SalesforceAuthProviderPluginBase implements
    *
    * @param string $id
    *   The plugin id.
-   * @param \Drupal\salesforce\Consumer\OAuthCredentials $credentials
+   * @param \Drupal\salesforce\Consumer\SalesforceCredentials $credentials
    *   The credentials.
    * @param \OAuth\Common\Http\Client\ClientInterface $httpClient
    *   The oauth http client.
@@ -56,7 +56,7 @@ class SalesforceOAuthPlugin extends SalesforceAuthProviderPluginBase implements
    * @throws \OAuth\OAuth2\Service\Exception\InvalidScopeException
    *   Comment.
    */
-  public function __construct($id, OAuthCredentials $credentials, ClientInterface $httpClient, SalesforceAuthTokenStorageInterface $storage) {
+  public function __construct($id, SalesforceCredentials $credentials, ClientInterface $httpClient, SalesforceAuthTokenStorageInterface $storage) {
     parent::__construct($credentials, $httpClient, $storage, [], new Uri($credentials->getLoginUrl()));
     $this->id = $id;
   }
@@ -66,7 +66,7 @@ class SalesforceOAuthPlugin extends SalesforceAuthProviderPluginBase implements
    */
   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
     $configuration = array_merge(self::defaultConfiguration(), $configuration);
-    $cred = new OAuthCredentials($configuration['consumer_key'], $configuration['login_url'], $configuration['consumer_secret']);
+    $cred = new SalesforceCredentials($configuration['consumer_key'], $configuration['login_url'], $configuration['consumer_secret']);
     return new static($configuration['id'], $cred, $container->get('salesforce.http_client_wrapper'), $container->get('salesforce.auth_token_storage'));
   }
 
diff --git a/modules/salesforce_pull/salesforce_pull.module b/modules/salesforce_pull/salesforce_pull.module
index 5904fe4e746b6b41f77af026b7697eb98fbe9513..b3adc649217aa81f65dd6c600d4b2f77c35fe653 100644
--- a/modules/salesforce_pull/salesforce_pull.module
+++ b/modules/salesforce_pull/salesforce_pull.module
@@ -9,8 +9,7 @@
  * Implements hook_cron().
  */
 function salesforce_pull_cron() {
-  $sfapi = \Drupal::service('salesforce.client');
-  if ($sfapi->isAuthorized()) {
+  if (\Drupal::service('plugin.manager.salesforce.auth_providers')->getToken()) {
     \Drupal::service('salesforce_pull.queue_handler')->getUpdatedRecords();
     \Drupal::service('salesforce_pull.delete_handler')->processDeletedRecords();
   }
diff --git a/modules/salesforce_push/src/Plugin/SalesforcePushQueueProcessor/Rest.php b/modules/salesforce_push/src/Plugin/SalesforcePushQueueProcessor/Rest.php
index ee9e7c294a4b009834db0f00fbe0eb916ca3de02..d27f739a611bb3a863b440b22359c694cbf01ebf 100644
--- a/modules/salesforce_push/src/Plugin/SalesforcePushQueueProcessor/Rest.php
+++ b/modules/salesforce_push/src/Plugin/SalesforcePushQueueProcessor/Rest.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\salesforce_push\Plugin\SalesforcePushQueueProcessor;
 
+use Drupal\salesforce\SalesforceAuthProviderInterface;
+use Drupal\salesforce\SalesforceAuthProviderPluginManager;
 use Symfony\Component\EventDispatcher\EventDispatcherInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Plugin\PluginBase;
@@ -34,13 +36,6 @@ class Rest extends PluginBase implements PushQueueProcessorInterface {
    */
   protected $queue;
 
-  /**
-   * Salesforce client service.
-   *
-   * @var \Drupal\salesforce\Rest\RestClientInterface
-   */
-  protected $client;
-
   /**
    * Storage handler for SF mappings.
    *
@@ -69,6 +64,13 @@ class Rest extends PluginBase implements PushQueueProcessorInterface {
    */
   protected $etm;
 
+  /**
+   * Auth manager.
+   *
+   * @var \Drupal\salesforce\SalesforceAuthProviderPluginManager
+   */
+  protected $authMan;
+
   /**
    * Rest constructor.
    *
@@ -90,14 +92,14 @@ class Rest extends PluginBase implements PushQueueProcessorInterface {
    * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
    * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
    */
-  public function __construct(array $configuration, $plugin_id, array $plugin_definition, PushQueueInterface $queue, RestClientInterface $client, EntityTypeManagerInterface $etm, EventDispatcherInterface $eventDispatcher) {
+  public function __construct(array $configuration, $plugin_id, array $plugin_definition, PushQueueInterface $queue, EntityTypeManagerInterface $etm, EventDispatcherInterface $eventDispatcher, SalesforceAuthProviderPluginManager $authMan) {
     parent::__construct($configuration, $plugin_id, $plugin_definition);
     $this->queue = $queue;
-    $this->client = $client;
     $this->etm = $etm;
     $this->mappingStorage = $etm->getStorage('salesforce_mapping');
     $this->mappedObjectStorage = $etm->getStorage('salesforce_mapped_object');
     $this->eventDispatcher = $eventDispatcher;
+    $this->authMan = $authMan;
   }
 
   /**
@@ -108,7 +110,8 @@ class Rest extends PluginBase implements PushQueueProcessorInterface {
       $container->get('queue.salesforce_push'),
       $container->get('salesforce.client'),
       $container->get('entity_type.manager'),
-      $container->get('event_dispatcher')
+      $container->get('event_dispatcher'),
+      $container->get('plugin.manager.salesforce.auth_providers')
     );
   }
 
@@ -116,7 +119,7 @@ class Rest extends PluginBase implements PushQueueProcessorInterface {
    * Process push queue items.
    */
   public function process(array $items) {
-    if (!$this->client->isAuthorized()) {
+    if (!$this->authMan->getToken()) {
       throw new SuspendQueueException('Salesforce client not authorized.');
     }
     foreach ($items as $item) {
diff --git a/modules/salesforce_push/tests/src/Unit/SalesforcePushQueueProcessorRestTest.php b/modules/salesforce_push/tests/src/Unit/SalesforcePushQueueProcessorRestTest.php
index d0cab159c4e432200cb198755e55c7241bbfd523..1d23fff25fd9246be4f1d1b810332c58ada29937 100644
--- a/modules/salesforce_push/tests/src/Unit/SalesforcePushQueueProcessorRestTest.php
+++ b/modules/salesforce_push/tests/src/Unit/SalesforcePushQueueProcessorRestTest.php
@@ -93,15 +93,6 @@ class SalesforcePushQueueProcessorRestTest extends UnitTestCase {
       $this->eventDispatcher,
     ]);
 
-    $this->client->expects($this->at(0))
-      ->method('isAuthorized')
-      ->willReturn(TRUE);
-
-    // Test suspend queue if not authorized.
-    $this->client->expects($this->at(1))
-      ->method('isAuthorized')
-      ->willReturn(FALSE);
-
     $this->handler->expects($this->once())
       ->method('processItem')
       ->willReturn(NULL);
diff --git a/modules/salesforce_soap/salesforce_soap.services.yml b/modules/salesforce_soap/salesforce_soap.services.yml
index 3bf301f2887e09e0e6e9a78b75eb0acd255654b4..1c54fdbe189f3fb21011769a257a6a86f465cbda 100644
--- a/modules/salesforce_soap/salesforce_soap.services.yml
+++ b/modules/salesforce_soap/salesforce_soap.services.yml
@@ -1,4 +1,4 @@
 services:
   salesforce.soap_client:
     class: Drupal\salesforce_soap\Soap\SoapClient
-    arguments: ['@salesforce.client']
+    arguments: ['@salesforce.client', '@plugin.manager.salesforce.auth_providers']
diff --git a/modules/salesforce_soap/src/Soap/SoapClient.php b/modules/salesforce_soap/src/Soap/SoapClient.php
index 28dbae35a4b5f1c4e87f16e1c61f10eccc0fddd8..3ed47af5f51fe0d91cf89f25cf2303fba92fa7b4 100644
--- a/modules/salesforce_soap/src/Soap/SoapClient.php
+++ b/modules/salesforce_soap/src/Soap/SoapClient.php
@@ -3,6 +3,7 @@
 namespace Drupal\salesforce_soap\Soap;
 
 use Drupal\salesforce\Rest\RestClientInterface;
+use Drupal\salesforce\SalesforceAuthProviderPluginManager;
 use SforcePartnerClient;
 
 /**
@@ -31,6 +32,13 @@ class SoapClient extends SforcePartnerClient implements SoapClientInterface {
    */
   protected $wsdl;
 
+  /**
+   * Auth manager.
+   *
+   * @var \Drupal\salesforce\SalesforceAuthProviderPluginManager
+   */
+  protected $authMan;
+
   /**
    * Constructor which initializes the consumer.
    *
@@ -40,7 +48,7 @@ class SoapClient extends SforcePartnerClient implements SoapClientInterface {
    *   (Optional) Path to the WSDL that should be used.  Defaults to using the
    *   partner WSDL from the developerforce/force.com-toolkit-for-php package.
    */
-  public function __construct(RestClientInterface $rest_api, $wsdl = NULL) {
+  public function __construct(RestClientInterface $rest_api, SalesforceAuthProviderPluginManager $authMan, $wsdl = NULL) {
     parent::__construct();
 
     $this->restApi = $rest_api;
@@ -66,19 +74,13 @@ class SoapClient extends SforcePartnerClient implements SoapClientInterface {
   public function connect() {
     $this->isConnected = FALSE;
     // Use the "isAuthorized" callback to initialize session headers.
-    if ($this->restApi->isAuthorized()) {
-      $this->createConnection($this->wsdl);
-      $token = $this->restApi->getAccessToken();
-      if (!$token) {
-        $token = $this->restApi->refreshToken();
-      }
-      $this->setSessionHeader($token);
-      $this->setEndPoint($this->restApi->getApiEndPoint('partner'));
-      $this->isConnected = TRUE;
-    }
-    else {
+    if (!$token = $this->authMan->getToken()) {
       throw new \Exception('Salesforce needs to be authorized to connect to this website.');
     }
+    $this->createConnection($this->wsdl);
+    $this->setSessionHeader($token);
+    $this->setEndPoint($this->authMan->getProvider()->getApiEndpoint('partner'));
+    $this->isConnected = TRUE;
   }
 
   /**
diff --git a/salesforce.install b/salesforce.install
index 41b194482b260a0b930d3a31453ba39ad93fe5f4..46da9e73e5121bdb84e8ca777ab42395b20fc4ed 100644
--- a/salesforce.install
+++ b/salesforce.install
@@ -303,3 +303,26 @@ function salesforce_update_8006() {
   \Drupal::service('salesforce.auth_token_storage')->updateToken();
   return "Updated legacy token to new plugin config.";
 }
+
+function salesforce_update_8401() {
+  // Enable salesforce_oauth module.
+  \Drupal::service('module_installer')->install(['salesforce_oauth']);
+  // purge old stateful values.
+  // clear old config.
+  throw new \Exception('foo');
+
+  \Drupal::configFactory()->getEditable('salesforce.settings')
+    ->clear('consumer_key')
+    ->clear('consumer_secret')
+    ->clear('login_url')
+    ->save();
+
+  \Drupal::state()->deleteMultiple([
+    'salesforce.access_token',
+    'salesforce.refresh_token',
+    'salesforce.identity',
+    'salesforce.instance_url',
+    'salesforce_encrypt.profile',
+  ]);
+
+}
diff --git a/salesforce.links.action.yml b/salesforce.links.action.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f9d6829e9934c6ed3cdccecc7fbc10cc43f545a4
--- /dev/null
+++ b/salesforce.links.action.yml
@@ -0,0 +1,14 @@
+salesforce_auth.add_action:
+  route_name: entity.salesforce_auth.add_form
+  title: 'Add Salesforce Auth Provider'
+  appears_on:
+    - entity.salesforce_auth.collection
+    - entity.salesforce_auth.edit_form
+    - salesforce.auth_config
+
+salesforce_auth.list_action:
+  route_name: entity.salesforce_auth.collection
+  title: 'Salesforce Auth Provider List'
+  appears_on:
+    - entity.salesforce_auth.add_form
+    - entity.salesforce_auth.edit_form
diff --git a/salesforce.links.menu.yml b/salesforce.links.menu.yml
index 2d7e8f1fc0f50f97c5e075f8ad79e5921bd9a3e2..92bae5c47a8bec58e1357a208fee1905a3a4c006 100644
--- a/salesforce.links.menu.yml
+++ b/salesforce.links.menu.yml
@@ -17,16 +17,14 @@ salesforce.global_settings:
   description: 'Manage global settings for Salesforce Suite.'
   weight: -100
 
-salesforce.authorize:
-  route_name: salesforce.authorize
+salesforce.auth_config:
+  route_name: salesforce.auth_config
   parent: salesforce.admin_config_salesforce
   title: Salesforce Authorization
-  description: 'Manage OAuth consumer key and secret and authorize. View existing authorization details.'
-  weight: 99
+  description: 'Salesforce Authorization.'
 
-salesforce.revoke:
-  route_name: salesforce.revoke
-  parent: salesforce.admin_config_salesforce
-  title: Revoke Salesforce Authorization
-  description: 'Revoke OAuth tokens.'
-  weight: 100
+entity.salesforce_auth.collection:
+  route_name: entity.salesforce_auth.collection
+  parent: salesforce.auth_config
+  title: Salesforce Authorization Providers
+  description: 'Salesforce Authorization Providers.'
diff --git a/salesforce.links.task.yml b/salesforce.links.task.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f2715a9190f5ba9da1b07ccb11ee906f7c18baf0
--- /dev/null
+++ b/salesforce.links.task.yml
@@ -0,0 +1,9 @@
+salesforce.auth_config:
+  route_name: salesforce.auth_config
+  base_route: salesforce.auth_config
+  title: 'Authorization'
+
+entity.salesforce_auth.collection:
+  route_name: entity.salesforce_auth.collection
+  base_route: salesforce.auth_config
+  title: 'Providers'
diff --git a/salesforce.module b/salesforce.module
index 12bde9d8fd1cc016f4d9884ac166c858a643c781..97a01b89a4cdc12052b07d795b475e2b31920003 100644
--- a/salesforce.module
+++ b/salesforce.module
@@ -18,9 +18,9 @@ function salesforce_help($route_name, RouteMatchInterface $route_match) {
       if (!\Drupal::moduleHandler()->moduleExists('salesforce_mapping')) {
         $output .= '<p>' . t('In order to configure Salesforce Mappings, you must first enable the <a href=":url">Salesforce Mapping</a> module.', [':url' => (new Url('system.modules_list'))->toString()]) . '</p>';
       }
-      $client = \Drupal::service('salesforce.client');
-      if (!$client->isAuthorized()) {
-        $output .= '<p>' . t('You must <a href=":authorize">authorize your account with Salesforce</a> in order to configure Salesforce Mappings.', [':authorize' => (new Url('salesforce.authorize'))->toString()]) . '</p>';
+
+      if (!\Drupal::service('plugin.manager.salesforce.auth_providers')->getToken()) {
+        $output .= '<p>' . t('You must <a href=":authorize">authorize your account with Salesforce</a> in order to configure Salesforce Mappings.', [':authorize' => (new Url('salesforce.admin_config_salesforce'))->toString()]) . '</p>';
       }
       return $output;
 
diff --git a/salesforce.routing.yml b/salesforce.routing.yml
index ffe3094f5ce7e01f8d546f576f64433392e44b75..adfd3683b09c05bfd53b94a0f80f235903b67de4 100644
--- a/salesforce.routing.yml
+++ b/salesforce.routing.yml
@@ -1,28 +1,3 @@
-salesforce.oauth_callback:
-  path: '/salesforce/oauth_callback'
-  defaults:
-    _controller: '\Drupal\salesforce\Controller\SalesforceController::oauthCallback'
-  requirements:
-    _permission: 'authorize salesforce'
-
-salesforce.authorize:
-  path: '/admin/config/salesforce/authorize'
-  defaults:
-    _form: '\Drupal\salesforce\Form\AuthorizeForm'
-    _title: 'Salesforce Authorization'
-    _description: 'Manage Salesforce OAuth consumer key and secret and authorize. View existing Salesforce authorization details.'
-  requirements:
-    _permission: 'authorize salesforce'
-
-salesforce.revoke:
-  path: '/admin/config/salesforce/revoke'
-  defaults:
-    _form: '\Drupal\salesforce\Form\RevokeAuthorizationForm'
-    _title: 'Revoke Salesforce Authorization'
-    _description: 'Revoke OAuth tokens.'
-  requirements:
-    _permission: 'authorize salesforce'
-
 salesforce.global_settings:
   path: '/admin/config/salesforce/settings'
   defaults:
@@ -49,3 +24,64 @@ salesforce.structure_index:
     _description: 'Manage Salesforce mappings.'
   requirements:
     _permission: 'administer salesforce'
+
+salesforce.auth_config:
+  path: '/admin/config/salesforce/authorize'
+  defaults:
+    _form: '\Drupal\salesforce\Form\SalesforceAuthSettings'
+    _title: 'Salesforce Authorization Config'
+  requirements:
+    _permission: 'authorize salesforce'
+
+entity.salesforce_auth.collection:
+  path: '/admin/config/salesforce/authorize/list'
+  defaults:
+    _entity_list: 'salesforce_auth'
+    _title: 'Salesforce Authorization Config'
+  requirements:
+    _permission: 'authorize salesforce'
+  options:
+    no_cache: TRUE
+
+entity.salesforce_auth.edit_form:
+  path: '/admin/config/salesforce/authorize/edit/{salesforce_auth}'
+  defaults:
+    _entity_form: 'salesforce_auth.default'
+  requirements:
+    _entity_access: 'salesforce_auth.update'
+  options:
+    no_cache: TRUE
+
+entity.salesforce_auth.add_form:
+  path: '/admin/config/salesforce/authorize/add'
+  defaults:
+    _entity_form: 'salesforce_auth.default'
+  requirements:
+    _entity_create_access: 'salesforce_auth'
+  options:
+    no_cache: TRUE
+
+entity.salesforce_auth.revoke:
+  path: '/admin/config/salesforce/authorize/revoke/{salesforce_auth}'
+  defaults:
+    _entity_form: 'salesforce_auth.revoke'
+  requirements:
+    _permission: 'authorize salesforce'
+  options:
+    no_cache: TRUE
+
+entity.salesforce_auth.delete_form:
+  path: '/admin/config/salesforce/authorize/delete/{salesforce_auth}'
+  defaults:
+    _entity_form: 'salesforce_auth.delete'
+  requirements:
+    _permission: 'authorize salesforce'
+  options:
+    no_cache: TRUE
+
+salesforce.oauth_callback:
+  path: '/salesforce/oauth_callback'
+  defaults:
+    _controller: '\Drupal\salesforce\Controller\SalesforceOAuthController::oauthCallback'
+  requirements:
+    _permission: 'authorize salesforce'
diff --git a/salesforce.services.yml b/salesforce.services.yml
index a72e9f999917c4349055b6749e24429d4f28d70d..a36c8753c52af6905fb85831c65f61cc4c9cf11e 100644
--- a/salesforce.services.yml
+++ b/salesforce.services.yml
@@ -1,7 +1,7 @@
 services:
   salesforce.client:
     class: Drupal\salesforce\Rest\RestClient
-    arguments: ['@http_client', '@config.factory', '@state', '@cache.default', '@serialization.json', '@datetime.time']
+    arguments: ['@http_client', '@config.factory', '@state', '@cache.default', '@serialization.json', '@datetime.time', '@plugin.manager.salesforce.auth_providers']
 
   plugin.manager.salesforce.auth_providers:
     class: Drupal\salesforce\SalesforceAuthProviderPluginManager
diff --git a/src/Consumer/OAuthCredentials.php b/src/Consumer/OAuthCredentials.php
deleted file mode 100644
index 091672b61318458ee2f54e9294cb6ff22a588af2..0000000000000000000000000000000000000000
--- a/src/Consumer/OAuthCredentials.php
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php
-
-namespace Drupal\salesforce\Consumer;
-
-/**
- * OAuth user agent credentials.
- */
-class OAuthCredentials extends SalesforceCredentials {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function __construct($consumerKey, $loginUrl, $consumerSecret) {
-    parent::__construct($consumerKey, $loginUrl);
-    $this->consumerSecret = $consumerSecret;
-  }
-
-}
diff --git a/src/Consumer/SalesforceCredentials.php b/src/Consumer/SalesforceCredentials.php
index 2ee0818f32e3e8ca03eefe003da0f83bc036e329..dfdb91c21830bbd3994d678d1724bf44847a708b 100644
--- a/src/Consumer/SalesforceCredentials.php
+++ b/src/Consumer/SalesforceCredentials.php
@@ -8,7 +8,7 @@ use OAuth\Common\Consumer\Credentials;
 /**
  * Salesforce credentials extension, for drupalisms.
  */
-abstract class SalesforceCredentials extends Credentials implements SalesforceCredentialsInterface {
+class SalesforceCredentials extends Credentials implements SalesforceCredentialsInterface {
 
   /**
    * Login URL e.g. https://test.salesforce.com or https://login.salesforce.com.
@@ -27,10 +27,11 @@ abstract class SalesforceCredentials extends Credentials implements SalesforceCr
   /**
    * {@inheritdoc}
    */
-  public function __construct($consumerKey, $loginUrl) {
+  public function __construct($consumerKey, $loginUrl, $consumerSecret = NULL) {
     parent::__construct($consumerKey, NULL, NULL);
     $this->loginUrl = $loginUrl;
     $this->consumerKey = $consumerKey;
+    $this->consumerSecret = $consumerSecret;
   }
 
   /**
diff --git a/src/Controller/SalesforceAuthListBuilder.php b/src/Controller/SalesforceAuthListBuilder.php
new file mode 100644
index 0000000000000000000000000000000000000000..0937ebe7643f6b3f0cd4d842910c48810c32c3ad
--- /dev/null
+++ b/src/Controller/SalesforceAuthListBuilder.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Drupal\salesforce\Controller;
+
+use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * List builder for salesforce_auth.
+ */
+class SalesforceAuthListBuilder extends ConfigEntityListBuilder {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildRow(EntityInterface $entity) {
+    /** @var \Drupal\salesforce\SalesforceAuthProviderInterface $plugin */
+    $plugin = $entity->getPlugin();
+    $row['label'] = $entity->label();
+    $row['url'] = $plugin->getLoginUrl();
+    $row['key'] = substr($plugin->getConsumerKey(), 0, 16) . '...';
+    $row['type'] = $plugin->label();
+    return $row + parent::buildRow($entity);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildHeader() {
+    $header['label'] = [
+      'data' => $this->t('Label'),
+    ];
+    $header['url'] = [
+      'data' => $this->t('Login URL'),
+    ];
+    $header['key'] = [
+      'data' => $this->t('Consumer Key'),
+    ];
+    $header['type'] = [
+      'data' => $this->t('Auth Type'),
+    ];
+
+    return $header + parent::buildHeader();
+  }
+
+}
diff --git a/src/Controller/SalesforceController.php b/src/Controller/SalesforceController.php
deleted file mode 100644
index 0ca9b91aa8a5e3931850e6d91ab42ced76c29626..0000000000000000000000000000000000000000
--- a/src/Controller/SalesforceController.php
+++ /dev/null
@@ -1,97 +0,0 @@
-<?php
-
-namespace Drupal\salesforce\Controller;
-
-use Drupal\Component\Utility\UrlHelper;
-use Drupal\Core\Controller\ControllerBase;
-use Drupal\Core\Render\MetadataBubblingUrlGenerator;
-use Drupal\salesforce\Rest\RestClientInterface;
-use GuzzleHttp\Client;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Symfony\Component\HttpFoundation\RedirectResponse;
-use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
-
-/**
- * OAuth callback handler.
- *
- * @deprecated will be removed in 8.x-4.0 release.
- */
-class SalesforceController extends ControllerBase {
-
-  protected $client;
-  protected $httpClient;
-
-  /**
-   * {@inheritdoc}
-   */
-  public function __construct(RestClientInterface $rest, Client $httpClient, MetadataBubblingUrlGenerator $url_generator) {
-    $this->client = $rest;
-    $this->httpClient = $httpClient;
-    $this->url_generator = $url_generator;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    return new static(
-      $container->get('salesforce.client'),
-      $container->get('http_client'),
-      $container->get('url_generator')
-    );
-  }
-
-  /**
-   * Wrapper for \Drupal::request().
-   *
-   * @return \Symfony\Component\HttpFoundation\Request
-   *   The currently active request object.
-   */
-  protected function request() {
-    return \Drupal::request();
-  }
-
-  /**
-   * Display a success message on successful oauth.
-   */
-  protected function successMessage() {
-    drupal_set_message(t('Successfully connected to %endpoint', ['%endpoint' => $this->client->getInstanceUrl()]));
-  }
-
-  /**
-   * OAuth step 2: Callback for the oauth redirect URI.
-   *
-   * Complete OAuth handshake by exchanging an authorization code for an access
-   * token.
-   *
-   * @deprecated will be removed in 8.x-4.0 release.
-   */
-  public function oauthCallback() {
-    // If no code is provided, return access denied.
-    if (empty($this->request()->get('code'))) {
-      throw new AccessDeniedHttpException();
-    }
-
-    $data = urldecode(UrlHelper::buildQuery([
-      'code' => $this->request()->get('code'),
-      'grant_type' => 'authorization_code',
-      'client_id' => $this->client->getConsumerKey(),
-      'client_secret' => $this->client->getConsumerSecret(),
-      'redirect_uri' => $this->client->getAuthCallbackUrl(),
-    ]));
-    $url = $this->client->getAuthTokenUrl();
-    $headers = [
-      // This is an undocumented requirement on SF's end.
-      'Content-Type' => 'application/x-www-form-urlencoded',
-    ];
-
-    $response = $this->httpClient->post($url, ['headers' => $headers, 'body' => $data]);
-
-    $this->client->handleAuthResponse($response);
-
-    $this->successMessage();
-
-    return new RedirectResponse($this->url_generator->generateFromRoute('salesforce.authorize', [], ["absolute" => TRUE], FALSE));
-  }
-
-}
diff --git a/src/Controller/SalesforceOAuthController.php b/src/Controller/SalesforceOAuthController.php
new file mode 100644
index 0000000000000000000000000000000000000000..0b235729ad572dcaacf2edba3e932219c24e4de1
--- /dev/null
+++ b/src/Controller/SalesforceOAuthController.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace Drupal\salesforce\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Messenger\MessengerInterface;
+use Drupal\Core\TempStore\PrivateTempStoreFactory;
+use Drupal\Core\Url;
+use Drupal\salesforce\Entity\SalesforceAuthConfig;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
+
+/**
+ *
+ */
+class SalesforceOAuthController extends ControllerBase {
+
+  protected $request;
+  protected $messenger;
+  protected $tempStore;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(RequestStack $stack, MessengerInterface $messenger, PrivateTempStoreFactory $tempStoreFactory) {
+    $this->request = $stack->getCurrentRequest();
+    $this->messenger = $messenger;
+    $this->tempStore = $tempStoreFactory->get('salesforce_oauth');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('request_stack'),
+      $container->get('messenger'),
+      $container->get('user.private_tempstore')
+    );
+  }
+
+  /**
+   * Pass-through to OAuth plugin.
+   */
+  public function oauthCallback() {
+    if (empty($this->request->get('code'))) {
+      throw new AccessDeniedHttpException();
+    }
+    $configId = $this->tempStore->get('config_id');
+
+    if (empty($configId) || !($config = SalesforceAuthConfig::load($configId)) || !($config->getPlugin() instanceof SalesforceAuthProviderInterface)) {
+      $this->messenger->addError('No OAuth config found. Please try again.');
+      return new RedirectResponse(Url::fromRoute('entity.salesforce_auth.collection')->toString());
+    }
+
+    /** @var \Drupal\salesforce\SalesforceAuthProviderInterface $oauth */
+    $oauth = $config->getPlugin();
+    if ($oauth->finalizeOauth()) {
+      $this->messenger()->addStatus(t('Successfully connected to Salesforce.'));
+    }
+    else {
+      $this->messenger()->addError(t('Salesforce auth failed.'));
+    }
+    return new RedirectResponse(Url::fromRoute('entity.salesforce_auth.collection')->toString());
+  }
+
+}
diff --git a/src/Form/AuthorizeForm.php b/src/Form/AuthorizeForm.php
deleted file mode 100644
index 9c032a4137beb48664c9c7182473c4301ddb2226..0000000000000000000000000000000000000000
--- a/src/Form/AuthorizeForm.php
+++ /dev/null
@@ -1,220 +0,0 @@
-<?php
-
-namespace Drupal\salesforce\Form;
-
-use Drupal\Component\Utility\UrlHelper;
-use Drupal\Core\Config\ConfigFactoryInterface;
-use Drupal\Core\Form\ConfigFormBase;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Routing\TrustedRedirectResponse;
-use Drupal\Core\State\StateInterface;
-use Drupal\Core\Url;
-use Drupal\salesforce\Rest\RestClientInterface;
-use Drupal\salesforce_encrypt\Rest\EncryptedRestClientInterface;
-use GuzzleHttp\Exception\RequestException;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
-use Drupal\salesforce\Event\SalesforceEvents;
-use Drupal\salesforce\Event\SalesforceErrorEvent;
-
-/**
- * Creates authorization form for Salesforce.
- *
- * @deprecated will be removed in 8.x-4.0 release.
- */
-class AuthorizeForm extends ConfigFormBase {
-
-  /**
-   * The Salesforce REST client.
-   *
-   * @var \Drupal\salesforce\Rest\RestClientInterface
-   */
-  protected $client;
-
-  /**
-   * The sevent dispatcher service..
-   *
-   * @var \Drupal\Core\State\StateInterface
-   */
-  protected $eventDispatcher;
-
-  /**
-   * The state keyvalue collection.
-   *
-   * @var \Drupal\Core\State\StateInterface
-   */
-  protected $state;
-
-  /**
-   * Constructs a \Drupal\system\ConfigFormBase object.
-   *
-   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
-   *   The factory for configuration objects.
-   * @param \Drupal\salesforce\Rest\RestClientInterface $salesforce_client
-   *   The factory for configuration objects.
-   * @param \Drupal\Core\State\StateInterface $state
-   *   The state keyvalue collection to use.
-   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
-   *   The event dispatcher.
-   */
-  public function __construct(ConfigFactoryInterface $config_factory, RestClientInterface $salesforce_client, StateInterface $state, EventDispatcherInterface $event_dispatcher) {
-    parent::__construct($config_factory);
-    $this->client = $salesforce_client;
-    $this->state = $state;
-    $this->eventDispatcher = $event_dispatcher;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    return new static(
-      $container->get('config.factory'),
-      $container->get('salesforce.client'),
-      $container->get('state'),
-      $container->get('event_dispatcher')
-    );
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getFormId() {
-    return 'salesforce_oauth';
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function getEditableConfigNames() {
-    return [
-      'salesforce.settings',
-    ];
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function buildForm(array $form, FormStateInterface $form_state) {
-    $config = $this->config('salesforce.settings');
-    $encrypted = is_subclass_of($this->client, EncryptedRestClientInterface::class);
-    $url = new Url('salesforce.oauth_callback', [], ['absolute' => TRUE]);
-    drupal_set_message($this->t('Callback URL: :url', [
-      ':url' => str_replace('http:', 'https:', $url->toString()),
-    ]));
-
-    $form['creds'] = [
-      '#title' => $this->t('API / OAuth Connection Settings'),
-      '#type' => 'details',
-      '#open' => TRUE,
-      '#description' => $this->t('Authorize this website to communicate with Salesforce by entering the consumer key and secret from a remote application. Submitting the form will redirect you to Salesforce where you will be asked to grant access.'),
-    ];
-    $form['creds']['consumer_key'] = [
-      '#title' => $this->t('Salesforce consumer key'),
-      '#type' => 'textfield',
-      '#description' => $this->t('Consumer key of the Salesforce remote application you want to grant access to'),
-      '#required' => TRUE,
-      '#default_value' => $encrypted ? $this->client->decrypt($config->get('consumer_key')) : $config->get('consumer_key'),
-    ];
-    $form['creds']['consumer_secret'] = [
-      '#title' => $this->t('Salesforce consumer secret'),
-      '#type' => 'textfield',
-      '#description' => $this->t('Consumer secret of the Salesforce remote application you want to grant access to'),
-      '#required' => TRUE,
-      '#default_value' => $encrypted ? $this->client->decrypt($config->get('consumer_secret')) : $config->get('consumer_secret'),
-    ];
-    $form['creds']['login_url'] = [
-      '#title' => $this->t('Login URL'),
-      '#type' => 'textfield',
-      '#default_value' => empty($config->get('login_url')) ? 'https://login.salesforce.com' : $config->get('login_url'),
-      '#description' => $this->t('Enter a login URL, either https://login.salesforce.com or https://test.salesforce.com.'),
-      '#required' => TRUE,
-    ];
-
-    // If fully configured, attempt to connect to Salesforce and return a list
-    // of resources.
-    if ($this->client->isAuthorized()) {
-      $form['creds']['#open'] = FALSE;
-      $form['creds']['#description'] = $this->t('Your Salesforce salesforce instance is currently authorized. Enter credentials here only to change credentials.');
-      try {
-        $resources = $this->client->listResources();
-        foreach ($resources->resources as $key => $path) {
-          $items[] = $key . ': ' . $path;
-        }
-        if (!empty($items)) {
-          $form['resources'] = [
-            '#title' => $this->t('Your Salesforce instance is authorized and has access to the following resources:'),
-            '#items' => $items,
-            '#theme' => 'item_list',
-          ];
-        }
-      }
-      catch (\Exception $e) {
-        // Do not allow any exceptions to interfere with displaying this page.
-        drupal_set_message($e->getMessage(), 'warning');
-        $this->eventDispatcher->dispatch(SalesforceEvents::ERROR, new SalesforceErrorEvent($e));
-      }
-    }
-    elseif (!$form_state->getUserInput()) {
-      // Don't set this message if the form was submitted.
-      drupal_set_message(t('Salesforce needs to be authorized to connect to this website.'), 'error');
-    }
-    $form = parent::buildForm($form, $form_state);
-    $form['creds']['actions'] = $form['actions'];
-    unset($form['actions']);
-    return $form;
-  }
-
-  /**
-   * Return whether or not the given URL is a valid endpoint.
-   *
-   * @return bool
-   *   True is the given url is valid.
-   */
-  public static function validEndpoint($url) {
-    return UrlHelper::isValid($url, TRUE);
-  }
-
-  /**
-   * Validate handler for auth form. Basic sanity checking.
-   */
-  public function validateForm(array &$form, FormStateInterface $form_state) {
-    if (!self::validEndpoint($form_state->getValue('login_url'))) {
-      $form_state->setErrorByName('login_url', t('Please enter a valid Salesforce login URL.'));
-    }
-
-    if (!is_numeric($form_state->getValue('consumer_secret'))) {
-      $form_state->setErrorByName('consumer_secret', t('Please enter a valid consumer secret.'));
-    }
-
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function submitForm(array &$form, FormStateInterface $form_state) {
-    $values = $form_state->getValues();
-    $this->client->setConsumerKey($values['consumer_key']);
-    $this->client->setConsumerSecret($values['consumer_secret']);
-    $this->client->setLoginUrl($values['login_url']);
-
-    try {
-      $path = $this->client->getAuthEndpointUrl();
-      $query = [
-        'redirect_uri' => $this->client->getAuthCallbackUrl(),
-        'response_type' => 'code',
-        'client_id' => $this->client->getConsumerKey(),
-      ];
-
-      // Send the user along to the Salesforce OAuth login form. If successful,
-      // the user will be redirected to {redirect_uri} to complete the OAuth
-      // handshake.
-      $form_state->setResponse(new TrustedRedirectResponse($path . '?' . http_build_query($query), 302));
-    }
-    catch (RequestException $e) {
-      drupal_set_message(t("Error during authorization: %message", ['%message' => $e->getMessage()]), 'error');
-      $this->eventDispatcher->dispatch(SalesforceEvents::ERROR, new SalesforceErrorEvent($e));
-    }
-  }
-
-}
diff --git a/src/Form/RevokeAuthorizationForm.php b/src/Form/RevokeAuthorizationForm.php
deleted file mode 100644
index 2fe13c9137d304ca8a97d8a28bab26072e8cc49a..0000000000000000000000000000000000000000
--- a/src/Form/RevokeAuthorizationForm.php
+++ /dev/null
@@ -1,119 +0,0 @@
-<?php
-
-namespace Drupal\salesforce\Form;
-
-use Drupal\Core\Config\ConfigFactoryInterface;
-use Drupal\Core\Form\ConfigFormBase;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\salesforce\Event\SalesforceEvents;
-use Drupal\salesforce\Event\SalesforceNoticeEvent;
-use Drupal\salesforce\Rest\RestClientInterface;
-
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
-
-/**
- * Revoke the current oauth creds.
- *
- * @deprecated will be removed in 8.x-4.0 release.
- */
-class RevokeAuthorizationForm extends ConfigFormBase {
-
-  /**
-   * The Salesforce REST client.
-   *
-   * @var \Drupal\salesforce\Rest\RestClientInterface
-   */
-  protected $client;
-
-  /**
-   * The sevent dispatcher service..
-   *
-   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
-   */
-  protected $eventDispatcher;
-
-  /**
-   * The state keyvalue collection.
-   *
-   * @var \Drupal\Core\State\StateInterface
-   */
-  protected $state;
-
-  /**
-   * Constructs a \Drupal\system\ConfigFormBase object.
-   *
-   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
-   *   The factory for configuration objects.
-   * @param \Drupal\salesforce\Rest\RestClientInterface $salesforce_client
-   *   The factory for configuration objects.
-   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
-   *   The event dispatcher.
-   */
-  public function __construct(ConfigFactoryInterface $config_factory, RestClientInterface $salesforce_client, EventDispatcherInterface $event_dispatcher) {
-    parent::__construct($config_factory);
-    $this->client = $salesforce_client;
-    $this->eventDispatcher = $event_dispatcher;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    return new static(
-      $container->get('config.factory'),
-      $container->get('salesforce.client'),
-      $container->get('event_dispatcher')
-    );
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getFormId() {
-    return 'salesforce_oauth';
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function getEditableConfigNames() {
-    return [
-      'salesforce.settings',
-    ];
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function buildForm(array $form, FormStateInterface $form_state) {
-    if (!$this->client->isAuthorized()) {
-      drupal_set_message($this->t('Drupal is not authenticated to Salesforce.'), 'warning');
-      return;
-    }
-    $form = parent::buildForm($form, $form_state);
-    $form['actions']['#title'] = 'Are you sure you want to revoke authorization?';
-    $form['actions']['#type'] = 'details';
-    $form['actions']['#open'] = TRUE;
-    $form['actions']['#description'] = t('Revoking authorization will destroy Salesforce OAuth and refresh tokens. Drupal will no longer be authorized to communicate with Salesforce.');
-    $form['actions']['submit']['#value'] = t('Revoke authorization');
-
-    // By default, render the form using system-config-form.html.twig.
-    $form['#theme'] = 'system_config_form';
-
-    return $form;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function submitForm(array &$form, FormStateInterface $form_state) {
-    $this->client->setAccessToken('');
-    $this->client->setRefreshToken('');
-    $this->client->setInstanceUrl('');
-    $this->client->setIdentity(FALSE);
-    drupal_set_message($this->t('Salesforce OAuth tokens have been revoked.'));
-    $this->eventDispatcher->dispatch(SalesforceEvents::NOTICE, new SalesforceNoticeEvent(NULL, "Salesforce OAuth tokens revoked."));
-  }
-
-}
diff --git a/src/Form/SalesforceAuthDeleteForm.php b/src/Form/SalesforceAuthDeleteForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..048ba71d8b024770bfcd8c4e877555ad24529d97
--- /dev/null
+++ b/src/Form/SalesforceAuthDeleteForm.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace Drupal\salesforce\Form;
+
+use Drupal\Core\Entity\EntityConfirmFormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+
+class SalesforceAuthDeleteForm extends EntityConfirmFormBase {
+  /**
+   * {@inheritdoc}
+   */
+  public function getQuestion() {
+    return $this->t('Are you sure you want to delete the Auth Config %name?', ['%name' => $this->entity->label()]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCancelUrl() {
+    return $this->entity->toUrl('collection');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfirmText() {
+    return $this->t('Delete');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    parent::validateForm($form, $form_state);
+    if ($form_state->getErrors()) {
+      return;
+    }
+    if (\Drupal::config('salesforce.settings')->get('salesforce_auth_provider') == $this->entity->id()) {
+      $form_state->setError($form, $this->t('You cannot delete the default auth provider. Please <a href="@href">assign a new auth provider</a> before deleting the active one.', ['@href' => Url::fromRoute('salesforce.auth_config')->toString()]));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $this->entity->delete();
+
+    // Set a message that the entity was deleted.
+    $this->messenger()->addStatus($this->t('Auth Config %label was deleted.', [
+      '%label' => $this->entity->label(),
+    ]));
+
+    $form_state->setRedirectUrl($this->getCancelUrl());
+  }
+
+}
\ No newline at end of file
diff --git a/src/Form/SalesforceAuthForm.php b/src/Form/SalesforceAuthForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..21eb56104a391737373ac8b56b5d54f49133bdb3
--- /dev/null
+++ b/src/Form/SalesforceAuthForm.php
@@ -0,0 +1,142 @@
+<?php
+
+namespace Drupal\salesforce\Form;
+
+use Drupal\Core\Entity\EntityForm;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\salesforce\Plugin\SalesforceAuthProviderFormInterface;
+use Drupal\salesforce_oauth\Entity\OAuthConfig;
+
+/**
+ * Entity form for salesforce_auth.
+ */
+class SalesforceAuthForm extends EntityForm {
+
+  /**
+   * The config entity.
+   *
+   * @var \Drupal\salesforce\Entity\SalesforceAuthConfig
+   */
+  protected $entity;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, FormStateInterface $form_state) {
+    $auth = $this->entity;
+    $form['label'] = [
+      '#title' => $this->t('Label'),
+      '#type' => 'textfield',
+      '#description' => $this->t('User-facing label for this project, e.g. "OAuth Full Sandbox"'),
+      '#default_value' => $auth->label(),
+    ];
+
+    $form['id'] = [
+      '#type' => 'machine_name',
+      '#default_value' => $auth->id(),
+      '#maxlength' => 32,
+      '#machine_name' => [
+        'exists' => [$this, 'exists'],
+        'source' => ['label'],
+      ],
+    ];
+
+    // This is the element that contains all of the dynamic parts of the form.
+    $form['settings'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Settings'),
+      '#open' => TRUE,
+    ];
+
+    $form['settings']['provider'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Auth provider'),
+      '#options' => $auth->getPluginsAsOptions(),
+      '#required' => TRUE,
+      '#default_value' => $auth->getPluginId(),
+      '#ajax' => [
+        'callback' => [$this, 'ajaxUpdateSettings'],
+        'event' => 'change',
+        'wrapper' => 'auth-settings',
+      ],
+    ];
+    $default = [
+      '#type' => 'container',
+      '#title' => $this->t('Auth provider settings'),
+      '#title_display' => FALSE,
+      '#tree' => TRUE,
+      '#prefix' => '<div id="auth-settings">',
+      '#suffix' => '</div>',
+    ];
+    $form['settings']['provider_settings'] = $default;
+    if ($auth->getPlugin()) {
+      $form['settings']['provider_settings'] += $auth->getPlugin()
+        ->buildConfigurationForm([], $form_state);
+    }
+    elseif ($form_state->getValue('provider')) {
+      $plugin = $this->entity->authManager()->createInstance($form_state->getValue('provider'));
+      $form['settings']['provider_settings'] += $plugin->buildConfigurationForm([], $form_state);
+    }
+    else {
+      $form['settings']['provider_settings'] = $default;
+    }
+    return parent::form($form, $form_state);
+  }
+
+  /**
+   * AJAX callback to update the dynamic settings on the form.
+   *
+   * @param array $form
+   *   The form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state.
+   *
+   * @return array
+   *   The element to update in the form.
+   */
+  public function ajaxUpdateSettings(array &$form, FormStateInterface $form_state) {
+    return $form['settings']['provider_settings'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    parent::validateForm($form, $form_state);
+
+    if (!$form_state->isSubmitted()) {
+      return;
+    }
+
+    $this->entity->getPlugin()->validateConfigurationForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    parent::submitForm($form, $form_state);
+    $this->entity->getPlugin()->submitConfigurationform($form, $form_state);
+    $form_state->setRedirectUrl($this->entity->toUrl('collection'));
+  }
+
+  public function save(array $form, FormStateInterface $form_state) {
+    parent::save($form, $form_state);
+    $this->entity->getPlugin()->save($form, $form_state);
+  }
+
+  /**
+   * Determines if the config already exists.
+   *
+   * @param string $id
+   *   The config ID.
+   *
+   * @return bool
+   *   TRUE if the config exists, FALSE otherwise.
+   */
+  public function exists($id) {
+    $action = \Drupal::entityTypeManager()->getStorage($this->entity->getEntityTypeId())->load($id);
+    return !empty($action);
+  }
+
+}
diff --git a/src/Form/SalesforceAuthRevokeForm.php b/src/Form/SalesforceAuthRevokeForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..a69b21c5086a7fa9fee8c2f0dadb6ef7952960b7
--- /dev/null
+++ b/src/Form/SalesforceAuthRevokeForm.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Drupal\salesforce\Form;
+
+use Drupal\Core\Entity\EntityConfirmFormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+
+class AuthConfigDeleteForm extends EntityConfirmFormBase {
+  /**
+   * {@inheritdoc}
+   */
+  public function getQuestion() {
+    return $this->t('Are you sure you want to delete the Auth Config %name?', ['%name' => $this->entity->label()]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCancelUrl() {
+    return $this->entity->toUrl('collection');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfirmText() {
+    return $this->t('Delete');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $this->entity->delete();
+
+    // Set a message that the entity was deleted.
+    $this->messenger()->addStatus($this->t('Auth Config %label was deleted.', [
+      '%label' => $this->entity->label(),
+    ]));
+
+    $form_state->setRedirectUrl($this->getCancelUrl());
+  }
+
+}
\ No newline at end of file
diff --git a/src/Form/SalesforceAuthSettings.php b/src/Form/SalesforceAuthSettings.php
new file mode 100644
index 0000000000000000000000000000000000000000..aca5f81bd0dbf093e9602a47760f7f1aae61b3a7
--- /dev/null
+++ b/src/Form/SalesforceAuthSettings.php
@@ -0,0 +1,98 @@
+<?php
+
+namespace Drupal\salesforce\Form;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Form\ConfigFormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\salesforce\Event\SalesforceEvents;
+use Drupal\salesforce\Event\SalesforceNoticeEvent;
+use Drupal\salesforce\SalesforceAuthManager;
+use Drupal\salesforce\SalesforceAuthProviderPluginManager;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+
+class SalesforceAuthSettings extends ConfigFormBase {
+
+  protected $salesforceAuth;
+  protected $eventDispatcher;
+
+  /**
+   * Constructs a \Drupal\system\ConfigFormBase object.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The factory for configuration objects.
+   */
+  public function __construct(ConfigFactoryInterface $config_factory, SalesforceAuthProviderPluginManager $salesforceAuth, EventDispatcherInterface $eventDispatcher) {
+    parent::__construct($config_factory);
+    $this->salesforceAuth = $salesforceAuth;
+    $this->eventDispatcher = $eventDispatcher;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('config.factory'),
+      $container->get('plugin.manager.salesforce.auth_providers'),
+      $container->get('event_dispatcher')
+    );
+  }
+
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'salesforce_auth_config';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEditableConfigNames() {
+    return ['salesforce.settings'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    if (!$this->salesforceAuth->hasProviders()) {
+      return ['#markup'=> 'No auth providers have been enabled. Please enable an auth provider and create an auth config before continuing.'];
+    }
+    $config = $this->config('salesforce.settings');
+    $form = parent::buildForm($form, $form_state);
+    $options = [];
+    /** @var \Drupal\salesforce\Entity\SalesforceAuthConfig $provider **/
+    foreach($this->salesforceAuth->getProviders() as $provider) {
+      $options[$provider->id()] = $provider->label() . ' (' . $provider->getPlugin()->label() . ')';
+    }
+    if (empty($options)) {
+      return ['#markup'=> 'No auth providers found. Please add an auth provider before continuing.'];
+    }
+    $options = ['' => '- None -'] + $options;
+    $form['provider'] = [
+      '#type' => 'radios',
+      '#title' => $this->t('Choose a default auth provider'),
+      '#options' => $options,
+      '#default_value' => $config->get('salesforce_auth_provider') ? $config->get('salesforce_auth_provider') : '',
+    ];
+    $form['#theme'] = 'system_config_form';
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $this->config('salesforce.settings')
+      ->set('salesforce_auth_provider', $form_state->getValue('provider') ? $form_state->getValue('provider') : NULL)
+      ->save();
+
+    $this->messenger()->addStatus($this->t('Authorization settings have been saved.'));
+    $this->eventDispatcher->dispatch(SalesforceEvents::NOTICE, new SalesforceNoticeEvent(NULL, "Authorization provider changed to %provider.", ['%provider' => $form_state->getValue('provider')]));
+  }
+
+}
\ No newline at end of file
diff --git a/src/Form/SettingsForm.php b/src/Form/SettingsForm.php
index 1dd19ddcd348eda07f503ddbda9e30c02ee6756a..fa66665e0919aa80d689f74c6ab52be9b7ef51d5 100644
--- a/src/Form/SettingsForm.php
+++ b/src/Form/SettingsForm.php
@@ -93,7 +93,7 @@ class SettingsForm extends ConfigFormBase {
       $versions = $this->getVersionOptions();
     }
     catch (\Exception $e) {
-      $href = new Url('salesforce.authorize');
+      $href = new Url('salesforce.admin_config_salesforce');
       drupal_set_message($this->t('Error when connecting to Salesforce. Please <a href="@href">check your credentials</a> and try again: %message', ['@href' => $href->toString(), '%message' => $e->getMessage()]), 'error');
     }
 
diff --git a/src/Rest/RestClient.php b/src/Rest/RestClient.php
index 26bfe039ac6463de613aa6dc28dd1af8802a5f25..eb28be374b5b4586f780a7d63c7a764c4658323c 100644
--- a/src/Rest/RestClient.php
+++ b/src/Rest/RestClient.php
@@ -4,11 +4,9 @@ namespace Drupal\salesforce\Rest;
 
 use Drupal\Component\Serialization\Json;
 use Drupal\Component\Utility\NestedArray;
-use Drupal\Component\Utility\UrlHelper;
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\State\StateInterface;
-use Drupal\Core\Url;
 use Drupal\salesforce\SalesforceAuthProviderPluginManager;
 use Drupal\salesforce\SelectQueryInterface;
 use Drupal\salesforce\SFID;
@@ -17,7 +15,6 @@ use Drupal\salesforce\SelectQuery;
 use Drupal\salesforce\SelectQueryResult;
 use GuzzleHttp\ClientInterface;
 use GuzzleHttp\Exception\RequestException;
-use GuzzleHttp\Psr7\Response;
 use Drupal\Component\Datetime\TimeInterface;
 
 /**
@@ -53,15 +50,6 @@ class RestClient implements RestClientInterface {
    */
   protected $url;
 
-  /**
-   * Salesforce mutable config object.  Useful for sets.
-   *
-   * @var \Drupal\Core\Config\Config
-   *
-   * @deprecated will be removed in 8.x-4.0 release.
-   */
-  protected $mutableConfig;
-
   /**
    * Salesforce immutable config object.  Useful for gets.
    *
@@ -90,16 +78,35 @@ class RestClient implements RestClientInterface {
    */
   protected $json;
 
-  protected $httpClientOptions;
+  /**
+   * Auth provider manager.
+   *
+   * @var \Drupal\salesforce\SalesforceAuthProviderPluginManager
+   */
+  protected $authManager;
 
   /**
-   * Token storage.
+   * Active auth provider.
    *
-   * @var \Drupal\salesforce\Storage\SalesforceAuthTokenStorage
+   * @var \Drupal\salesforce\SalesforceAuthProviderInterface
+   */
+  protected $authProvider;
+
+  /**
+   * Active auth provider config.
    *
-   * @deprecated BC legacy auth scheme only. will be removed in 8.x-4.0.
+   * @var \Drupal\salesforce\Entity\SalesforceAuthConfig
    */
-  private $storage;
+  protected $authConfig;
+
+  /**
+   * Active auth token.
+   *
+   * @var \OAuth\OAuth2\Token\TokenInterface
+   */
+  protected $authToken;
+
+  protected $httpClientOptions;
 
   const CACHE_LIFETIME = 300;
   const LONGTERM_CACHE_LIFETIME = 86400;
@@ -120,54 +127,35 @@ class RestClient implements RestClientInterface {
    * @param \Drupal\Component\Datetime\TimeInterface $time
    *   The Time service.
    */
-  public function __construct(ClientInterface $http_client, ConfigFactoryInterface $config_factory, StateInterface $state, CacheBackendInterface $cache, Json $json, TimeInterface $time) {
+  public function __construct(ClientInterface $http_client, ConfigFactoryInterface $config_factory, StateInterface $state, CacheBackendInterface $cache, Json $json, TimeInterface $time, SalesforceAuthProviderPluginManager $authManager) {
     $this->configFactory = $config_factory;
     $this->httpClient = $http_client;
-    $this->mutableConfig = $this->configFactory->getEditable('salesforce.settings');
     $this->immutableConfig = $this->configFactory->get('salesforce.settings');
     $this->state = $state;
     $this->cache = $cache;
     $this->json = $json;
     $this->time = $time;
     $this->httpClientOptions = [];
+    $this->authManager = $authManager;
+    $this->authProvider = $authManager->getProvider();
+    $this->authConfig = $authManager->getConfig();
+    $this->authToken = $authManager->getToken();
     return $this;
   }
 
-  /**
-   * Storage helper.
-   *
-   * @return \Drupal\salesforce\Storage\SalesforceAuthTokenStorage
-   *   The auth token storage service.
-   *
-   * @deprecated BC legacy auth scheme only. will be removed in 8.x-4.0.
-   */
-  private function storage() {
-    if (!$this->storage) {
-      $this->storage = \Drupal::service('salesforce.auth_token_storage');
-    }
-    return $this->storage;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function isAuthorized() {
-    return $this->getConsumerKey() && $this->getConsumerSecret() && $this->getRefreshToken();
-  }
-
   /**
    * {@inheritdoc}
    */
   public function apiCall($path, array $params = [], $method = 'GET', $returnObject = FALSE) {
-    if (!$this->getAccessToken()) {
-      $this->refreshToken();
+    if (!$this->authToken) {
+      $this->authManager->refreshToken();
     }
 
     if (strpos($path, '/') === 0) {
-      $url = $this->getInstanceUrl() . $path;
+      $url = $this->authProvider->getInstanceUrl() . $path;
     }
     else {
-      $url = $this->getApiEndPoint() . $path;
+      $url = $this->authProvider->getApiEndPoint() . $path;
     }
 
     try {
@@ -185,9 +173,9 @@ class RestClient implements RestClientInterface {
 
     if ($this->response->getStatusCode() == 401) {
       // The session ID or OAuth token used has expired or is invalid: refresh
-      // token. If refreshToken() throws an exception, or if apiHttpRequest()
+      // token. If refresh_token() throws an exception, or if apiHttpRequest()
       // throws anything but a RequestException, let it bubble up.
-      $this->refreshToken();
+      $this->authManager->refreshToken();
       try {
         $this->response = new RestResponse($this->apiHttpRequest($url, $params, $method));
       }
@@ -229,12 +217,12 @@ class RestClient implements RestClientInterface {
    * @throws \GuzzleHttp\Exception\RequestException
    */
   protected function apiHttpRequest($url, array $params, $method) {
-    if (!$this->getAccessToken()) {
+    if (!$this->authManager->getToken()) {
       throw new \Exception('Missing OAuth Token');
     }
 
     $headers = [
-      'Authorization' => 'OAuth ' . $this->getAccessToken(),
+      'Authorization' => 'OAuth ' . $this->authToken->getAccessToken(),
       'Content-type' => 'application/json',
     ];
     $data = NULL;
@@ -248,11 +236,11 @@ class RestClient implements RestClientInterface {
    * {@inheritdoc}
    */
   public function httpRequestRaw($url) {
-    if (!$this->getAccessToken()) {
+    if (!$this->authManager->getToken()) {
       throw new \Exception('Missing OAuth Token');
     }
     $headers = [
-      'Authorization' => 'OAuth ' . $this->getAccessToken(),
+      'Authorization' => 'OAuth ' . $this->authToken->getAccessToken(),
       'Content-type' => 'application/json',
     ];
     $response = $this->httpRequest($url, NULL, $headers);
@@ -335,262 +323,6 @@ class RestClient implements RestClientInterface {
     return $data;
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function getApiEndPoint($api_type = 'rest') {
-    $url = &drupal_static(__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);
-    }
-    return $url;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getApiVersion() {
-    if ($this->immutableConfig->get('use_latest')) {
-      $versions = $this->getVersions();
-      $version = end($versions);
-      return $version['version'];
-    }
-    return $this->immutableConfig->get('rest_api_version.version');
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setApiVersion($use_latest = TRUE, $version = NULL) {
-    if ($use_latest) {
-      $this->mutableConfig->set('use_latest', $use_latest);
-    }
-    else {
-      $versions = $this->getVersions();
-      if (empty($versions[$version])) {
-        throw new \Exception("Version $version is not available.");
-      }
-      $version = $versions[$version];
-      $this->mutableConfig->set('rest_api_version', $version);
-    }
-    $this->mutableConfig->save();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getConsumerKey() {
-    return $this->immutableConfig->get('consumer_key');
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setConsumerKey($value) {
-    $this->mutableConfig->set('consumer_key', $value)->save();
-    SalesforceAuthProviderPluginManager::updateAuthConfig();
-    return $this;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getConsumerSecret() {
-    return $this->immutableConfig->get('consumer_secret');
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setConsumerSecret($value) {
-    $this->mutableConfig->set('consumer_secret', $value)->save();
-    SalesforceAuthProviderPluginManager::updateAuthConfig();
-    return $this;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getLoginUrl() {
-    $login_url = $this->immutableConfig->get('login_url');
-    return empty($login_url) ? 'https://login.salesforce.com' : $login_url;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setLoginUrl($value) {
-    $this->mutableConfig->set('login_url', $value)->save();
-    SalesforceAuthProviderPluginManager::updateAuthConfig();
-    return $this;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getInstanceUrl() {
-    return $this->state->get('salesforce.instance_url');
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setInstanceUrl($url) {
-    $this->state->set('salesforce.instance_url', $url);
-    return $this;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getAccessToken() {
-    $access_token = $this->state->get('salesforce.access_token');
-    return isset($access_token) && mb_strlen($access_token) !== 0 ? $access_token : FALSE;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setAccessToken($token) {
-    $this->state->set('salesforce.access_token', $token);
-    $this->storage()->updateToken();
-    return $this;
-  }
-
-  /**
-   * Get refresh token.
-   */
-  public function getRefreshToken() {
-    return $this->state->get('salesforce.refresh_token');
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setRefreshToken($token) {
-    $this->state->set('salesforce.refresh_token', $token);
-    $this->storage()->updateToken();
-    return $this;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function refreshToken() {
-    $refresh_token = $this->getRefreshToken();
-    if (empty($refresh_token)) {
-      throw new \Exception(t('There is no refresh token.'));
-    }
-
-    $data = UrlHelper::buildQuery([
-      'grant_type' => 'refresh_token',
-      'refresh_token' => urldecode($refresh_token),
-      'client_id' => $this->getConsumerKey(),
-      'client_secret' => $this->getConsumerSecret(),
-    ]);
-
-    $url = $this->getAuthTokenUrl();
-    $headers = [
-      // This is an undocumented requirement on Salesforce's end.
-      'Content-Type' => 'application/x-www-form-urlencoded',
-    ];
-    $response = $this->httpRequest($url, $data, $headers, 'POST');
-
-    $this->handleAuthResponse($response);
-    return $this;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function handleAuthResponse(Response $response) {
-    if ($response->getStatusCode() != 200) {
-      throw new \Exception($response->getReasonPhrase(), $response->getStatusCode());
-    }
-
-    $data = (new RestResponse($response))->data;
-
-    $this
-      ->setAccessToken($data['access_token'])
-      ->initializeIdentity($data['id'])
-      ->setInstanceUrl($data['instance_url']);
-
-    // Do not overwrite an existing refresh token with an empty value.
-    if (!empty($data['refresh_token'])) {
-      $this->setRefreshToken($data['refresh_token']);
-    }
-    return $this;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function initializeIdentity($id) {
-    $headers = [
-      'Authorization' => 'OAuth ' . $this->getAccessToken(),
-      'Content-type' => 'application/json',
-    ];
-    $response = $this->httpRequest($id, NULL, $headers);
-
-    if ($response->getStatusCode() != 200) {
-      throw new \Exception(t('Unable to access identity service.'), $response->getStatusCode());
-    }
-    $data = (new RestResponse($response))->data;
-
-    $this->setIdentity($data);
-    return $this;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setIdentity($data) {
-    $this->state->set('salesforce.identity', $data);
-    $this->storage()->updateIdentity();
-    return $this;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getIdentity() {
-    return $this->state->get('salesforce.identity');
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getAuthCallbackUrl() {
-    return Url::fromRoute('salesforce.oauth_callback', [], [
-      'absolute' => TRUE,
-      'https' => TRUE,
-    ])->toString();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getAuthEndpointUrl() {
-    return $this->getLoginUrl() . '/services/oauth2/authorize';
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getAuthTokenUrl() {
-    return $this->getLoginUrl() . '/services/oauth2/token';
-  }
-
   /**
    * {@inheritdoc}
    */
@@ -600,7 +332,7 @@ class RestClient implements RestClientInterface {
     }
 
     $versions = [];
-    $id = $this->getIdentity();
+    $id = $this->authProvider->getIdentity();
     if (!empty($id)) {
       $url = str_replace('v{version}/', '', $id['urls']['rest']);
       $response = new RestResponse($this->httpRequest($url));
@@ -667,9 +399,6 @@ class RestClient implements RestClientInterface {
           }
         }
       }
-      else {
-        $sobjects[$object['name']] = $object;
-      }
     }
     return $sobjects;
   }
@@ -701,7 +430,7 @@ class RestClient implements RestClientInterface {
         'records' => [],
       ]);
     }
-    $version_path = parse_url($this->getApiEndPoint(), PHP_URL_PATH);
+    $version_path = parse_url($this->authProvider->getApiEndPoint(), PHP_URL_PATH);
     $next_records_url = str_replace($version_path, '', $results->nextRecordsUrl());
     return new SelectQueryResult($this->apiCall($next_records_url));
   }
diff --git a/src/Rest/RestClientInterface.php b/src/Rest/RestClientInterface.php
index 8e6c35476aed579b1f37a1cf51d43f6d1120eee5..c986ae80ac0865fb60dddb32da1713c0aa70dec2 100644
--- a/src/Rest/RestClientInterface.php
+++ b/src/Rest/RestClientInterface.php
@@ -12,13 +12,6 @@ use GuzzleHttp\Psr7\Response;
  */
 interface RestClientInterface {
 
-  /**
-   * Determine if this SF instance is fully configured.
-   *
-   * @deprecated will be removed in 8.x-4.0 release.
-   */
-  public function isAuthorized();
-
   /**
    * Make a call to the Salesforce REST API.
    *
@@ -106,42 +99,6 @@ interface RestClientInterface {
    */
   public function getHttpClientOption($option_name);
 
-  /**
-   * Get the API end point for a given type of the API.
-   *
-   * @param string $api_type
-   *   E.g., rest, partner, enterprise.
-   *
-   * @return string
-   *   Complete URL endpoint for API access.
-   *
-   * @deprecated will be removed in 8.x-4.0 release.
-   */
-  public function getApiEndPoint($api_type = 'rest');
-
-  /**
-   * Wrapper for config rest_api_version.version.
-   *
-   * @return string
-   *   The SF API version.
-   */
-  public function getApiVersion();
-
-  /**
-   * Setter for config salesforce.settings rest_api_version and use_latest.
-   *
-   * @param bool $use_latest
-   *   Use the latest version, instead of an explicit version number.
-   * @param int $version
-   *   The explicit version number. Mutually exclusive with $use_latest.
-   *
-   * @throws \Exception
-   * @throws \GuzzleHttp\Exception\RequestException
-   *
-   * @deprecated will be removed in 8.x-4.0 release.
-   */
-  public function setApiVersion($use_latest = TRUE, $version = NULL);
-
   /**
    * Get the api usage, as returned in the most recent API request header.
    *
@@ -151,207 +108,6 @@ interface RestClientInterface {
    */
   public function getApiUsage();
 
-  /**
-   * Consumer key getter.
-   *
-   * @return string|null
-   *   Consumer key.
-   *
-   * @deprecated will be removed in 8.x-4.0 release.
-   */
-  public function getConsumerKey();
-
-  /**
-   * Consumer key setter.
-   *
-   * @param string $value
-   *   Consumer key value.
-   *
-   * @return $this
-   *
-   * @deprecated will be removed in 8.x-4.0 release.
-   */
-  public function setConsumerKey($value);
-
-  /**
-   * Comsumer secret getter.
-   *
-   * @return string|null
-   *   Consumer secret.
-   *
-   * @deprecated will be removed in 8.x-4.0 release.
-   */
-  public function getConsumerSecret();
-
-  /**
-   * Consumer key setter.
-   *
-   * @param string $value
-   *   Consumer secret value.
-   *
-   * @return $this
-   *
-   * @deprecated will be removed in 8.x-4.0 release.
-   */
-  public function setConsumerSecret($value);
-
-  /**
-   * Login url getter.
-   *
-   * @return string|null
-   *   Login url.
-   *
-   * @deprecated will be removed in 8.x-4.0 release.
-   */
-  public function getLoginUrl();
-
-  /**
-   * Login url setter.
-   *
-   * @param string $value
-   *   The login url.
-   *
-   * @return $this
-   *
-   * @deprecated will be removed in 8.x-4.0 release.
-   */
-  public function setLoginUrl($value);
-
-  /**
-   * Get the SF instance URL. Useful for linking to objects.
-   *
-   * @return string|null
-   *   The instance url.
-   *
-   * @deprecated will be removed in 8.x-4.0 release.
-   */
-  public function getInstanceUrl();
-
-  /**
-   * Set the SF instance URL.
-   *
-   * @param string $url
-   *   The url.
-   *
-   * @return $this
-   *
-   * @deprecated will be removed in 8.x-4.0 release.
-   */
-  public function setInstanceUrl($url);
-
-  /**
-   * Get the access token.
-   *
-   * @return string|null
-   *   The access token.
-   *
-   * @deprecated will be removed in 8.x-4.0 release.
-   */
-  public function getAccessToken();
-
-  /**
-   * Set the access token.
-   *
-   * @param string $token
-   *   Access token from Salesforce.
-   *
-   * @deprecated will be removed in 8.x-4.0 release.
-   */
-  public function setAccessToken($token);
-
-  /**
-   * Set the refresh token.
-   *
-   * @param string $token
-   *   Refresh token from Salesforce.
-   *
-   * @deprecated will be removed in 8.x-4.0 release.
-   */
-  public function setRefreshToken($token);
-
-  /**
-   * Refresh access token based on the refresh token.
-   *
-   * @throws \Exception
-   *
-   * @deprecated will be removed in 8.x-4.0 release.
-   */
-  public function refreshToken();
-
-  /**
-   * Helper callback for OAuth handshake, and refreshToken()
-   *
-   * @param \GuzzleHttp\Psr7\Response $response
-   *   Response object from refreshToken or authToken endpoints.
-   *
-   * @see SalesforceController::oauthCallback()
-   * @see self::refreshToken()
-   *
-   * @deprecated will be removed in 8.x-4.0 release.
-   */
-  public function handleAuthResponse(Response $response);
-
-  /**
-   * Retrieve and store the Salesforce identity given an ID url.
-   *
-   * @param string $id
-   *   Identity URL.
-   *
-   * @throws \Exception
-   *
-   * @deprecated will be removed in 8.x-4.0 release.
-   */
-  public function initializeIdentity($id);
-
-  /**
-   * Return the Salesforce identity, which is stored in a variable.
-   *
-   * @return array
-   *   Returns FALSE is no identity has been stored.
-   *
-   * @deprecated will be removed in 8.x-4.0 release.
-   */
-  public function getIdentity();
-
-  /**
-   * Set the Salesforce identity, which is stored in a variable.
-   *
-   * @deprecated will be removed in 8.x-4.0 release.
-   */
-  public function setIdentity($data);
-
-  /**
-   * Helper to build the redirect URL for OAUTH workflow.
-   *
-   * @return string
-   *   Redirect URL.
-   *
-   * @see \Drupal\salesforce\Controller\SalesforceController
-   *
-   * @deprecated will be removed in 8.x-4.0 release.
-   */
-  public function getAuthCallbackUrl();
-
-  /**
-   * Get Salesforce oauth login endpoint. (OAuth step 1)
-   *
-   * @return string
-   *   REST OAuth Login URL.
-   *
-   * @deprecated will be removed in 8.x-4.0 release.
-   */
-  public function getAuthEndpointUrl();
-
-  /**
-   * Get Salesforce oauth token endpoint. (OAuth step 2)
-   *
-   * @return string
-   *   REST OAuth Token URL.
-   *
-   * @deprecated will be removed in 8.x-4.0 release.
-   */
-  public function getAuthTokenUrl();
-
   /**
    * Wrapper for "Versions" resource to list information about API releases.
    *
diff --git a/src/Rest/RestResponse.php b/src/Rest/RestResponse.php
index 298ef578910ac577f40f1ff98bcf91c5c6801023..a7d2f50b7be0b9d684c48d234f365f9808f404b3 100644
--- a/src/Rest/RestResponse.php
+++ b/src/Rest/RestResponse.php
@@ -4,6 +4,7 @@ namespace Drupal\salesforce\Rest;
 
 use Drupal\Component\Serialization\Json;
 use GuzzleHttp\Psr7\Response;
+use Psr\Http\Message\ResponseInterface;
 
 /**
  * Class RestResponse.
@@ -34,7 +35,7 @@ class RestResponse extends Response {
    * @param \GuzzleHttp\Psr7\Response $response
    *   A response.
    */
-  public function __construct(Response $response) {
+  public function __construct(ResponseInterface $response) {
     $this->response = $response;
     parent::__construct($response->getStatusCode(), $response->getHeaders(), $response->getBody(), $response->getProtocolVersion(), $response->getReasonPhrase());
     $this->handleJsonResponse();
diff --git a/src/Rest/RestResponse_Describe.php b/src/Rest/RestResponse_Describe.php
deleted file mode 100644
index 14759a13ba940ff3ec35ef7f53c9e427deb8aad2..0000000000000000000000000000000000000000
--- a/src/Rest/RestResponse_Describe.php
+++ /dev/null
@@ -1,12 +0,0 @@
-<?php
-
-namespace Drupal\salesforce\Rest;
-
-/**
- * Use RestResponseDescribe.
- *
- * @deprecated will be removed in 8.x-4.0 release.
- */
-class RestResponse_Describe extends RestResponseDescribe {
-
-}
diff --git a/src/Rest/RestResponse_Resources.php b/src/Rest/RestResponse_Resources.php
deleted file mode 100644
index cbad6fce7e563ca4be94340ccaf87740338c34af..0000000000000000000000000000000000000000
--- a/src/Rest/RestResponse_Resources.php
+++ /dev/null
@@ -1,12 +0,0 @@
-<?php
-
-namespace Drupal\salesforce\Rest;
-
-/**
- * Use RestResponseResources.
- *
- * @deprecated will be removed in 8.x-4.0 release.
- */
-class RestResponse_Resources extends RestResponseResources {
-
-}
diff --git a/src/SalesforceAuthProviderInterface.php b/src/SalesforceAuthProviderInterface.php
index 130de7a7910102b22da60d29d06204d4ca378033..aee8ea850900a5363e1333613d54999a568305a4 100644
--- a/src/SalesforceAuthProviderInterface.php
+++ b/src/SalesforceAuthProviderInterface.php
@@ -17,6 +17,7 @@ interface SalesforceAuthProviderInterface extends ServiceInterface, PluginFormIn
   const AUTH_TOKEN_PATH = '/services/oauth2/token';
   const AUTH_ENDPOINT_PATH = '/services/oauth2/authorize';
   const SOAP_CLASS_PATH = '/services/Soap/class/';
+  const LATEST_API_VERSION = '44.0';
 
   /**
    * Id of this service.
@@ -130,6 +131,25 @@ interface SalesforceAuthProviderInterface extends ServiceInterface, PluginFormIn
    */
   public function getAccessTokenEndpoint();
 
+  /**
+   * Get the globally configured API version to use.
+   *
+   * @return string
+   *   The string name of the API version.
+   */
+  public function getApiVersion();
+
+  /**
+   * API Url for this plugin.
+   *
+   * @param string $api_type
+   *   (optional) Which API for which to retrieve URL, defaults to "rest".
+   *
+   * @return string.
+   *   The URL
+   */
+  public function getApiEndpoint($api_type = 'rest');
+
   /**
    * Instance URL for this connection.
    *
@@ -150,4 +170,24 @@ interface SalesforceAuthProviderInterface extends ServiceInterface, PluginFormIn
    */
   public function save(array $form, FormStateInterface $form_state);
 
+  /**
+   * The auth provider service.
+   *
+   * @return \Drupal\salesforce\SalesforceAuthProviderInterface
+   *   The auth provider service.
+   */
+  public function service();
+
+  /**
+   * Complete the OAuth user-agent handshake.
+   *
+   * @return bool
+   *   TRUE if oauth finalization was successful.
+   *
+   * @throws \OAuth\Common\Http\Exception\TokenResponseException
+   *
+   * @see \Drupal\salesforce\Controller\SalesforceOAuthController
+   */
+  public function finalizeOauth();
+
 }
diff --git a/src/SalesforceAuthProviderPluginBase.php b/src/SalesforceAuthProviderPluginBase.php
index f65a73e8ac61900a315cd59978679fc0d3f8b9fc..a0580ac90b07e29274f152d77442b0bb4930148c 100644
--- a/src/SalesforceAuthProviderPluginBase.php
+++ b/src/SalesforceAuthProviderPluginBase.php
@@ -186,6 +186,38 @@ abstract class SalesforceAuthProviderPluginBase extends Salesforce implements Sa
     return $this->getAccessToken()->getExtraParams()['instance_url'];
   }
 
+  /**
+   * {@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);
+    }
+    return $url;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getApiVersion() {
+    $version = \Drupal::config('salesforce.settings')->get('rest_api_version.version');
+    if (empty($version) || \Drupal::config('salesforce.settings')->get('use_latest')) {
+      return self::LATEST_API_VERSION;
+    }
+    return \Drupal::config('salesforce.settings')->get('rest_api_version.version');
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/src/SalesforceAuthProviderPluginInterface.php b/src/SalesforceAuthProviderPluginInterface.php
deleted file mode 100644
index 9e96fbf4db4b1e9ebdd7545326d556efee1400c0..0000000000000000000000000000000000000000
--- a/src/SalesforceAuthProviderPluginInterface.php
+++ /dev/null
@@ -1,29 +0,0 @@
-<?php
-
-namespace Drupal\salesforce;
-
-use Drupal\Component\Plugin\PluginInspectionInterface;
-use Drupal\Core\Plugin\PluginFormInterface;
-
-/**
- * Auth provider plugin interface.
- */
-interface SalesforceAuthProviderPluginInterface extends PluginFormInterface, PluginInspectionInterface {
-
-  /**
-   * The auth provider service.
-   *
-   * @return \Drupal\salesforce\SalesforceAuthProviderInterface
-   *   The auth provider service.
-   */
-  public function service();
-
-  /**
-   * Login URL set for this auth provider.
-   *
-   * @return string
-   *   Login URL set for this auth provider.
-   */
-  public function getLoginUrl();
-
-}
diff --git a/src/SalesforceAuthProviderPluginManager.php b/src/SalesforceAuthProviderPluginManager.php
index 782b5583676fdeb96e7f06db96c00b5c2388db37..c5742a3655e0109e0422e24101828300a1d7498d 100644
--- a/src/SalesforceAuthProviderPluginManager.php
+++ b/src/SalesforceAuthProviderPluginManager.php
@@ -61,7 +61,7 @@ class SalesforceAuthProviderPluginManager extends DefaultPluginManager {
   /**
    * Backwards-compatibility for legacy singleton auth.
    *
-   * @deprecated BC legacy auth scheme only. will be removed in 8.x-4.0.
+   * @deprecated BC legacy auth scheme only. will be removed in 8.x-4.1.
    */
   public static function updateAuthConfig() {
     $oauth = self::getAuthConfig();
@@ -79,7 +79,7 @@ class SalesforceAuthProviderPluginManager extends DefaultPluginManager {
   /**
    * Backwards-compatibility for legacy singleton auth.
    *
-   * @deprecated BC legacy auth scheme only. will be removed in 8.x-4.0.
+   * @deprecated BC legacy auth scheme only. will be removed in 8.x-4.1.
    */
   public static function getAuthConfig() {
     $config = \Drupal::configFactory()->getEditable('salesforce.settings');
@@ -192,7 +192,7 @@ class SalesforceAuthProviderPluginManager extends DefaultPluginManager {
   /**
    * Force a refresh of the active token and return the fresh token.
    *
-   * @return \OAuth\OAuth2\Token\TokenInterface|null
+   * @return \OAuth\Common\Token\TokenInterface
    *   The token.
    */
   public function refreshToken() {
diff --git a/src/SalesforceOAuthPluginInterface.php b/src/SalesforceOAuthPluginInterface.php
deleted file mode 100644
index a4534c0e04bd2399dd6ae38bacb16afb0a9832f5..0000000000000000000000000000000000000000
--- a/src/SalesforceOAuthPluginInterface.php
+++ /dev/null
@@ -1,35 +0,0 @@
-<?php
-
-namespace Drupal\salesforce;
-
-/**
- * OAuth user-agent plugin interface.
- *
- * OAuth user-agent flow requires a 2-part handshake to complete authentication.
- * This interface exposes methods to make the handshake possible.
- *
- * @deprecated BC legacy auth scheme only. will be removed in 8.x-4.0.
- */
-interface SalesforceOAuthPluginInterface extends SalesforceAuthProviderPluginInterface {
-
-  /**
-   * Complete the OAuth user-agent handshake.
-   *
-   * @return bool
-   *   TRUE if oauth finalization was successful.
-   *
-   * @throws \OAuth\Common\Http\Exception\TokenResponseException
-   *
-   * @see \Drupal\salesforce\Controller\SalesforceOAuthController
-   */
-  public function finalizeOauth();
-
-  /**
-   * Getter for consumer secret.
-   *
-   * @return string
-   *   The consumer secret.
-   */
-  public function getConsumerSecret();
-
-}
diff --git a/src/Storage/SalesforceAuthTokenStorage.php b/src/Storage/SalesforceAuthTokenStorage.php
index d7de34c94d50e532edfd961327028590ce796157..9cbe52778418ceb631e3f6b7e79ea77ef573fa1c 100644
--- a/src/Storage/SalesforceAuthTokenStorage.php
+++ b/src/Storage/SalesforceAuthTokenStorage.php
@@ -34,42 +34,6 @@ class SalesforceAuthTokenStorage implements SalesforceAuthTokenStorageInterface
     $this->state = $state;
   }
 
-  /**
-   * Backwards-compatibility for legacy singleton auth.
-   *
-   * @return string
-   *   Id of the active oauth.
-   *
-   * @deprecated BC legacy auth scheme only. will be removed in 8.x-4.0.
-   */
-  private function service() {
-    $oauth = SalesforceAuthProviderPluginManager::getAuthConfig();
-    return $oauth->id();
-  }
-
-  /**
-   * Backwards-compatibility for legacy singleton auth.
-   *
-   * @deprecated BC legacy auth scheme only. will be removed in 8.x-4.0.
-   */
-  public function updateToken() {
-    $this->storeAccessToken($this->service(),
-      new SalesforceToken(
-        $this->state->get('salesforce.access_token'),
-        $this->state->get('salesforce.refresh_token')));
-    return $this;
-  }
-
-  /**
-   * Backwards-compatibility for legacy singleton auth.
-   *
-   * @deprecated BC legacy auth scheme only. will be removed in 8.x-4.0.
-   */
-  public function updateIdentity() {
-    $this->storeIdentity($this->service(), $this->state->get('salesforce.identity'));
-    return $this;
-  }
-
   /**
    * Token storage key for given service.
    *
diff --git a/src/Tests/TestRestClient.php b/src/Tests/TestRestClient.php
index 0f1f6eb1e52bdca5c6a4514dc882ca7d739bc93c..fa5185cfc9b634a060985277a2a81689770fe635 100644
Binary files a/src/Tests/TestRestClient.php and b/src/Tests/TestRestClient.php differ
diff --git a/tests/src/Unit/AuthorizeFormTest.php b/tests/src/Unit/AuthorizeFormTest.php
deleted file mode 100644
index 83471e168f347b71d97a46d221f031be1587c9ac..0000000000000000000000000000000000000000
--- a/tests/src/Unit/AuthorizeFormTest.php
+++ /dev/null
@@ -1,81 +0,0 @@
-<?php
-
-namespace Drupal\Tests\salesforce\Unit;
-
-use Drupal\Core\Config\ConfigFactoryInterface;
-use Drupal\Core\Form\FormState;
-use Drupal\Core\PathProcessor\OutboundPathProcessorInterface;
-use Drupal\Core\Routing\TrustedRedirectResponse;
-use Drupal\Core\State\StateInterface;
-use Drupal\Core\Utility\UnroutedUrlAssembler;
-use Drupal\Tests\UnitTestCase;
-use Drupal\salesforce\Form\AuthorizeForm;
-use Drupal\salesforce\Rest\RestClient;
-use Prophecy\Argument;
-use Symfony\Component\DependencyInjection\ContainerBuilder;
-use Symfony\Component\HttpFoundation\RequestStack;
-use Drupal\Core\Logger\LoggerChannelFactory;
-
-/**
- * @coversDefaultClass \Drupal\salesforce\Form\AuthorizeForm
- * @group salesforce
- */
-class AuthorizeFormTest extends UnitTestCase {
-
-  /**
-   * Set up for each test.
-   */
-  public function setUp() {
-    parent::setUp();
-
-    $this->example_url = 'https://example.com';
-    $this->consumer_key = $this->randomMachineName();
-
-    $this->config_factory = $this->prophesize(ConfigFactoryInterface::class);
-    $this->state = $this->prophesize(StateInterface::class);
-    $this->client = $this->prophesize(RestClient::class);
-    $this->request_stack = $this->prophesize(RequestStack::class);
-    $this->obpath = $this->prophesize(OutboundPathProcessorInterface::class);
-    $this->logger = $this->prophesize(LoggerChannelFactory::class);
-    $this->unrouted_url_assembler = new UnroutedUrlAssembler($this->request_stack->reveal(), $this->obpath->reveal());
-    $this->event_dispatcher = $this->getMock('\Symfony\Component\EventDispatcher\EventDispatcherInterface');
-
-    $this->client->getAuthCallbackUrl()->willReturn($this->example_url);
-    $this->client->getAuthEndpointUrl()->willReturn($this->example_url);
-    $this->client->getConsumerKey()->willReturn($this->consumer_key);
-
-    $this->client->setConsumerKey(Argument::any())->willReturn(NULL);
-    $this->client->setConsumerSecret(Argument::any())->willReturn(NULL);
-    $this->client->setLoginUrl(Argument::any())->willReturn(NULL);
-
-    $container = new ContainerBuilder();
-    $container->set('config.factory', $this->config_factory->reveal());
-    $container->set('salesforce.client', $this->client->reveal());
-    $container->set('state', $this->state->reveal());
-    $container->set('unrouted_url_assembler', $this->unrouted_url_assembler);
-    $container->set('logger.factory', $this->logger->reveal());
-    $container->set('event_dispatcher', $this->event_dispatcher);
-    \Drupal::setContainer($container);
-  }
-
-  /**
-   * @covers ::submitForm
-   */
-  public function testSubmitForm() {
-    $form_state = new FormState();
-    $form_state->setValues([
-      'consumer_key' => $this->consumer_key,
-      'consumer_secret' => $this->randomMachineName(),
-      'login_url' => $this->example_url,
-    ]);
-
-    $form = AuthorizeForm::create(\Drupal::getContainer());
-    $form_array = [];
-    $form->submitForm($form_array, $form_state);
-    /** @var \Drupal\Core\Routing\TrustedRedirectResponse $response */
-    $response = $form_state->getResponse();
-    $this->assertTrue($response instanceof TrustedRedirectResponse);
-    $this->assertEquals($this->example_url . '?redirect_uri=' . urlencode($this->example_url) . '&response_type=code&client_id=' . $form_state->getValue('consumer_key'), $response->getTargetUrl());
-  }
-
-}
diff --git a/tests/src/Unit/RestClientTest.php b/tests/src/Unit/RestClientTest.php
index 28e9ab29f3c52fbc4ee0b7a242f555f69269f082..f6e4a2cb4c2eb6b925062af972015124b5de59b2 100644
--- a/tests/src/Unit/RestClientTest.php
+++ b/tests/src/Unit/RestClientTest.php
@@ -3,6 +3,11 @@
 namespace Drupal\Tests\salesforce\Unit;
 
 use Drupal\Component\Serialization\Json;
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Config\ConfigFactory;
+use Drupal\Core\State\State;
+use Drupal\salesforce\SalesforceAuthProviderPluginManager;
+use Drupal\salesforce\Token\SalesforceToken;
 use Drupal\Tests\UnitTestCase;
 use Drupal\salesforce\Rest\RestClient;
 use Drupal\salesforce\Rest\RestResponse;
@@ -12,6 +17,8 @@ use Drupal\salesforce\SFID;
 use Drupal\salesforce\SObject;
 use Drupal\salesforce\SelectQueryResult;
 use Drupal\salesforce\SelectQuery;
+use OAuth\OAuth2\Token\TokenInterface;
+use GuzzleHttp\Client;
 use GuzzleHttp\Psr7\Response as GuzzleResponse;
 use GuzzleHttp\Psr7\Request as GuzzleRequest;
 use GuzzleHttp\Exception\RequestException;
@@ -32,27 +39,29 @@ class RestClientTest extends UnitTestCase {
     parent::setUp();
     $this->salesforce_id = '1234567890abcde';
     $this->methods = [
-      'getConsumerKey',
-      'getConsumerSecret',
-      'getRefreshToken',
-      'getAccessToken',
-      'refreshToken',
-      'getApiEndPoint',
       'httpRequest',
     ];
 
-    $this->httpClient = $this->getMock('\GuzzleHttp\Client');
+    $this->httpClient = $this->getMock(Client::CLASS);
     $this->configFactory =
-      $this->getMockBuilder('\Drupal\Core\Config\ConfigFactory')
+      $this->getMockBuilder(ConfigFactory::CLASS)
         ->disableOriginalConstructor()
         ->getMock();
     $this->state =
-      $this->getMockBuilder('\Drupal\Core\State\State')
+      $this->getMockBuilder(State::CLASS)
         ->disableOriginalConstructor()
         ->getMock();
-    $this->cache = $this->getMock('\Drupal\Core\Cache\CacheBackendInterface');
+    $this->cache = $this->getMock(CacheBackendInterface::CLASS);
     $this->json = $this->getMock(Json::CLASS);
     $this->time = $this->getMock(TimeInterface::CLASS);
+    $this->authToken = $this->getMock(TokenInterface::CLASS);
+    $this->authMan =
+      $this->getMockBuilder(SalesforceAuthProviderPluginManager::CLASS)
+        ->disableOriginalConstructor()
+        ->getMock();
+    $this->authMan->expects($this->any())
+      ->method('getToken')
+      ->willReturn($this->authToken);
   }
 
   /**
@@ -86,27 +95,6 @@ class RestClientTest extends UnitTestCase {
     }
   }
 
-  /**
-   * @covers ::isAuthorized
-   */
-  public function testAuthorized() {
-    $this->initClient();
-    $this->client->expects($this->at(0))
-      ->method('getConsumerKey')
-      ->willReturn($this->randomMachineName());
-    $this->client->expects($this->at(1))
-      ->method('getConsumerSecret')
-      ->willReturn($this->randomMachineName());
-    $this->client->expects($this->at(2))
-      ->method('getRefreshToken')
-      ->willReturn($this->randomMachineName());
-
-    $this->assertTrue($this->client->isAuthorized());
-
-    // Next one will fail because mocks only return for specific invocations.
-    $this->assertFalse($this->client->isAuthorized());
-  }
-
   /**
    * @covers ::apiCall
    */
@@ -154,10 +142,10 @@ class RestClientTest extends UnitTestCase {
 
     // First httpRequest() is position 4.
     // @TODO this is extremely brittle, exposes complexity in underlying client. Refactor this.
-    $this->client->expects($this->at(3))
+    $this->client->expects($this->at(2))
       ->method('httpRequest')
       ->willReturn($response_401);
-    $this->client->expects($this->at(4))
+    $this->client->expects($this->at(3))
       ->method('httpRequest')
       ->willReturn($response_200);
 
diff --git a/tests/src/Unit/SalesforceControllerTest.php b/tests/src/Unit/SalesforceControllerTest.php
deleted file mode 100644
index 94891e0ffa9ea13e1dbd576803d5023c378feb72..0000000000000000000000000000000000000000
--- a/tests/src/Unit/SalesforceControllerTest.php
+++ /dev/null
@@ -1,130 +0,0 @@
-<?php
-
-namespace Drupal\Tests\salesforce\Unit;
-
-use Drupal\Component\Serialization\Json;
-use Drupal\Core\Render\MetadataBubblingUrlGenerator;
-use Drupal\salesforce\Controller\SalesforceController;
-use Drupal\salesforce\Rest\RestClient;
-use Drupal\Tests\UnitTestCase;
-
-use GuzzleHttp\Psr7\Response;
-use Symfony\Component\DependencyInjection\ContainerBuilder;
-use Symfony\Component\HttpFoundation\RedirectResponse;
-use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\HttpFoundation\RequestStack;
-use Drupal\Component\Datetime\TimeInterface;
-
-/**
- * @coversDefaultClass \Drupal\salesforce\Controller\SalesforceController
- * @group salesforce
- *
- * @deprecated BC legacy auth scheme only. will be removed in 8.x-4.0.
- */
-class SalesforceControllerTest extends UnitTestCase {
-
-  /**
-   * Set up for each test.
-   */
-  public function setUp() {
-    parent::setUp();
-
-    $this->example_url = 'https://example.com';
-
-    $this->httpClient = $this->getMock('\GuzzleHttp\Client', ['post']);
-    $this->httpClient->expects($this->once())
-      ->method('post')
-      ->willReturn(new Response());
-    $this->configFactory =
-      $this->getMockBuilder('\Drupal\Core\Config\ConfigFactory')
-        ->disableOriginalConstructor()
-        ->getMock();
-    $this->state =
-      $this->getMockBuilder('\Drupal\Core\State\State')
-        ->disableOriginalConstructor()
-        ->getMock();
-    $this->cache = $this->getMock('\Drupal\Core\Cache\CacheBackendInterface');
-    $this->json = $this->getMock(Json::CLASS);
-    $this->time = $this->getMock(TimeInterface::CLASS);
-
-    $args = [
-      $this->httpClient,
-      $this->configFactory,
-      $this->state,
-      $this->cache,
-      $this->json,
-      $this->time,
-    ];
-
-    $this->client = $this->getMock(RestClient::class, [
-      'getConsumerKey',
-      'getConsumerSecret',
-      'getAuthCallbackUrl',
-      'getAuthTokenUrl',
-      'handleAuthResponse',
-    ], $args);
-    $this->client->expects($this->once())
-      ->method('getConsumerKey')
-      ->willReturn($this->randomMachineName());
-    $this->client->expects($this->once())
-      ->method('getConsumerSecret')
-      ->willReturn($this->randomMachineName());
-    $this->client->expects($this->once())
-      ->method('getAuthCallbackUrl')
-      ->willReturn($this->example_url);
-    $this->client->expects($this->once())
-      ->method('getAuthTokenUrl')
-      ->willReturn($this->example_url);
-    $this->client->expects($this->once())
-      ->method('handleAuthResponse')
-      ->willReturn($this->client);
-
-    $this->request = new Request(['code' => $this->randomMachineName()]);
-
-    $this->request_stack = $this->getMock(RequestStack::class);
-    $this->request_stack->expects($this->exactly(2))
-      ->method('getCurrentRequest')
-      ->willReturn($this->request);
-
-    $this->url_generator = $this->prophesize(MetadataBubblingUrlGenerator::class);
-    $this->url_generator->generateFromRoute(
-      'salesforce.authorize',
-      [],
-      ["absolute" => TRUE],
-      FALSE
-    )
-      ->willReturn('foo/bar');
-
-    $container = new ContainerBuilder();
-    $container->set('salesforce.client', $this->client);
-    $container->set('http_client', $this->httpClient);
-    $container->set('request_stack', $this->request_stack);
-    $container->set('url.generator', $this->url_generator->reveal());
-    $container->set('datetime.time', $this->time);
-    \Drupal::setContainer($container);
-
-  }
-
-  /**
-   * @covers ::oauthCallback
-   */
-  public function testOauthCallback() {
-    $this->controller = $this->getMock(
-      SalesforceController::class,
-      ['successMessage'],
-      [
-        $this->client,
-        $this->httpClient,
-        $this->url_generator->reveal(),
-      ]
-    );
-    $this->controller
-      ->expects($this->once())
-      ->method('successMessage')
-      ->willReturn(NULL);
-    $expected = new RedirectResponse('foo/bar');
-    $actual = $this->controller->oauthCallback();
-    $this->assertEquals($expected->getTargetUrl(), $actual->getTargetUrl());
-  }
-
-}