From 74955ab1d5403d11f83725e13547c28a03875c97 Mon Sep 17 00:00:00 2001
From: Aaron Bauman <aaron@messageagency.com>
Date: Fri, 15 Mar 2019 12:10:43 -0400
Subject: [PATCH] Update new auth plugins with dependencies, refactor with
 annotation definition and super-class helpers, fix confusion between plugin
 definition and configuration

---
 config/schema/salesforce.schema.yml           |   2 +-
 .../src/Consumer/JWTCredentials.php           |  20 ++++
 .../SalesforceJWTPlugin.php                   |  70 +++++------
 .../salesforce_mapping_ui.module              |   2 +-
 .../Consumer/SalesforceOAuthCredentials.php   |  20 ++++
 .../SalesforceOAuthPlugin.php                 |  69 ++---------
 .../src/Plugin/QueueWorker/PullBase.php       |   2 +-
 modules/salesforce_push/src/PushQueue.php     |   2 +-
 salesforce.install                            |   2 +-
 src/Annotation/SalesforceAuthProvider.php     |  33 +++++
 src/Consumer/SalesforceCredentials.php        |  19 ++-
 .../SalesforceCredentialsInterface.php        |  18 +++
 src/Controller/SalesforceAuthListBuilder.php  |   4 +-
 src/Entity/SalesforceAuthConfig.php           |  42 ++++++-
 src/Form/SalesforceAuthForm.php               |   2 +-
 src/Plugin/SalesforceAuthProvider/Broken.php  |  62 ++++++++++
 src/SalesforceAuthProviderInterface.php       |   8 --
 src/SalesforceAuthProviderPluginBase.php      | 113 +++++++++++++-----
 src/SalesforceAuthProviderPluginManager.php   |   7 ++
 ...orceAuthProviderPluginManagerInterface.php |   3 +-
 20 files changed, 353 insertions(+), 147 deletions(-)
 create mode 100644 src/Annotation/SalesforceAuthProvider.php
 create mode 100644 src/Plugin/SalesforceAuthProvider/Broken.php

diff --git a/config/schema/salesforce.schema.yml b/config/schema/salesforce.schema.yml
index a05925dc..6d0d17ec 100644
--- a/config/schema/salesforce.schema.yml
+++ b/config/schema/salesforce.schema.yml
@@ -69,7 +69,7 @@ salesforce.salesforce_auth.*:
       label: 'Label'
       translatable: true
     provider:
-      type: string
+      type: salesforce.auth_provider_settings.[id]
       label: 'Provider Plugin'
     provider_settings:
       type: salesforce.auth_provider_settings.[%parent.provider]
diff --git a/modules/salesforce_jwt/src/Consumer/JWTCredentials.php b/modules/salesforce_jwt/src/Consumer/JWTCredentials.php
index e5f516f6..978be30a 100644
--- a/modules/salesforce_jwt/src/Consumer/JWTCredentials.php
+++ b/modules/salesforce_jwt/src/Consumer/JWTCredentials.php
@@ -34,6 +34,19 @@ class JWTCredentials extends SalesforceCredentials {
     $this->keyId = $keyId;
   }
 
+  /**
+   * Constructor helper.
+   *
+   * @param array $configuration
+   *   Plugin configuration.
+   *
+   * @return \Drupal\salesforce_jwt\Consumer\JWTCredentials
+   *   Credentials, valid or not.
+   */
+  public static function create(array $configuration) {
+    return new static($configuration['consumer_key'], $configuration['login_url'], $configuration['login_user'], $configuration['encrypt_key']);
+  }
+
   /**
    * Login user getter.
    *
@@ -54,4 +67,11 @@ class JWTCredentials extends SalesforceCredentials {
     return $this->keyId;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isValid() {
+    return !empty($this->loginUser) && !empty($this->consumerId) && !empty($this->keyId);
+  }
+
 }
diff --git a/modules/salesforce_jwt/src/Plugin/SalesforceAuthProvider/SalesforceJWTPlugin.php b/modules/salesforce_jwt/src/Plugin/SalesforceAuthProvider/SalesforceJWTPlugin.php
index c2a6f6d2..51ad878c 100644
--- a/modules/salesforce_jwt/src/Plugin/SalesforceAuthProvider/SalesforceJWTPlugin.php
+++ b/modules/salesforce_jwt/src/Plugin/SalesforceAuthProvider/SalesforceJWTPlugin.php
@@ -5,7 +5,6 @@ namespace Drupal\salesforce_jwt\Plugin\SalesforceAuthProvider;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Url;
 use Drupal\key\KeyRepositoryInterface;
-use Drupal\salesforce_jwt\Consumer\JWTCredentials;
 use Drupal\salesforce\SalesforceAuthProviderPluginBase;
 use OAuth\Common\Http\Uri\Uri;
 use Drupal\salesforce\Storage\SalesforceAuthTokenStorageInterface;
@@ -19,7 +18,8 @@ use Firebase\JWT\JWT;
  *
  * @Plugin(
  *   id = "jwt",
- *   label = @Translation("Salesforce JWT OAuth")
+ *   label = @Translation("Salesforce JWT OAuth"),
+ *   credentials_class = "\Drupal\salesforce_jwt\Consumer\JWTCredentials"
  * )
  */
 class SalesforceJWTPlugin extends SalesforceAuthProviderPluginBase {
@@ -38,23 +38,15 @@ class SalesforceJWTPlugin extends SalesforceAuthProviderPluginBase {
    */
   protected $keyRepository;
 
-  /**
-   * {@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 array $configuration
+   *   Configuration.
+   * @param string $plugin_id
+   *   Plugin id.
+   * @param mixed $plugin_definition
+   *   Plugin definition.
    * @param \OAuth\Common\Http\Client\ClientInterface $httpClient
    *   Http client wrapper.
    * @param \Drupal\salesforce\Storage\SalesforceAuthTokenStorageInterface $storage
@@ -65,10 +57,9 @@ class SalesforceJWTPlugin extends SalesforceAuthProviderPluginBase {
    * @throws \OAuth\OAuth2\Service\Exception\InvalidScopeException
    *   On error.
    */
-  public function __construct($id, JWTCredentials $credentials, ClientInterface $httpClient, SalesforceAuthTokenStorageInterface $storage, KeyRepositoryInterface $keyRepository) {
-    parent::__construct($credentials, $httpClient, $storage, [], new Uri($credentials->getLoginUrl()));
-    $this->id = $id;
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, ClientInterface $httpClient, SalesforceAuthTokenStorageInterface $storage, KeyRepositoryInterface $keyRepository) {
     $this->keyRepository = $keyRepository;
+    parent::__construct($configuration, $plugin_id, $plugin_definition, $httpClient, $storage);
   }
 
   /**
@@ -76,8 +67,7 @@ class SalesforceJWTPlugin extends SalesforceAuthProviderPluginBase {
    */
   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'), $container->get('key.repository'));
+    return new static($configuration, $plugin_id, $plugin_definition, $container->get('salesforce.http_client_wrapper'), $container->get('salesforce.auth_token_storage'), $container->get('key.repository'));
   }
 
   /**
@@ -95,7 +85,7 @@ class SalesforceJWTPlugin extends SalesforceAuthProviderPluginBase {
    * {@inheritdoc}
    */
   public function getLoginUrl() {
-    return $this->credentials->getLoginUrl();
+    return $this->getCredentials()->getLoginUrl();
   }
 
   /**
@@ -111,7 +101,7 @@ class SalesforceJWTPlugin extends SalesforceAuthProviderPluginBase {
       '#type' => 'textfield',
       '#description' => t('Consumer key of the Salesforce remote application you want to grant access to'),
       '#required' => TRUE,
-      '#default_value' => $this->credentials->getConsumerKey(),
+      '#default_value' => $this->getCredentials()->getConsumerKey(),
     ];
 
     $form['login_user'] = [
@@ -119,13 +109,13 @@ class SalesforceJWTPlugin extends SalesforceAuthProviderPluginBase {
       '#type' => 'textfield',
       '#description' => $this->t('User account to issue token to'),
       '#required' => TRUE,
-      '#default_value' => $this->credentials->getLoginUser(),
+      '#default_value' => $this->getCredentials()->getLoginUser(),
     ];
 
     $form['login_url'] = [
       '#title' => t('Login URL'),
       '#type' => 'textfield',
-      '#default_value' => $this->credentials->getLoginUrl(),
+      '#default_value' => $this->getCredentials()->getLoginUrl(),
       '#description' => t('Enter a login URL, either https://login.salesforce.com or https://test.salesforce.com.'),
       '#required' => TRUE,
     ];
@@ -137,7 +127,7 @@ class SalesforceJWTPlugin extends SalesforceAuthProviderPluginBase {
       '#type' => 'select',
       '#options' => $this->keyRepository->getKeyNamesAsOptions(['type' => 'authentication']),
       '#required' => TRUE,
-      '#default_value' => $this->credentials->getKeyId(),
+      '#default_value' => $this->getCredentials()->getKeyId(),
     ];
 
     return $form;
@@ -148,12 +138,10 @@ class SalesforceJWTPlugin extends SalesforceAuthProviderPluginBase {
    */
   public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
     parent::validateConfigurationForm($form, $form_state);
-    $this->setConfiguration($form_state->getValues());
+    $this->setConfiguration($form_state->getValue('provider_settings'));
     try {
-      $values = $form_state->getValues();
-      $this->id = $values['id'];
-      $settings = $values['provider_settings'];
-      $this->credentials = new JWTCredentials($settings['consumer_key'], $settings['login_url'], $settings['login_user'], $settings['encrypt_key']);
+      // Bootstrap here by setting ID to provide a key to token storage.
+      $this->id = $form_state->getValue('id');
       $this->requestAccessToken($this->generateAssertion());
     }
     catch (\Exception $e) {
@@ -210,7 +198,7 @@ class SalesforceJWTPlugin extends SalesforceAuthProviderPluginBase {
    *   JWT Assertion.
    */
   protected function generateAssertion() {
-    $key = $this->keyRepository->getKey($this->credentials->getKeyId())->getKeyValue();
+    $key = $this->keyRepository->getKey($this->getCredentials()->getKeyId())->getKeyValue();
     $token = $this->generateAssertionClaim();
     return JWT::encode($token, $key, 'RS256');
   }
@@ -222,12 +210,24 @@ class SalesforceJWTPlugin extends SalesforceAuthProviderPluginBase {
    *   The claim array.
    */
   protected function generateAssertionClaim() {
+    $cred = $this->getCredentials();
     return [
-      'iss' => $this->credentials->getConsumerKey(),
-      'sub' => $this->credentials->getLoginUser(),
-      'aud' => $this->credentials->getLoginUrl(),
+      'iss' => $cred->getConsumerKey(),
+      'sub' => $cred->getLoginUser(),
+      'aud' => $cred->getLoginUrl(),
       'exp' => \Drupal::time()->getCurrentTime() + 60,
     ];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getPluginDefinition() {
+    $definition = parent::getPluginDefinition();
+    if ($this->configuration['encrypt_key'] && $key = $this->keyRepository->getKey($this->configuration['encrypt_key'])) {
+      $definition['config_dependencies']['config'][] = $key->getConfigDependencyName();
+    }
+    return $definition;
+  }
+
 }
diff --git a/modules/salesforce_mapping_ui/salesforce_mapping_ui.module b/modules/salesforce_mapping_ui/salesforce_mapping_ui.module
index 96393f82..30f252a1 100644
--- a/modules/salesforce_mapping_ui/salesforce_mapping_ui.module
+++ b/modules/salesforce_mapping_ui/salesforce_mapping_ui.module
@@ -29,7 +29,7 @@ function salesforce_mapping_ui_entity_type_alter(array &$entity_types) {
       $entity_type->setLinkTemplate('salesforce', "/$entity_type_id/{{$entity_type_id}}/salesforce");
     }
   }
-  // Set our UI classes for SalesforceMappingEntity
+  // Set our UI classes for SalesforceMappingEntity.
   $entity_types['salesforce_mapping']->setHandlerClass('list_builder', SalesforceMappingList::class);
   $entity_types['salesforce_mapping']->setFormClass('add', SalesforceMappingAddForm::class);
   $entity_types['salesforce_mapping']->setFormClass('edit', SalesforceMappingEditForm::class);
diff --git a/modules/salesforce_oauth/src/Consumer/SalesforceOAuthCredentials.php b/modules/salesforce_oauth/src/Consumer/SalesforceOAuthCredentials.php
index e3a93f81..0649af9a 100644
--- a/modules/salesforce_oauth/src/Consumer/SalesforceOAuthCredentials.php
+++ b/modules/salesforce_oauth/src/Consumer/SalesforceOAuthCredentials.php
@@ -19,6 +19,19 @@ class SalesforceOAuthCredentials extends SalesforceCredentials {
     $this->loginUrl = $loginUrl;
   }
 
+  /**
+   * Constructor helper.
+   *
+   * @param array $configuration
+   *   Plugin configuration.
+   *
+   * @return \Drupal\salesforce_oauth\Consumer\SalesforceOAuthCredentials
+   *   Credentials, valid or not.
+   */
+  public static function create(array $configuration) {
+    return new static($configuration['consumer_key'], $configuration['consumer_secret'], $configuration['login_url']);
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -39,4 +52,11 @@ class SalesforceOAuthCredentials extends SalesforceCredentials {
     ])->toString();
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isValid() {
+    return !empty($this->loginUrl) && !empty($this->consumerSecret) && !empty($this->consumerId);
+  }
+
 }
diff --git a/modules/salesforce_oauth/src/Plugin/SalesforceAuthProvider/SalesforceOAuthPlugin.php b/modules/salesforce_oauth/src/Plugin/SalesforceAuthProvider/SalesforceOAuthPlugin.php
index 97cdc7b5..4411b3ca 100644
--- a/modules/salesforce_oauth/src/Plugin/SalesforceAuthProvider/SalesforceOAuthPlugin.php
+++ b/modules/salesforce_oauth/src/Plugin/SalesforceAuthProvider/SalesforceOAuthPlugin.php
@@ -3,72 +3,28 @@
 namespace Drupal\salesforce_oauth\Plugin\SalesforceAuthProvider;
 
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Routing\TrustedRedirectResponse;
 use Drupal\Core\Url;
-use Drupal\salesforce\Consumer\SalesforceCredentials;
-use Drupal\salesforce\SalesforceAuthProviderInterface;
 use Drupal\salesforce\SalesforceAuthProviderPluginBase;
-use Drupal\salesforce\Storage\SalesforceAuthTokenStorageInterface;
-use Drupal\salesforce_oauth\Consumer\SalesforceOAuthCredentials;
-use OAuth\Common\Http\Client\ClientInterface;
-use OAuth\Common\Http\Uri\Uri;
-use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Salesforce OAuth user-agent flow auth provider plugin.
  *
  * @Plugin(
  *   id = "oauth",
- *   label = @Translation("Salesforce OAuth User-Agent")
+ *   label = @Translation("Salesforce OAuth User-Agent"),
+ *   credentials_class = "\Drupal\salesforce_oauth\Consumer\SalesforceOAuthCredentials"
  * )
  */
-class SalesforceOAuthPlugin extends SalesforceAuthProviderPluginBase implements SalesforceAuthProviderInterface {
+class SalesforceOAuthPlugin extends SalesforceAuthProviderPluginBase {
 
   /**
    * Credentials.
    *
-   * @var \Drupal\salesforce\Consumer\SalesforceCredentials
+   * @var \Drupal\salesforce_oauth\Consumer\SalesforceOAuthCredentials
    */
   protected $credentials;
 
-  /**
-   * {@inheritdoc}
-   */
-  const SERVICE_TYPE = 'oauth';
-
-  /**
-   * {@inheritdoc}
-   */
-  const LABEL = 'OAuth';
-
-  /**
-   * SalesforceOAuthPlugin constructor.
-   *
-   * @param string $id
-   *   The plugin id.
-   * @param \Drupal\salesforce\Consumer\SalesforceCredentials $credentials
-   *   The credentials.
-   * @param \OAuth\Common\Http\Client\ClientInterface $httpClient
-   *   The oauth http client.
-   * @param \Drupal\salesforce\Storage\SalesforceAuthTokenStorageInterface $storage
-   *   Auth token storage service.
-   *
-   * @throws \OAuth\OAuth2\Service\Exception\InvalidScopeException
-   *   Comment.
-   */
-  public function __construct($id, SalesforceCredentials $credentials, ClientInterface $httpClient, SalesforceAuthTokenStorageInterface $storage) {
-    parent::__construct($credentials, $httpClient, $storage, [], new Uri($credentials->getLoginUrl()));
-    $this->id = $id;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
-    $configuration = array_merge(self::defaultConfiguration(), $configuration);
-    $cred = new SalesforceOAuthCredentials($configuration['consumer_key'], $configuration['consumer_secret'], $configuration['login_url']);
-    return new static($configuration['id'], $cred, $container->get('salesforce.http_client_wrapper'), $container->get('salesforce.auth_token_storage'));
-  }
-
   /**
    * {@inheritdoc}
    */
@@ -88,7 +44,7 @@ class SalesforceOAuthPlugin extends SalesforceAuthProviderPluginBase implements
       '#type' => 'textfield',
       '#description' => t('Consumer key of the Salesforce remote application you want to grant access to'),
       '#required' => TRUE,
-      '#default_value' => $this->credentials->getConsumerKey(),
+      '#default_value' => $this->getCredentials()->getConsumerKey(),
     ];
 
     $form['consumer_secret'] = [
@@ -96,13 +52,13 @@ class SalesforceOAuthPlugin extends SalesforceAuthProviderPluginBase implements
       '#type' => 'textfield',
       '#description' => $this->t('Consumer secret of the Salesforce remote application.'),
       '#required' => TRUE,
-      '#default_value' => $this->credentials->getConsumerSecret(),
+      '#default_value' => $this->getCredentials()->getConsumerSecret(),
     ];
 
     $form['login_url'] = [
       '#title' => t('Login URL'),
       '#type' => 'textfield',
-      '#default_value' => $this->credentials->getLoginUrl(),
+      '#default_value' => $this->getCredentials()->getLoginUrl(),
       '#description' => t('Enter a login URL, either https://login.salesforce.com or https://test.salesforce.com.'),
       '#required' => TRUE,
     ];
@@ -114,7 +70,6 @@ class SalesforceOAuthPlugin extends SalesforceAuthProviderPluginBase implements
    */
   public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
     parent::submitConfigurationForm($form, $form_state);
-    $settings = $form_state->getValue('provider_settings');
 
     // Write the config id to private temp store, so that we can use the same
     // callback URL for all OAuth applications in Salesforce.
@@ -125,16 +80,16 @@ class SalesforceOAuthPlugin extends SalesforceAuthProviderPluginBase implements
     try {
       $path = $this->getAuthorizationEndpoint();
       $query = [
-        'redirect_uri' => $this->credentials->getCallbackUrl(),
+        'redirect_uri' => $this->getCredentials()->getCallbackUrl(),
         'response_type' => 'code',
-        'client_id' => $settings['consumer_key'],
+        'client_id' => $this->getCredentials()->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, and thence to the entity listing. Upon failure, the user
       // redirect URI will send the user back to the edit form.
-      $form_state->setRedirectUrl(Url::fromUri($path . '?' . http_build_query($query)));
+      $form_state->setResponse(new TrustedRedirectResponse(Url::fromUri($path . '?' . http_build_query($query))->toString()));
     }
     catch (\Exception $e) {
       $form_state->setError($form, $this->t("Error during authorization: %message", ['%message' => $e->getMessage()]));
@@ -145,7 +100,7 @@ class SalesforceOAuthPlugin extends SalesforceAuthProviderPluginBase implements
    * {@inheritdoc}
    */
   public function getConsumerSecret() {
-    return $this->credentials->getConsumerSecret();
+    return $this->getCredentials()->getConsumerSecret();
   }
 
 }
diff --git a/modules/salesforce_pull/src/Plugin/QueueWorker/PullBase.php b/modules/salesforce_pull/src/Plugin/QueueWorker/PullBase.php
index eb28daff..ec0dfe00 100644
--- a/modules/salesforce_pull/src/Plugin/QueueWorker/PullBase.php
+++ b/modules/salesforce_pull/src/Plugin/QueueWorker/PullBase.php
@@ -107,7 +107,7 @@ abstract class PullBase extends QueueWorkerBase implements ContainerFactoryPlugi
    *
    * @throws \Exception
    */
-  public function processItem($item) {
+  public function processItem($item) { // @codingStandardsIgnoreLine
     $sf_object = $item->getSobject();
     $mapping = $this->mappingStorage->load($item->getMappingId());
     if (!$mapping) {
diff --git a/modules/salesforce_push/src/PushQueue.php b/modules/salesforce_push/src/PushQueue.php
index 146791ca..6a070588 100644
--- a/modules/salesforce_push/src/PushQueue.php
+++ b/modules/salesforce_push/src/PushQueue.php
@@ -229,7 +229,7 @@ class PushQueue extends DatabaseQueue implements PushQueueInterface {
    *
    * @TODO convert $data to a proper class and make sure that's what we get for this argument.
    */
-  protected function doCreateItem($data) {
+  protected function doCreateItem($data) { // @codingStandardsIgnoreLine
     if (empty($data['name'])
     || empty($data['entity_id'])
     || empty($data['op'])) {
diff --git a/salesforce.install b/salesforce.install
index 6e232c30..1f8ca954 100644
--- a/salesforce.install
+++ b/salesforce.install
@@ -398,7 +398,7 @@ function salesforce_update_8402() {
     return;
   }
   $credentials = $authConfig->getPlugin()->getCredentials();
-  if (!$credentials instanceof \Drupal\salesforce_oauth\Consumer\SalesforceOAuthCredentials) {
+  if (!$credentials instanceof \Drupal\salesforce_oauth\Consumer\SalesforceOAuthCredentials) { // @codingStandardsIgnoreLine
     // If we're not using OAuth, we are done.
     return;
   }
diff --git a/src/Annotation/SalesforceAuthProvider.php b/src/Annotation/SalesforceAuthProvider.php
new file mode 100644
index 00000000..cb8fe8b3
--- /dev/null
+++ b/src/Annotation/SalesforceAuthProvider.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Drupal\salesforce\Annotation;
+
+use Drupal\Component\Annotation\Plugin;
+
+/**
+ * SalesforceAuthProvider annotation definition.
+ */
+class SalesforceAuthProvider extends Plugin {
+
+  /**
+   * The plugin ID of the auth provider.
+   *
+   * @var string
+   */
+  public $id;
+
+  /**
+   * The human-readable name of the key provider.
+   *
+   * @var \Drupal\Core\Annotation\Translation
+   */
+  public $label;
+
+  /**
+   * The credentials class specific to this provider.
+   *
+   * @var string
+   */
+  public $credentials_class; // @codingStandardsIgnoreLine
+
+}
diff --git a/src/Consumer/SalesforceCredentials.php b/src/Consumer/SalesforceCredentials.php
index 533a9871..5973870e 100644
--- a/src/Consumer/SalesforceCredentials.php
+++ b/src/Consumer/SalesforceCredentials.php
@@ -5,9 +5,9 @@ namespace Drupal\salesforce\Consumer;
 use OAuth\Common\Consumer\Credentials;
 
 /**
- * Class SalesforceCredentials.
+ * Stub class SalesforceCredentials. Used for broken / fallback plugin only.
  */
-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.
@@ -37,4 +37,19 @@ abstract class SalesforceCredentials extends Credentials implements SalesforceCr
     return $this->loginUrl;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isValid() {
+    // This class is a stub.
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(array $configuration) {
+    return new static($configuration['consumer_key'], $configuration['consumer_secret'], NULL);
+  }
+
 }
diff --git a/src/Consumer/SalesforceCredentialsInterface.php b/src/Consumer/SalesforceCredentialsInterface.php
index 000e1d10..914d44b6 100644
--- a/src/Consumer/SalesforceCredentialsInterface.php
+++ b/src/Consumer/SalesforceCredentialsInterface.php
@@ -23,4 +23,22 @@ interface SalesforceCredentialsInterface {
    */
   public function getLoginUrl();
 
+  /**
+   * Sanity check for credentials validity.
+   *
+   * @return bool
+   *   TRUE if credentials are set properly. Otherwise false.
+   */
+  public function isValid();
+
+  /**
+   * Create helper.
+   *
+   * @param array $configuration
+   *   Plugin configuration.
+   *
+   * @return static
+   */
+  public static function create(array $configuration);
+
 }
diff --git a/src/Controller/SalesforceAuthListBuilder.php b/src/Controller/SalesforceAuthListBuilder.php
index 4c189d7e..002d138f 100644
--- a/src/Controller/SalesforceAuthListBuilder.php
+++ b/src/Controller/SalesforceAuthListBuilder.php
@@ -18,8 +18,8 @@ class SalesforceAuthListBuilder extends ConfigEntityListBuilder {
     /** @var \Drupal\salesforce\SalesforceAuthProviderInterface $plugin */
     $plugin = $entity->getPlugin();
     $row['default'] = $entity->authManager()
-      ->getProvider() && $entity->authManager()
-      ->getProvider()
+      ->getConfig() && $entity->authManager()
+      ->getConfig()
       ->id() == $entity->id()
       ? $this->t('Default') : '';
     $row['label'] = $entity->label();
diff --git a/src/Entity/SalesforceAuthConfig.php b/src/Entity/SalesforceAuthConfig.php
index 2d96212c..06e589e5 100644
--- a/src/Entity/SalesforceAuthConfig.php
+++ b/src/Entity/SalesforceAuthConfig.php
@@ -3,7 +3,8 @@
 namespace Drupal\salesforce\Entity;
 
 use Drupal\Core\Config\Entity\ConfigEntityBase;
-use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityWithPluginCollectionInterface;
+use Drupal\Core\Plugin\DefaultSingleLazyPluginCollection;
 
 /**
  * Defines a Salesforce Auth entity.
@@ -33,7 +34,7 @@ use Drupal\Core\Entity\EntityInterface;
  *   admin_permission = "authorize salesforce",
  * )
  */
-class SalesforceAuthConfig extends ConfigEntityBase implements EntityInterface {
+class SalesforceAuthConfig extends ConfigEntityBase implements EntityWithPluginCollectionInterface {
 
   /**
    * Auth id. e.g. "oauth_full_sandbox".
@@ -70,6 +71,13 @@ class SalesforceAuthConfig extends ConfigEntityBase implements EntityInterface {
    */
   protected $manager;
 
+  /**
+   * The plugin provider.
+   *
+   * @var \Drupal\salesforce\SalesforceAuthProviderInterface
+   */
+  protected $plugin;
+
   /**
    * Id getter.
    */
@@ -93,9 +101,20 @@ class SalesforceAuthConfig extends ConfigEntityBase implements EntityInterface {
    * @throws \Drupal\Component\Plugin\Exception\PluginException
    */
   public function getPlugin() {
-    $settings = $this->provider_settings ?: [];
-    $settings += ['id' => $this->id()];
-    return $this->provider ? $this->authManager()->createInstance($this->provider, $settings) : NULL;
+    if (!$this->plugin) {
+      $this->plugin = $this->provider ? $this->authManager()->createInstance($this->provider, $this->getProviderSettings()) : NULL;
+    }
+    return $this->plugin;
+  }
+
+  /**
+   * Wrapper for provider settings to inject instance id, from auth config.
+   *
+   * @return array
+   *   Provider settings.
+   */
+  public function getProviderSettings() {
+    return $this->provider_settings + ['id' => $this->id()];
   }
 
   /**
@@ -141,6 +160,10 @@ class SalesforceAuthConfig extends ConfigEntityBase implements EntityInterface {
    */
   public function getPluginsAsOptions() {
     foreach ($this->authManager()->getDefinitions() as $id => $definition) {
+      if ($id == 'broken') {
+        // Do not add the fallback provider.
+        continue;
+      }
       $options[$id] = ($definition['label']);
     }
     if (!empty($options)) {
@@ -149,4 +172,13 @@ class SalesforceAuthConfig extends ConfigEntityBase implements EntityInterface {
     return [];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getPluginCollections() {
+    return [
+      'auth_provider' => new DefaultSingleLazyPluginCollection($this->authManager(), $this->provider, $this->getProviderSettings()),
+    ];
+  }
+
 }
diff --git a/src/Form/SalesforceAuthForm.php b/src/Form/SalesforceAuthForm.php
index 648669c5..cddca11b 100644
--- a/src/Form/SalesforceAuthForm.php
+++ b/src/Form/SalesforceAuthForm.php
@@ -88,7 +88,7 @@ class SalesforceAuthForm extends EntityForm {
     $form['save_default'] = [
       '#type' => 'checkbox',
       '#title' => 'Save and set default',
-      '#default_value' => $this->entity->isNew() || ($this->entity->authManager()->getProvider() && $this->entity->authManager()->getProvider()->id() == $this->entity->id())
+      '#default_value' => $this->entity->isNew() || ($this->entity->authManager()->getProvider() && $this->entity->authManager()->getProvider()->id() == $this->entity->id()),
     ];
     return parent::form($form, $form_state);
   }
diff --git a/src/Plugin/SalesforceAuthProvider/Broken.php b/src/Plugin/SalesforceAuthProvider/Broken.php
new file mode 100644
index 00000000..9782fa75
--- /dev/null
+++ b/src/Plugin/SalesforceAuthProvider/Broken.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace Drupal\salesforce\Plugin\SalesforceAuthProvider;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\salesforce\Consumer\SalesforceCredentials;
+use Drupal\salesforce\SalesforceAuthProviderPluginBase;
+use OAuth\Common\Token\TokenInterface;
+use OAuth\OAuth2\Service\Exception\MissingRefreshTokenException;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Fallback for broken / missing plugin.
+ *
+ * @Plugin(
+ *   id = "broken",
+ *   label = @Translation("Broken or missing provider"),
+ *   credentials_class = "Drupal\salesforce\Consumer\SalesforceCredentials"
+ * )
+ */
+class Broken extends SalesforceAuthProviderPluginBase {
+
+  /**
+   * Broken constructor.
+   *
+   * @param array $configuration
+   *   Configuration.
+   * @param string $plugin_id
+   *   Plugin id.
+   * @param mixed $plugin_definition
+   *   Plugin definition.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition) {
+    $this->configuration = $configuration;
+    $this->pluginDefinition = $plugin_definition;
+    $this->id = $plugin_id;
+    $this->credentials = new SalesforceCredentials('', '', '');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function refreshAccessToken(TokenInterface $token) {
+    throw new MissingRefreshTokenException();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static($configuration, $plugin_id, $plugin_definition);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    $this->messenger()->addError($this->t('Auth provider for %id is missing or broken.', ['%id' => $this->id]));
+    return $form;
+  }
+
+}
diff --git a/src/SalesforceAuthProviderInterface.php b/src/SalesforceAuthProviderInterface.php
index f149b5d1..776e3cee 100644
--- a/src/SalesforceAuthProviderInterface.php
+++ b/src/SalesforceAuthProviderInterface.php
@@ -35,14 +35,6 @@ interface SalesforceAuthProviderInterface extends ServiceInterface, PluginFormIn
    */
   public function label();
 
-  /**
-   * Auth type id for this service, e.g. oauth, jwt, etc.
-   *
-   * @return string
-   *   Provider type for this auth provider.
-   */
-  public function type();
-
   /**
    * Perform a refresh of the given token.
    *
diff --git a/src/SalesforceAuthProviderPluginBase.php b/src/SalesforceAuthProviderPluginBase.php
index 9a7b0f7e..b5aaac02 100644
--- a/src/SalesforceAuthProviderPluginBase.php
+++ b/src/SalesforceAuthProviderPluginBase.php
@@ -6,9 +6,12 @@ use Drupal\Core\DependencyInjection\DependencySerializationTrait;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Messenger\MessengerTrait;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\salesforce\Storage\SalesforceAuthTokenStorageInterface;
+use OAuth\Common\Http\Client\ClientInterface;
 use OAuth\Common\Http\Exception\TokenResponseException;
 use OAuth\Common\Http\Uri\Uri;
 use OAuth\OAuth2\Service\Salesforce;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Shared methods for auth providers.
@@ -41,12 +44,60 @@ abstract class SalesforceAuthProviderPluginBase extends Salesforce implements Sa
   protected $storage;
 
   /**
-   * Machine name identifier.
+   * Provider id, e.g. jwt, oauth.
+   *
+   * @var string
+   */
+  protected $pluginId;
+
+  /**
+   * Plugin definition.
+   *
+   * @var array
+   */
+  protected $pluginDefinition;
+
+  /**
+   * Instance id, e.g. "sandbox1" or "production".
    *
    * @var string
    */
   protected $id;
 
+  /**
+   * SalesforceOAuthPlugin constructor.
+   *
+   * @param array $configuration
+   *   Plugin configuration.
+   * @param string $plugin_id
+   *   Plugin id.
+   * @param mixed $plugin_definition
+   *   Plugin definition.
+   * @param \OAuth\Common\Http\Client\ClientInterface $httpClient
+   *   The oauth http client.
+   * @param \Drupal\salesforce\Storage\SalesforceAuthTokenStorageInterface $storage
+   *   Auth token storage service.
+   *
+   * @throws \OAuth\OAuth2\Service\Exception\InvalidScopeException
+   *   Comment.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, ClientInterface $httpClient, SalesforceAuthTokenStorageInterface $storage) {
+    $this->id = $configuration['id'];
+    $this->configuration = $configuration;
+    $this->pluginDefinition = $plugin_definition;
+    $this->pluginId = $plugin_id;
+    $this->credentials = $this->getCredentials();
+    parent::__construct($this->getCredentials(), $httpClient, $storage, [], new Uri($this->getCredentials()->getLoginUrl()));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    $configuration = array_merge(self::defaultConfiguration(), $configuration);
+    return new static($configuration, $plugin_id, $plugin_definition, $container->get('salesforce.http_client_wrapper'), $container->get('salesforce.auth_token_storage'));
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -57,18 +108,32 @@ abstract class SalesforceAuthProviderPluginBase extends Salesforce implements Sa
     ];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function label() {
+    return $this->getPluginDefinition()['label'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function id() {
+    return $this->id;
+  }
+
   /**
    * {@inheritdoc}
    */
   public function getPluginId() {
-    return $this->getConfiguration('id');
+    return $this->pluginId;
   }
 
   /**
    * {@inheritdoc}
    */
   public function getPluginDefinition() {
-    return $this->getConfiguration();
+    return $this->pluginDefinition;
   }
 
   /**
@@ -81,13 +146,6 @@ abstract class SalesforceAuthProviderPluginBase extends Salesforce implements Sa
     return $this->configuration;
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function getCredentials() {
-    return $this->credentials;
-  }
-
   /**
    * {@inheritdoc}
    */
@@ -106,14 +164,17 @@ abstract class SalesforceAuthProviderPluginBase extends Salesforce implements Sa
    * {@inheritdoc}
    */
   public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
-    $this->setConfiguration($form_state->getValues());
+    $this->setConfiguration($form_state->getValue('provider_settings'));
   }
 
   /**
    * {@inheritdoc}
    */
   public function save(array $form, FormStateInterface $form_state) {
-    // Initialize identity.
+    // Initialize identity if token is available.
+    if (!$this->hasAccessToken()) {
+      return TRUE;
+    }
     $token = $this->getAccessToken();
     $headers = [
       'Authorization' => 'OAuth ' . $token->getAccessToken(),
@@ -129,43 +190,33 @@ abstract class SalesforceAuthProviderPluginBase extends Salesforce implements Sa
   /**
    * {@inheritdoc}
    */
-  public function id() {
-    return $this->id;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function type() {
-    return static::SERVICE_TYPE;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function label() {
-    return static::LABEL;
+  public function getCredentials() {
+    if (!$this->credentials || !$this->credentials->isValid()) {
+      $pluginDefinition = $this->getPluginDefinition();
+      $this->credentials = $pluginDefinition['credentials_class']::create($this->configuration);
+    }
+    return $this->credentials;
   }
 
   /**
    * {@inheritdoc}
    */
   public function getAuthorizationEndpoint() {
-    return new Uri($this->credentials->getLoginUrl() . static::AUTH_ENDPOINT_PATH);
+    return new Uri($this->getCredentials()->getLoginUrl() . static::AUTH_ENDPOINT_PATH);
   }
 
   /**
    * {@inheritdoc}
    */
   public function getAccessTokenEndpoint() {
-    return new Uri($this->credentials->getLoginUrl() . static::AUTH_TOKEN_PATH);
+    return new Uri($this->getCredentials()->getLoginUrl() . static::AUTH_TOKEN_PATH);
   }
 
   /**
    * {@inheritdoc}
    */
   public function hasAccessToken() {
-    return $this->storage->hasAccessToken($this->id());
+    return $this->storage ? $this->storage->hasAccessToken($this->id()) : FALSE;
   }
 
   /**
diff --git a/src/SalesforceAuthProviderPluginManager.php b/src/SalesforceAuthProviderPluginManager.php
index a139e474..1d409c2e 100644
--- a/src/SalesforceAuthProviderPluginManager.php
+++ b/src/SalesforceAuthProviderPluginManager.php
@@ -162,4 +162,11 @@ class SalesforceAuthProviderPluginManager extends DefaultPluginManager implement
     return $this->config;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getFallbackPluginId($plugin_id, array $configuration = []) {
+    return 'broken';
+  }
+
 }
diff --git a/src/SalesforceAuthProviderPluginManagerInterface.php b/src/SalesforceAuthProviderPluginManagerInterface.php
index 25b11425..695975a4 100644
--- a/src/SalesforceAuthProviderPluginManagerInterface.php
+++ b/src/SalesforceAuthProviderPluginManagerInterface.php
@@ -3,13 +3,14 @@
 namespace Drupal\salesforce;
 
 use Drupal\Component\Plugin\Discovery\CachedDiscoveryInterface;
+use Drupal\Component\Plugin\FallbackPluginManagerInterface;
 use Drupal\Component\Plugin\PluginManagerInterface;
 use Drupal\Core\Cache\CacheableDependencyInterface;
 
 /**
  * Auth provider plugin manager interface.
  */
-interface SalesforceAuthProviderPluginManagerInterface extends PluginManagerInterface, CachedDiscoveryInterface, CacheableDependencyInterface {
+interface SalesforceAuthProviderPluginManagerInterface extends PluginManagerInterface, CachedDiscoveryInterface, CacheableDependencyInterface, FallbackPluginManagerInterface {
 
   /**
    * All Salesforce auth providers.
-- 
GitLab