diff --git a/services_tfa/src/Plugin/ServiceDefinition/GenericValidation.php b/services_tfa/src/Plugin/ServiceDefinition/GenericValidation.php
index 18678f8..c6d5b87 100644
--- a/services_tfa/src/Plugin/ServiceDefinition/GenericValidation.php
+++ b/services_tfa/src/Plugin/ServiceDefinition/GenericValidation.php
@@ -6,7 +6,7 @@ use Drupal\Core\DependencyInjection\DependencySerializationTrait;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\services\ServiceDefinitionBase;
-use Drupal\tfa\TfaValidationPluginManager;
+use Drupal\tfa\TfaPluginManager;
 use Drupal\user\UserDataInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\Request;
@@ -30,12 +30,13 @@ use Symfony\Component\Serializer\SerializerInterface;
  */
 class GenericValidation extends ServiceDefinitionBase implements ContainerFactoryPluginInterface {
   use DependencySerializationTrait;
+
   /**
    * Validation plugin manager.
    *
-   * @var \Drupal\tfa\TfaValidationPluginManager
+   * @var \Drupal\tfa\TfaPluginManager
    */
-  protected $tfaValidationManager;
+  protected $tfaPluginManager;
 
   /**
    * The validation plugin object.
@@ -45,9 +46,9 @@ class GenericValidation extends ServiceDefinitionBase implements ContainerFactor
   protected $validationPlugin;
 
   /**
-   * The validation plugin object.
+   * User data service.
    *
-   * @var \Drupal\tfa\Plugin\TfaValidationInterface
+   * @var \Drupal\user\UserDataInterface
    */
   protected $userData;
 
@@ -62,13 +63,13 @@ class GenericValidation extends ServiceDefinitionBase implements ContainerFactor
    *   The plugin implementation definition.
    * @param \Drupal\user\UserDataInterface $user_data
    *   User data service.
-   * @param \Drupal\tfa\TfaValidationPluginManager $tfa_validation_manager
-   *   Validation plugin manager.
+   * @param \Drupal\tfa\TfaPluginManager $tfa_plugin_manager
+   *   Tfa plugin manager.
    */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, UserDataInterface $user_data, TfaValidationPluginManager $tfa_validation_manager) {
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, UserDataInterface $user_data, TfaPluginManager $tfa_plugin_manager) {
     parent::__construct($configuration, $plugin_id, $plugin_definition);
     $this->userData = $user_data;
-    $this->tfaValidationManager = $tfa_validation_manager;
+    $this->tfaPluginManager = $tfa_plugin_manager;
   }
 
   /**
@@ -80,7 +81,7 @@ class GenericValidation extends ServiceDefinitionBase implements ContainerFactor
       $plugin_id,
       $plugin_definition,
       $container->get('user.data'),
-      $container->get('plugin.manager.tfa.validation')
+      $container->get('plugin.manager.tfa')
     );
   }
 
@@ -103,7 +104,7 @@ class GenericValidation extends ServiceDefinitionBase implements ContainerFactor
     $plugin_id = $request->get('plugin_id');
 
     if ($uid && $code && $plugin_id) {
-      $this->validationPlugin = $this->tfaValidationManager->createInstance($plugin_id, ['uid' => $uid]);
+      $this->validationPlugin = $this->tfaPluginManager->createInstance($plugin_id, ['uid' => $uid]);
       // @todo validateRequest is not part of TfaValidationInterface.
       $valid = $this->validationPlugin->validateRequest($code);
       if ($this->validationPlugin->isAlreadyAccepted()) {
diff --git a/src/Annotation/TfaSetup.php b/src/Annotation/Tfa.php
similarity index 80%
rename from src/Annotation/TfaSetup.php
rename to src/Annotation/Tfa.php
index 318392f..2bcdbce 100644
--- a/src/Annotation/TfaSetup.php
+++ b/src/Annotation/Tfa.php
@@ -5,11 +5,11 @@ namespace Drupal\tfa\Annotation;
 use Drupal\Component\Annotation\Plugin;
 
 /**
- * Defines a TFA Setup annotation object.
+ * Defines a TFA annotation object.
  *
  * @Annotation
  */
-class TfaSetup extends Plugin {
+class Tfa extends Plugin {
 
   /**
    * The plugin ID.
@@ -19,7 +19,7 @@ class TfaSetup extends Plugin {
   public $id;
 
   /**
-   * The human-readable name of the Tfa setup.
+   * The human-readable name of the plugin.
    *
    * @var \Drupal\Core\Annotation\Translation
    *
@@ -28,7 +28,7 @@ class TfaSetup extends Plugin {
   public $title;
 
   /**
-   * The description shown to users.
+   * The description of the plugin.
    *
    * @var \Drupal\Core\Annotation\Translation
    *
diff --git a/src/Annotation/TfaLogin.php b/src/Annotation/TfaLogin.php
deleted file mode 100644
index f644429..0000000
--- a/src/Annotation/TfaLogin.php
+++ /dev/null
@@ -1,46 +0,0 @@
-<?php
-
-namespace Drupal\tfa\Annotation;
-
-use Drupal\Component\Annotation\Plugin;
-
-/**
- * Defines a TFA Login annotation object.
- *
- * @Annotation
- */
-class TfaLogin extends Plugin {
-
-  /**
-   * The plugin ID.
-   *
-   * @var string
-   */
-  public $id;
-
-  /**
-   * The human-readable name of the Tfa login.
-   *
-   * @var \Drupal\Core\Annotation\Translation
-   *
-   * @ingroup plugin_translatable
-   */
-  public $title;
-
-  /**
-   * The description shown to users.
-   *
-   * @var \Drupal\Core\Annotation\Translation
-   *
-   * @ingroup plugin_translatable
-   */
-  public $description;
-
-  /**
-   * Plugin ID for user-specific setup plugin for this login plugin.
-   *
-   * @var string
-   */
-  public $setupPluginId;
-
-}
diff --git a/src/Annotation/TfaSend.php b/src/Annotation/TfaSend.php
deleted file mode 100644
index 271fffb..0000000
--- a/src/Annotation/TfaSend.php
+++ /dev/null
@@ -1,46 +0,0 @@
-<?php
-
-namespace Drupal\tfa\Annotation;
-
-use Drupal\Component\Annotation\Plugin;
-
-/**
- * Defines a TFA Send annotation object.
- *
- * @Annotation
- */
-class TfaSend extends Plugin {
-
-  /**
-   * The plugin ID.
-   *
-   * @var string
-   */
-  public $id;
-
-  /**
-   * The human-readable name of the Tfa send.
-   *
-   * @var \Drupal\Core\Annotation\Translation
-   *
-   * @ingroup plugin_translatable
-   */
-  public $title;
-
-  /**
-   * The description shown to users.
-   *
-   * @var \Drupal\Core\Annotation\Translation
-   *
-   * @ingroup plugin_translatable
-   */
-  public $description;
-
-  /**
-   * Plugin ID for user-specific setup plugin for this send plugin.
-   *
-   * @var string
-   */
-  public $setupPluginId;
-
-}
diff --git a/src/Annotation/TfaValidation.php b/src/Annotation/TfaValidation.php
deleted file mode 100644
index 1f6c34d..0000000
--- a/src/Annotation/TfaValidation.php
+++ /dev/null
@@ -1,46 +0,0 @@
-<?php
-
-namespace Drupal\tfa\Annotation;
-
-use Drupal\Component\Annotation\Plugin;
-
-/**
- * Defines a TFA Validation annotation object.
- *
- * @Annotation
- */
-class TfaValidation extends Plugin {
-
-  /**
-   * The plugin ID.
-   *
-   * @var string
-   */
-  public $id;
-
-  /**
-   * The human-readable name of the Tfa validation.
-   *
-   * @var \Drupal\Core\Annotation\Translation
-   *
-   * @ingroup plugin_translatable
-   */
-  public $title;
-
-  /**
-   * The description shown to users.
-   *
-   * @var \Drupal\Core\Annotation\Translation
-   *
-   * @ingroup plugin_translatable
-   */
-  public $description;
-
-  /**
-   * Plugin ID for user-specific setup plugin for this validation plugin.
-   *
-   * @var string
-   */
-  public $setupPluginId;
-
-}
diff --git a/src/Form/EntryForm.php b/src/Form/EntryForm.php
index f0c6011..a3ce46a 100644
--- a/src/Form/EntryForm.php
+++ b/src/Form/EntryForm.php
@@ -7,8 +7,7 @@ use Drupal\Core\Flood\FloodInterface;
 use Drupal\Core\Form\FormBase;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Url;
-use Drupal\tfa\TfaLoginPluginManager;
-use Drupal\tfa\TfaValidationPluginManager;
+use Drupal\tfa\TfaPluginManager;
 use Drupal\user\UserDataInterface;
 use Drupal\user\Entity\User;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -19,18 +18,11 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
 class EntryForm extends FormBase {
 
   /**
-   * Validation plugin manager.
+   * The TFA plugin manager.
    *
-   * @var \Drupal\tfa\TfaValidationPluginManager
+   * @var \Drupal\tfa\TfaPluginManager
    */
-  protected $tfaValidationManager;
-
-  /**
-   * Login plugin manager.
-   *
-   * @var \Drupal\tfa\TfaLoginPluginManager
-   */
-  protected $tfaLoginManager;
+  protected $tfaPluginManager;
 
   /**
    * The validation plugin object.
@@ -84,10 +76,8 @@ class EntryForm extends FormBase {
   /**
    * EntryForm constructor.
    *
-   * @param \Drupal\tfa\TfaValidationPluginManager $tfa_validation_manager
-   *   Plugin manager for validation plugins.
-   * @param \Drupal\tfa\TfaLoginPluginManager $tfa_login_manager
-   *   Plugin manager for login plugins.
+   * @param \Drupal\tfa\TfaPluginManager $tfa_plugin_manager
+   *   The TFA plugin manager.
    * @param \Drupal\Core\Flood\FloodInterface $flood
    *   The flood control mechanism.
    * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
@@ -95,9 +85,8 @@ class EntryForm extends FormBase {
    * @param \Drupal\user\UserDataInterface $user_data
    *   User data service.
    */
-  public function __construct(TfaValidationPluginManager $tfa_validation_manager, TfaLoginPluginManager $tfa_login_manager, FloodInterface $flood, DateFormatterInterface $date_formatter, UserDataInterface $user_data) {
-    $this->tfaValidationManager = $tfa_validation_manager;
-    $this->tfaLoginManager = $tfa_login_manager;
+  public function __construct(TfaPluginManager $tfa_plugin_manager, FloodInterface $flood, DateFormatterInterface $date_formatter, UserDataInterface $user_data) {
+    $this->tfaPluginManager = $tfa_plugin_manager;
     $this->tfaSettings = $this->config('tfa.settings');
     $this->flood = $flood;
     $this->dateFormatter = $date_formatter;
@@ -114,8 +103,7 @@ class EntryForm extends FormBase {
    */
   public static function create(ContainerInterface $container) {
     return new static(
-      $container->get('plugin.manager.tfa.validation'),
-      $container->get('plugin.manager.tfa.login'),
+      $container->get('plugin.manager.tfa'),
       $container->get('flood'),
       $container->get('date.formatter'),
       $container->get('user.data')
@@ -134,7 +122,7 @@ class EntryForm extends FormBase {
    */
   public function buildForm(array $form, FormStateInterface $form_state, $uid = NULL, string $hash = '') {
     $alternate_plugin = $this->getRequest()->get('plugin');
-    $validation_plugin_definitions = $this->tfaValidationManager->getDefinitions();
+    $validation_plugin_definitions = $this->tfaPluginManager->getValidationDefinitions();
     $user_settings = $this->userData->get('tfa', $uid, 'tfa_user_settings');
     $user_enabled_validation_plugins = $user_settings['data']['plugins'] ?? [];
 
@@ -147,10 +135,12 @@ class EntryForm extends FormBase {
     }
 
     // Get current validation plugin form.
-    $this->tfaValidationPlugin = $this->tfaValidationManager->createInstance($validation_plugin, ['uid' => $uid]);
+    $this->tfaValidationPlugin = $this->tfaPluginManager->createInstance($validation_plugin, ['uid' => $uid]);
     $form = $this->tfaValidationPlugin->getForm($form, $form_state);
 
-    $this->tfaLoginPlugins = $this->tfaLoginManager->getPlugins(['uid' => $uid]);
+    foreach ($this->tfaPluginManager->getLoginDefinitions() as $plugin_id => $definition) {
+      $this->tfaLoginPlugins[] = $this->tfaPluginManager->createInstance($plugin_id, ['uid' => $uid]);
+    }
     if ($this->tfaLoginPlugins) {
       foreach ($this->tfaLoginPlugins as $login_plugin) {
         if (method_exists($login_plugin, 'getForm')) {
@@ -284,7 +274,7 @@ class EntryForm extends FormBase {
   /**
    * Run TFA process finalization.
    */
-  public function finalize() {
+  protected function finalize() {
     // Invoke plugin finalize.
     if (method_exists($this->tfaValidationPlugin, 'finalize')) {
       $this->tfaValidationPlugin->finalize();
diff --git a/src/Form/SettingsForm.php b/src/Form/SettingsForm.php
index a976100..07ebda8 100644
--- a/src/Form/SettingsForm.php
+++ b/src/Form/SettingsForm.php
@@ -8,10 +8,7 @@ use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Link;
 use Drupal\Core\Url;
 use Drupal\encrypt\EncryptionProfileManagerInterface;
-use Drupal\tfa\TfaLoginPluginManager;
-use Drupal\tfa\TfaSendPluginManager;
-use Drupal\tfa\TfaSetupPluginManager;
-use Drupal\tfa\TfaValidationPluginManager;
+use Drupal\tfa\TfaPluginManager;
 use Drupal\user\UserDataInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
@@ -21,32 +18,11 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
 class SettingsForm extends ConfigFormBase {
 
   /**
-   * The login plugin manager to fetch plugin information.
+   * The TFA plugin manager to fetch plugin information.
    *
-   * @var \Drupal\tfa\TfaLoginPluginManager
+   * @var \Drupal\tfa\TfaPluginManager
    */
-  protected $tfaLogin;
-
-  /**
-   * The send plugin manager to fetch plugin information.
-   *
-   * @var \Drupal\tfa\TfaSendPluginManager
-   */
-  protected $tfaSend;
-
-  /**
-   * The validation plugin manager to fetch plugin information.
-   *
-   * @var \Drupal\tfa\TfaValidationPluginManager
-   */
-  protected $tfaValidation;
-
-  /**
-   * The setup plugin manager to fetch plugin information.
-   *
-   * @var \Drupal\tfa\TfaSetupPluginManager
-   */
-  protected $tfaSetup;
+  protected $pluginManager;
 
   /**
    * Provides the user data service object.
@@ -67,25 +43,16 @@ class SettingsForm extends ConfigFormBase {
    *
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   The config factory object.
-   * @param \Drupal\tfa\TfaLoginPluginManager $tfa_login
-   *   The login plugin manager.
-   * @param \Drupal\tfa\TfaSendPluginManager $tfa_send
-   *   The send plugin manager.
-   * @param \Drupal\tfa\TfaValidationPluginManager $tfa_validation
-   *   The validation plugin manager.
-   * @param \Drupal\tfa\TfaSetupPluginManager $tfa_setup
-   *   The setup plugin manager.
+   * @param \Drupal\tfa\TfaPluginManager $plugin_manager
+   *   The TFA plugin manager.
    * @param \Drupal\user\UserDataInterface $user_data
    *   The user data service.
    * @param \Drupal\encrypt\EncryptionProfileManagerInterface $encryption_profile_manager
    *   Encrypt profile manager.
    */
-  public function __construct(ConfigFactoryInterface $config_factory, TfaLoginPluginManager $tfa_login, TfaSendPluginManager $tfa_send, TfaValidationPluginManager $tfa_validation, TfaSetupPluginManager $tfa_setup, UserDataInterface $user_data, EncryptionProfileManagerInterface $encryption_profile_manager) {
+  public function __construct(ConfigFactoryInterface $config_factory, TfaPluginManager $plugin_manager, UserDataInterface $user_data, EncryptionProfileManagerInterface $encryption_profile_manager) {
     parent::__construct($config_factory);
-    $this->tfaLogin = $tfa_login;
-    $this->tfaSend = $tfa_send;
-    $this->tfaSetup = $tfa_setup;
-    $this->tfaValidation = $tfa_validation;
+    $this->pluginManager = $plugin_manager;
     $this->encryptionProfileManager = $encryption_profile_manager;
     // User Data service to store user-based data in key value pairs.
     $this->userData = $user_data;
@@ -97,10 +64,7 @@ class SettingsForm extends ConfigFormBase {
   public static function create(ContainerInterface $container) {
     return new static(
       $container->get('config.factory'),
-      $container->get('plugin.manager.tfa.login'),
-      $container->get('plugin.manager.tfa.send'),
-      $container->get('plugin.manager.tfa.validation'),
-      $container->get('plugin.manager.tfa.setup'),
+      $container->get('plugin.manager.tfa'),
       $container->get('user.data'),
       $container->get('encrypt.encryption_profile.manager')
     );
@@ -113,6 +77,13 @@ class SettingsForm extends ConfigFormBase {
     return 'tfa_settings_form';
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEditableConfigNames() {
+    return ['tfa.settings'];
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -121,25 +92,29 @@ class SettingsForm extends ConfigFormBase {
     $form = [];
 
     // Get Login Plugins.
-    $login_plugins = $this->tfaLogin->getDefinitions();
+    $login_plugins = $this->pluginManager->getLoginDefinitions();
 
     // Get Send Plugins.
-    $send_plugins = $this->tfaSend->getDefinitions();
+    $send_plugins = $this->pluginManager->getSendDefinitions();
 
     // Get Validation Plugins.
-    $validation_plugins = $this->tfaValidation->getDefinitions();
+    $validation_plugins = $this->pluginManager->getValidationDefinitions();
     // Get validation plugin labels.
     $validation_plugins_labels = [];
     foreach ($validation_plugins as $key => $plugin) {
       $validation_plugins_labels[$plugin['id']] = $plugin['label']->render();
     }
+
     // Fetching all available encryption profiles.
     $encryption_profiles = $this->encryptionProfileManager->getAllEncryptionProfiles();
 
-    $plugins_empty = $this->dataEmptyCheck($validation_plugins, $this->t('No plugins available for validation. See the TFA help documentation for setup.'));
-    $encryption_profiles_empty = $this->dataEmptyCheck($encryption_profiles, $this->t('No Encryption profiles available. <a href=":add_profile_url">Add an encryption profile</a>.', [':add_profile_url' => Url::fromRoute('entity.encryption_profile.add_form')->toString()]));
-
-    if ($plugins_empty || $encryption_profiles_empty) {
+    if (empty($validation_plugins)) {
+      $this->messenger()->addError($this->t('No plugins available for validation. See the TFA help documentation for setup.'));
+      $form_state->cleanValues();
+      return $form;
+    }
+    if (empty($encryption_profiles)) {
+      $this->messenger()->addError($this->t('No Encryption profiles available. <a href=":add_profile_url">Add an encryption profile</a>.', [':add_profile_url' => Url::fromRoute('entity.encryption_profile.add_form')->toString()]));
       $form_state->cleanValues();
       // Return form instead of parent::BuildForm to avoid the save button.
       return $form;
@@ -155,9 +130,7 @@ class SettingsForm extends ConfigFormBase {
     ];
 
     $enabled_state = [
-      'visible' => [
-        ':input[name="tfa_enabled"]' => ['checked' => TRUE],
-      ],
+      'visible' => [':input[name="tfa_enabled"]' => ['checked' => TRUE]],
     ];
 
     $form['tfa_required_roles'] = [
@@ -180,7 +153,7 @@ class SettingsForm extends ConfigFormBase {
       '#states' => $enabled_state,
       '#required' => TRUE,
     ];
-    $form['tfa_validate'] = [
+    $form['tfa_default_validation_plugin'] = [
       '#type' => 'select',
       '#title' => $this->t('Default Validation plugin'),
       '#options' => $validation_plugins_labels,
@@ -200,8 +173,9 @@ class SettingsForm extends ConfigFormBase {
       '#tree' => TRUE,
       '#states' => $enabled_state,
     ];
+
     foreach ($validation_plugins_labels as $key => $val) {
-      $instance = $this->tfaValidation->createInstance($key, [
+      $instance = $this->pluginManager->createInstance($key, [
         'uid' => $this->currentUser()->id(),
       ]);
 
@@ -213,7 +187,7 @@ class SettingsForm extends ConfigFormBase {
               ':input[name="tfa_allowed_validation_plugins[' . $key . ']"]' => ['checked' => TRUE],
             ],
             [
-              'select[name="tfa_validate"]' => ['value' => $key],
+              'select[name="tfa_default_validation_plugin"]' => ['value' => $key],
             ],
           ],
         ];
@@ -226,7 +200,7 @@ class SettingsForm extends ConfigFormBase {
           '#tag' => 'h3',
           '#value' => $val,
         ];
-        $form['validation_plugin_settings'][$key . '_container']['form'] = $instance->buildConfigurationForm($config, $validation_enabled_state);
+        $form['validation_plugin_settings'][$key . '_container']['form'] = $instance->buildConfigurationForm();
         $form['validation_plugin_settings'][$key . '_container']['form']['#parents'] = [
           'validation_plugin_settings',
           $key,
@@ -275,6 +249,7 @@ class SettingsForm extends ConfigFormBase {
       '#options' => $login_form_array,
       '#default_value' => ($config->get('login_plugins')) ? $config->get('login_plugins') : [],
       '#description' => $this->t('Plugins that can allow a user to skip the TFA process. If any plugin returns true the user will not be required to follow TFA. <strong>Use with caution.</strong>'),
+      '#states' => $enabled_state,
     ];
 
     // Enable send plugins.
@@ -402,10 +377,10 @@ class SettingsForm extends ConfigFormBase {
    * {@inheritdoc}
    */
   public function submitForm(array &$form, FormStateInterface $form_state) {
-    $validation_plugin = $form_state->getValue('tfa_validate');
+    $default_validation_plugin = $form_state->getValue('tfa_default_validation_plugin');
     $allowed_validation_plugins = $form_state->getValue('tfa_allowed_validation_plugins');
     // Default validation plugin must always be allowed.
-    $allowed_validation_plugins[$validation_plugin] = $validation_plugin;
+    $allowed_validation_plugins[$default_validation_plugin] = $default_validation_plugin;
     $validation_plugin_settings = $form_state->getValue('validation_plugin_settings');
     if (empty($validation_plugin_settings)) {
       $validation_plugin_settings = [];
@@ -424,7 +399,7 @@ class SettingsForm extends ConfigFormBase {
       ->set('send_plugins', array_filter($send_plugins))
       ->set('login_plugins', array_filter($login_plugins))
       ->set('allowed_validation_plugins', array_filter($allowed_validation_plugins))
-      ->set('default_validation_plugin', $validation_plugin)
+      ->set('default_validation_plugin', $default_validation_plugin)
       ->set('validation_plugin_settings', $validation_plugin_settings)
       ->set('validation_skip', $form_state->getValue('validation_skip'))
       ->set('encryption', $form_state->getValue('encryption_profile'))
@@ -441,31 +416,4 @@ class SettingsForm extends ConfigFormBase {
     parent::submitForm($form, $form_state);
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  protected function getEditableConfigNames() {
-    return ['tfa.settings'];
-  }
-
-  /**
-   * Check whether the given data is empty and set appropriate message.
-   *
-   * @param array $data
-   *   Data to be checked.
-   * @param string $message
-   *   Error message to show if data is empty.
-   *
-   * @return bool
-   *   TRUE if data is empty otherwise FALSE.
-   */
-  protected function dataEmptyCheck(array $data, $message) {
-    if (empty($data)) {
-      $this->messenger()->addError($message);
-      return TRUE;
-    }
-
-    return FALSE;
-  }
-
 }
diff --git a/src/Form/TfaDisableForm.php b/src/Form/TfaDisableForm.php
index a74f7a4..baa216b 100644
--- a/src/Form/TfaDisableForm.php
+++ b/src/Form/TfaDisableForm.php
@@ -6,8 +6,7 @@ use Drupal\Core\Form\FormBase;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Mail\MailManagerInterface;
 use Drupal\Core\Password\PasswordInterface;
-use Drupal\tfa\TfaDataTrait;
-use Drupal\tfa\TfaValidationPluginManager;
+use Drupal\tfa\TfaUserDataTrait;
 use Drupal\user\Entity\User;
 use Drupal\user\UserDataInterface;
 use Drupal\user\UserStorageInterface;
@@ -17,21 +16,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
  * TFA disable form router.
  */
 class TfaDisableForm extends FormBase {
-  use TfaDataTrait;
-
-  /**
-   * The validation plugin manager.
-   *
-   * @var \Drupal\tfa\TfaValidationPluginManager
-   */
-  protected $manager;
-
-  /**
-   * Provides the user data service object.
-   *
-   * @var \Drupal\user\UserDataInterface
-   */
-  protected $userData;
+  use TfaUserDataTrait;
 
   /**
    * The password hashing service.
@@ -57,8 +42,6 @@ class TfaDisableForm extends FormBase {
   /**
    * TFA disable form constructor.
    *
-   * @param \Drupal\tfa\TfaValidationPluginManager $manager
-   *   The validation plugin manager.
    * @param \Drupal\user\UserDataInterface $user_data
    *   The user data object to store user information.
    * @param \Drupal\Core\Password\PasswordInterface $password_checker
@@ -68,8 +51,7 @@ class TfaDisableForm extends FormBase {
    * @param \Drupal\user\UserStorageInterface $user_storage
    *   The user storage.
    */
-  public function __construct(TfaValidationPluginManager $manager, UserDataInterface $user_data, PasswordInterface $password_checker, MailManagerInterface $mail_manager, UserStorageInterface $user_storage) {
-    $this->manager = $manager;
+  public function __construct(UserDataInterface $user_data, PasswordInterface $password_checker, MailManagerInterface $mail_manager, UserStorageInterface $user_storage) {
     $this->userData = $user_data;
     $this->passwordChecker = $password_checker;
     $this->mailManager = $mail_manager;
@@ -81,7 +63,6 @@ class TfaDisableForm extends FormBase {
    */
   public static function create(ContainerInterface $container) {
     return new static(
-      $container->get('plugin.manager.tfa.validation'),
       $container->get('user.data'),
       $container->get('password'),
       $container->get('plugin.manager.mail'),
@@ -190,7 +171,7 @@ class TfaDisableForm extends FormBase {
     }
 
     // Delete all user data.
-    $this->deleteUserData('tfa', NULL, $account->id(), $this->userData);
+    $this->deleteUserData('tfa', NULL, $account->id());
 
     $this->logger('tfa')->notice('TFA disabled for user @name UID @uid', [
       '@name' => $account->getAccountName(),
diff --git a/src/Form/TfaLoginForm.php b/src/Form/TfaLoginForm.php
index 2226beb..f3059c4 100644
--- a/src/Form/TfaLoginForm.php
+++ b/src/Form/TfaLoginForm.php
@@ -19,18 +19,11 @@ class TfaLoginForm extends UserLoginForm {
   use TfaLoginTrait;
 
   /**
-   * The validation plugin manager to fetch plugin information.
+   * The TFA plugin manager.
    *
-   * @var \Drupal\tfa\TfaValidationPluginManager
+   * @var \Drupal\tfa\TfaPluginManager
    */
-  protected $tfaValidationManager;
-
-  /**
-   * The login plugin manager to fetch plugin information.
-   *
-   * @var \Drupal\tfa\TfaLoginPluginManager
-   */
-  protected $tfaLoginManager;
+  protected $tfaPluginManager;
 
   /**
    * The current validation plugin.
@@ -68,8 +61,7 @@ class TfaLoginForm extends UserLoginForm {
   public static function create(ContainerInterface $container) {
     $instance = parent::create($container);
 
-    $instance->tfaValidationManager = $container->get('plugin.manager.tfa.validation');
-    $instance->tfaLoginManager = $container->get('plugin.manager.tfa.login');
+    $instance->tfaPluginManager = $container->get('plugin.manager.tfa');
     $instance->userData = $container->get('user.data');
     $instance->destination = $container->get('redirect.destination');
 
@@ -104,12 +96,10 @@ class TfaLoginForm extends UserLoginForm {
     /** @var \Drupal\user\Entity\User $user */
     $user = $this->userStorage->load($uid);
     $this->tfaContext = new TfaContext(
-      $this->tfaValidationManager,
-      $this->tfaLoginManager,
+      $this->tfaPluginManager,
       $this->configFactory(),
       $user,
-      $this->userData,
-      $this->getRequest()
+      $this->userData
     );
 
     /* Uncomment when things go wrong and you get logged out.
@@ -144,7 +134,7 @@ class TfaLoginForm extends UserLoginForm {
    * @param \Drupal\Core\Form\FormStateInterface $form_state
    *   The state of the login form.
    */
-  public function loginWithTfa(FormStateInterface $form_state) {
+  protected function loginWithTfa(FormStateInterface $form_state) {
     $user = $this->tfaContext->getUser();
     if ($this->tfaContext->pluginAllowsLogin()) {
       $this->tfaContext->doUserLogin();
@@ -174,7 +164,7 @@ class TfaLoginForm extends UserLoginForm {
    * @param \Drupal\Core\Form\FormStateInterface $form_state
    *   The state of the login form.
    */
-  public function loginWithoutTfa(FormStateInterface $form_state) {
+  protected function loginWithoutTfa(FormStateInterface $form_state) {
     // User may be able to skip TFA, depending on module settings and number of
     // prior attempts.
     $remaining = $this->tfaContext->remainingSkips();
diff --git a/src/Form/TfaOverviewForm.php b/src/Form/TfaOverviewForm.php
index 6fb3471..7a2904f 100644
--- a/src/Form/TfaOverviewForm.php
+++ b/src/Form/TfaOverviewForm.php
@@ -6,11 +6,8 @@ use Drupal\Core\Datetime\DateFormatterInterface;
 use Drupal\Core\Form\FormBase;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Url;
-use Drupal\tfa\TfaDataTrait;
-use Drupal\tfa\TfaLoginPluginManager;
-use Drupal\tfa\TfaSendPluginManager;
-use Drupal\tfa\TfaSetupPluginManager;
-use Drupal\tfa\TfaValidationPluginManager;
+use Drupal\tfa\TfaPluginManager;
+use Drupal\tfa\TfaUserDataTrait;
 use Drupal\user\UserDataInterface;
 use Drupal\user\UserInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -19,49 +16,21 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
  * TFA account setup overview page.
  */
 class TfaOverviewForm extends FormBase {
-  use TfaDataTrait;
+  use TfaUserDataTrait;
 
   /**
-   * The setup plugin manager to fetch setup information.
-   *
-   * @var \Drupal\tfa\TfaLoginPluginManager
-   */
-  protected $tfaSetup;
-
-  /**
-   * Validation plugin manager.
-   *
-   * @var \Drupal\tfa\TfaValidationPluginManager
-   */
-  protected $tfaValidation;
-
-  /**
-   * Login plugin manager.
-   *
-   * @var \Drupal\tfa\TfaLoginPluginManager
-   */
-  protected $tfaLogin;
-
-  /**
-   * Send plugin manager.
-   *
-   * @var \Drupal\tfa\TfaSendPluginManager
-   */
-  protected $tfaSend;
-
-  /**
-   * Provides the user data service object.
+   * The date formatter service.
    *
-   * @var \Drupal\user\UserDataInterface
+   * @var \Drupal\Core\Datetime\DateFormatterInterface
    */
-  protected $userData;
+  protected $dateFormatter;
 
   /**
-   * The date formatter service.
+   * The TFA plugin manager.
    *
-   * @var \Drupal\Core\Datetime\DateFormatterInterface
+   * @var \Drupal\tfa\TfaPluginManager
    */
-  protected $dateFormatter;
+  protected $tfaPluginManager;
 
   /**
    * TFA Overview form constructor.
@@ -70,22 +39,13 @@ class TfaOverviewForm extends FormBase {
    *   The user data service.
    * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
    *   The date formatter service.
-   * @param \Drupal\tfa\TfaSetupPluginManager $tfa_setup_manager
-   *   The setup plugin manager.
-   * @param \Drupal\tfa\TfaValidationPluginManager $tfa_validation_manager
-   *   The validation plugin manager.
-   * @param \Drupal\tfa\TfaLoginPluginManager $tfa_login_manager
-   *   The login plugin manager.
-   * @param \Drupal\tfa\TfaSendPluginManager $tfa_send_manager
-   *   The send plugin manager.
+   * @param \Drupal\tfa\TfaPluginManager $tfa_plugin_manager
+   *   The TFA plugin manager.
    */
-  public function __construct(UserDataInterface $user_data, DateFormatterInterface $date_formatter, TfaSetupPluginManager $tfa_setup_manager, TfaValidationPluginManager $tfa_validation_manager, TfaLoginPluginManager $tfa_login_manager, TfaSendPluginManager $tfa_send_manager) {
+  public function __construct(UserDataInterface $user_data, DateFormatterInterface $date_formatter, TfaPluginManager $tfa_plugin_manager) {
     $this->userData = $user_data;
     $this->dateFormatter = $date_formatter;
-    $this->tfaSetup = $tfa_setup_manager;
-    $this->tfaValidation = $tfa_validation_manager;
-    $this->tfaLogin = $tfa_login_manager;
-    $this->tfaSend = $tfa_send_manager;
+    $this->tfaPluginManager = $tfa_plugin_manager;
   }
 
   /**
@@ -95,10 +55,7 @@ class TfaOverviewForm extends FormBase {
     return new static(
       $container->get('user.data'),
       $container->get('date.formatter'),
-      $container->get('plugin.manager.tfa.setup'),
-      $container->get('plugin.manager.tfa.validation'),
-      $container->get('plugin.manager.tfa.login'),
-      $container->get('plugin.manager.tfa.send')
+      $container->get('plugin.manager.tfa'),
     );
   }
 
@@ -106,7 +63,7 @@ class TfaOverviewForm extends FormBase {
    * {@inheritdoc}
    */
   public function getFormId() {
-    return 'tfa_base_overview';
+    return 'tfa_overview';
   }
 
   /**
@@ -122,7 +79,7 @@ class TfaOverviewForm extends FormBase {
     ];
     // $form_state['storage']['account'] = $user;.
     $config = $this->config('tfa.settings');
-    $user_tfa = $this->tfaGetTfaData($user->id(), $this->userData);
+    $user_tfa = $this->tfaGetTfaData($user->id());
     $enabled = isset($user_tfa['status']) && $user_tfa['status'];
 
     if (!empty($user_tfa)) {
@@ -155,7 +112,7 @@ class TfaOverviewForm extends FormBase {
       $enabled = isset($user_tfa['status'], $user_tfa['data']) && !empty($user_tfa['data']['plugins']) && $user_tfa['status'];
       $enabled_plugins = $user_tfa['data']['plugins'] ?? [];
 
-      $validation_plugins = $this->tfaValidation->getDefinitions();
+      $validation_plugins = $this->tfaPluginManager->getValidationDefinitions();
       if ($validation_plugins) {
         $output['validation'] = [
           '#type' => 'details',
@@ -171,7 +128,7 @@ class TfaOverviewForm extends FormBase {
       }
 
       if ($enabled) {
-        $login_plugins = $this->tfaLogin->getDefinitions();
+        $login_plugins = $this->tfaPluginManager->getLoginDefinitions();
         if ($login_plugins) {
           $output['login'] = [
             '#type' => 'details',
@@ -186,7 +143,7 @@ class TfaOverviewForm extends FormBase {
           }
         }
 
-        $send_plugins = $this->tfaSend->getDefinitions();
+        $send_plugins = $this->tfaPluginManager->getSendDefinitions();
         if ($send_plugins) {
           $output['send'] = [
             '#type' => 'details',
@@ -253,8 +210,9 @@ class TfaOverviewForm extends FormBase {
       'plugin_id' => $plugin['id'],
     ];
     try {
-      return $this->tfaSetup
-        ->createInstance($plugin['setupPluginId'], ['uid' => $account->id()])
+      // @todo check if plugin is a TfaSetupInterface.
+      return $this->tfaPluginManager
+        ->createInstance($plugin['id'], ['uid' => $account->id()])
         ->getOverview($params);
     }
     catch (\Exception $e) {
@@ -284,9 +242,9 @@ class TfaOverviewForm extends FormBase {
    */
   public function resetSkipValidationAttempts(array $form, FormStateInterface $form_state) {
     $account = $form_state->getValue('account');
-    $tfa_data = $this->tfaGetTfaData($account->id(), $this->userData);
+    $tfa_data = $this->tfaGetTfaData($account->id());
     $tfa_data['validation_skipped'] = 0;
-    $this->tfaSaveTfaData($account->id(), $this->userData, $tfa_data);
+    $this->tfaSaveTfaData($account->id(), $tfa_data);
     $this->messenger()->addMessage($this->t('Validation attempts have been reset for user @name.', [
       '@name' => $account->getDisplayName(),
     ]));
diff --git a/src/Form/TfaSetupForm.php b/src/Form/TfaSetupForm.php
index db7652c..9e72cf5 100644
--- a/src/Form/TfaSetupForm.php
+++ b/src/Form/TfaSetupForm.php
@@ -2,18 +2,14 @@
 
 namespace Drupal\tfa\Form;
 
-use Drupal\Component\Plugin\Exception\PluginNotFoundException;
 use Drupal\Core\Form\FormBase;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Mail\MailManagerInterface;
 use Drupal\Core\Password\PasswordInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
-use Drupal\tfa\TfaDataTrait;
-use Drupal\tfa\TfaLoginPluginManager;
-use Drupal\tfa\TfaSendPluginManager;
-use Drupal\tfa\TfaSetupPluginManager;
+use Drupal\tfa\TfaUserDataTrait;
+use Drupal\tfa\TfaPluginManager;
 use Drupal\tfa\TfaSetup;
-use Drupal\tfa\TfaValidationPluginManager;
 use Drupal\user\Entity\User;
 use Drupal\user\UserDataInterface;
 use Drupal\user\UserStorageInterface;
@@ -24,43 +20,15 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  * TFA setup form router.
  */
 class TfaSetupForm extends FormBase {
-  use TfaDataTrait;
+  use TfaUserDataTrait;
   use StringTranslationTrait;
 
   /**
-   * The setup plugin manager.
+   * The TFA plugin manager.
    *
-   * @var \Drupal\tfa\TfaSetupPluginManager
+   * @var \Drupal\tfa\TfaPluginManager
    */
-  protected $tfaSetup;
-
-  /**
-   * The validation plugin manager.
-   *
-   * @var \Drupal\tfa\TfaValidationPluginManager
-   */
-  protected $tfaValidation;
-
-  /**
-   * The login plugin manager.
-   *
-   * @var \Drupal\tfa\TfaLoginPluginManager
-   */
-  protected $tfaLogin;
-
-  /**
-   * The send plugin manager.
-   *
-   * @var \Drupal\tfa\TfaSendPluginManager
-   */
-  protected $tfaSend;
-
-  /**
-   * Provides the user data service object.
-   *
-   * @var \Drupal\user\UserDataInterface
-   */
-  protected $userData;
+  protected $tfaPluginManager;
 
   /**
    * The password hashing service.
@@ -86,16 +54,10 @@ class TfaSetupForm extends FormBase {
   /**
    * TFA Setup form constructor.
    *
-   * @param \Drupal\tfa\TfaSetupPluginManager $tfa_setup_manager
-   *   The setup plugin manager.
+   * @param \Drupal\tfa\TfaPluginManager $tfa_plugin_manager
+   *   The TFA plugin manager.
    * @param \Drupal\user\UserDataInterface $user_data
    *   The user data object to store user information.
-   * @param \Drupal\tfa\TfaValidationPluginManager $tfa_validation_manager
-   *   The validation plugin manager.
-   * @param \Drupal\tfa\TfaLoginPluginManager $tfa_login_manager
-   *   The login plugin manager.
-   * @param \Drupal\tfa\TfaSendPluginManager $tfa_send_manager
-   *   The send plugin manager.
    * @param \Drupal\Core\Password\PasswordInterface $password_checker
    *   The password service.
    * @param \Drupal\Core\Mail\MailManagerInterface $mail_manager
@@ -103,12 +65,9 @@ class TfaSetupForm extends FormBase {
    * @param \Drupal\user\UserStorageInterface $user_storage
    *   The user storage.
    */
-  public function __construct(TfaSetupPluginManager $tfa_setup_manager, UserDataInterface $user_data, TfaValidationPluginManager $tfa_validation_manager, TfaLoginPluginManager $tfa_login_manager, TfaSendPluginManager $tfa_send_manager, PasswordInterface $password_checker, MailManagerInterface $mail_manager, UserStorageInterface $user_storage) {
-    $this->tfaSetup = $tfa_setup_manager;
+  public function __construct(TfaPluginManager $tfa_plugin_manager, UserDataInterface $user_data, PasswordInterface $password_checker, MailManagerInterface $mail_manager, UserStorageInterface $user_storage) {
+    $this->tfaPluginManager = $tfa_plugin_manager;
     $this->userData = $user_data;
-    $this->tfaValidation = $tfa_validation_manager;
-    $this->tfaLogin = $tfa_login_manager;
-    $this->tfaSend = $tfa_send_manager;
     $this->passwordChecker = $password_checker;
     $this->mailManager = $mail_manager;
     $this->userStorage = $user_storage;
@@ -119,11 +78,8 @@ class TfaSetupForm extends FormBase {
    */
   public static function create(ContainerInterface $container) {
     return new static(
-      $container->get('plugin.manager.tfa.setup'),
+      $container->get('plugin.manager.tfa'),
       $container->get('user.data'),
-      $container->get('plugin.manager.tfa.validation'),
-      $container->get('plugin.manager.tfa.login'),
-      $container->get('plugin.manager.tfa.send'),
       $container->get('password'),
       $container->get('plugin.manager.mail'),
       $container->get('entity_type.manager')->getStorage('user')
@@ -137,40 +93,10 @@ class TfaSetupForm extends FormBase {
     return 'tfa_setup';
   }
 
-  /**
-   * Find the correct plugin that is being setup.
-   *
-   * @param string $plugin_id
-   *   Plugin ID.
-   *
-   * @return array|null
-   *   Plugin definitions.
-   *
-   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
-   */
-  protected function findPlugin($plugin_id) {
-    $plugin = $this->tfaValidation->getDefinition($plugin_id, FALSE);
-    if (empty($plugin)) {
-      $plugin = $this->tfaLogin->getDefinition($plugin_id, FALSE);
-    }
-    if (empty($plugin)) {
-      $plugin = $this->tfaSend->getDefinition($plugin_id, FALSE);
-    }
-
-    if (empty($plugin)) {
-      throw new PluginNotFoundException($plugin_id, sprintf('The "%s" plugin does not exist.', $plugin_id));
-    }
-
-    return $plugin;
-  }
-
   /**
    * {@inheritdoc}
    */
   public function buildForm(array $form, FormStateInterface $form_state, User $user = NULL, $method = 'tfa_totp', $reset = 0) {
-    if (!$this->tfaSetup->hasDefinition($method . '_setup')) {
-      throw new NotFoundHttpException($this->t('Plugin @plugin not found.', ['@plugin' => $method]));
-    }
     /** @var \Drupal\user\Entity\User $account */
     $account = $this->userStorage->load($this->currentUser()->id());
 
@@ -178,7 +104,7 @@ class TfaSetupForm extends FormBase {
       '#type' => 'value',
       '#value' => $user,
     ];
-    $tfa_data = $this->tfaGetTfaData($user->id(), $this->userData);
+    $tfa_data = $this->tfaGetTfaData($user->id());
     $enabled = isset($tfa_data['status'], $tfa_data['data']) && !empty($tfa_data['data']['plugins']) && $tfa_data['status'];
 
     $storage = $form_state->getStorage();
@@ -230,8 +156,8 @@ class TfaSetupForm extends FormBase {
 
       // Record methods progressed.
       $storage['steps'][] = $method;
-      $plugin = $this->findPlugin($method);
-      $setup_plugin = $this->tfaSetup->createInstance($plugin['setupPluginId'], ['uid' => $user->id()]);
+      $plugin = $this->tfaPluginManager->getDefinition($method, FALSE);
+      $setup_plugin = $this->tfaPluginManager->createInstance($plugin['id'], ['uid' => $user->id()]);
       $tfa_setup = new TfaSetup($setup_plugin);
       $form = $tfa_setup->getForm($form, $form_state, $reset);
       $storage[$method] = $tfa_setup;
@@ -370,7 +296,7 @@ class TfaSetupForm extends FormBase {
       // Log and notify if this was full setup.
       if (!empty($storage['step_method'])) {
         $data = ['plugins' => $storage['step_method']];
-        $this->tfaSaveTfaData($account->id(), $this->userData, $data);
+        $this->tfaSaveTfaData($account->id(), $data);
         $this->logger('tfa')->info('TFA enabled for user @name UID @uid', [
           '@name' => $account->getAccountName(),
           '@uid' => $account->id(),
@@ -385,7 +311,7 @@ class TfaSetupForm extends FormBase {
   /**
    * Steps eligible for TFA setup.
    */
-  private function tfaFullSetupSteps() {
+  protected function tfaFullSetupSteps() {
     $config = $this->config('tfa.settings');
     $steps = [
       $config->get('default_validation_plugin'),
@@ -413,7 +339,7 @@ class TfaSetupForm extends FormBase {
    * @param bool $skipped_step
    *   Whether the step was skipped.
    */
-  private function tfaNextSetupStep(FormStateInterface &$form_state, $this_step, TfaSetup $step_class, $skipped_step = FALSE) {
+  protected function tfaNextSetupStep(FormStateInterface &$form_state, $this_step, TfaSetup $step_class, $skipped_step = FALSE) {
     $storage = $form_state->getStorage();
     // Remove this step from steps left.
     $storage['steps_left'] = array_diff($storage['steps_left'], [$this_step]);
diff --git a/src/Plugin/Tfa/TfaHotp.php b/src/Plugin/Tfa/TfaHotp.php
new file mode 100644
index 0000000..08cff12
--- /dev/null
+++ b/src/Plugin/Tfa/TfaHotp.php
@@ -0,0 +1,535 @@
+<?php
+
+namespace Drupal\tfa\Plugin\Tfa;
+
+use chillerlan\QRCode\QRCode;
+use Drupal\Component\Datetime\TimeInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Link;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Url;
+use Drupal\encrypt\EncryptionProfileManagerInterface;
+use Drupal\encrypt\EncryptServiceInterface;
+use Drupal\tfa\Plugin\TfaSetupInterface;
+use Drupal\tfa\Plugin\TfaValidationInterface;
+use Drupal\tfa\TfaPluginBase;
+use Drupal\user\Entity\User;
+use Drupal\user\UserDataInterface;
+use Otp\GoogleAuthenticator;
+use Otp\Otp;
+use ParagonIE\ConstantTime\Encoding;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * HOTP validation class for performing HOTP validation.
+ *
+ * @Tfa(
+ *   id = "tfa_hotp",
+ *   label = @Translation("TFA HMAC-based one-time password (HOTP)"),
+ *   description = @Translation("TFA HOTP Validation Plugin"),
+ *   helpLinks = {
+ *    "Google Authenticator (Android/iOS)" = "https://googleauthenticator.net",
+ *    "Microsoft Authenticator (Android/iOS)" = "https://www.microsoft.com/en-us/security/mobile-authenticator-app",
+ *    "FreeOTP (Android)" = "https://freeotp.github.io",
+ *   },
+ *   setupMessages = {
+ *    "saved" = @Translation("Application code verified."),
+ *    "skipped" = @Translation("Application codes not enabled."),
+ *   }
+ * )
+ */
+class TfaHotp extends TfaPluginBase implements TfaValidationInterface, TfaSetupInterface, ContainerFactoryPluginInterface {
+
+  /**
+   * Object containing the external validation library.
+   *
+   * @var object
+   */
+  public $auth;
+
+  /**
+   * The counter window in which the validation should be done.
+   *
+   * @var int
+   */
+  protected $counterWindow;
+
+  /**
+   * Whether or not the prefix should use the site name.
+   *
+   * @var bool
+   */
+  protected $siteNamePrefix;
+
+  /**
+   * Name prefix.
+   *
+   * @var string
+   */
+  protected $namePrefix;
+
+  /**
+   * Configurable name of the issuer.
+   *
+   * @var string
+   */
+  protected $issuer;
+
+  /**
+   * The Datetime service.
+   *
+   * @var \Drupal\Component\Datetime\TimeInterface
+   */
+  protected $time;
+
+  /**
+   * Un-encrypted seed.
+   *
+   * @var string
+   */
+  protected $seed;
+
+  /**
+   * Encryption profile.
+   *
+   * @var \Drupal\encrypt\EncryptionProfileManagerInterface
+   */
+  protected $encryptionProfile;
+
+  /**
+   * Encryption service.
+   *
+   * @var \Drupal\encrypt\EncryptService
+   */
+  protected $encryptService;
+
+  /**
+   * Constructs a new Tfa plugin object.
+   *
+   * @param array $configuration
+   *   The plugin configuration.
+   * @param string $plugin_id
+   *   The plugin id.
+   * @param mixed $plugin_definition
+   *   The plugin definition.
+   * @param \Drupal\user\UserDataInterface $user_data
+   *   User data object to store user specific information.
+   * @param \Drupal\encrypt\EncryptionProfileManagerInterface $encryption_profile_manager
+   *   Encryption profile manager.
+   * @param \Drupal\encrypt\EncryptServiceInterface $encrypt_service
+   *   Encryption service.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The configuration factory.
+   * @param \Drupal\Component\Datetime\TimeInterface $time
+   *   The datetime service.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, UserDataInterface $user_data, EncryptionProfileManagerInterface $encryption_profile_manager, EncryptServiceInterface $encrypt_service, ConfigFactoryInterface $config_factory, TimeInterface $time) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->auth = new \StdClass();
+    $this->auth->otp = new Otp();
+    $this->auth->ga = new GoogleAuthenticator();
+    $plugin_settings = $config_factory->get('tfa.settings')->get('validation_plugin_settings');
+    $settings = $plugin_settings[$plugin_id] ?? [];
+    $settings = array_replace([
+      'counter_window' => 10,
+      'site_name_prefix' => TRUE,
+      'name_prefix' => 'TFA',
+      'issuer' => 'Drupal',
+    ], $settings);
+
+    $this->userData = $user_data;
+    $this->counterWindow = $settings['counter_window'];
+    $this->siteNamePrefix = $settings['site_name_prefix'];
+    $this->namePrefix = $settings['name_prefix'];
+    $this->issuer = $settings['issuer'];
+    $this->time = $time;
+
+    $this->encryptionProfile = $encryption_profile_manager->getEncryptionProfile($config_factory->get('tfa.settings')->get('encryption'));
+    $this->encryptService = $encrypt_service;
+
+    // Generate seed.
+    $this->setSeed($this->createSeed());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('user.data'),
+      $container->get('encrypt.encryption_profile.manager'),
+      $container->get('encryption'),
+      $container->get('config.factory'),
+      $container->get('datetime.time')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function ready() {
+    return ($this->getSeed() !== FALSE);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getForm(array $form, FormStateInterface $form_state) {
+    $message = $this->t('Verification code is application generated and @length digits long.', ['@length' => $this->codeLength]);
+    if ($this->getUserData('tfa', 'tfa_recovery_code', $this->uid)) {
+      $message .= '<br/>' . $this->t("Can't access your account? Use one of your recovery codes.");
+    }
+    $form['code'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Application verification code'),
+      '#description' => $message,
+      '#required'  => TRUE,
+      '#attributes' => ['autocomplete' => 'off'],
+    ];
+
+    $form['actions']['#type'] = 'actions';
+    $form['actions']['login'] = [
+      '#type' => 'submit',
+      '#button_type' => 'primary',
+      '#value' => $this->t('Verify'),
+    ];
+
+    return $form;
+  }
+
+  /**
+   * The configuration form for this validation plugin.
+   *
+   * @return array
+   *   Form array specific for this validation plugin.
+   */
+  public function buildConfigurationForm() {
+    $settings_form['counter_window'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Counter Window'),
+      '#default_value' => ($this->counterWindow) ?: 5,
+      '#description' => $this->t('How far ahead from current counter should we check the code.'),
+      '#size' => 2,
+      '#required' => TRUE,
+    ];
+
+    $settings_form['site_name_prefix'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Use site name as OTP QR code name prefix.'),
+      '#default_value' => $this->siteNamePrefix,
+      '#description' => $this->t('If checked, the site name will be used instead of a static string. This can be useful for multi-site installations.'),
+    ];
+
+    $settings_form['name_prefix'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('OTP QR Code Prefix'),
+      '#default_value' => $this->namePrefix ?? 'tfa',
+      '#description' => $this->t('Prefix for OTP QR code names. Suffix is account username.'),
+      '#size' => 15,
+      '#states' => [
+        'visible' => [':input[name="validation_plugin_settings[tfa_hotp][site_name_prefix]"]' => ['checked' => FALSE]],
+      ],
+    ];
+
+    $settings_form['issuer'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Issuer'),
+      '#default_value' => $this->issuer,
+      '#description' => $this->t('The provider or service this account is associated with.'),
+      '#size' => 15,
+      '#required' => TRUE,
+    ];
+
+    return $settings_form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array $form, FormStateInterface $form_state) {
+    $values = $form_state->getValues();
+    if (!$this->validate($values['code'])) {
+      $this->errorMessages['code'] = $this->t('Invalid application code. Please try again.');
+      if ($this->alreadyAccepted) {
+        $form_state->clearErrors();
+        $this->errorMessages['code'] = $this->t('Invalid code, it was recently used for a login. Please try a new code.');
+      }
+      return FALSE;
+    }
+    else {
+      // Store accepted code to prevent replay attacks.
+      $this->storeAcceptedCode($values['code']);
+      return TRUE;
+    }
+  }
+
+  /**
+   * Simple validate for web services.
+   *
+   * @param int $code
+   *   OTP Code.
+   *
+   * @return bool
+   *   True if validation was successful otherwise false.
+   */
+  public function validateRequest($code) {
+    if ($this->validate($code)) {
+      $this->storeAcceptedCode($code);
+      return TRUE;
+    }
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function validate($code) {
+    // Strip whitespace.
+    $code = preg_replace('/\s+/', '', $code);
+    if ($this->alreadyAcceptedCode($code)) {
+      $this->isValid = FALSE;
+    }
+    else {
+      // Get OTP seed.
+      $seed = $this->getSeed();
+      $counter = $this->getHotpCounter();
+      $this->isValid = ($seed && ($counter = $this->auth->otp->checkHotpResync(Encoding::base32DecodeUpper($seed), $counter, $code, $this->counterWindow)));
+      $this->setUserData('tfa', [$this->pluginId . '_counter' => ++$counter], $this->uid);
+    }
+    return $this->isValid;
+  }
+
+  /**
+   * Get seed for this account.
+   *
+   * @return string
+   *   Decrypted account OTP seed or FALSE if none exists.
+   */
+  protected function getSeed() {
+    // Lookup seed for account and decrypt.
+    $result = $this->getUserData('tfa', $this->pluginId . '_seed', $this->uid);
+
+    if (!empty($result)) {
+      $encrypted = base64_decode($result['seed']);
+      $seed = $this->encryptService->decrypt($encrypted, $this->encryptionProfile);
+      if (!empty($seed)) {
+        return $seed;
+      }
+    }
+    return FALSE;
+  }
+
+  /**
+   * Save seed for account.
+   *
+   * @param string $seed
+   *   Un-encrypted seed.
+   */
+  public function storeSeed($seed) {
+    // Encrypt seed for storage.
+    $encrypted = $this->encryptService->encrypt($seed, $this->encryptionProfile);
+
+    $record = [
+      $this->pluginId . '_seed' => [
+        'seed' => base64_encode($encrypted),
+        'created' => $this->time->getRequestTime(),
+      ],
+    ];
+
+    $this->setUserData('tfa', $record, $this->uid);
+  }
+
+  /**
+   * Delete the seed of the current validated user.
+   */
+  protected function deleteSeed() {
+    $this->deleteUserData('tfa', $this->pluginId . '_seed', $this->uid);
+  }
+
+  /**
+   * Get the HOTP counter.
+   *
+   * @return int
+   *   The current value of the HOTP counter, or 1 if no value was found.
+   */
+  public function getHotpCounter() {
+    return ($this->getUserData('tfa', $this->pluginId . '_counter', $this->uid)) ?: 1;
+  }
+
+  // ================================== SETUP ==================================
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSetupForm(array $form, FormStateInterface $form_state) {
+    $help_links = $this->getHelpLinks();
+
+    $items = [];
+    foreach ($help_links as $item => $link) {
+      $items[] = Link::fromTextAndUrl($item, Url::fromUri($link, ['attributes' => ['target' => '_blank']]));
+    }
+
+    $form['apps'] = [
+      '#theme' => 'item_list',
+      '#items' => $items,
+      '#title' => $this->t('Install authentication code application on your mobile or desktop device:'),
+    ];
+    $form['info'] = [
+      '#type' => 'html_tag',
+      '#tag' => 'p',
+      '#value' => $this->t('The two-factor authentication application will be used during this setup and for generating codes during regular authentication. If the application supports it, scan the QR code below to get the setup code otherwise you can manually enter the text code.'),
+    ];
+    $form['seed'] = [
+      '#type' => 'textfield',
+      '#value' => $this->seed,
+      '#disabled' => TRUE,
+      '#description' => $this->t('Enter this code into your two-factor authentication app or scan the QR code below.'),
+    ];
+
+    // QR image of seed.
+    $form['qr_image'] = [
+      '#prefix' => '<div class="tfa-qr-code"',
+      '#theme' => 'image',
+      '#uri' => $this->getQrCodeUri(),
+      '#alt' => $this->t('QR code for TFA setup'),
+      '#suffix' => '</div>',
+    ];
+
+    // QR code css giving it a fixed width.
+    $form['page']['#attached']['html_head'][] = [
+      [
+        '#tag' => 'style',
+        '#value' => ".tfa-qr-code { width:200px }",
+      ],
+      'qrcode-css',
+    ];
+
+    // Include code entry form.
+    $form = $this->getForm($form, $form_state);
+    $form['actions']['login']['#value'] = $this->t('Verify and save');
+    // Alter code description.
+    $form['code']['#description'] = $this->t('A verification code will be generated after you scan the above QR code or manually enter the setup code. The verification code is six digits long.');
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateSetupForm(array $form, FormStateInterface $form_state) {
+    if (!$this->validateSetup($form_state->getValue('code'))) {
+      $this->errorMessages['code'] = $this->t('Invalid application code. Please try again.');
+      return FALSE;
+    }
+    $this->storeAcceptedCode($form_state->getValue('code'));
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function validateSetup($code) {
+    // The counter is set as 1 because that is the initial value.
+    // This ensures that things work even if we reset the application.
+    $code = preg_replace('/\s+/', '', $code);
+    $counter = $this->auth->otp->checkHotpResync(Encoding::base32DecodeUpper($this->seed), 1, $code, $this->counterWindow);
+    $this->setUserData('tfa', [$this->pluginId . '_counter' => ++$counter], $this->uid);
+    return ((bool) $counter);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitSetupForm(array $form, FormStateInterface $form_state) {
+    // Write seed for user.
+    $this->storeSeed($this->seed);
+    return TRUE;
+  }
+
+  /**
+   * Get a base64 qrcode image uri of seed.
+   *
+   * @return string
+   *   QR-code uri.
+   */
+  protected function getQrCodeUri() {
+    return (new QRCode)->render('otpauth://hotp/' . $this->accountName() . '?secret=' . $this->seed . '&counter=1&issuer=' . urlencode($this->issuer));
+  }
+
+  /**
+   * Create OTP seed for account.
+   *
+   * @return string
+   *   Un-encrypted seed.
+   */
+  protected function createSeed() {
+    return $this->auth->ga->generateRandom();
+  }
+
+  /**
+   * Setter for OTP secret key.
+   *
+   * @param string $seed
+   *   The OTP secret key.
+   */
+  public function setSeed($seed) {
+    $this->seed = $seed;
+  }
+
+  /**
+   * Get account name for QR image.
+   *
+   * @return string
+   *   URL encoded string.
+   */
+  protected function accountName() {
+    /** @var \Drupal\user\Entity\User $account */
+    $account = User::load($this->configuration['uid']);
+    $prefix = $this->siteNamePrefix ? preg_replace('@[^a-z0-9-]+@', '-', strtolower(\Drupal::config('system.site')->get('name'))) : $this->namePrefix;
+    return urlencode((!empty($prefix) ? $prefix . '-' : '') . $account->getAccountName());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOverview(array $params) {
+    $plugin_text = $this->t('Validation Plugin: @plugin',
+      [
+        '@plugin' => str_replace(' Setup', '', $this->getLabel()),
+      ]
+    );
+    $output = [
+      'heading' => [
+        '#type' => 'html_tag',
+        '#tag' => 'h2',
+        '#value' => $this->t('TFA application'),
+      ],
+      'validation_plugin' => [
+        '#type' => 'markup',
+        '#markup' => '<p>' . $plugin_text . '</p>',
+      ],
+      'description' => [
+        '#type' => 'html_tag',
+        '#tag' => 'p',
+        '#value' => $this->t('Generate verification codes from a mobile or desktop application.'),
+      ],
+      'link' => [
+        '#theme' => 'links',
+        '#links' => [
+          'admin' => [
+            'title' => !$params['enabled'] ? $this->t('Set up application') : $this->t('Reset application'),
+            'url' => Url::fromRoute('tfa.validation.setup', [
+              'user' => $params['account']->id(),
+              'method' => $params['plugin_id'],
+            ]),
+          ],
+        ],
+      ],
+    ];
+    return $output;
+  }
+
+}
diff --git a/src/Plugin/TfaValidation/TfaRecoveryCode.php b/src/Plugin/Tfa/TfaRecoveryCode.php
similarity index 57%
rename from src/Plugin/TfaValidation/TfaRecoveryCode.php
rename to src/Plugin/Tfa/TfaRecoveryCode.php
index 3d85b24..516b5a8 100644
--- a/src/Plugin/TfaValidation/TfaRecoveryCode.php
+++ b/src/Plugin/Tfa/TfaRecoveryCode.php
@@ -1,16 +1,16 @@
 <?php
 
-namespace Drupal\tfa\Plugin\TfaValidation;
+namespace Drupal\tfa\Plugin\Tfa;
 
-use Drupal\Core\Config\Config;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
-use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\Url;
 use Drupal\encrypt\EncryptionProfileManagerInterface;
 use Drupal\encrypt\EncryptServiceInterface;
-use Drupal\tfa\Plugin\TfaBasePlugin;
+use Drupal\tfa\Plugin\TfaSetupInterface;
 use Drupal\tfa\Plugin\TfaValidationInterface;
+use Drupal\tfa\TfaPluginBase;
 use Drupal\tfa\TfaRandomTrait;
 use Drupal\user\UserDataInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -18,19 +18,19 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
 /**
  * Recovery validation class for performing recovery codes validation.
  *
- * @TfaValidation(
+ * @Tfa(
  *   id = "tfa_recovery_code",
  *   label = @Translation("TFA Recovery Code"),
  *   description = @Translation("TFA Recovery Code Validation Plugin"),
- *   setupPluginId = "tfa_recovery_code_setup",
+ *   setupMessages = {
+ *    "saved" = @Translation("Recovery codes saved."),
+ *    "skipped" = @Translation("Recovery codes not saved."),
+ *   }
  * )
  */
-class TfaRecoveryCode extends TfaBasePlugin implements TfaValidationInterface, ContainerFactoryPluginInterface {
-
+class TfaRecoveryCode extends TfaPluginBase implements TfaValidationInterface, TfaSetupInterface, ContainerFactoryPluginInterface {
   use TfaRandomTrait;
 
-  use StringTranslationTrait;
-
   /**
    * The number of recovery codes to generate.
    *
@@ -38,6 +38,20 @@ class TfaRecoveryCode extends TfaBasePlugin implements TfaValidationInterface, C
    */
   protected $codeLimit = 10;
 
+  /**
+   * Encryption profile.
+   *
+   * @var \Drupal\encrypt\EncryptionProfileManagerInterface
+   */
+  protected $encryptionProfile;
+
+  /**
+   * Encryption service.
+   *
+   * @var \Drupal\encrypt\EncryptService
+   */
+  protected $encryptService;
+
   /**
    * Constructs a new Tfa plugin object.
    *
@@ -57,8 +71,11 @@ class TfaRecoveryCode extends TfaBasePlugin implements TfaValidationInterface, C
    *   The configuration factory.
    */
   public function __construct(array $configuration, $plugin_id, $plugin_definition, UserDataInterface $user_data, EncryptionProfileManagerInterface $encryption_profile_manager, EncryptServiceInterface $encrypt_service, ConfigFactoryInterface $config_factory) {
-    parent::__construct($configuration, $plugin_id, $plugin_definition, $user_data, $encryption_profile_manager, $encrypt_service);
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
 
+    $this->userData = $user_data;
+    $this->encryptionProfile = $encryption_profile_manager->getEncryptionProfile($config_factory->get('tfa.settings')->get('encryption'));
+    $this->encryptService = $encrypt_service;
     $codes_amount = $config_factory->get('tfa.settings')->get('validation_plugin_settings.tfa_recovery_code.recovery_codes_amount');
     if (!empty($codes_amount)) {
       $this->codeLimit = $codes_amount;
@@ -111,15 +128,10 @@ class TfaRecoveryCode extends TfaBasePlugin implements TfaValidationInterface, C
   /**
    * Configuration form for the recovery code plugin.
    *
-   * @param \Drupal\Core\Config\Config $config
-   *   Config object for tfa settings.
-   * @param array $state
-   *   Form state array determines if this form should be shown.
-   *
    * @return array
    *   Form array specific for this validation plugin.
    */
-  public function buildConfigurationForm(Config $config, array $state = []) {
+  public function buildConfigurationForm() {
     $settings_form['recovery_codes_amount'] = [
       '#type' => 'number',
       '#title' => $this->t('Recovery Codes Amount'),
@@ -127,7 +139,6 @@ class TfaRecoveryCode extends TfaBasePlugin implements TfaValidationInterface, C
       '#description' => $this->t('Number of Recovery Codes To Generate.'),
       '#min' => 1,
       '#size' => 2,
-      '#states' => $state,
       '#required' => TRUE,
     ];
 
@@ -191,9 +202,9 @@ class TfaRecoveryCode extends TfaBasePlugin implements TfaValidationInterface, C
    * @throws \Drupal\encrypt\Exception\EncryptException
    */
   public function getCodes() {
-    $codes = $this->getUserData('tfa', 'tfa_recovery_code', $this->uid, $this->userData) ?: [];
+    $codes = $this->getUserData('tfa', $this->pluginId, $this->uid) ?: [];
     array_walk($codes, function (&$v, $k) {
-      $v = $this->decrypt($v);
+      $v = $this->encryptService->decrypt($v, $this->encryptionProfile);
     });
     return $codes;
   }
@@ -211,11 +222,11 @@ class TfaRecoveryCode extends TfaBasePlugin implements TfaValidationInterface, C
 
     // Encrypt code for storage.
     array_walk($codes, function (&$v, $k) {
-      $v = $this->encrypt($v);
+      $v = $this->encryptService->encrypt($v, $this->encryptionProfile);
     });
-    $data = ['tfa_recovery_code' => $codes];
+    $data = [$this->pluginId => $codes];
 
-    $this->setUserData('tfa', $data, $this->uid, $this->userData);
+    $this->setUserData('tfa', $data, $this->uid);
   }
 
   /**
@@ -223,7 +234,7 @@ class TfaRecoveryCode extends TfaBasePlugin implements TfaValidationInterface, C
    */
   protected function deleteCodes() {
     // Delete any existing codes.
-    $this->deleteUserData('tfa', 'tfa_recovery_code', $this->uid, $this->userData);
+    $this->deleteUserData('tfa', $this->pluginId, $this->uid);
   }
 
   /**
@@ -252,4 +263,122 @@ class TfaRecoveryCode extends TfaBasePlugin implements TfaValidationInterface, C
     return $this->isValid;
   }
 
+  // ================================== SETUP ==================================
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOverview(array $params) {
+    return [
+      'heading' => [
+        '#type' => 'html_tag',
+        '#tag' => 'h3',
+        '#value' => $this->t('Recovery Codes'),
+      ],
+      'description' => [
+        '#type' => 'html_tag',
+        '#tag' => 'p',
+        '#value' => $this->t('Generate one-time use codes for two-factor login. These are generally used to recover your account in case you lose access to another 2nd-factor device.'),
+      ],
+      'setup' => [
+        '#theme' => 'links',
+        '#links' => [
+          'reset' => [
+            'title' => !$params['enabled'] ? $this->t('Generate codes') : $this->t('Reset codes'),
+            'url' => Url::fromRoute('tfa.plugin.reset', [
+              'user' => $params['account']->id(),
+              'method' => $params['plugin_id'],
+              'reset' => 1,
+            ]),
+          ],
+        ],
+      ],
+      'show_codes' => [
+        '#theme' => 'links',
+        '#access' => $params['enabled'],
+        '#links' => [
+          'show' => [
+            'title' => $this->t('Show codes'),
+            'url' => Url::fromRoute('tfa.validation.setup', [
+              'user' => $params['account']->id(),
+              'method' => $params['plugin_id'],
+            ]),
+          ],
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * Get the setup form for the validation method.
+   *
+   * @param array $form
+   *   The configuration form array.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   * @param int $reset
+   *   Whether or not the user is resetting the application.
+   *
+   * @return array
+   *   Form API array.
+   */
+  public function getSetupForm(array $form, FormStateInterface $form_state, $reset = 0) {
+    $codes = $this->getCodes();
+
+    // If $reset has a value, we're setting up new codes.
+    if (!empty($reset)) {
+      $codes = $this->generateCodes();
+
+      // Make the human friendly.
+      foreach ($codes as $key => $code) {
+        $codes[$key] = implode(' ', str_split($code, 3));
+      }
+      $form['recovery_codes'] = [
+        '#type' => 'value',
+        '#value' => $codes,
+      ];
+    }
+
+    $form['recovery_codes_output'] = [
+      '#title' => $this->t('Recovery Codes'),
+      '#theme' => 'item_list',
+      '#items' => $codes,
+    ];
+    $form['description'] = [
+      '#type' => 'html_tag',
+      '#tag' => 'p',
+      '#value' => $this->t('Print or copy these codes and store them somewhere safe before continuing.'),
+    ];
+
+    if (!empty($reset)) {
+      $form['actions'] = ['#type' => 'actions'];
+      $form['actions']['save'] = [
+        '#type' => 'submit',
+        '#button_type' => 'primary',
+        '#value' => $this->t('Save codes to account'),
+      ];
+    }
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateSetupForm(array $form, FormStateInterface $form_state) {
+    if (!empty($form_state->getValue('recovery_codes'))) {
+      return TRUE;
+    }
+
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitSetupForm(array $form, FormStateInterface $form_state) {
+    $this->storeCodes($form_state->getValue('recovery_codes'));
+    return TRUE;
+  }
+
 }
diff --git a/src/Plugin/Tfa/TfaTotp.php b/src/Plugin/Tfa/TfaTotp.php
new file mode 100644
index 0000000..f33cdc3
--- /dev/null
+++ b/src/Plugin/Tfa/TfaTotp.php
@@ -0,0 +1,529 @@
+<?php
+
+namespace Drupal\tfa\Plugin\Tfa;
+
+use chillerlan\QRCode\QRCode;
+use Drupal\Component\Datetime\TimeInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Link;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Url;
+use Drupal\encrypt\EncryptionProfileManagerInterface;
+use Drupal\encrypt\EncryptServiceInterface;
+use Drupal\tfa\Plugin\TfaSetupInterface;
+use Drupal\tfa\Plugin\TfaValidationInterface;
+use Drupal\tfa\TfaPluginBase;
+use Drupal\user\Entity\User;
+use Drupal\user\UserDataInterface;
+use Otp\GoogleAuthenticator;
+use Otp\Otp;
+use ParagonIE\ConstantTime\Encoding;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * TOTP validation class for performing TOTP validation.
+ *
+ * @Tfa(
+ *   id = "tfa_totp",
+ *   label = @Translation("TFA Time-based one-time password (TOTP)"),
+ *   description = @Translation("TFA TOTP Validation Plugin"),
+ *   helpLinks = {
+ *    "Google Authenticator (Android/iOS)" = "https://googleauthenticator.net",
+ *    "Microsoft Authenticator (Android/iOS)" = "https://www.microsoft.com/en-us/security/mobile-authenticator-app",
+ *    "Authy (Android/iOS/Desktop)" = "https://authy.com",
+ *    "FreeOTP (Android/iOS)" = "https://freeotp.github.io",
+ *    "GAuth Authenticator (Desktop)" = "https://github.com/gbraadnl/gauth",
+ *   },
+ *   setupMessages = {
+ *    "saved" = @Translation("Application code verified."),
+ *    "skipped" = @Translation("Application codes not enabled."),
+ *   }
+ * )
+ */
+class TfaTotp extends TfaPluginBase implements TfaValidationInterface, TfaSetupInterface, ContainerFactoryPluginInterface {
+
+  /**
+   * Object containing the external validation library.
+   *
+   * @var object
+   */
+  public $auth;
+
+  /**
+   * The time-window in which the validation should be done.
+   *
+   * @var int
+   */
+  protected $timeSkew;
+
+  /**
+   * Whether or not the prefix should use the site name.
+   *
+   * @var bool
+   */
+  protected $siteNamePrefix;
+
+  /**
+   * Name prefix.
+   *
+   * @var string
+   */
+  protected $namePrefix;
+
+  /**
+   * Configurable name of the issuer.
+   *
+   * @var string
+   */
+  protected $issuer;
+
+  /**
+   * The Datetime service.
+   *
+   * @var \Drupal\Component\Datetime\TimeInterface
+   */
+  protected $time;
+
+  /**
+   * Encryption profile.
+   *
+   * @var \Drupal\encrypt\EncryptionProfileManagerInterface
+   */
+  protected $encryptionProfile;
+
+  /**
+   * Encryption service.
+   *
+   * @var \Drupal\encrypt\EncryptService
+   */
+  protected $encryptService;
+
+  /**
+   * Constructs a new Tfa plugin object.
+   *
+   * @param array $configuration
+   *   The plugin configuration.
+   * @param string $plugin_id
+   *   The plugin id.
+   * @param mixed $plugin_definition
+   *   The plugin definition.
+   * @param \Drupal\user\UserDataInterface $user_data
+   *   User data object to store user specific information.
+   * @param \Drupal\encrypt\EncryptionProfileManagerInterface $encryption_profile_manager
+   *   Encryption profile manager.
+   * @param \Drupal\encrypt\EncryptServiceInterface $encrypt_service
+   *   Encryption service.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The configuration factory.
+   * @param \Drupal\Component\Datetime\TimeInterface $time
+   *   The datetime service.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, UserDataInterface $user_data, EncryptionProfileManagerInterface $encryption_profile_manager, EncryptServiceInterface $encrypt_service, ConfigFactoryInterface $config_factory, TimeInterface $time) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+
+    $this->auth = new \StdClass();
+    $this->auth->otp = new Otp();
+    $this->auth->ga = new GoogleAuthenticator();
+    // Allow codes within tolerance range of 2 * 30 second units.
+    $plugin_settings = $config_factory->get('tfa.settings')->get('validation_plugin_settings');
+    $settings = $plugin_settings[$plugin_id] ?? [];
+    $settings = array_replace([
+      'time_skew' => 2,
+      'site_name_prefix' => TRUE,
+      'name_prefix' => 'TFA',
+      'issuer' => 'Drupal',
+    ], $settings);
+
+    $this->userData = $user_data;
+    $this->timeSkew = $settings['time_skew'];
+    $this->siteNamePrefix = $settings['site_name_prefix'];
+    $this->namePrefix = $settings['name_prefix'];
+    $this->issuer = $settings['issuer'];
+    $this->time = $time;
+
+    $this->encryptionProfile = $encryption_profile_manager->getEncryptionProfile($config_factory->get('tfa.settings')->get('encryption'));
+    $this->encryptService = $encrypt_service;
+
+    // Generate seed.
+    $this->setSeed($this->createSeed());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('user.data'),
+      $container->get('encrypt.encryption_profile.manager'),
+      $container->get('encryption'),
+      $container->get('config.factory'),
+      $container->get('datetime.time')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function ready() {
+    return ($this->getSeed() !== FALSE);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getForm(array $form, FormStateInterface $form_state) {
+    $message = $this->t('Verification code is application generated and @length digits long.', ['@length' => $this->codeLength]);
+    if ($this->getUserData('tfa', 'tfa_recovery_code', $this->uid)) {
+      $message .= '<br/>' . $this->t("Can't access your account? Use one of your recovery codes.");
+    }
+    $form['code'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Application verification code'),
+      '#description' => $message,
+      '#required'  => TRUE,
+      '#attributes' => [
+        'autocomplete' => 'off',
+        'autofocus' => 'autofocus',
+      ],
+    ];
+
+    $form['actions']['#type'] = 'actions';
+    $form['actions']['login'] = [
+      '#type' => 'submit',
+      '#button_type' => 'primary',
+      '#value' => $this->t('Verify'),
+    ];
+
+    return $form;
+  }
+
+  /**
+   * The configuration form for this validation plugin.
+   *
+   * @return array
+   *   Form array specific for this validation plugin.
+   */
+  public function buildConfigurationForm() {
+    $settings_form['time_skew'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Number of Accepted Codes'),
+      '#default_value' => $this->timeSkew,
+      '#description' => $this->t('Number of past codes to consider valid. Codes are generated every 30 seconds, so setting this value to 10 would allow each code to work for five minutes.'),
+      '#size' => 2,
+      '#required' => TRUE,
+    ];
+
+    $settings_form['site_name_prefix'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Use site name as OTP QR code name prefix.'),
+      '#default_value' => $this->siteNamePrefix,
+      '#description' => $this->t('If checked, the site name will be used instead of a static string. This can be useful for multi-site installations.'),
+    ];
+
+    $settings_form['name_prefix'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('OTP QR Code Prefix'),
+      '#default_value' => $this->namePrefix ?? 'tfa',
+      '#description' => $this->t('Prefix for OTP QR code names. Suffix is account username.'),
+      '#size' => 15,
+      '#states' => [
+        'visible' => [':input[name="validation_plugin_settings[tfa_totp][site_name_prefix]"]' => ['checked' => FALSE]],
+      ],
+    ];
+
+    $settings_form['issuer'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Issuer'),
+      '#default_value' => $this->issuer,
+      '#description' => $this->t('The provider or service this account is associated with.'),
+      '#size' => 15,
+      '#required' => TRUE,
+    ];
+
+    return $settings_form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array $form, FormStateInterface $form_state) {
+    $values = $form_state->getValues();
+    if (!$this->validate($values['code'])) {
+      $this->errorMessages['code'] = $this->t('Invalid application code. Please try again.');
+      if ($this->alreadyAccepted) {
+        $form_state->clearErrors();
+        $this->errorMessages['code'] = $this->t('Invalid code, it was recently used for a login. Please try a new code.');
+      }
+      return FALSE;
+    }
+    else {
+      // Store accepted code to prevent replay attacks.
+      $this->storeAcceptedCode($values['code']);
+      return TRUE;
+    }
+  }
+
+  /**
+   * Simple validate for web services.
+   *
+   * @param int $code
+   *   OTP Code.
+   *
+   * @return bool
+   *   True if validation was successful otherwise false.
+   */
+  public function validateRequest($code) {
+    if ($this->validate($code)) {
+      $this->storeAcceptedCode($code);
+      return TRUE;
+    }
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function validate($code) {
+    // Strip whitespace.
+    $code = preg_replace('/\s+/', '', $code);
+    if ($this->alreadyAcceptedCode($code)) {
+      $this->isValid = FALSE;
+    }
+    else {
+      // Get OTP seed.
+      $seed = $this->getSeed();
+      $this->isValid = ($seed && $this->auth->otp->checkTotp(Encoding::base32DecodeUpper($seed), $code, $this->timeSkew));
+    }
+    return $this->isValid;
+  }
+
+  /**
+   * Get seed for this account.
+   *
+   * @return string
+   *   Decrypted account OTP seed or FALSE if none exists.
+   */
+  protected function getSeed() {
+    // Lookup seed for account and decrypt.
+    $result = $this->getUserData('tfa', $this->pluginId . '_seed', $this->uid);
+
+    if (!empty($result)) {
+      $encrypted = base64_decode($result['seed']);
+      $seed = $this->encryptService->decrypt($encrypted, $this->encryptionProfile);
+      if (!empty($seed)) {
+        return $seed;
+      }
+    }
+    return FALSE;
+  }
+
+  /**
+   * Save seed for account.
+   *
+   * @param string $seed
+   *   Un-encrypted seed.
+   */
+  public function storeSeed($seed) {
+    // Encrypt seed for storage.
+    $encrypted = $this->encryptService->encrypt($seed, $this->encryptionProfile);
+
+    $record = [
+      $this->pluginId . '_seed' => [
+        'seed' => base64_encode($encrypted),
+        'created' => $this->time->getRequestTime(),
+      ],
+    ];
+
+    $this->setUserData('tfa', $record, $this->uid);
+  }
+
+  /**
+   * Delete the seed of the current validated user.
+   */
+  protected function deleteSeed() {
+    $this->deleteUserData('tfa', $this->pluginId . '_seed', $this->uid);
+  }
+
+  /**
+   * Get the value of the time-window in which the validation should be done.
+   *
+   * @return int
+   *   The current value of the time skew.
+   */
+  public function getTimeSkew() {
+    return $this->timeSkew;
+  }
+
+  // ================================== SETUP ==================================
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSetupForm(array $form, FormStateInterface $form_state) {
+    $help_links = $this->getHelpLinks();
+
+    $items = [];
+    foreach ($help_links as $item => $link) {
+      $items[] = Link::fromTextAndUrl($item, Url::fromUri($link, ['attributes' => ['target' => '_blank']]));
+    }
+
+    $form['apps'] = [
+      '#theme' => 'item_list',
+      '#items' => $items,
+      '#title' => $this->t('Install authentication code application on your mobile or desktop device:'),
+    ];
+    $form['info'] = [
+      '#type' => 'html_tag',
+      '#tag' => 'p',
+      '#value' => $this->t('The two-factor authentication application will be used during this setup and for generating codes during regular authentication. If the application supports it, scan the QR code below to get the setup code otherwise you can manually enter the text code.'),
+    ];
+    $form['seed'] = [
+      '#type' => 'textfield',
+      '#value' => $this->seed,
+      '#disabled' => TRUE,
+      '#description' => $this->t('Enter this code into your two-factor authentication app or scan the QR code below.'),
+    ];
+
+    // QR image of seed.
+    $form['qr_image'] = [
+      '#prefix' => '<div class="tfa-qr-code"',
+      '#theme' => 'image',
+      '#uri' => $this->getQrCodeUri(),
+      '#alt' => $this->t('QR code for TFA setup'),
+      '#suffix' => '</div>',
+    ];
+
+    // QR code css giving it a fixed width.
+    $form['page']['#attached']['html_head'][] = [
+      [
+        '#tag' => 'style',
+        '#value' => ".tfa-qr-code { width:200px }",
+      ],
+      'qrcode-css',
+    ];
+
+    // Include code entry form.
+    $form = $this->getForm($form, $form_state);
+    $form['actions']['login']['#value'] = $this->t('Verify and save');
+    // Alter code description.
+    $form['code']['#description'] = $this->t('A verification code will be generated after you scan the above QR code or manually enter the setup code. The verification code is six digits long.');
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateSetupForm(array $form, FormStateInterface $form_state) {
+    if (!$this->validateSetup($form_state->getValue('code'))) {
+      $this->errorMessages['code'] = $this->t('Invalid application code. Please try again.');
+      return FALSE;
+    }
+    $this->storeAcceptedCode($form_state->getValue('code'));
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function validateSetup($code) {
+    $code = preg_replace('/\s+/', '', $code);
+    return $this->auth->otp->checkTotp(Encoding::base32DecodeUpper($this->seed), $code, $this->timeSkew);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitSetupForm(array $form, FormStateInterface $form_state) {
+    // Write seed for user.
+    $this->storeSeed($this->seed);
+    return TRUE;
+  }
+
+  /**
+   * Get a base64 qrcode image uri of seed.
+   *
+   * @return string
+   *   QR-code uri.
+   */
+  protected function getQrCodeUri() {
+    return (new QRCode)->render('otpauth://totp/' . $this->accountName() . '?secret=' . $this->seed . '&issuer=' . urlencode($this->issuer));
+  }
+
+  /**
+   * Create OTP seed for account.
+   *
+   * @return string
+   *   Un-encrypted seed.
+   */
+  protected function createSeed() {
+    return $this->auth->ga->generateRandom();
+  }
+
+  /**
+   * Setter for OTP secret key.
+   *
+   * @param string $seed
+   *   The OTP secret key.
+   */
+  public function setSeed($seed) {
+    $this->seed = $seed;
+  }
+
+  /**
+   * Get account name for QR image.
+   *
+   * @return string
+   *   URL encoded string.
+   */
+  protected function accountName() {
+    /** @var \Drupal\user\Entity\User $account */
+    $account = User::load($this->configuration['uid']);
+    $prefix = $this->siteNamePrefix ? preg_replace('@[^a-z0-9-]+@', '-', strtolower(\Drupal::config('system.site')->get('name'))) : $this->namePrefix;
+    return urlencode((!empty($prefix) ? $prefix . '-' : '') . $account->getAccountName());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOverview(array $params) {
+    $plugin_text = $this->t('Validation Plugin: @plugin',
+      [
+        '@plugin' => str_replace(' Setup', '', $this->getLabel()),
+      ]
+    );
+    $output = [
+      'heading' => [
+        '#type' => 'html_tag',
+        '#tag' => 'h2',
+        '#value' => $this->t('TFA application'),
+      ],
+      'validation_plugin' => [
+        '#type' => 'markup',
+        '#markup' => '<p>' . $plugin_text . '</p>',
+      ],
+      'description' => [
+        '#type' => 'html_tag',
+        '#tag' => 'p',
+        '#value' => $this->t('Generate verification codes from a mobile or desktop application.'),
+      ],
+      'link' => [
+        '#theme' => 'links',
+        '#links' => [
+          'admin' => [
+            'title' => !$params['enabled'] ? $this->t('Set up application') : $this->t('Reset application'),
+            'url' => Url::fromRoute('tfa.validation.setup', [
+              'user' => $params['account']->id(),
+              'method' => $params['plugin_id'],
+            ]),
+          ],
+        ],
+      ],
+    ];
+    return $output;
+  }
+
+}
diff --git a/src/Plugin/Tfa/TfaTrustedBrowser.php b/src/Plugin/Tfa/TfaTrustedBrowser.php
new file mode 100644
index 0000000..141e9ff
--- /dev/null
+++ b/src/Plugin/Tfa/TfaTrustedBrowser.php
@@ -0,0 +1,473 @@
+<?php
+
+namespace Drupal\tfa\Plugin\Tfa;
+
+use Drupal\Component\Utility\Html;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Url;
+use Drupal\tfa\Plugin\TfaLoginInterface;
+use Drupal\tfa\Plugin\TfaSetupInterface;
+use Drupal\tfa\TfaPluginBase;
+use Drupal\user\UserDataInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Trusted browser validation class.
+ *
+ * @Tfa(
+ *   id = "tfa_trusted_browser",
+ *   label = @Translation("TFA Trusted Browser"),
+ *   description = @Translation("TFA Trusted Browser Plugin"),
+ *   setupMessages = {
+ *    "saved" = @Translation("Browser saved."),
+ *    "skipped" = @Translation("Browser not saved."),
+ *   }
+ * )
+ */
+class TfaTrustedBrowser extends TfaPluginBase implements TfaLoginInterface, TfaSetupInterface, ContainerFactoryPluginInterface {
+
+  /**
+   * Trust browser.
+   *
+   * @var bool
+   */
+  protected $trustBrowser;
+
+  /**
+   * The cookie name.
+   *
+   * @var string
+   */
+  protected $cookieName;
+
+  /**
+   * Cookie expiration time.
+   *
+   * @var string
+   */
+  protected $expiration;
+
+  /**
+   * Constructs a new Tfa plugin object.
+   *
+   * @param array $configuration
+   *   The plugin configuration.
+   * @param string $plugin_id
+   *   The plugin id.
+   * @param mixed $plugin_definition
+   *   The plugin definition.
+   * @param \Drupal\user\UserDataInterface $user_data
+   *   User data object to store user specific information.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The configuration factory.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, UserDataInterface $user_data, ConfigFactoryInterface $config_factory) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+
+    $config = $config_factory->get('tfa.settings');
+    $this->cookieName = $config->get('trust_cookie_name') ?: 'tfa-trusted-browser';
+    // Expiration defaults to 30 days.
+    $this->expiration = $config->get('trust_cookie_expiration') ?: 86400 * 30;
+    $this->userData = $user_data;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('user.data'),
+      $container->get('config.factory')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function loginAllowed() {
+    if (isset($_COOKIE[$this->cookieName]) && $this->trustedBrowser($_COOKIE[$this->cookieName]) !== FALSE) {
+      // Update browser last used time.
+      $id = $_COOKIE[$this->cookieName];
+      $result = $this->getUserData('tfa', 'tfa_trusted_browser', $this->uid);
+      $result[$id]['last_used'] = \Drupal::time()->getRequestTime();
+      $data = [
+        'tfa_trusted_browser' => $result,
+      ];
+      $this->setUserData('tfa', $data, $this->uid);
+      return TRUE;
+    }
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getForm(array $form, FormStateInterface $form_state) {
+    $form['trust_browser'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Remember this browser?'),
+      '#description' => $this->t('Not recommended if you are on a public or shared computer.'),
+    ];
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array $form, FormStateInterface $form_state) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array $form, FormStateInterface &$form_state) {
+    $trust_browser = $form_state->getValue('trust_browser');
+    if (!empty($trust_browser)) {
+      $this->setTrusted($this->generateBrowserId(), $this->getAgent());
+    }
+  }
+
+  /**
+   * Finalize the browser setup.
+   *
+   * @throws \Exception
+   */
+  public function finalize() {
+    if ($this->trustBrowser) {
+      $name = $this->getAgent();
+      $this->setTrusted($this->generateBrowserId(), $name);
+    }
+  }
+
+  /**
+   * Generate a random value to identify the browser.
+   *
+   * @return string
+   *   Base64 encoded browser id.
+   *
+   * @throws \Exception
+   */
+  protected function generateBrowserId() {
+    $id = base64_encode(random_bytes(32));
+    return strtr($id, ['+' => '-', '/' => '_', '=' => '']);
+  }
+
+  /**
+   * Store browser value and issue cookie for user.
+   *
+   * @param string $id
+   *   Trusted browser id.
+   * @param string $name
+   *   The custom browser name.
+   */
+  protected function setTrusted($id, $name = '') {
+    // Currently broken.
+    // Store id for account.
+    $records = $this->getUserData('tfa', 'tfa_trusted_browser', $this->configuration['uid']) ?: [];
+    $request_time = \Drupal::time()->getRequestTime();
+
+    $records[$id] = [
+      'created' => $request_time,
+      'ip' => \Drupal::request()->getClientIp(),
+      'name' => $name,
+    ];
+
+    $data = [
+      'tfa_trusted_browser' => $records,
+    ];
+
+    $this->setUserData('tfa', $data, $this->configuration['uid']);
+    // Issue cookie with ID.
+    $cookie_secure = ini_get('session.cookie_secure');
+    $expiration = $request_time + $this->expiration;
+    $domain = strpos($_SERVER['HTTP_HOST'], 'localhost') === FALSE ? $_SERVER['HTTP_HOST'] : FALSE;
+    setcookie($this->cookieName, $id, $expiration, '/', $domain, (empty($cookie_secure) ? FALSE : TRUE), TRUE);
+    $name = empty($name) ? $this->getAgent() : $name;
+    // @todo use services defined in module instead this procedural way.
+    \Drupal::logger('tfa')->info('Set trusted browser for user UID @uid, browser @name', [
+      '@name' => $name,
+      '@uid' => $this->uid,
+    ]);
+  }
+
+  /**
+   * Check if browser id matches user's saved browser.
+   *
+   * @param string $id
+   *   The browser ID.
+   *
+   * @return bool
+   *   TRUE if ID exists otherwise FALSE.
+   */
+  protected function trustedBrowser($id) {
+    // Check if $id has been saved for this user.
+    $result = $this->getUserData('tfa', 'tfa_trusted_browser', $this->uid);
+    if (isset($result[$id])) {
+      return TRUE;
+    }
+
+    return FALSE;
+  }
+
+  /**
+   * Delete users trusted browser.
+   *
+   * @param string $id
+   *   (optional) Id of the browser to be purged.
+   *
+   * @return bool
+   *   TRUE is id found and purged otherwise FALSE.
+   */
+  protected function deleteTrusted($id = '') {
+    $result = $this->getUserData('tfa', 'tfa_trusted_browser', $this->uid);
+    if ($id) {
+      if (isset($result[$id])) {
+        unset($result[$id]);
+        $data = [
+          'tfa_trusted_browser' => $result,
+        ];
+        $this->setUserData('tfa', $data, $this->uid);
+        return TRUE;
+      }
+    }
+    else {
+      $this->deleteUserData('tfa', 'tfa_trusted_browser', $this->uid);
+      return TRUE;
+    }
+
+    return FALSE;
+  }
+
+  /**
+   * Get simplified browser name from user agent.
+   *
+   * @param string $name
+   *   Default browser name.
+   *
+   * @return string
+   *   Simplified browser name.
+   */
+  protected function getAgent($name = '') {
+    if (isset($_SERVER['HTTP_USER_AGENT'])) {
+      // Match popular user agents.
+      $agent = $_SERVER['HTTP_USER_AGENT'];
+      if (preg_match("/like\sGecko\)\sChrome\//", $agent)) {
+        $name = 'Chrome';
+      }
+      elseif (strpos($_SERVER['HTTP_USER_AGENT'], 'Firefox') !== FALSE) {
+        $name = 'Firefox';
+      }
+      elseif (strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE') !== FALSE) {
+        $name = 'Internet Explorer';
+      }
+      elseif (strpos($_SERVER['HTTP_USER_AGENT'], 'Safari') !== FALSE) {
+        $name = 'Safari';
+      }
+      else {
+        // Otherwise filter agent and truncate to column size.
+        $name = substr($agent, 0, 255);
+      }
+    }
+    return $name;
+  }
+
+  // ================================== SETUP ==================================
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSetupForm(array $form, FormStateInterface $form_state) {
+    $existing = $this->getTrustedBrowsers();
+    $time = $this->expiration / 86400;
+    $form['info'] = [
+      '#type' => 'markup',
+      '#markup' => '<p>' . $this->t("Trusted browsers are a method for
+      simplifying login by avoiding verification code entry for a set amount of
+      time, @time days from marking a browser as trusted. After @time days, to
+      log in you'll need to enter a verification code with your username and
+      password during which you can again mark the browser as trusted.", ['@time' => $time]) . '</p>',
+    ];
+    // Present option to trust this browser if it's not currently trusted.
+    if (isset($_COOKIE[$this->cookieName]) && $this->trustedBrowser($_COOKIE[$this->cookieName]) !== FALSE) {
+      $current_trusted = $_COOKIE[$this->cookieName];
+    }
+    else {
+      $current_trusted = FALSE;
+      $form['trust'] = [
+        '#type' => 'checkbox',
+        '#title' => $this->t('Trust this browser?'),
+        '#default_value' => empty($existing) ? 1 : 0,
+      ];
+      // Optional field to name this browser.
+      $form['name'] = [
+        '#type' => 'textfield',
+        '#title' => $this->t('Name this browser'),
+        '#maxlength' => 255,
+        '#description' => $this->t('Optionally, name the browser on your browser (e.g.
+        "home firefox" or "office desktop windows"). Your current browser user
+        agent is %browser', ['%browser' => $_SERVER['HTTP_USER_AGENT']]),
+        '#default_value' => $this->getAgent(),
+        '#states' => [
+          'visible' => [
+            ':input[name="trust"]' => ['checked' => TRUE],
+          ],
+        ],
+      ];
+    }
+    if (!empty($existing)) {
+      $form['existing'] = [
+        '#type' => 'fieldset',
+        '#title' => $this->t('Existing browsers'),
+        '#description' => $this->t('Leave checked to keep these browsers in your trusted log in list.'),
+        '#tree' => TRUE,
+      ];
+
+      foreach ($existing as $browser_id => $browser) {
+        $date_formatter = \Drupal::service('date.formatter');
+        $vars = [
+          '@set' => $date_formatter->format($browser['created']),
+        ];
+
+        if (isset($browser['last_used'])) {
+          $vars['@time'] = $date_formatter->format($browser['last_used']);
+        }
+
+        if ($current_trusted == $browser_id) {
+          $name = '<strong>' . $this->t('@name (current browser)', ['@name' => $browser['name']]) . '</strong>';
+        }
+        else {
+          $name = Html::escape($browser['name']);
+        }
+
+        if (empty($browser['last_used'])) {
+          $message = $this->t('Marked trusted @set', $vars);
+        }
+        else {
+          $message = $this->t('Marked trusted @set, last used for log in @time', $vars);
+        }
+        $form['existing']['trusted_browser_' . $browser_id] = [
+          '#type' => 'checkbox',
+          '#title' => $name,
+          '#description' => $message,
+          '#default_value' => 1,
+        ];
+      }
+    }
+    $form['actions'] = ['#type' => 'actions'];
+    $form['actions']['save'] = [
+      '#type' => 'submit',
+      '#button_type' => 'primary',
+      '#value' => $this->t('Save'),
+    ];
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateSetupForm(array $form, FormStateInterface $form_state) {
+    // Do nothing, no validation required.
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitSetupForm(array $form, FormStateInterface $form_state) {
+    $values = $form_state->getValues();
+    if (isset($values['existing'])) {
+      $count = 0;
+      foreach ($values['existing'] as $element => $value) {
+        $id = str_replace('trusted_browser_', '', $element);
+        if (!$value) {
+          $this->deleteTrusted($id);
+          $count++;
+        }
+      }
+      if ($count) {
+        \Drupal::logger('tfa')->notice('Removed @num TFA trusted browsers during trusted browser setup', ['@num' => $count]);
+      }
+    }
+    if (!empty($values['trust']) && $values['trust']) {
+      $name = '';
+      if (!empty($values['name'])) {
+        $name = $values['name'];
+      }
+      elseif (isset($_SERVER['HTTP_USER_AGENT'])) {
+        $name = $this->getAgent();
+      }
+      $this->setTrusted($this->generateBrowserId(), $name);
+    }
+    return TRUE;
+  }
+
+  /**
+   * Get list of trusted browsers.
+   *
+   * @return array
+   *   List of current trusted browsers.
+   */
+  public function getTrustedBrowsers() {
+    return $this->getUserData('tfa', 'tfa_trusted_browser', $this->uid) ?: [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOverview(array $params) {
+    $trusted_browsers = [];
+    foreach ($this->getTrustedBrowsers() as $device) {
+      $date_formatter = \Drupal::service('date.formatter');
+      $vars = [
+        '@set' => $date_formatter->format($device['created']),
+        '@browser' => $device['name'],
+      ];
+      if (empty($device['last_used'])) {
+        $message = $this->t('@browser, set @set', $vars);
+      }
+      else {
+        $vars['@time'] = $date_formatter->format($device['last_used']);
+        $message = $this->t('@browser, set @set, last used @time', $vars);
+      }
+      $trusted_browsers[] = $message;
+    }
+    $output = [
+      'heading' => [
+        '#type' => 'html_tag',
+        '#tag' => 'h3',
+        '#value' => $this->t('Trusted browsers'),
+      ],
+      'description' => [
+        '#type' => 'html_tag',
+        '#tag' => 'p',
+        '#value' => $this->t('Browsers that will not require a verification code during login.'),
+      ],
+    ];
+    $output['list'] = [
+      '#theme' => 'item_list',
+      '#items' => $trusted_browsers,
+      '#empty' => $this->t('No trusted browsers found.'),
+    ];
+
+    $output['link'] = [
+      '#theme' => 'links',
+      '#links' => [
+        'admin' => [
+          'title' => $this->t('Configure trusted browsers'),
+          'url' => Url::fromRoute('tfa.validation.setup', [
+            'user' => $params['account']->id(),
+            'method' => $params['plugin_id'],
+          ]),
+        ],
+      ],
+    ];
+
+    return $output;
+  }
+
+}
diff --git a/src/Plugin/TfaBasePlugin.php b/src/Plugin/TfaBasePlugin.php
deleted file mode 100644
index a48fe7d..0000000
--- a/src/Plugin/TfaBasePlugin.php
+++ /dev/null
@@ -1,254 +0,0 @@
-<?php
-
-namespace Drupal\tfa\Plugin;
-
-use Drupal\Component\Plugin\PluginBase;
-use Drupal\Core\DependencyInjection\DependencySerializationTrait;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Site\Settings;
-use Drupal\encrypt\EncryptionProfileManagerInterface;
-use Drupal\encrypt\EncryptServiceInterface;
-use Drupal\tfa\TfaDataTrait;
-use Drupal\user\UserDataInterface;
-use Drupal\Component\Utility\Crypt;
-
-/**
- * Base plugin class.
- */
-abstract class TfaBasePlugin extends PluginBase {
-  use DependencySerializationTrait;
-  use TfaDataTrait;
-
-  /**
-   * The user submitted code to be validated.
-   *
-   * @var string
-   */
-  protected $code;
-
-  /**
-   * The allowed code length.
-   *
-   * @var int
-   */
-  protected $codeLength;
-
-  /**
-   * The error for the current validation.
-   *
-   * @var string[]
-   */
-  protected $errorMessages;
-
-  /**
-   * Whether the validation succeeded or not.
-   *
-   * @var bool
-   */
-  protected $isValid;
-
-  /**
-   * Whether the code has been used before.
-   *
-   * @var string
-   */
-  protected $alreadyAccepted;
-
-  /**
-   * Provides the user data service object.
-   *
-   * @var \Drupal\user\UserDataInterface
-   */
-  protected $userData;
-
-  /**
-   * The user id.
-   *
-   * @var int
-   */
-  protected $uid;
-
-  /**
-   * Encryption profile.
-   *
-   * @var \Drupal\encrypt\EncryptionProfileManagerInterface
-   */
-  protected $encryptionProfile;
-
-  /**
-   * Encryption service.
-   *
-   * @var \Drupal\encrypt\EncryptService
-   */
-  protected $encryptService;
-
-  /**
-   * Constructs a new Tfa plugin object.
-   *
-   * @param array $configuration
-   *   The plugin configuration.
-   * @param string $plugin_id
-   *   The plugin id.
-   * @param mixed $plugin_definition
-   *   The plugin definition.
-   * @param \Drupal\user\UserDataInterface $user_data
-   *   User data object to store user specific information.
-   * @param \Drupal\encrypt\EncryptionProfileManagerInterface $encryption_profile_manager
-   *   Encryption profile manager.
-   * @param \Drupal\encrypt\EncryptServiceInterface $encrypt_service
-   *   Encryption service.
-   */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, UserDataInterface $user_data, EncryptionProfileManagerInterface $encryption_profile_manager, EncryptServiceInterface $encrypt_service) {
-    parent::__construct($configuration, $plugin_id, $plugin_definition);
-    // Default code length is 6.
-    $this->codeLength = 6;
-    $this->isValid = FALSE;
-
-    // User Data service to store user-based data in key value pairs.
-    $this->userData = $user_data;
-
-    // Encryption profile manager and service.
-    $encryptionProfileId = \Drupal::config('tfa.settings')->get('encryption');
-    $this->encryptionProfile = $encryption_profile_manager->getEncryptionProfile($encryptionProfileId);
-    $this->encryptService = $encrypt_service;
-    $this->uid = $this->configuration['uid'];
-  }
-
-  /**
-   * Determine if the plugin can run for the current TFA context.
-   *
-   * @return bool
-   *   True or False based on the checks performed.
-   */
-  abstract public function ready();
-
-  /**
-   * Get error messages suitable for form_set_error().
-   *
-   * @return array
-   *   An array of error strings.
-   */
-  public function getErrorMessages() {
-    return $this->errorMessages;
-  }
-
-  /**
-   * Submit form.
-   *
-   * @param array $form
-   *   An associative array containing the structure of the form.
-   * @param \Drupal\Core\Form\FormStateInterface $form_state
-   *   The current state of the form.
-   *
-   * @return bool
-   *   Whether plugin form handling is complete. Plugins should return FALSE to
-   *   invoke multi-step.
-   */
-  public function submitForm(array $form, FormStateInterface &$form_state) {
-    return $this->isValid;
-  }
-
-  /**
-   * Validate code.
-   *
-   * Note, plugins overriding validate() should be sure to set isValid property
-   * correctly or else also override submitForm().
-   *
-   * @param string $code
-   *   Code to be validated.
-   *
-   * @return bool
-   *   Whether code is valid.
-   */
-  protected function validate($code) {
-    if (hash_equals((string) $code, (string) $this->code)) {
-      $this->isValid = TRUE;
-      return TRUE;
-    }
-    else {
-      return FALSE;
-    }
-  }
-
-  /**
-   * Encrypt a plaintext string.
-   *
-   * Should be used when writing codes to storage.
-   *
-   * @param string $data
-   *   The string to be encrypted.
-   *
-   * @return string
-   *   The encrypted string.
-   *
-   * @throws \Drupal\encrypt\Exception\EncryptException
-   */
-  protected function encrypt($data) {
-    return $this->encryptService->encrypt($data, $this->encryptionProfile);
-  }
-
-  /**
-   * Decrypt a encrypted string.
-   *
-   * Should be used when reading codes from storage.
-   *
-   * @param string $data
-   *   The string to be decrypted.
-   *
-   * @return string
-   *   The decrypted string.
-   *
-   * @throws \Drupal\encrypt\Exception\EncryptionMethodCanNotDecryptException
-   * @throws \Drupal\encrypt\Exception\EncryptException
-   */
-  protected function decrypt($data) {
-    return $this->encryptService->decrypt($data, $this->encryptionProfile);
-  }
-
-  /**
-   * Store validated code to prevent replay attack.
-   *
-   * @param string $code
-   *   The validated code.
-   */
-  protected function storeAcceptedCode($code) {
-    $code = preg_replace('/\s+/', '', $code);
-    $hash = Crypt::hashBase64(Settings::getHashSalt() . $code);
-
-    // Store the hash made using the code in users_data.
-    $store_data = ['tfa_accepted_code_' . $hash => \Drupal::time()->getRequestTime()];
-    $this->setUserData('tfa', $store_data, $this->uid, $this->userData);
-  }
-
-  /**
-   * Whether code has already been used.
-   *
-   * @param string $code
-   *   The code to be checked.
-   *
-   * @return bool
-   *   TRUE if already used otherwise FALSE
-   */
-  protected function alreadyAcceptedCode($code) {
-    $hash = Crypt::hashBase64(Settings::getHashSalt() . $code);
-    // Check if the code has already been used or not.
-    $key    = 'tfa_accepted_code_' . $hash;
-    $result = $this->getUserData('tfa', $key, $this->uid, $this->userData);
-    if (!empty($result)) {
-      $this->alreadyAccepted = TRUE;
-      return TRUE;
-    }
-    return FALSE;
-  }
-
-  /**
-   * Get the plugin label.
-   *
-   * @return string
-   *   The plugin label.
-   */
-  public function getLabel() {
-    return ($this->pluginDefinition['label']) ?: '';
-  }
-
-}
diff --git a/src/Plugin/TfaLogin/TfaTrustedBrowser.php b/src/Plugin/TfaLogin/TfaTrustedBrowser.php
deleted file mode 100644
index 0260a44..0000000
--- a/src/Plugin/TfaLogin/TfaTrustedBrowser.php
+++ /dev/null
@@ -1,265 +0,0 @@
-<?php
-
-namespace Drupal\tfa\Plugin\TfaLogin;
-
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\StringTranslation\StringTranslationTrait;
-use Drupal\encrypt\EncryptionProfileManagerInterface;
-use Drupal\encrypt\EncryptServiceInterface;
-use Drupal\tfa\Plugin\TfaBasePlugin;
-use Drupal\tfa\Plugin\TfaLoginInterface;
-use Drupal\tfa\Plugin\TfaValidationInterface;
-use Drupal\user\UserDataInterface;
-
-/**
- * Trusted browser validation class.
- *
- * @TfaLogin(
- *   id = "tfa_trusted_browser",
- *   label = @Translation("TFA Trusted Browser"),
- *   description = @Translation("TFA Trusted Browser Plugin"),
- *   setupPluginId = "tfa_trusted_browser_setup",
- * )
- */
-class TfaTrustedBrowser extends TfaBasePlugin implements TfaLoginInterface, TfaValidationInterface {
-  use StringTranslationTrait;
-
-  /**
-   * Trust browser.
-   *
-   * @var bool
-   */
-  protected $trustBrowser;
-
-  /**
-   * The cookie name.
-   *
-   * @var string
-   */
-  protected $cookieName;
-
-  /**
-   * Cookie expiration time.
-   *
-   * @var string
-   */
-  protected $expiration;
-
-  /**
-   * {@inheritdoc}
-   */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, UserDataInterface $user_data, EncryptionProfileManagerInterface $encryption_profile_manager, EncryptServiceInterface $encrypt_service) {
-    parent::__construct($configuration, $plugin_id, $plugin_definition, $user_data, $encryption_profile_manager, $encrypt_service);
-    $config = \Drupal::config('tfa.settings');
-    $this->cookieName = $config->get('trust_cookie_name') ?: 'tfa-trusted-browser';
-    // Expiration defaults to 30 days.
-    $this->expiration = $config->get('trust_cookie_expiration') ?: 86400 * 30;
-    $this->userData = $user_data;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function loginAllowed() {
-    if (isset($_COOKIE[$this->cookieName]) && $this->trustedBrowser($_COOKIE[$this->cookieName]) !== FALSE) {
-      $this->setUsed($_COOKIE[$this->cookieName]);
-      return TRUE;
-    }
-    return FALSE;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getForm(array $form, FormStateInterface $form_state) {
-    $form['trust_browser'] = [
-      '#type' => 'checkbox',
-      '#title' => $this->t('Remember this browser?'),
-      '#description' => $this->t('Not recommended if you are on a public or shared computer.'),
-    ];
-    return $form;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function validateForm(array $form, FormStateInterface $form_state) {
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function submitForm(array $form, FormStateInterface &$form_state) {
-    $trust_browser = $form_state->getValue('trust_browser');
-    if (!empty($trust_browser)) {
-      $this->setTrusted($this->generateBrowserId(), $this->getAgent());
-    }
-  }
-
-  /**
-   * Finalize the browser setup.
-   *
-   * @throws \Exception
-   */
-  public function finalize() {
-    if ($this->trustBrowser) {
-      $name = $this->getAgent();
-      $this->setTrusted($this->generateBrowserId(), $name);
-    }
-  }
-
-  /**
-   * Generate a random value to identify the browser.
-   *
-   * @return string
-   *   Base64 encoded browser id.
-   *
-   * @throws \Exception
-   */
-  protected function generateBrowserId() {
-    $id = base64_encode(random_bytes(32));
-    return strtr($id, ['+' => '-', '/' => '_', '=' => '']);
-  }
-
-  /**
-   * Store browser value and issue cookie for user.
-   *
-   * @param string $id
-   *   Trusted browser id.
-   * @param string $name
-   *   The custom browser name.
-   */
-  protected function setTrusted($id, $name = '') {
-    // Currently broken.
-    // Store id for account.
-    $records = $this->getUserData('tfa', 'tfa_trusted_browser', $this->configuration['uid'], $this->userData) ?: [];
-    $request_time = \Drupal::time()->getRequestTime();
-
-    $records[$id] = [
-      'created' => $request_time,
-      'ip' => \Drupal::request()->getClientIp(),
-      'name' => $name,
-    ];
-
-    $data = [
-      'tfa_trusted_browser' => $records,
-    ];
-
-    $this->setUserData('tfa', $data, $this->configuration['uid'], $this->userData);
-    // Issue cookie with ID.
-    $cookie_secure = ini_get('session.cookie_secure');
-    $expiration = $request_time + $this->expiration;
-    $domain = strpos($_SERVER['HTTP_HOST'], 'localhost') === FALSE ? $_SERVER['HTTP_HOST'] : FALSE;
-    setcookie($this->cookieName, $id, $expiration, '/', $domain, (empty($cookie_secure) ? FALSE : TRUE), TRUE);
-    $name = empty($name) ? $this->getAgent() : $name;
-    // @todo use services defined in module instead this procedural way.
-    \Drupal::logger('tfa')->info('Set trusted browser for user UID @uid, browser @name', [
-      '@name' => $name,
-      '@uid' => $this->uid,
-    ]);
-  }
-
-  /**
-   * Updated browser last used time.
-   *
-   * @param int $id
-   *   Internal browser ID to update.
-   */
-  protected function setUsed($id) {
-    $result = $this->getUserData('tfa', 'tfa_trusted_browser', $this->uid, $this->userData);
-    $result[$id]['last_used'] = \Drupal::time()->getRequestTime();
-    $data = [
-      'tfa_trusted_browser' => $result,
-    ];
-    $this->setUserData('tfa', $data, $this->uid, $this->userData);
-  }
-
-  /**
-   * Check if browser id matches user's saved browser.
-   *
-   * @param string $id
-   *   The browser ID.
-   *
-   * @return bool
-   *   TRUE if ID exists otherwise FALSE.
-   */
-  protected function trustedBrowser($id) {
-    // Check if $id has been saved for this user.
-    $result = $this->getUserData('tfa', 'tfa_trusted_browser', $this->uid, $this->userData);
-    if (isset($result[$id])) {
-      return TRUE;
-    }
-
-    return FALSE;
-  }
-
-  /**
-   * Delete users trusted browser.
-   *
-   * @param string $id
-   *   (optional) Id of the browser to be purged.
-   *
-   * @return bool
-   *   TRUE is id found and purged otherwise FALSE.
-   */
-  protected function deleteTrusted($id = '') {
-    $result = $this->getUserData('tfa', 'tfa_trusted_browser', $this->uid, $this->userData);
-    if ($id) {
-      if (isset($result[$id])) {
-        unset($result[$id]);
-        $data = [
-          'tfa_trusted_browser' => $result,
-        ];
-        $this->setUserData('tfa', $data, $this->uid, $this->userData);
-        return TRUE;
-      }
-    }
-    else {
-      $this->deleteUserData('tfa', 'tfa_trusted_browser', $this->uid, $this->userData);
-      return TRUE;
-    }
-
-    return FALSE;
-  }
-
-  /**
-   * Get simplified browser name from user agent.
-   *
-   * @param string $name
-   *   Default browser name.
-   *
-   * @return string
-   *   Simplified browser name.
-   */
-  protected function getAgent($name = '') {
-    if (isset($_SERVER['HTTP_USER_AGENT'])) {
-      // Match popular user agents.
-      $agent = $_SERVER['HTTP_USER_AGENT'];
-      if (preg_match("/like\sGecko\)\sChrome\//", $agent)) {
-        $name = 'Chrome';
-      }
-      elseif (strpos($_SERVER['HTTP_USER_AGENT'], 'Firefox') !== FALSE) {
-        $name = 'Firefox';
-      }
-      elseif (strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE') !== FALSE) {
-        $name = 'Internet Explorer';
-      }
-      elseif (strpos($_SERVER['HTTP_USER_AGENT'], 'Safari') !== FALSE) {
-        $name = 'Safari';
-      }
-      else {
-        // Otherwise filter agent and truncate to column size.
-        $name = substr($agent, 0, 255);
-      }
-    }
-    return $name;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function ready() {
-    return TRUE;
-  }
-
-}
diff --git a/src/Plugin/TfaLoginInterface.php b/src/Plugin/TfaLoginInterface.php
index f092005..62f34c2 100644
--- a/src/Plugin/TfaLoginInterface.php
+++ b/src/Plugin/TfaLoginInterface.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\tfa\Plugin;
 
+use Drupal\Core\Form\FormStateInterface;
+
 /**
  * Interface TfaLoginInterface.
  *
@@ -10,6 +12,32 @@ namespace Drupal\tfa\Plugin;
  */
 interface TfaLoginInterface {
 
+  /**
+   * Get TFA process form from plugin.
+   *
+   * @param array $form
+   *   The configuration form array.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return array
+   *   Form API array.
+   */
+  public function getForm(array $form, FormStateInterface $form_state);
+
+  /**
+   * Validate form.
+   *
+   * @param array $form
+   *   The configuration form array.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return bool
+   *   Whether form passes validation or not
+   */
+  public function validateForm(array $form, FormStateInterface $form_state);
+
   /**
    * Whether login is allowed.
    *
diff --git a/src/Plugin/TfaSetup/TfaHotpSetup.php b/src/Plugin/TfaSetup/TfaHotpSetup.php
deleted file mode 100644
index fd2dc3b..0000000
--- a/src/Plugin/TfaSetup/TfaHotpSetup.php
+++ /dev/null
@@ -1,239 +0,0 @@
-<?php
-
-namespace Drupal\tfa\Plugin\TfaSetup;
-
-use Drupal\Component\Datetime\TimeInterface;
-use Drupal\Core\Config\ConfigFactoryInterface;
-use Drupal\tfa\Plugin\TfaValidation\TfaHotpValidation;
-use ParagonIE\ConstantTime\Encoding;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Link;
-use Drupal\Core\Url;
-use Drupal\encrypt\EncryptionProfileManagerInterface;
-use Drupal\encrypt\EncryptServiceInterface;
-use Drupal\tfa\Plugin\TfaSetupInterface;
-use Drupal\user\Entity\User;
-use Drupal\user\UserDataInterface;
-use chillerlan\QRCode\QRCode;
-
-/**
- * HOTP setup class to setup HOTP validation.
- *
- * @TfaSetup(
- *   id = "tfa_hotp_setup",
- *   label = @Translation("TFA HOTP Setup"),
- *   description = @Translation("TFA HOTP Setup Plugin"),
- *   helpLinks = {
- *    "Google Authenticator (Android/iOS)" = "https://googleauthenticator.net",
- *    "Microsoft Authenticator (Android/iOS)" = "https://www.microsoft.com/en-us/security/mobile-authenticator-app",
- *    "FreeOTP (Android)" = "https://freeotp.github.io"
- *   },
- *   setupMessages = {
- *    "saved" = @Translation("Application code verified."),
- *    "skipped" = @Translation("Application codes not enabled.")
- *   }
- * )
- */
-class TfaHotpSetup extends TfaHotpValidation implements TfaSetupInterface {
-
-  /**
-   * Un-encrypted seed.
-   *
-   * @var string
-   */
-  protected $seed;
-
-  /**
-   * {@inheritdoc}
-   */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, UserDataInterface $user_data, EncryptionProfileManagerInterface $encryption_profile_manager, EncryptServiceInterface $encrypt_service, ConfigFactoryInterface $config_factory, TimeInterface $time) {
-    parent::__construct($configuration, $plugin_id, $plugin_definition, $user_data, $encryption_profile_manager, $encrypt_service, $config_factory, $time);
-    // Generate seed.
-    $this->setSeed($this->createSeed());
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getSetupForm(array $form, FormStateInterface $form_state) {
-    $help_links = $this->getHelpLinks();
-
-    $items = [];
-    foreach ($help_links as $item => $link) {
-      $items[] = Link::fromTextAndUrl($item, Url::fromUri($link, ['attributes' => ['target' => '_blank']]));
-    }
-
-    $form['apps'] = [
-      '#theme' => 'item_list',
-      '#items' => $items,
-      '#title' => $this->t('Install authentication code application on your mobile or desktop device:'),
-    ];
-    $form['info'] = [
-      '#type' => 'html_tag',
-      '#tag' => 'p',
-      '#value' => $this->t('The two-factor authentication application will be used during this setup and for generating codes during regular authentication. If the application supports it, scan the QR code below to get the setup code otherwise you can manually enter the text code.'),
-    ];
-    $form['seed'] = [
-      '#type' => 'textfield',
-      '#value' => $this->seed,
-      '#disabled' => TRUE,
-      '#description' => $this->t('Enter this code into your two-factor authentication app or scan the QR code below.'),
-    ];
-
-    // QR image of seed.
-    $form['qr_image'] = [
-      '#prefix' => '<div class="tfa-qr-code"',
-      '#theme' => 'image',
-      '#uri' => $this->getQrCodeUri(),
-      '#alt' => $this->t('QR code for TFA setup'),
-      '#suffix' => '</div>',
-    ];
-
-    // QR code css giving it a fixed width.
-    $form['page']['#attached']['html_head'][] = [
-      [
-        '#tag' => 'style',
-        '#value' => ".tfa-qr-code { width:200px }",
-      ],
-      'qrcode-css',
-    ];
-
-    // Include code entry form.
-    $form = $this->getForm($form, $form_state);
-    $form['actions']['login']['#value'] = $this->t('Verify and save');
-    // Alter code description.
-    $form['code']['#description'] = $this->t('A verification code will be generated after you scan the above QR code or manually enter the setup code. The verification code is six digits long.');
-    return $form;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function validateSetupForm(array $form, FormStateInterface $form_state) {
-    if (!$this->validate($form_state->getValue('code'))) {
-      $this->errorMessages['code'] = $this->t('Invalid application code. Please try again.');
-      return FALSE;
-    }
-    $this->storeAcceptedCode($form_state->getValue('code'));
-    return TRUE;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function validate($code) {
-    // The counter is set as 1 because that is the initial value.
-    // This ensures that things work even if we reset the application.
-    $code = preg_replace('/\s+/', '', $code);
-    $counter = $this->auth->otp->checkHotpResync(Encoding::base32DecodeUpper($this->seed), 1, $code, $this->counterWindow);
-    $this->setUserData('tfa', ['tfa_hotp_counter' => ++$counter], $this->uid, $this->userData);
-    return ((bool) $counter);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function submitSetupForm(array $form, FormStateInterface $form_state) {
-    // Write seed for user.
-    $this->storeSeed($this->seed);
-    return TRUE;
-  }
-
-  /**
-   * Get a base64 qrcode image uri of seed.
-   *
-   * @return string
-   *   QR-code uri.
-   */
-  protected function getQrCodeUri() {
-    return (new QRCode)->render('otpauth://hotp/' . $this->accountName() . '?secret=' . $this->seed . '&counter=1&issuer=' . urlencode($this->issuer));
-  }
-
-  /**
-   * Create OTP seed for account.
-   *
-   * @return string
-   *   Un-encrypted seed.
-   */
-  protected function createSeed() {
-    return $this->auth->ga->generateRandom();
-  }
-
-  /**
-   * Setter for OTP secret key.
-   *
-   * @param string $seed
-   *   The OTP secret key.
-   */
-  public function setSeed($seed) {
-    $this->seed = $seed;
-  }
-
-  /**
-   * Get account name for QR image.
-   *
-   * @return string
-   *   URL encoded string.
-   */
-  protected function accountName() {
-    /** @var \Drupal\user\Entity\User $account */
-    $account = User::load($this->configuration['uid']);
-    $prefix = $this->siteNamePrefix ? preg_replace('@[^a-z0-9-]+@', '-', strtolower(\Drupal::config('system.site')->get('name'))) : $this->namePrefix;
-    return urlencode((!empty($prefix) ? $prefix . '-' : '') . $account->getAccountName());
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getOverview(array $params) {
-    $plugin_text = $this->t('Validation Plugin: @plugin',
-      [
-        '@plugin' => str_replace(' Setup', '', $this->getLabel()),
-      ]
-    );
-    $output = [
-      'heading' => [
-        '#type' => 'html_tag',
-        '#tag' => 'h2',
-        '#value' => $this->t('TFA application'),
-      ],
-      'validation_plugin' => [
-        '#type' => 'markup',
-        '#markup' => '<p>' . $plugin_text . '</p>',
-      ],
-      'description' => [
-        '#type' => 'html_tag',
-        '#tag' => 'p',
-        '#value' => $this->t('Generate verification codes from a mobile or desktop application.'),
-      ],
-      'link' => [
-        '#theme' => 'links',
-        '#links' => [
-          'admin' => [
-            'title' => !$params['enabled'] ? $this->t('Set up application') : $this->t('Reset application'),
-            'url' => Url::fromRoute('tfa.validation.setup', [
-              'user' => $params['account']->id(),
-              'method' => $params['plugin_id'],
-            ]),
-          ],
-        ],
-      ],
-    ];
-    return $output;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getHelpLinks() {
-    return $this->pluginDefinition['helpLinks'];
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getSetupMessages() {
-    return ($this->pluginDefinition['setupMessages']) ?: '';
-  }
-
-}
diff --git a/src/Plugin/TfaSetup/TfaRecoveryCodeSetup.php b/src/Plugin/TfaSetup/TfaRecoveryCodeSetup.php
deleted file mode 100644
index 60ec4b8..0000000
--- a/src/Plugin/TfaSetup/TfaRecoveryCodeSetup.php
+++ /dev/null
@@ -1,162 +0,0 @@
-<?php
-
-namespace Drupal\tfa\Plugin\TfaSetup;
-
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Url;
-use Drupal\tfa\Plugin\TfaSetupInterface;
-use Drupal\tfa\Plugin\TfaValidation\TfaRecoveryCode;
-
-/**
- * TFA Recovery Code Setup Plugin.
- *
- * @TfaSetup(
- *   id = "tfa_recovery_code_setup",
- *   label = @Translation("TFA Recovery Code Setup"),
- *   description = @Translation("TFA Recovery Code Setup Plugin"),
- *   setupMessages = {
- *    "saved" = @Translation("Recovery codes saved."),
- *    "skipped" = @Translation("Recovery codes not saved.")
- *   }
- * )
- */
-class TfaRecoveryCodeSetup extends TfaRecoveryCode implements TfaSetupInterface {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function ready() {
-    return TRUE;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getOverview(array $params) {
-    return [
-      'heading' => [
-        '#type' => 'html_tag',
-        '#tag' => 'h3',
-        '#value' => $this->t('Recovery Codes'),
-      ],
-      'description' => [
-        '#type' => 'html_tag',
-        '#tag' => 'p',
-        '#value' => $this->t('Generate one-time use codes for two-factor login. These are generally used to recover your account in case you lose access to another 2nd-factor device.'),
-      ],
-      'setup' => [
-        '#theme' => 'links',
-        '#links' => [
-          'reset' => [
-            'title' => !$params['enabled'] ? $this->t('Generate codes') : $this->t('Reset codes'),
-            'url' => Url::fromRoute('tfa.plugin.reset', [
-              'user' => $params['account']->id(),
-              'method' => $params['plugin_id'],
-              'reset' => 1,
-            ]),
-          ],
-        ],
-      ],
-      'show_codes' => [
-        '#theme' => 'links',
-        '#access' => $params['enabled'],
-        '#links' => [
-          'show' => [
-            'title' => $this->t('Show codes'),
-            'url' => Url::fromRoute('tfa.validation.setup', [
-              'user' => $params['account']->id(),
-              'method' => $params['plugin_id'],
-            ]),
-          ],
-        ],
-      ],
-    ];
-  }
-
-  /**
-   * Get the setup form for the validation method.
-   *
-   * @param array $form
-   *   The configuration form array.
-   * @param \Drupal\Core\Form\FormStateInterface $form_state
-   *   The current state of the form.
-   * @param int $reset
-   *   Whether or not the user is resetting the application.
-   *
-   * @return array
-   *   Form API array.
-   */
-  public function getSetupForm(array $form, FormStateInterface $form_state, $reset = 0) {
-    $codes = $this->getCodes();
-
-    // If $reset has a value, we're setting up new codes.
-    if (!empty($reset)) {
-      $codes = $this->generateCodes();
-
-      // Make the human friendly.
-      foreach ($codes as $key => $code) {
-        $codes[$key] = implode(' ', str_split($code, 3));
-      }
-      $form['recovery_codes'] = [
-        '#type' => 'value',
-        '#value' => $codes,
-      ];
-    }
-
-    $form['recovery_codes_output'] = [
-      '#title' => $this->t('Recovery Codes'),
-      '#theme' => 'item_list',
-      '#items' => $codes,
-    ];
-    $form['description'] = [
-      '#type' => 'html_tag',
-      '#tag' => 'p',
-      '#value' => $this->t('Print or copy these codes and store them somewhere safe before continuing.'),
-    ];
-
-    if (!empty($reset)) {
-      $form['actions'] = ['#type' => 'actions'];
-      $form['actions']['save'] = [
-        '#type' => 'submit',
-        '#button_type' => 'primary',
-        '#value' => $this->t('Save codes to account'),
-      ];
-    }
-
-    return $form;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function validateSetupForm(array $form, FormStateInterface $form_state) {
-    if (!empty($form_state->getValue('recovery_codes'))) {
-      return TRUE;
-    }
-
-    return FALSE;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function submitSetupForm(array $form, FormStateInterface $form_state) {
-    $this->storeCodes($form_state->getValue('recovery_codes'));
-    return TRUE;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getHelpLinks() {
-    return [];
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getSetupMessages() {
-    return ($this->pluginDefinition['setupMessages']) ?: [];
-  }
-
-}
diff --git a/src/Plugin/TfaSetup/TfaTotpSetup.php b/src/Plugin/TfaSetup/TfaTotpSetup.php
deleted file mode 100644
index fc7bd89..0000000
--- a/src/Plugin/TfaSetup/TfaTotpSetup.php
+++ /dev/null
@@ -1,237 +0,0 @@
-<?php
-
-namespace Drupal\tfa\Plugin\TfaSetup;
-
-use Drupal\Component\Datetime\TimeInterface;
-use Drupal\Core\Config\ConfigFactoryInterface;
-use Drupal\tfa\Plugin\TfaValidation\TfaTotpValidation;
-use ParagonIE\ConstantTime\Encoding;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Link;
-use Drupal\Core\Url;
-use Drupal\encrypt\EncryptionProfileManagerInterface;
-use Drupal\encrypt\EncryptServiceInterface;
-use Drupal\tfa\Plugin\TfaSetupInterface;
-use Drupal\user\Entity\User;
-use Drupal\user\UserDataInterface;
-use chillerlan\QRCode\QRCode;
-
-/**
- * TOTP setup class to setup TOTP validation.
- *
- * @TfaSetup(
- *   id = "tfa_totp_setup",
- *   label = @Translation("TFA TOTP Setup"),
- *   description = @Translation("TFA TOTP Setup Plugin"),
- *   helpLinks = {
- *    "Google Authenticator (Android/iOS)" = "https://googleauthenticator.net",
- *    "Microsoft Authenticator (Android/iOS)" = "https://www.microsoft.com/en-us/security/mobile-authenticator-app",
- *    "Authy (Android/iOS/Desktop)" = "https://authy.com",
- *    "FreeOTP (Android/iOS)" = "https://freeotp.github.io",
- *    "GAuth Authenticator (Desktop)" = "https://github.com/gbraadnl/gauth"
- *   },
- *   setupMessages = {
- *    "saved" = @Translation("Application code verified."),
- *    "skipped" = @Translation("Application codes not enabled.")
- *   }
- * )
- */
-class TfaTotpSetup extends TfaTotpValidation implements TfaSetupInterface {
-
-  /**
-   * Un-encrypted seed.
-   *
-   * @var string
-   */
-  protected $seed;
-
-  /**
-   * {@inheritdoc}
-   */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, UserDataInterface $user_data, EncryptionProfileManagerInterface $encryption_profile_manager, EncryptServiceInterface $encrypt_service, ConfigFactoryInterface $config_factory, TimeInterface $time) {
-    parent::__construct($configuration, $plugin_id, $plugin_definition, $user_data, $encryption_profile_manager, $encrypt_service, $config_factory, $time);
-    // Generate seed.
-    $this->setSeed($this->createSeed());
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getSetupForm(array $form, FormStateInterface $form_state) {
-    $help_links = $this->getHelpLinks();
-
-    $items = [];
-    foreach ($help_links as $item => $link) {
-      $items[] = Link::fromTextAndUrl($item, Url::fromUri($link, ['attributes' => ['target' => '_blank']]));
-    }
-
-    $form['apps'] = [
-      '#theme' => 'item_list',
-      '#items' => $items,
-      '#title' => $this->t('Install authentication code application on your mobile or desktop device:'),
-    ];
-    $form['info'] = [
-      '#type' => 'html_tag',
-      '#tag' => 'p',
-      '#value' => $this->t('The two-factor authentication application will be used during this setup and for generating codes during regular authentication. If the application supports it, scan the QR code below to get the setup code otherwise you can manually enter the text code.'),
-    ];
-    $form['seed'] = [
-      '#type' => 'textfield',
-      '#value' => $this->seed,
-      '#disabled' => TRUE,
-      '#description' => $this->t('Enter this code into your two-factor authentication app or scan the QR code below.'),
-    ];
-
-    // QR image of seed.
-    $form['qr_image'] = [
-      '#prefix' => '<div class="tfa-qr-code"',
-      '#theme' => 'image',
-      '#uri' => $this->getQrCodeUri(),
-      '#alt' => $this->t('QR code for TFA setup'),
-      '#suffix' => '</div>',
-    ];
-
-    // QR code css giving it a fixed width.
-    $form['page']['#attached']['html_head'][] = [
-      [
-        '#tag' => 'style',
-        '#value' => ".tfa-qr-code { width:200px }",
-      ],
-      'qrcode-css',
-    ];
-
-    // Include code entry form.
-    $form = $this->getForm($form, $form_state);
-    $form['actions']['login']['#value'] = $this->t('Verify and save');
-    // Alter code description.
-    $form['code']['#description'] = $this->t('A verification code will be generated after you scan the above QR code or manually enter the setup code. The verification code is six digits long.');
-    return $form;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function validateSetupForm(array $form, FormStateInterface $form_state) {
-    if (!$this->validate($form_state->getValue('code'))) {
-      $this->errorMessages['code'] = $this->t('Invalid application code. Please try again.');
-      return FALSE;
-    }
-    $this->storeAcceptedCode($form_state->getValue('code'));
-    return TRUE;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function validate($code) {
-    $code = preg_replace('/\s+/', '', $code);
-    return $this->auth->otp->checkTotp(Encoding::base32DecodeUpper($this->seed), $code, $this->timeSkew);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function submitSetupForm(array $form, FormStateInterface $form_state) {
-    // Write seed for user.
-    $this->storeSeed($this->seed);
-    return TRUE;
-  }
-
-  /**
-   * Get a base64 qrcode image uri of seed.
-   *
-   * @return string
-   *   QR-code uri.
-   */
-  protected function getQrCodeUri() {
-    return (new QRCode)->render('otpauth://totp/' . $this->accountName() . '?secret=' . $this->seed . '&issuer=' . urlencode($this->issuer));
-  }
-
-  /**
-   * Create OTP seed for account.
-   *
-   * @return string
-   *   Un-encrypted seed.
-   */
-  protected function createSeed() {
-    return $this->auth->ga->generateRandom();
-  }
-
-  /**
-   * Setter for OTP secret key.
-   *
-   * @param string $seed
-   *   The OTP secret key.
-   */
-  public function setSeed($seed) {
-    $this->seed = $seed;
-  }
-
-  /**
-   * Get account name for QR image.
-   *
-   * @return string
-   *   URL encoded string.
-   */
-  protected function accountName() {
-    /** @var \Drupal\user\Entity\User $account */
-    $account = User::load($this->configuration['uid']);
-    $prefix = $this->siteNamePrefix ? preg_replace('@[^a-z0-9-]+@', '-', strtolower(\Drupal::config('system.site')->get('name'))) : $this->namePrefix;
-    return urlencode((!empty($prefix) ? $prefix . '-' : '') . $account->getAccountName());
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getOverview(array $params) {
-    $plugin_text = $this->t('Validation Plugin: @plugin',
-      [
-        '@plugin' => str_replace(' Setup', '', $this->getLabel()),
-      ]
-    );
-    $output = [
-      'heading' => [
-        '#type' => 'html_tag',
-        '#tag' => 'h2',
-        '#value' => $this->t('TFA application'),
-      ],
-      'validation_plugin' => [
-        '#type' => 'markup',
-        '#markup' => '<p>' . $plugin_text . '</p>',
-      ],
-      'description' => [
-        '#type' => 'html_tag',
-        '#tag' => 'p',
-        '#value' => $this->t('Generate verification codes from a mobile or desktop application.'),
-      ],
-      'link' => [
-        '#theme' => 'links',
-        '#links' => [
-          'admin' => [
-            'title' => !$params['enabled'] ? $this->t('Set up application') : $this->t('Reset application'),
-            'url' => Url::fromRoute('tfa.validation.setup', [
-              'user' => $params['account']->id(),
-              'method' => $params['plugin_id'],
-            ]),
-          ],
-        ],
-      ],
-    ];
-    return $output;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getHelpLinks() {
-    return $this->pluginDefinition['helpLinks'];
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getSetupMessages() {
-    return ($this->pluginDefinition['setupMessages']) ?: '';
-  }
-
-}
diff --git a/src/Plugin/TfaSetup/TfaTrustedBrowserSetup.php b/src/Plugin/TfaSetup/TfaTrustedBrowserSetup.php
deleted file mode 100644
index fa9ca45..0000000
--- a/src/Plugin/TfaSetup/TfaTrustedBrowserSetup.php
+++ /dev/null
@@ -1,257 +0,0 @@
-<?php
-
-namespace Drupal\tfa\Plugin\TfaSetup;
-
-use Drupal\Component\Utility\Html;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\StringTranslation\StringTranslationTrait;
-use Drupal\Core\Url;
-use Drupal\tfa\Plugin\TfaLogin\TfaTrustedBrowser;
-use Drupal\tfa\Plugin\TfaSetupInterface;
-
-/**
- * TFA Trusted Browser Setup Plugin.
- *
- * @TfaSetup(
- *   id = "tfa_trusted_browser_setup",
- *   label = @Translation("TFA Trusted Browser Setup"),
- *   description = @Translation("TFA Trusted Browser Setup Plugin"),
- *   setupMessages = {
- *    "saved" = @Translation("Browser saved."),
- *    "skipped" = @Translation("Browser not saved.")
- *   }
- * )
- */
-class TfaTrustedBrowserSetup extends TfaTrustedBrowser implements TfaSetupInterface {
-  use StringTranslationTrait;
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getSetupForm(array $form, FormStateInterface $form_state) {
-    $existing = $this->getTrustedBrowsers();
-    $time = $this->expiration / 86400;
-    $form['info'] = [
-      '#type' => 'markup',
-      '#markup' => '<p>' . $this->t("Trusted browsers are a method for
-      simplifying login by avoiding verification code entry for a set amount of
-      time, @time days from marking a browser as trusted. After @time days, to
-      log in you'll need to enter a verification code with your username and
-      password during which you can again mark the browser as trusted.", ['@time' => $time]) . '</p>',
-    ];
-    // Present option to trust this browser if it's not currently trusted.
-    if (isset($_COOKIE[$this->cookieName]) && $this->trustedBrowser($_COOKIE[$this->cookieName]) !== FALSE) {
-      $current_trusted = $_COOKIE[$this->cookieName];
-    }
-    else {
-      $current_trusted = FALSE;
-      $form['trust'] = [
-        '#type' => 'checkbox',
-        '#title' => $this->t('Trust this browser?'),
-        '#default_value' => empty($existing) ? 1 : 0,
-      ];
-      // Optional field to name this browser.
-      $form['name'] = [
-        '#type' => 'textfield',
-        '#title' => $this->t('Name this browser'),
-        '#maxlength' => 255,
-        '#description' => $this->t('Optionally, name the browser on your browser (e.g.
-        "home firefox" or "office desktop windows"). Your current browser user
-        agent is %browser', ['%browser' => $_SERVER['HTTP_USER_AGENT']]),
-        '#default_value' => $this->getAgent(),
-        '#states' => [
-          'visible' => [
-            ':input[name="trust"]' => ['checked' => TRUE],
-          ],
-        ],
-      ];
-    }
-    if (!empty($existing)) {
-      $form['existing'] = [
-        '#type' => 'fieldset',
-        '#title' => $this->t('Existing browsers'),
-        '#description' => $this->t('Leave checked to keep these browsers in your trusted log in list.'),
-        '#tree' => TRUE,
-      ];
-
-      foreach ($existing as $browser_id => $browser) {
-        $date_formatter = \Drupal::service('date.formatter');
-        $vars = [
-          '@set' => $date_formatter->format($browser['created']),
-        ];
-
-        if (isset($browser['last_used'])) {
-          $vars['@time'] = $date_formatter->format($browser['last_used']);
-        }
-
-        if ($current_trusted == $browser_id) {
-          $name = '<strong>' . $this->t('@name (current browser)', ['@name' => $browser['name']]) . '</strong>';
-        }
-        else {
-          $name = Html::escape($browser['name']);
-        }
-
-        if (empty($browser['last_used'])) {
-          $message = $this->t('Marked trusted @set', $vars);
-        }
-        else {
-          $message = $this->t('Marked trusted @set, last used for log in @time', $vars);
-        }
-        $form['existing']['trusted_browser_' . $browser_id] = [
-          '#type' => 'checkbox',
-          '#title' => $name,
-          '#description' => $message,
-          '#default_value' => 1,
-        ];
-      }
-    }
-    $form['actions'] = ['#type' => 'actions'];
-    $form['actions']['save'] = [
-      '#type' => 'submit',
-      '#button_type' => 'primary',
-      '#value' => $this->t('Save'),
-    ];
-    return $form;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function validateSetupForm(array $form, FormStateInterface $form_state) {
-    // Do nothing, no validation required.
-    return TRUE;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function submitSetupForm(array $form, FormStateInterface $form_state) {
-    $values = $form_state->getValues();
-    if (isset($values['existing'])) {
-      $count = 0;
-      foreach ($values['existing'] as $element => $value) {
-        $id = str_replace('trusted_browser_', '', $element);
-        if (!$value) {
-          $this->deleteTrusted($id);
-          $count++;
-        }
-      }
-      if ($count) {
-        \Drupal::logger('tfa')->notice('Removed @num TFA trusted browsers during trusted browser setup', ['@num' => $count]);
-      }
-    }
-    if (!empty($values['trust']) && $values['trust']) {
-      $name = '';
-      if (!empty($values['name'])) {
-        $name = $values['name'];
-      }
-      elseif (isset($_SERVER['HTTP_USER_AGENT'])) {
-        $name = $this->getAgent();
-      }
-      $this->setTrusted($this->generateBrowserId(), $name);
-    }
-    return TRUE;
-  }
-
-  /**
-   * Get list of trusted browsers.
-   *
-   * @return array
-   *   List of current trusted browsers.
-   */
-  public function getTrustedBrowsers() {
-    return $this->getUserData('tfa', 'tfa_trusted_browser', $this->uid, $this->userData) ?: [];
-  }
-
-  /**
-   * Delete a trusted browser by its ID.
-   *
-   * @param int $id
-   *   ID of the browser to delete.
-   *
-   * @return bool
-   *   TRUE if successful otherwise FALSE.
-   */
-  public function deleteTrustedId($id) {
-    return $this->deleteTrusted($id);
-  }
-
-  /**
-   * Delete all trusted browsers.
-   *
-   * @return bool
-   *   TRUE if successful otherwise FALSE.
-   */
-  public function deleteTrustedBrowsers() {
-    return $this->deleteTrusted();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getOverview(array $params) {
-    $trusted_browsers = [];
-    foreach ($this->getTrustedBrowsers() as $device) {
-      $date_formatter = \Drupal::service('date.formatter');
-      $vars = [
-        '@set' => $date_formatter->format($device['created']),
-        '@browser' => $device['name'],
-      ];
-      if (empty($device['last_used'])) {
-        $message = $this->t('@browser, set @set', $vars);
-      }
-      else {
-        $vars['@time'] = $date_formatter->format($device['last_used']);
-        $message = $this->t('@browser, set @set, last used @time', $vars);
-      }
-      $trusted_browsers[] = $message;
-    }
-    $output = [
-      'heading' => [
-        '#type' => 'html_tag',
-        '#tag' => 'h3',
-        '#value' => $this->t('Trusted browsers'),
-      ],
-      'description' => [
-        '#type' => 'html_tag',
-        '#tag' => 'p',
-        '#value' => $this->t('Browsers that will not require a verification code during login.'),
-      ],
-    ];
-    $output['list'] = [
-      '#theme' => 'item_list',
-      '#items' => $trusted_browsers,
-      '#empty' => $this->t('No trusted browsers found.'),
-    ];
-
-    $output['link'] = [
-      '#theme' => 'links',
-      '#links' => [
-        'admin' => [
-          'title' => $this->t('Configure trusted browsers'),
-          'url' => Url::fromRoute('tfa.validation.setup', [
-            'user' => $params['account']->id(),
-            'method' => $params['plugin_id'],
-          ]),
-        ],
-      ],
-    ];
-
-    return $output;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getHelpLinks() {
-    return ($this->pluginDefinition['helpLinks']) ?: '';
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getSetupMessages() {
-    return ($this->pluginDefinition['setupMessages']) ?: '';
-  }
-
-}
diff --git a/src/Plugin/TfaSetupInterface.php b/src/Plugin/TfaSetupInterface.php
index 0b958a5..b57afa9 100644
--- a/src/Plugin/TfaSetupInterface.php
+++ b/src/Plugin/TfaSetupInterface.php
@@ -52,22 +52,6 @@ interface TfaSetupInterface {
    */
   public function submitSetupForm(array $form, FormStateInterface $form_state);
 
-  /**
-   * Returns a list of links containing helpful information for plugin use.
-   *
-   * @return string[]
-   *   An array containing help links for e.g., OTP generation.
-   */
-  public function getHelpLinks();
-
-  /**
-   * Returns a list of messages for plugin step.
-   *
-   * @return string[]
-   *   An array containing messages to be used during plugin setup.
-   */
-  public function getSetupMessages();
-
   /**
    * Return process error messages.
    *
diff --git a/src/Plugin/TfaValidation/TfaHotpValidation.php b/src/Plugin/TfaValidation/TfaHotpValidation.php
deleted file mode 100644
index 6234c67..0000000
--- a/src/Plugin/TfaValidation/TfaHotpValidation.php
+++ /dev/null
@@ -1,336 +0,0 @@
-<?php
-
-namespace Drupal\tfa\Plugin\TfaValidation;
-
-use Drupal\Component\Datetime\TimeInterface;
-use Drupal\Core\Config\Config;
-use Drupal\Core\Config\ConfigFactoryInterface;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
-use Drupal\Core\StringTranslation\StringTranslationTrait;
-use Drupal\encrypt\EncryptionProfileManagerInterface;
-use Drupal\encrypt\EncryptServiceInterface;
-use Drupal\tfa\Plugin\TfaBasePlugin;
-use Drupal\tfa\Plugin\TfaValidationInterface;
-use Drupal\user\UserDataInterface;
-use Otp\GoogleAuthenticator;
-use Otp\Otp;
-use ParagonIE\ConstantTime\Encoding;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-
-/**
- * HOTP validation class for performing HOTP validation.
- *
- * @TfaValidation(
- *   id = "tfa_hotp",
- *   label = @Translation("TFA HMAC-based one-time password (HOTP)"),
- *   description = @Translation("TFA HOTP Validation Plugin"),
- *   setupPluginId = "tfa_hotp_setup",
- * )
- */
-class TfaHotpValidation extends TfaBasePlugin implements TfaValidationInterface, ContainerFactoryPluginInterface {
-  use StringTranslationTrait;
-
-  /**
-   * Object containing the external validation library.
-   *
-   * @var object
-   */
-  public $auth;
-
-  /**
-   * The counter window in which the validation should be done.
-   *
-   * @var int
-   */
-  protected $counterWindow;
-
-  /**
-   * Whether or not the prefix should use the site name.
-   *
-   * @var bool
-   */
-  protected $siteNamePrefix;
-
-  /**
-   * Name prefix.
-   *
-   * @var string
-   */
-  protected $namePrefix;
-
-  /**
-   * Configurable name of the issuer.
-   *
-   * @var string
-   */
-  protected $issuer;
-
-  /**
-   * Whether the code has already been used or not.
-   *
-   * @var bool
-   */
-  protected $alreadyAccepted;
-
-  /**
-   * The Datetime service.
-   *
-   * @var \Drupal\Component\Datetime\TimeInterface
-   */
-  protected $time;
-
-  /**
-   * {@inheritdoc}
-   */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, UserDataInterface $user_data, EncryptionProfileManagerInterface $encryption_profile_manager, EncryptServiceInterface $encrypt_service, ConfigFactoryInterface $config_factory, TimeInterface $time) {
-    parent::__construct($configuration, $plugin_id, $plugin_definition, $user_data, $encryption_profile_manager, $encrypt_service);
-    $this->auth = new \StdClass();
-    $this->auth->otp = new Otp();
-    $this->auth->ga = new GoogleAuthenticator();
-    $plugin_settings = $config_factory->get('tfa.settings')->get('validation_plugin_settings');
-    $settings = $plugin_settings['tfa_hotp'] ?? [];
-    $settings = array_replace([
-      'counter_window' => 10,
-      'site_name_prefix' => TRUE,
-      'name_prefix' => 'TFA',
-      'issuer' => 'Drupal',
-    ], $settings);
-
-    $this->counterWindow = $settings['counter_window'];
-    $this->siteNamePrefix = $settings['site_name_prefix'];
-    $this->namePrefix = $settings['name_prefix'];
-    $this->issuer = $settings['issuer'];
-    $this->alreadyAccepted = FALSE;
-    $this->time = $time;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
-    return new static(
-      $configuration,
-      $plugin_id,
-      $plugin_definition,
-      $container->get('user.data'),
-      $container->get('encrypt.encryption_profile.manager'),
-      $container->get('encryption'),
-      $container->get('config.factory'),
-      $container->get('datetime.time')
-    );
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function ready() {
-    return ($this->getSeed() !== FALSE);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getForm(array $form, FormStateInterface $form_state) {
-    $message = $this->t('Verification code is application generated and @length digits long.', ['@length' => $this->codeLength]);
-    if ($this->getUserData('tfa', 'tfa_recovery_code', $this->uid, $this->userData)) {
-      $message .= '<br/>' . $this->t("Can't access your account? Use one of your recovery codes.");
-    }
-    $form['code'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('Application verification code'),
-      '#description' => $message,
-      '#required'  => TRUE,
-      '#attributes' => ['autocomplete' => 'off'],
-    ];
-
-    $form['actions']['#type'] = 'actions';
-    $form['actions']['login'] = [
-      '#type' => 'submit',
-      '#button_type' => 'primary',
-      '#value' => $this->t('Verify'),
-    ];
-
-    return $form;
-  }
-
-  /**
-   * The configuration form for this validation plugin.
-   *
-   * @param \Drupal\Core\Config\Config $config
-   *   Config object for tfa settings.
-   * @param array $state
-   *   Form state array determines if this form should be shown.
-   *
-   * @return array
-   *   Form array specific for this validation plugin.
-   */
-  public function buildConfigurationForm(Config $config, array $state = []) {
-    $settings_form['counter_window'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('Counter Window'),
-      '#default_value' => ($this->counterWindow) ?: 5,
-      '#description' => $this->t('How far ahead from current counter should we check the code.'),
-      '#size' => 2,
-      '#states' => $state,
-      '#required' => TRUE,
-    ];
-
-    $settings_form['site_name_prefix'] = [
-      '#type' => 'checkbox',
-      '#title' => $this->t('Use site name as OTP QR code name prefix.'),
-      '#default_value' => $this->siteNamePrefix,
-      '#description' => $this->t('If checked, the site name will be used instead of a static string. This can be useful for multi-site installations.'),
-      '#states' => $state,
-    ];
-
-    // Hide custom name prefix when site name prefix is selected.
-    $state['visible'] += [
-      ':input[name="validation_plugin_settings[tfa_hotp][site_name_prefix]"]' => ['checked' => FALSE],
-    ];
-
-    $settings_form['name_prefix'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('OTP QR Code Prefix'),
-      '#default_value' => $this->namePrefix ?? 'tfa',
-      '#description' => $this->t('Prefix for OTP QR code names. Suffix is account username.'),
-      '#size' => 15,
-      '#states' => $state,
-    ];
-
-    $settings_form['issuer'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('Issuer'),
-      '#default_value' => $this->issuer,
-      '#description' => $this->t('The provider or service this account is associated with.'),
-      '#size' => 15,
-      '#required' => TRUE,
-    ];
-
-    return $settings_form;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function validateForm(array $form, FormStateInterface $form_state) {
-    $values = $form_state->getValues();
-    if (!$this->validate($values['code'])) {
-      $this->errorMessages['code'] = $this->t('Invalid application code. Please try again.');
-      if ($this->alreadyAccepted) {
-        $form_state->clearErrors();
-        $this->errorMessages['code'] = $this->t('Invalid code, it was recently used for a login. Please try a new code.');
-      }
-      return FALSE;
-    }
-    else {
-      // Store accepted code to prevent replay attacks.
-      $this->storeAcceptedCode($values['code']);
-      return TRUE;
-    }
-  }
-
-  /**
-   * Simple validate for web services.
-   *
-   * @param int $code
-   *   OTP Code.
-   *
-   * @return bool
-   *   True if validation was successful otherwise false.
-   */
-  public function validateRequest($code) {
-    if ($this->validate($code)) {
-      $this->storeAcceptedCode($code);
-      return TRUE;
-    }
-    return FALSE;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function validate($code) {
-    // Strip whitespace.
-    $code = preg_replace('/\s+/', '', $code);
-    if ($this->alreadyAcceptedCode($code)) {
-      $this->isValid = FALSE;
-    }
-    else {
-      // Get OTP seed.
-      $seed = $this->getSeed();
-      $counter = $this->getHotpCounter();
-      $this->isValid = ($seed && ($counter = $this->auth->otp->checkHotpResync(Encoding::base32DecodeUpper($seed), $counter, $code, $this->counterWindow)));
-      $this->setUserData('tfa', ['tfa_hotp_counter' => ++$counter], $this->uid, $this->userData);
-    }
-    return $this->isValid;
-  }
-
-  /**
-   * Returns whether code has already been used or not.
-   *
-   * @return bool
-   *   True is code already used otherwise false.
-   */
-  public function isAlreadyAccepted() {
-    return $this->alreadyAccepted;
-  }
-
-  /**
-   * Get seed for this account.
-   *
-   * @return string
-   *   Decrypted account OTP seed or FALSE if none exists.
-   */
-  protected function getSeed() {
-    // Lookup seed for account and decrypt.
-    $result = $this->getUserData('tfa', 'tfa_hotp_seed', $this->uid, $this->userData);
-
-    if (!empty($result)) {
-      $encrypted = base64_decode($result['seed']);
-      $seed = $this->decrypt($encrypted);
-      if (!empty($seed)) {
-        return $seed;
-      }
-    }
-    return FALSE;
-  }
-
-  /**
-   * Save seed for account.
-   *
-   * @param string $seed
-   *   Un-encrypted seed.
-   */
-  public function storeSeed($seed) {
-    // Encrypt seed for storage.
-    $encrypted = $this->encrypt($seed);
-
-    $record = [
-      'tfa_hotp_seed' => [
-        'seed' => base64_encode($encrypted),
-        'created' => $this->time->getRequestTime(),
-      ],
-    ];
-
-    $this->setUserData('tfa', $record, $this->uid, $this->userData);
-  }
-
-  /**
-   * Delete the seed of the current validated user.
-   */
-  protected function deleteSeed() {
-    $this->deleteUserData('tfa', 'tfa_hotp_seed', $this->uid, $this->userData);
-  }
-
-  /**
-   * Get the HOTP counter.
-   *
-   * @return int
-   *   The current value of the HOTP counter, or 1 if no value was found.
-   */
-  public function getHotpCounter() {
-    return ($this->getUserData('tfa', 'tfa_hotp_counter', $this->uid, $this->userData)) ?: 1;
-  }
-
-}
diff --git a/src/Plugin/TfaValidation/TfaTotpValidation.php b/src/Plugin/TfaValidation/TfaTotpValidation.php
deleted file mode 100644
index 8f555a4..0000000
--- a/src/Plugin/TfaValidation/TfaTotpValidation.php
+++ /dev/null
@@ -1,338 +0,0 @@
-<?php
-
-namespace Drupal\tfa\Plugin\TfaValidation;
-
-use Drupal\Component\Datetime\TimeInterface;
-use Drupal\Core\Config\Config;
-use Drupal\Core\Config\ConfigFactoryInterface;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
-use Drupal\Core\StringTranslation\StringTranslationTrait;
-use Drupal\encrypt\EncryptionProfileManagerInterface;
-use Drupal\encrypt\EncryptServiceInterface;
-use Drupal\tfa\Plugin\TfaBasePlugin;
-use Drupal\tfa\Plugin\TfaValidationInterface;
-use Drupal\user\UserDataInterface;
-use Otp\GoogleAuthenticator;
-use Otp\Otp;
-use ParagonIE\ConstantTime\Encoding;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-
-/**
- * TOTP validation class for performing TOTP validation.
- *
- * @TfaValidation(
- *   id = "tfa_totp",
- *   label = @Translation("TFA Time-based one-time password (TOTP)"),
- *   description = @Translation("TFA TOTP Validation Plugin"),
- *   setupPluginId = "tfa_totp_setup",
- * )
- */
-class TfaTotpValidation extends TfaBasePlugin implements TfaValidationInterface, ContainerFactoryPluginInterface {
-  use StringTranslationTrait;
-
-  /**
-   * Object containing the external validation library.
-   *
-   * @var object
-   */
-  public $auth;
-
-  /**
-   * The time-window in which the validation should be done.
-   *
-   * @var int
-   */
-  protected $timeSkew;
-
-  /**
-   * Whether or not the prefix should use the site name.
-   *
-   * @var bool
-   */
-  protected $siteNamePrefix;
-
-  /**
-   * Name prefix.
-   *
-   * @var string
-   */
-  protected $namePrefix;
-
-  /**
-   * Configurable name of the issuer.
-   *
-   * @var string
-   */
-  protected $issuer;
-
-  /**
-   * Whether the code has already been used or not.
-   *
-   * @var bool
-   */
-  protected $alreadyAccepted;
-
-  /**
-   * The Datetime service.
-   *
-   * @var \Drupal\Component\Datetime\TimeInterface
-   */
-  protected $time;
-
-  /**
-   * {@inheritdoc}
-   */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, UserDataInterface $user_data, EncryptionProfileManagerInterface $encryption_profile_manager, EncryptServiceInterface $encrypt_service, ConfigFactoryInterface $config_factory, TimeInterface $time) {
-    parent::__construct($configuration, $plugin_id, $plugin_definition, $user_data, $encryption_profile_manager, $encrypt_service);
-    $this->auth = new \StdClass();
-    $this->auth->otp = new Otp();
-    $this->auth->ga = new GoogleAuthenticator();
-    // Allow codes within tolerance range of 2 * 30 second units.
-    $plugin_settings = $config_factory->get('tfa.settings')->get('validation_plugin_settings');
-    $settings = $plugin_settings['tfa_totp'] ?? [];
-    $settings = array_replace([
-      'time_skew' => 2,
-      'site_name_prefix' => TRUE,
-      'name_prefix' => 'TFA',
-      'issuer' => 'Drupal',
-    ], $settings);
-
-    $this->timeSkew = $settings['time_skew'];
-    $this->siteNamePrefix = $settings['site_name_prefix'];
-    $this->namePrefix = $settings['name_prefix'];
-    $this->issuer = $settings['issuer'];
-    $this->alreadyAccepted = FALSE;
-    $this->time = $time;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
-    return new static(
-      $configuration,
-      $plugin_id,
-      $plugin_definition,
-      $container->get('user.data'),
-      $container->get('encrypt.encryption_profile.manager'),
-      $container->get('encryption'),
-      $container->get('config.factory'),
-      $container->get('datetime.time')
-    );
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function ready() {
-    return ($this->getSeed() !== FALSE);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getForm(array $form, FormStateInterface $form_state) {
-    $message = $this->t('Verification code is application generated and @length digits long.', ['@length' => $this->codeLength]);
-    if ($this->getUserData('tfa', 'tfa_recovery_code', $this->uid, $this->userData)) {
-      $message .= '<br/>' . $this->t("Can't access your account? Use one of your recovery codes.");
-    }
-    $form['code'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('Application verification code'),
-      '#description' => $message,
-      '#required'  => TRUE,
-      '#attributes' => [
-        'autocomplete' => 'off',
-        'autofocus' => 'autofocus',
-      ],
-    ];
-
-    $form['actions']['#type'] = 'actions';
-    $form['actions']['login'] = [
-      '#type' => 'submit',
-      '#button_type' => 'primary',
-      '#value' => $this->t('Verify'),
-    ];
-
-    return $form;
-  }
-
-  /**
-   * The configuration form for this validation plugin.
-   *
-   * @param \Drupal\Core\Config\Config $config
-   *   Config object for tfa settings.
-   * @param array $state
-   *   Form state array determines if this form should be shown.
-   *
-   * @return array
-   *   Form array specific for this validation plugin.
-   */
-  public function buildConfigurationForm(Config $config, array $state = []) {
-    $settings_form['time_skew'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('Number of Accepted Codes'),
-      '#default_value' => $this->timeSkew,
-      '#description' => $this->t('Number of past codes to consider valid. Codes are generated every 30 seconds, so setting this value to 10 would allow each code to work for five minutes.'),
-      '#size' => 2,
-      '#states' => $state,
-      '#required' => TRUE,
-    ];
-
-    $settings_form['site_name_prefix'] = [
-      '#type' => 'checkbox',
-      '#title' => $this->t('Use site name as OTP QR code name prefix.'),
-      '#default_value' => $this->siteNamePrefix,
-      '#description' => $this->t('If checked, the site name will be used instead of a static string. This can be useful for multi-site installations.'),
-      '#states' => $state,
-    ];
-
-    // Hide custom name prefix when site name prefix is selected.
-    $state['visible'] += [
-      ':input[name="validation_plugin_settings[tfa_totp][site_name_prefix]"]' => ['checked' => FALSE],
-    ];
-
-    $settings_form['name_prefix'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('OTP QR Code Prefix'),
-      '#default_value' => $this->namePrefix ?? 'tfa',
-      '#description' => $this->t('Prefix for OTP QR code names. Suffix is account username.'),
-      '#size' => 15,
-      '#states' => $state,
-    ];
-
-    $settings_form['issuer'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('Issuer'),
-      '#default_value' => $this->issuer,
-      '#description' => $this->t('The provider or service this account is associated with.'),
-      '#size' => 15,
-      '#required' => TRUE,
-    ];
-
-    return $settings_form;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function validateForm(array $form, FormStateInterface $form_state) {
-    $values = $form_state->getValues();
-    if (!$this->validate($values['code'])) {
-      $this->errorMessages['code'] = $this->t('Invalid application code. Please try again.');
-      if ($this->alreadyAccepted) {
-        $form_state->clearErrors();
-        $this->errorMessages['code'] = $this->t('Invalid code, it was recently used for a login. Please try a new code.');
-      }
-      return FALSE;
-    }
-    else {
-      // Store accepted code to prevent replay attacks.
-      $this->storeAcceptedCode($values['code']);
-      return TRUE;
-    }
-  }
-
-  /**
-   * Simple validate for web services.
-   *
-   * @param int $code
-   *   OTP Code.
-   *
-   * @return bool
-   *   True if validation was successful otherwise false.
-   */
-  public function validateRequest($code) {
-    if ($this->validate($code)) {
-      $this->storeAcceptedCode($code);
-      return TRUE;
-    }
-    return FALSE;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function validate($code) {
-    // Strip whitespace.
-    $code = preg_replace('/\s+/', '', $code);
-    if ($this->alreadyAcceptedCode($code)) {
-      $this->isValid = FALSE;
-    }
-    else {
-      // Get OTP seed.
-      $seed = $this->getSeed();
-      $this->isValid = ($seed && $this->auth->otp->checkTotp(Encoding::base32DecodeUpper($seed), $code, $this->timeSkew));
-    }
-    return $this->isValid;
-  }
-
-  /**
-   * Returns whether code has already been used or not.
-   *
-   * @return bool
-   *   True is code already used otherwise false.
-   */
-  public function isAlreadyAccepted() {
-    return $this->alreadyAccepted;
-  }
-
-  /**
-   * Get seed for this account.
-   *
-   * @return string
-   *   Decrypted account OTP seed or FALSE if none exists.
-   */
-  protected function getSeed() {
-    // Lookup seed for account and decrypt.
-    $result = $this->getUserData('tfa', 'tfa_totp_seed', $this->uid, $this->userData);
-
-    if (!empty($result)) {
-      $encrypted = base64_decode($result['seed']);
-      $seed = $this->decrypt($encrypted);
-      if (!empty($seed)) {
-        return $seed;
-      }
-    }
-    return FALSE;
-  }
-
-  /**
-   * Save seed for account.
-   *
-   * @param string $seed
-   *   Un-encrypted seed.
-   */
-  public function storeSeed($seed) {
-    // Encrypt seed for storage.
-    $encrypted = $this->encrypt($seed);
-
-    $record = [
-      'tfa_totp_seed' => [
-        'seed' => base64_encode($encrypted),
-        'created' => $this->time->getRequestTime(),
-      ],
-    ];
-
-    $this->setUserData('tfa', $record, $this->uid, $this->userData);
-  }
-
-  /**
-   * Delete the seed of the current validated user.
-   */
-  protected function deleteSeed() {
-    $this->deleteUserData('tfa', 'tfa_totp_seed', $this->uid, $this->userData);
-  }
-
-  /**
-   * Get the value of the time-window in which the validation should be done.
-   *
-   * @return int
-   *   The current value of the time skew.
-   */
-  public function getTimeSkew() {
-    return $this->timeSkew;
-  }
-
-}
diff --git a/src/TfaContext.php b/src/TfaContext.php
index 88b5c3d..e7368ce 100644
--- a/src/TfaContext.php
+++ b/src/TfaContext.php
@@ -5,7 +5,6 @@ namespace Drupal\tfa;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\user\UserDataInterface;
 use Drupal\user\UserInterface;
-use Symfony\Component\HttpFoundation\Request;
 
 /**
  * Provide context for the current login attempt.
@@ -16,21 +15,7 @@ use Symfony\Component\HttpFoundation\Request;
  */
 class TfaContext implements TfaContextInterface {
   // Access to user's TFA settings.
-  use TfaDataTrait;
-
-  /**
-   * Validation plugin manager.
-   *
-   * @var \Drupal\tfa\TfaValidationPluginManager
-   */
-  protected $tfaValidationManager;
-
-  /**
-   * Login plugin manager.
-   *
-   * @var \Drupal\tfa\TfaLoginPluginManager
-   */
-  protected $tfaLoginManager;
+  use TfaUserDataTrait;
 
   /**
    * The current validation plugin id being used by this context instance.
@@ -60,27 +45,6 @@ class TfaContext implements TfaContextInterface {
    */
   protected $user;
 
-  /**
-   * User data service.
-   *
-   * @var \Drupal\user\UserDataInterface
-   */
-  protected $userData;
-
-  /**
-   * Current request object.
-   *
-   * @var \Symfony\Component\HttpFoundation\Request
-   */
-  protected $request;
-
-  /**
-   * Array of login plugins for a given user.
-   *
-   * @var \Drupal\tfa\Plugin\TfaLoginInterface[]
-   */
-  protected $userLoginPlugins;
-
   /**
    * Array of login plugins.
    *
@@ -91,23 +55,26 @@ class TfaContext implements TfaContextInterface {
   /**
    * {@inheritdoc}
    */
-  public function __construct(TfaValidationPluginManager $tfa_validation_manager, TfaLoginPluginManager $tfa_plugin_manager, ConfigFactoryInterface $config_factory, UserInterface $user, UserDataInterface $user_data, Request $request) {
-    $this->tfaValidationManager = $tfa_validation_manager;
-    $this->tfaLoginManager = $tfa_plugin_manager;
+  public function __construct(TfaPluginManager $tfa_plugin_manager, ConfigFactoryInterface $config_factory, UserInterface $user, UserDataInterface $user_data) {
     $this->tfaSettings = $config_factory->get('tfa.settings');
     $this->user = $user;
     $this->userData = $user_data;
-    $this->request = $request;
 
-    $this->tfaLoginPlugins = $this->tfaLoginManager->getPlugins(['uid' => $user->id()]);
+    $this->tfaLoginPlugins = [];
+    $login_definitions = $tfa_plugin_manager->getLoginDefinitions();
+    if (!empty($login_definitions)) {
+      foreach ($login_definitions as $plugin_id => $definition) {
+        $this->tfaLoginPlugins[] = $tfa_plugin_manager->createInstance($plugin_id, ['uid' => $user->id()]);
+      }
+    }
     // If possible, set up an instance of tfaValidationPlugin and the user's
     // list of plugins.
     $this->validationPluginName = $this->tfaSettings->get('default_validation_plugin');
     if (!empty($this->validationPluginName)) {
-      $this->tfaValidationPlugin = $this->tfaValidationManager
-        ->createInstance($this->validationPluginName, ['uid' => $user->id()]);
-      $this->userLoginPlugins = $this->tfaLoginManager
-        ->getPlugins(['uid' => $user->id()]);
+      $this->tfaValidationPlugin = $tfa_plugin_manager->createInstance($this->validationPluginName, ['uid' => $user->id()]);
+    }
+    else {
+      $this->tfaValidationPlugin = NULL;
     }
   }
 
@@ -130,7 +97,7 @@ class TfaContext implements TfaContextInterface {
    */
   public function isTfaRequired() {
     // If TFA has been set up for the user, then it is required.
-    $user_tfa_data = $this->tfaGetTfaData($this->getUser()->id(), $this->userData);
+    $user_tfa_data = $this->tfaGetTfaData($this->getUser()->id());
     if (!empty($user_tfa_data['status']) && !empty($user_tfa_data['data']['plugins'])) {
       return TRUE;
     }
@@ -158,7 +125,7 @@ class TfaContext implements TfaContextInterface {
       return FALSE;
     }
 
-    $user_tfa_data = $this->tfaGetTfaData($this->getUser()->id(), $this->userData);
+    $user_tfa_data = $this->tfaGetTfaData($this->getUser()->id());
     $validation_skipped = $user_tfa_data['validation_skipped'] ?? 0;
     return max(0, $allowed_skips - $validation_skipped);
   }
@@ -167,10 +134,10 @@ class TfaContext implements TfaContextInterface {
    * {@inheritdoc}
    */
   public function hasSkipped() {
-    $user_tfa_data = $this->tfaGetTfaData($this->getUser()->id(), $this->userData);
+    $user_tfa_data = $this->tfaGetTfaData($this->getUser()->id());
     $validation_skipped = $user_tfa_data['validation_skipped'] ?? 0;
     $user_tfa_data['validation_skipped'] = $validation_skipped + 1;
-    $this->tfaSaveTfaData($this->getUser()->id(), $this->userData, $user_tfa_data);
+    $this->tfaSaveTfaData($this->getUser()->id(), $user_tfa_data);
   }
 
   /**
diff --git a/src/TfaContextInterface.php b/src/TfaContextInterface.php
index 8322ccf..a2ebdfd 100644
--- a/src/TfaContextInterface.php
+++ b/src/TfaContextInterface.php
@@ -2,34 +2,11 @@
 
 namespace Drupal\tfa;
 
-use Drupal\Core\Config\ConfigFactoryInterface;
-use Drupal\user\UserDataInterface;
-use Drupal\user\UserInterface;
-use Symfony\Component\HttpFoundation\Request;
-
 /**
  * Provide context for the current login attempt.
  */
 interface TfaContextInterface {
 
-  /**
-   * TfaContextInterface constructor.
-   *
-   * @param \Drupal\tfa\TfaValidationPluginManager $tfa_validation_manager
-   *   The plugin manager for TFA validation plugins.
-   * @param \Drupal\tfa\TfaLoginPluginManager $tfa_plugin_manager
-   *   The plugin manager for TFA login plugins.
-   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
-   *   The configuration service.
-   * @param \Drupal\user\UserInterface $user
-   *   The user currently attempting to log in.
-   * @param \Drupal\user\UserDataInterface $user_data
-   *   The user data service.
-   * @param \Symfony\Component\HttpFoundation\Request $request
-   *   The current request.
-   */
-  public function __construct(TfaValidationPluginManager $tfa_validation_manager, TfaLoginPluginManager $tfa_plugin_manager, ConfigFactoryInterface $config_factory, UserInterface $user, UserDataInterface $user_data, Request $request);
-
   /**
    * Get the user object.
    *
diff --git a/src/TfaInterface.php b/src/TfaInterface.php
new file mode 100644
index 0000000..2a3b74b
--- /dev/null
+++ b/src/TfaInterface.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Drupal\tfa;
+
+/**
+ * Interface for tfa plugins.
+ */
+interface TfaInterface {
+
+  /**
+   * Returns the translated plugin label.
+   *
+   * @return string
+   *   The translated title.
+   */
+  public function getLabel();
+
+  /**
+   * Returns a list of links containing helpful information for plugin use.
+   *
+   * @return string[]
+   *   An array containing help links for e.g., OTP generation.
+   */
+  public function getHelpLinks();
+
+  /**
+   * Returns a list of messages for plugin step.
+   *
+   * @return string[]
+   *   An array containing messages to be used during plugin setup.
+   */
+  public function getSetupMessages();
+
+}
diff --git a/src/TfaLoginPluginManager.php b/src/TfaLoginPluginManager.php
deleted file mode 100644
index 65842d8..0000000
--- a/src/TfaLoginPluginManager.php
+++ /dev/null
@@ -1,167 +0,0 @@
-<?php
-
-namespace Drupal\tfa;
-
-use Drupal\Component\Plugin\Factory\DefaultFactory;
-use Drupal\Core\Cache\CacheBackendInterface;
-use Drupal\Core\Config\ConfigFactoryInterface;
-use Drupal\Core\Extension\ModuleHandlerInterface;
-use Drupal\Core\Plugin\DefaultPluginManager;
-use Drupal\encrypt\EncryptionProfileManagerInterface;
-use Drupal\encrypt\EncryptServiceInterface;
-use Drupal\user\UserDataInterface;
-
-/**
- * The login plugin manager.
- */
-class TfaLoginPluginManager extends DefaultPluginManager {
-
-  /**
-   * Provides the user data service object.
-   *
-   * @var \Drupal\user\UserDataInterface
-   */
-  protected $userData;
-
-  /**
-   * TFA configuration object.
-   *
-   * @var \Drupal\Core\Config\ImmutableConfig
-   */
-  protected $tfaSettings;
-
-  /**
-   * Encryption profile manager.
-   *
-   * @var \Drupal\encrypt\EncryptionProfileManagerInterface
-   */
-  protected $encryptionProfileManager;
-
-  /**
-   * Encryption service.
-   *
-   * @var \Drupal\encrypt\EncryptService
-   */
-  protected $encryptService;
-
-  /**
-   * Constructs a new TfaValidation plugin manager.
-   *
-   * @param \Traversable $namespaces
-   *   An object that implements \Traversable which contains the root paths
-   *   keyed by the corresponding namespace to look for plugin implementations.
-   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
-   *   Cache backend instance to use.
-   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
-   *   The module handler.
-   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
-   *   The configuration factory.
-   * @param \Drupal\user\UserDataInterface $user_data
-   *   User data service.
-   * @param \Drupal\encrypt\EncryptionProfileManagerInterface $encryption_profile_manager
-   *   Encryption profile manager.
-   * @param \Drupal\encrypt\EncryptServiceInterface $encrypt_service
-   *   Encryption service.
-   */
-  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, ConfigFactoryInterface $config_factory, UserDataInterface $user_data, EncryptionProfileManagerInterface $encryption_profile_manager, EncryptServiceInterface $encrypt_service) {
-    parent::__construct('Plugin/TfaLogin', $namespaces, $module_handler, 'Drupal\tfa\Plugin\TfaLoginInterface', 'Drupal\tfa\Annotation\TfaLogin');
-    $this->alterInfo('tfa_login_info');
-    $this->setCacheBackend($cache_backend, 'tfa_login');
-    $this->tfaSettings = $config_factory->get('tfa.settings');
-    $this->userData = $user_data;
-    $this->encryptService = $encrypt_service;
-    $this->encryptionProfileManager = $encryption_profile_manager;
-  }
-
-  /**
-   * Create an instance of a plugin.
-   *
-   * @param string $plugin_id
-   *   The id of the setup plugin.
-   * @param array $configuration
-   *   Configuration data for the setup plugin.
-   *
-   * @return object
-   *   The plugin instance.
-   *
-   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
-   * @throws \Drupal\Component\Plugin\Exception\PluginException
-   */
-  public function createInstance($plugin_id, array $configuration = []) {
-    $plugin_definition = $this->getDefinition($plugin_id);
-    /** @var \Drupal\tfa\Plugin\TfaBasePlugin $plugin_class */
-    $plugin_class = DefaultFactory::getPluginClass($plugin_id, $plugin_definition);
-    // If the plugin provides a factory method, pass the container to it.
-    if (is_subclass_of($plugin_class, 'Drupal\Core\Plugin\ContainerFactoryPluginInterface')) {
-      // phpcs:ignore DrupalPractice.Objects.GlobalDrupal.GlobalDrupal
-      $plugin = $plugin_class::create(\Drupal::getContainer(), $configuration, $plugin_id, $plugin_definition, $this->userData, $this->encryptionProfileManager, $this->encryptService);
-    }
-    else {
-      $plugin = new $plugin_class($configuration, $plugin_id, $plugin_definition, $this->userData, $this->encryptionProfileManager, $this->encryptService);
-    }
-    return $plugin;
-  }
-
-  /**
-   * Returns an array of enabled login plugins.
-   *
-   * @param array $configuration
-   *   The configuration array.
-   *
-   * @return array|null
-   *   An array of login plugins.
-   *
-   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
-   * @throws \Drupal\Component\Plugin\Exception\PluginException
-   */
-  public function getPlugins(array $configuration = []) {
-    $plugin_ids = $this->tfaSettings->get('login_plugins');
-    $plugins = [];
-    if (!empty($plugin_ids)) {
-      foreach ($plugin_ids as $plugin_id) {
-        $plugins[$plugin_id] = $this->createInstance($plugin_id, $configuration);
-      }
-      return $plugins;
-    }
-    return NULL;
-  }
-
-  /**
-   * {@inheritdoc}
-   *
-   * Provide some backwards compatibility with the old implicit setupPluginId.
-   * This will give other modules more time to update their plugins.
-   *
-   * @deprecated in tfa:8.x-1.0-alpha7 and is removed from tfa:8.x-2.0. Please
-   * specify the setupPluginId property in the plugin annotation.
-   * @see https://www.drupal.org/project/tfa/issues/2925066
-   */
-  public function getDefinitions() {
-    $definitions = parent::getDefinitions();
-    foreach ($definitions as &$definition) {
-      if (empty($definition['setupPluginId'])) {
-        $definition['setupPluginId'] = $definition['id'] . '_setup';
-      }
-    }
-    return $definitions;
-  }
-
-  /**
-   * {@inheritdoc}
-   *
-   * Provide some backwards compatibility with the old implicit setupPluginId.
-   * This will give other modules more time to update their plugins.
-   *
-   * @deprecated in tfa:8.x-1.0-alpha7 and is removed from tfa:8.x-2.0. Please
-   * specify the setupPluginId property in the plugin annotation.
-   * @see https://www.drupal.org/project/tfa/issues/2925066
-   */
-  public function getDefinition($plugin_id, $exception_on_invalid = TRUE) {
-    $plugin = parent::getDefinition($plugin_id, $exception_on_invalid);
-    if (is_array($plugin) && empty($plugin['setupPluginId'])) {
-      $plugin['setupPluginId'] = $plugin_id . '_setup';
-    }
-    return $plugin;
-  }
-
-}
diff --git a/src/TfaPluginBase.php b/src/TfaPluginBase.php
new file mode 100644
index 0000000..83ade92
--- /dev/null
+++ b/src/TfaPluginBase.php
@@ -0,0 +1,151 @@
+<?php
+
+namespace Drupal\tfa;
+
+use Drupal\Component\Plugin\PluginBase;
+use Drupal\Component\Utility\Crypt;
+use Drupal\Core\DependencyInjection\DependencySerializationTrait;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Site\Settings;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Base class for tfa plugins.
+ */
+abstract class TfaPluginBase extends PluginBase implements TfaInterface, ContainerFactoryPluginInterface {
+  use DependencySerializationTrait;
+  use StringTranslationTrait;
+  use TfaUserDataTrait;
+
+  /**
+   * The error for the current validation.
+   *
+   * @var string[]
+   */
+  protected $errorMessages;
+
+  /**
+   * The allowed code length.
+   *
+   * @var int
+   */
+  protected $codeLength;
+
+  /**
+   * The user id.
+   *
+   * @var int
+   */
+  protected $uid;
+
+  /**
+   * Whether the code has been used before.
+   *
+   * @var string
+   */
+  protected $alreadyAccepted;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+
+    // Default code length is 6.
+    $this->codeLength = 6;
+
+    $this->uid = $this->configuration['uid'];
+    $this->alreadyAccepted = FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLabel() {
+    // Cast the label to a string since it is a TranslatableMarkup object.
+    return (string) $this->pluginDefinition['label'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getHelpLinks() {
+    return $this->pluginDefinition['helpLinks'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSetupMessages() {
+    return ($this->pluginDefinition['setupMessages']) ?: '';
+  }
+
+  /**
+   * Get error messages suitable for form_set_error().
+   *
+   * @return array
+   *   An array of error strings.
+   */
+  public function getErrorMessages() {
+    return $this->errorMessages;
+  }
+
+  /**
+   * Returns whether code has already been used or not.
+   *
+   * @return bool
+   *   True is code already used otherwise false.
+   */
+  public function isAlreadyAccepted() {
+    return $this->alreadyAccepted;
+  }
+
+  /**
+   * Store validated code to prevent replay attack.
+   *
+   * @param string $code
+   *   The validated code.
+   */
+  protected function storeAcceptedCode($code) {
+    $code = preg_replace('/\s+/', '', $code);
+    $hash = Crypt::hashBase64(Settings::getHashSalt() . $code);
+
+    // Store the hash made using the code in users_data.
+    $store_data = ['tfa_accepted_code_' . $hash => \Drupal::time()->getRequestTime()];
+    $this->setUserData('tfa', $store_data, $this->uid);
+  }
+
+  /**
+   * Whether code has already been used.
+   *
+   * @param string $code
+   *   The code to be checked.
+   *
+   * @return bool
+   *   TRUE if already used otherwise FALSE
+   */
+  protected function alreadyAcceptedCode($code) {
+    $hash = Crypt::hashBase64(Settings::getHashSalt() . $code);
+    // Check if the code has already been used or not.
+    $key    = 'tfa_accepted_code_' . $hash;
+    $result = $this->getUserData('tfa', $key, $this->uid);
+    if (!empty($result)) {
+      $this->alreadyAccepted = TRUE;
+      return TRUE;
+    }
+    return FALSE;
+  }
+
+}
diff --git a/src/TfaPluginManager.php b/src/TfaPluginManager.php
new file mode 100644
index 0000000..c7d5d80
--- /dev/null
+++ b/src/TfaPluginManager.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Drupal\tfa;
+
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Plugin\DefaultPluginManager;
+
+/**
+ * Tfa plugin manager.
+ */
+class TfaPluginManager extends DefaultPluginManager {
+
+  /**
+   * Constructs TfaPluginManager object.
+   *
+   * @param \Traversable $namespaces
+   *   An object that implements \Traversable which contains the root paths
+   *   keyed by the corresponding namespace to look for plugin implementations.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
+   *   Cache backend instance to use.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler to invoke the alter hook with.
+   */
+  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
+    parent::__construct(
+      'Plugin/Tfa',
+      $namespaces,
+      $module_handler,
+      'Drupal\tfa\TfaInterface',
+      'Drupal\tfa\Annotation\Tfa'
+    );
+    $this->alterInfo('tfa_info');
+    $this->setCacheBackend($cache_backend, 'tfa_plugins');
+  }
+
+  public function getValidationDefinitions() {
+    return $this->getClassDefinitions('\Drupal\tfa\Plugin\TfaValidationInterface');
+  }
+
+  public function getLoginDefinitions() {
+    return $this->getClassDefinitions('\Drupal\tfa\Plugin\TfaLoginInterface');
+  }
+
+  public function getSendDefinitions() {
+    return $this->getClassDefinitions('\Drupal\tfa\Plugin\TfaSendInterface');
+  }
+
+  public function getClassDefinitions(string $class) {
+    $all_plugins = $this->getDefinitions();
+    $plugins = [];
+
+    foreach ($all_plugins as $key => $plugin) {
+      if (is_a($plugin['class'], $class, TRUE)) {
+        $plugins[$key] = $plugin;
+      }
+    }
+    return $plugins;
+  }
+
+}
diff --git a/src/TfaSendPluginManager.php b/src/TfaSendPluginManager.php
deleted file mode 100644
index 73f9e1e..0000000
--- a/src/TfaSendPluginManager.php
+++ /dev/null
@@ -1,69 +0,0 @@
-<?php
-
-namespace Drupal\tfa;
-
-use Drupal\Core\Cache\CacheBackendInterface;
-use Drupal\Core\Extension\ModuleHandlerInterface;
-use Drupal\Core\Plugin\DefaultPluginManager;
-
-/**
- * The send plugin manager.
- */
-class TfaSendPluginManager extends DefaultPluginManager {
-
-  /**
-   * Constructs a new TfaSend plugin manager.
-   *
-   * @param \Traversable $namespaces
-   *   An object that implements \Traversable which contains the root paths
-   *   keyed by the corresponding namespace to look for plugin implementations.
-   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
-   *   Cache backend instance to use.
-   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
-   *   The module handler.
-   */
-  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
-    parent::__construct('Plugin/TfaSend', $namespaces, $module_handler, 'Drupal\tfa\Plugin\TfaSendInterface', 'Drupal\tfa\Annotation\TfaSend');
-    $this->alterInfo('tfa_send_info');
-    $this->setCacheBackend($cache_backend, 'tfa_send');
-  }
-
-  /**
-   * {@inheritdoc}
-   *
-   * Provide some backwards compatibility with the old implicit setupPluginId.
-   * This will give other modules more time to update their plugins.
-   *
-   * @deprecated in tfa:8.x-1.0-alpha7 and is removed from tfa:8.x-2.0. Please
-   * specify the setupPluginId property in the plugin annotation.
-   * @see https://www.drupal.org/project/tfa/issues/2925066
-   */
-  public function getDefinitions() {
-    $definitions = parent::getDefinitions();
-    foreach ($definitions as &$definition) {
-      if (empty($definition['setupPluginId'])) {
-        $definition['setupPluginId'] = $definition['id'] . '_setup';
-      }
-    }
-    return $definitions;
-  }
-
-  /**
-   * {@inheritdoc}
-   *
-   * Provide some backwards compatibility with the old implicit setupPluginId.
-   * This will give other modules more time to update their plugins.
-   *
-   * @deprecated in tfa:8.x-1.0-alpha7 and is removed from tfa:8.x-2.0. Please
-   * specify the setupPluginId property in the plugin annotation.
-   * @see https://www.drupal.org/project/tfa/issues/2925066
-   */
-  public function getDefinition($plugin_id, $exception_on_invalid = TRUE) {
-    $plugin = parent::getDefinition($plugin_id, $exception_on_invalid);
-    if (is_array($plugin) && empty($plugin['setupPluginId'])) {
-      $plugin['setupPluginId'] = $plugin_id . '_setup';
-    }
-    return $plugin;
-  }
-
-}
diff --git a/src/TfaSetupPluginManager.php b/src/TfaSetupPluginManager.php
deleted file mode 100644
index eab0e98..0000000
--- a/src/TfaSetupPluginManager.php
+++ /dev/null
@@ -1,93 +0,0 @@
-<?php
-
-namespace Drupal\tfa;
-
-use Drupal\Component\Plugin\Factory\DefaultFactory;
-use Drupal\Core\Cache\CacheBackendInterface;
-use Drupal\Core\Extension\ModuleHandlerInterface;
-use Drupal\Core\Plugin\DefaultPluginManager;
-use Drupal\encrypt\EncryptionProfileManagerInterface;
-use Drupal\encrypt\EncryptService;
-use Drupal\user\UserDataInterface;
-
-/**
- * The setup plugin manager.
- */
-class TfaSetupPluginManager extends DefaultPluginManager {
-
-  /**
-   * Provides the user data service object.
-   *
-   * @var \Drupal\user\UserDataInterface
-   */
-  protected $userData;
-
-  /**
-   * Encryption profile manager.
-   *
-   * @var \Drupal\encrypt\EncryptionProfileManagerInterface
-   */
-  protected $encryptionProfileManager;
-
-  /**
-   * Encryption service.
-   *
-   * @var \Drupal\encrypt\EncryptService
-   */
-  protected $encryptService;
-
-  /**
-   * Constructs a new TfaSetup plugin manager.
-   *
-   * @param \Traversable $namespaces
-   *   An object that implements \Traversable which contains the root paths
-   *   keyed by the corresponding namespace to look for plugin implementations.
-   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
-   *   Cache backend instance to use.
-   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
-   *   The module handler.
-   * @param \Drupal\user\UserDataInterface $user_data
-   *   User data service.
-   * @param \Drupal\encrypt\EncryptionProfileManagerInterface $encryption_profile_manager
-   *   Encryption profile manager.
-   * @param \Drupal\encrypt\EncryptService $encrypt_service
-   *   Encryption service.
-   */
-  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, UserDataInterface $user_data, EncryptionProfileManagerInterface $encryption_profile_manager, EncryptService $encrypt_service) {
-    parent::__construct('Plugin/TfaSetup', $namespaces, $module_handler, 'Drupal\tfa\Plugin\TfaSetupInterface', 'Drupal\tfa\Annotation\TfaSetup');
-    $this->alterInfo('tfa_setup_info');
-    $this->setCacheBackend($cache_backend, 'tfa_setup');
-    $this->userData = $user_data;
-    $this->encryptService = $encrypt_service;
-    $this->encryptionProfileManager = $encryption_profile_manager;
-  }
-
-  /**
-   * Create an instance of a setup plugin.
-   *
-   * @param string $plugin_id
-   *   The id of the setup plugin.
-   * @param array $configuration
-   *   Configuration data for the setup plugin.
-   *
-   * @return object
-   *   Require setup plugin instance
-   *
-   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
-   * @throws \Drupal\Component\Plugin\Exception\PluginException
-   */
-  public function createInstance($plugin_id, array $configuration = []) {
-    $plugin_definition = $this->getDefinition($plugin_id);
-    $plugin_class = DefaultFactory::getPluginClass($plugin_id, $plugin_definition);
-    // If the plugin provides a factory method, pass the container to it.
-    if (is_subclass_of($plugin_class, 'Drupal\Core\Plugin\ContainerFactoryPluginInterface')) {
-      // phpcs:ignore DrupalPractice.Objects.GlobalDrupal.GlobalDrupal
-      $plugin = $plugin_class::create(\Drupal::getContainer(), $configuration, $plugin_id, $plugin_definition, $this->userData, $this->encryptionProfileManager, $this->encryptService);
-    }
-    else {
-      $plugin = new $plugin_class($configuration, $plugin_id, $plugin_definition, $this->userData, $this->encryptionProfileManager, $this->encryptService);
-    }
-    return $plugin;
-  }
-
-}
diff --git a/src/TfaDataTrait.php b/src/TfaUserDataTrait.php
similarity index 57%
rename from src/TfaDataTrait.php
rename to src/TfaUserDataTrait.php
index eb348b2..79f0ee5 100644
--- a/src/TfaDataTrait.php
+++ b/src/TfaUserDataTrait.php
@@ -2,12 +2,17 @@
 
 namespace Drupal\tfa;
 
-use Drupal\user\UserDataInterface;
-
 /**
  * Provides methods to save tfa user settings.
  */
-trait TfaDataTrait {
+trait TfaUserDataTrait {
+
+  /**
+   * Provides the user data service object.
+   *
+   * @var \Drupal\user\UserDataInterface
+   */
+  protected $userData;
 
   /**
    * Store user specific information.
@@ -18,11 +23,9 @@ trait TfaDataTrait {
    *   The value to store. Non-scalar values are serialized automatically.
    * @param int $uid
    *   The user id.
-   * @param \Drupal\user\UserDataInterface $user_data
-   *   User data object to store user specific information.
    */
-  protected function setUserData($module, array $data, $uid, UserDataInterface $user_data) {
-    $user_data->set(
+  protected function setUserData($module, array $data, $uid) {
+    $this->userData->set(
       $module,
       $uid,
       key($data),
@@ -39,14 +42,12 @@ trait TfaDataTrait {
    *   The name of the data key.
    * @param int $uid
    *   The user id.
-   * @param \Drupal\user\UserDataInterface $user_data
-   *   User data object to store user specific information.
    *
    * @return mixed|array
    *   The stored value is returned, or NULL if no value was found.
    */
-  protected function getUserData($module, $key, $uid, UserDataInterface $user_data) {
-    return $user_data->get($module, $uid, $key);
+  protected function getUserData($module, $key, $uid) {
+    return $this->userData->get($module, $uid, $key);
   }
 
   /**
@@ -58,11 +59,9 @@ trait TfaDataTrait {
    *   The name of the data key.
    * @param int $uid
    *   The user id.
-   * @param \Drupal\user\UserDataInterface $user_data
-   *   User data object to store user specific information.
    */
-  protected function deleteUserData($module, $key, $uid, UserDataInterface $user_data) {
-    $user_data->delete($module, $uid, $key);
+  protected function deleteUserData($module, $key, $uid) {
+    $this->userData->delete($module, $uid, $key);
   }
 
   /**
@@ -73,14 +72,12 @@ trait TfaDataTrait {
    *
    * @param int $uid
    *   The user id.
-   * @param \Drupal\user\UserDataInterface $user_data
-   *   User data.
    * @param array $data
    *   Data to be saved.
    */
-  public function tfaSaveTfaData($uid, UserDataInterface $user_data, array $data = []) {
+  public function tfaSaveTfaData($uid, array $data = []) {
     // Check if existing data and update.
-    $existing = $this->tfaGetTfaData($uid, $user_data);
+    $existing = $this->tfaGetTfaData($uid);
 
     if (isset($existing['validation_skipped']) && !isset($data['validation_skipped'])) {
       $validation_skipped = $existing['validation_skipped'];
@@ -112,15 +109,13 @@ trait TfaDataTrait {
     }
 
     $record = [
-      'tfa_user_settings' => [
-        'saved' => \Drupal::time()->getRequestTime(),
-        'status' => $status,
-        'data' => $tfa_data,
-        'validation_skipped' => $validation_skipped,
-      ],
+      'saved' => \Drupal::time()->getRequestTime(),
+      'status' => $status,
+      'data' => $tfa_data,
+      'validation_skipped' => $validation_skipped,
     ];
 
-    $this->setUserData('tfa', $record, $uid, $user_data);
+    $this->userData->set('tfa', $uid, 'tfa_user_settings', $record);
   }
 
   /**
@@ -128,22 +123,16 @@ trait TfaDataTrait {
    *
    * @param int $uid
    *   User account id.
-   * @param \Drupal\user\UserDataInterface $user_data
-   *   User data object to store user specific information.
    *
    * @return array
    *   TFA data.
    */
-  protected function tfaGetTfaData($uid, UserDataInterface $user_data) {
-    $result = $this->getUserData('tfa', 'tfa_user_settings', $uid, $user_data);
+  protected function tfaGetTfaData($uid) {
+    $result = $this->userData->get('tfa', $uid, 'tfa_user_settings');
 
     if (!empty($result)) {
-      return [
-        'status' => $result['status'] == '1',
-        'saved' => $result['saved'],
-        'data' => $result['data'],
-        'validation_skipped' => $result['validation_skipped'],
-      ];
+      $result['status'] = ($result['status'] == '1');
+      return $result;
     }
     return [];
   }
diff --git a/src/TfaValidationPluginManager.php b/src/TfaValidationPluginManager.php
deleted file mode 100644
index b363844..0000000
--- a/src/TfaValidationPluginManager.php
+++ /dev/null
@@ -1,137 +0,0 @@
-<?php
-
-namespace Drupal\tfa;
-
-use Drupal\Component\Plugin\Discovery\DiscoveryCachedTrait;
-use Drupal\Component\Plugin\Factory\DefaultFactory;
-use Drupal\Core\Cache\CacheBackendInterface;
-use Drupal\Core\Config\ConfigFactoryInterface;
-use Drupal\Core\Extension\ModuleHandlerInterface;
-use Drupal\Core\Plugin\DefaultPluginManager;
-use Drupal\encrypt\EncryptionProfileManagerInterface;
-use Drupal\encrypt\EncryptServiceInterface;
-use Drupal\user\UserDataInterface;
-
-/**
- * The validation plugin manager.
- */
-class TfaValidationPluginManager extends DefaultPluginManager {
-  use DiscoveryCachedTrait;
-
-  /**
-   * Provides the user data service object.
-   *
-   * @var \Drupal\user\UserDataInterface
-   */
-  protected $userData;
-
-  /**
-   * Encryption profile manager.
-   *
-   * @var \Drupal\encrypt\EncryptionProfileManagerInterface
-   */
-  protected $encryptionProfileManager;
-
-  /**
-   * Encryption service.
-   *
-   * @var \Drupal\encrypt\EncryptService
-   */
-  protected $encryptService;
-
-  /**
-   * Constructs a new TfaValidation plugin manager.
-   *
-   * @param \Traversable $namespaces
-   *   An object that implements \Traversable which contains the root paths
-   *   keyed by the corresponding namespace to look for plugin implementations.
-   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
-   *   Cache backend instance to use.
-   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
-   *   The module handler.
-   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
-   *   The configuration factory.
-   * @param \Drupal\user\UserDataInterface $user_data
-   *   User data service.
-   * @param \Drupal\encrypt\EncryptionProfileManagerInterface $encryption_profile_manager
-   *   Encryption profile manager.
-   * @param \Drupal\encrypt\EncryptServiceInterface $encrypt_service
-   *   Encryption service.
-   */
-  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, ConfigFactoryInterface $config_factory, UserDataInterface $user_data, EncryptionProfileManagerInterface $encryption_profile_manager, EncryptServiceInterface $encrypt_service) {
-    parent::__construct('Plugin/TfaValidation', $namespaces, $module_handler, 'Drupal\tfa\Plugin\TfaValidationInterface', 'Drupal\tfa\Annotation\TfaValidation');
-    $this->alterInfo('tfa_validation');
-    $this->setCacheBackend($cache_backend, 'tfa_validation');
-    $this->userData = $user_data;
-    $this->encryptService = $encrypt_service;
-    $this->encryptionProfileManager = $encryption_profile_manager;
-  }
-
-  /**
-   * Create an instance of a validation plugin.
-   *
-   * @param string $plugin_id
-   *   The id of the setup plugin.
-   * @param array $configuration
-   *   Configuration data for the setup plugin.
-   *
-   * @return object
-   *   Required validation plugin instance
-   *
-   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
-   * @throws \Drupal\Component\Plugin\Exception\PluginException
-   */
-  public function createInstance($plugin_id, array $configuration = []) {
-    $plugin_definition = $this->getDefinition($plugin_id);
-    $plugin_class = DefaultFactory::getPluginClass($plugin_id, $plugin_definition);
-
-    // If the plugin provides a factory method, pass the container to it.
-    if (is_subclass_of($plugin_class, 'Drupal\Core\Plugin\ContainerFactoryPluginInterface')) {
-      // phpcs:ignore DrupalPractice.Objects.GlobalDrupal.GlobalDrupal
-      $plugin = $plugin_class::create(\Drupal::getContainer(), $configuration, $plugin_id, $plugin_definition, $this->userData, $this->encryptionProfileManager, $this->encryptService);
-    }
-    else {
-      $plugin = new $plugin_class($configuration, $plugin_id, $plugin_definition, $this->userData, $this->encryptionProfileManager, $this->encryptService);
-    }
-    return $plugin;
-  }
-
-  /**
-   * {@inheritdoc}
-   *
-   * Provide some backwards compatibility with the old implicit setupPluginId.
-   * This will give other modules more time to update their plugins.
-   *
-   * @deprecated in tfa:8.x-1.0-alpha7 and is removed from tfa:8.x-2.0. Please
-   * specify the setupPluginId property in the plugin annotation.
-   * @see https://www.drupal.org/project/tfa/issues/2925066
-   */
-  public function getDefinitions() {
-    $definitions = parent::getDefinitions();
-    foreach ($definitions as &$definition) {
-      if (empty($definition['setupPluginId'])) {
-        $definition['setupPluginId'] = $definition['id'] . '_setup';
-      }
-    }
-    return $definitions;
-  }
-
-  /**
-   * {@inheritdoc}
-   *
-   * Provide some backwards compatibility with the old implicit setupPluginId.
-   * This will give other modules more time to update their plugins.
-   *
-   * @deprecated in tfa:8.x-1.0-alpha7 and is removed from tfa:8.x-2.0. Please
-   * specify the setupPluginId property in the plugin annotation.
-   * @see https://www.drupal.org/project/tfa/issues/2925066
-   */
-  public function getDefinition($plugin_id, $exception_on_invalid = TRUE) {
-    $plugin = parent::getDefinition($plugin_id, $exception_on_invalid);
-    if (is_array($plugin) && empty($plugin['setupPluginId'])) {
-      $plugin['setupPluginId'] = $plugin_id . '_setup';
-    }
-    return $plugin;
-  }
-
-}
diff --git a/tests/modules/tfa_test_plugins/src/Plugin/Tfa/TfaTestValidationPlugin.php b/tests/modules/tfa_test_plugins/src/Plugin/Tfa/TfaTestValidationPlugin.php
new file mode 100644
index 0000000..fa3191b
--- /dev/null
+++ b/tests/modules/tfa_test_plugins/src/Plugin/Tfa/TfaTestValidationPlugin.php
@@ -0,0 +1,220 @@
+<?php
+
+namespace Drupal\tfa_test_plugins\Plugin\Tfa;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\Url;
+use Drupal\encrypt\EncryptionProfileManagerInterface;
+use Drupal\encrypt\EncryptServiceInterface;
+use Drupal\tfa\Plugin\TfaSetupInterface;
+use Drupal\tfa\Plugin\TfaValidationInterface;
+use Drupal\tfa\TfaPluginBase;
+use Drupal\user\Entity\User;
+use Drupal\user\UserDataInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * TFA Test Validation Plugin.
+ *
+ * @package Drupal\tfa_test_plugins
+ *
+ * @Tfa(
+ *   id = "tfa_test_plugins_validation",
+ *   label = @Translation("TFA Test Validation Plugin"),
+ *   description = @Translation("TFA Test Validation Plugin"),
+ *   helpLinks = {},
+ *   setupMessages = {}
+ * )
+ */
+class TfaTestValidationPlugin extends TfaPluginBase implements TfaValidationInterface, TfaSetupInterface, ContainerFactoryPluginInterface {
+  use StringTranslationTrait;
+
+  /**
+   * Encryption profile.
+   *
+   * @var \Drupal\encrypt\EncryptionProfileManagerInterface
+   */
+  protected $encryptionProfile;
+
+  /**
+   * Encryption service.
+   *
+   * @var \Drupal\encrypt\EncryptService
+   */
+  protected $encryptService;
+
+  /**
+   * Constructs a new Tfa plugin object.
+   *
+   * @param array $configuration
+   *   The plugin configuration.
+   * @param string $plugin_id
+   *   The plugin id.
+   * @param mixed $plugin_definition
+   *   The plugin definition.
+   * @param \Drupal\user\UserDataInterface $user_data
+   *   User data object to store user specific information.
+   * @param \Drupal\encrypt\EncryptionProfileManagerInterface $encryption_profile_manager
+   *   Encryption profile manager.
+   * @param \Drupal\encrypt\EncryptServiceInterface $encrypt_service
+   *   Encryption service.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The configuration factory.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, UserDataInterface $user_data, EncryptionProfileManagerInterface $encryption_profile_manager, EncryptServiceInterface $encrypt_service, ConfigFactoryInterface $config_factory) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+
+    $this->userData = $user_data;
+    $this->encryptionProfile = $encryption_profile_manager->getEncryptionProfile($config_factory->get('tfa.settings')->get('encryption'));
+    $this->encryptService = $encrypt_service;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('user.data'),
+      $container->get('encrypt.encryption_profile.manager'),
+      $container->get('encryption'),
+      $container->get('config.factory')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getForm(array $form, FormStateInterface $form_state) {
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array $form, FormStateInterface $form_state) {
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function ready() {
+    return TRUE;
+  }
+
+  // ================================== SETUP ==================================
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSetupForm(array $form, FormStateInterface $form_state) {
+    $form['user']['#markup'] = $this->t('<p>TFA Setup for @name</p>', [
+      '@name' => User::load($this->configuration['uid'])->getDisplayName(),
+    ]);
+    $form['expected_field'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Expected field'),
+      '#required' => TRUE,
+    ];
+    $form['actions']['#type'] = 'actions';
+    $form['actions']['login'] = [
+      '#type'  => 'submit',
+      '#button_type' => 'primary',
+      '#value' => $this->t('Verify and save'),
+    ];
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateSetupForm(array $form, FormStateInterface $form_state) {
+    $expected_value = $form_state->getValue('expected_field');
+
+    if (empty($expected_value)) {
+      $form_state->setError($form['expected_field'], $this->t('Missing expected value.'));
+      return FALSE;
+    }
+
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitSetupForm(array $form, FormStateInterface $form_state) {
+    $encrypted = $this->encryptService->encrypt($form_state->getValue('expected_field'), $this->encryptionProfile);
+    $record = [
+      'test_data' => [
+        'expected_field' => base64_encode($encrypted),
+      ],
+    ];
+    $this->setUserData($this->pluginDefinition['id'], $record, $this->uid, $this->userData);
+
+    return TRUE;
+  }
+
+  /**
+   * Get and decode the data expected during setup.
+   *
+   * @return null|string
+   *   The string if found, otherwise NULL;
+   *
+   * @throws \Drupal\encrypt\Exception\EncryptionMethodCanNotDecryptException
+   * @throws \Drupal\encrypt\Exception\EncryptException
+   */
+  public function getExpectedFieldData() {
+    $data = $this->getUserData($this->pluginDefinition['id'], 'test_data', $this->uid, $this->userData);
+    if (!empty($data['expected_field'])) {
+      return $this->encryptService->decrypt(base64_decode($data['expected_field']), $this->encryptionProfile);
+    }
+
+    return NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getHelpLinks() {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSetupMessages() {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOverview(array $params) {
+    return [
+      'heading' => [
+        '#type' => 'html_tag',
+        '#tag' => 'h2',
+        '#value' => $this->t('TFA application'),
+      ],
+      'link' => [
+        '#theme' => 'links',
+        '#links' => [
+          'admin' => [
+            'title' => !$params['enabled'] ? $this->t('Set up test application') : $this->t('Reset test application'),
+            'url' => Url::fromRoute('tfa.validation.setup', [
+              'user' => $params['account']->id(),
+              'method' => $params['plugin_id'],
+            ]),
+          ],
+        ],
+      ],
+    ];
+  }
+
+}
diff --git a/tests/modules/tfa_test_plugins/src/Plugin/TfaSetup/TfaTestValidationPluginSetupPlugin.php b/tests/modules/tfa_test_plugins/src/Plugin/TfaSetup/TfaTestValidationPluginSetupPlugin.php
deleted file mode 100644
index 214e176..0000000
--- a/tests/modules/tfa_test_plugins/src/Plugin/TfaSetup/TfaTestValidationPluginSetupPlugin.php
+++ /dev/null
@@ -1,143 +0,0 @@
-<?php
-
-namespace Drupal\tfa_test_plugins\Plugin\TfaSetup;
-
-use Drupal\Core\Form\FormStateInterface;
-
-use Drupal\Core\StringTranslation\StringTranslationTrait;
-use Drupal\Core\Url;
-use Drupal\tfa\Plugin\TfaBasePlugin;
-use Drupal\tfa\Plugin\TfaSetupInterface;
-use Drupal\user\Entity\User;
-
-/**
- * TFA Test Validation Plugin Setup Plugin.
- *
- * @package Drupal\tfa_test_plugins
- *
- * @TfaSetup(
- *   id = "tfa_test_plugins_validation_setup",
- *   label = @Translation("TFA Test Validation Plugin Setup"),
- *   description = @Translation("TFA Test Validation Plugin Setup Plugin"),
- *   helpLinks = {},
- *   setupMessages = {}
- * )
- */
-class TfaTestValidationPluginSetupPlugin extends TfaBasePlugin implements TfaSetupInterface {
-  use StringTranslationTrait;
-
-  /**
-   * {@inheritdoc}
-   */
-  public function ready() {
-    return TRUE;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getSetupForm(array $form, FormStateInterface $form_state) {
-    $form['user']['#markup'] = $this->t('<p>TFA Setup for @name</p>', [
-      '@name' => User::load($this->configuration['uid'])->getDisplayName(),
-    ]);
-    $form['expected_field'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('Expected field'),
-      '#required' => TRUE,
-    ];
-    $form['actions']['#type'] = 'actions';
-    $form['actions']['login'] = [
-      '#type'  => 'submit',
-      '#button_type' => 'primary',
-      '#value' => $this->t('Verify and save'),
-    ];
-    return $form;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function validateSetupForm(array $form, FormStateInterface $form_state) {
-    $expected_value = $form_state->getValue('expected_field');
-
-    if (empty($expected_value)) {
-      $form_state->setError($form['expected_field'], $this->t('Missing expected value.'));
-      return FALSE;
-    }
-
-    return TRUE;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function submitSetupForm(array $form, FormStateInterface $form_state) {
-    $encrypted = $this->encrypt($form_state->getValue('expected_field'));
-    $record = [
-      'test_data' => [
-        'expected_field' => base64_encode($encrypted),
-      ],
-    ];
-    $this->setUserData($this->pluginDefinition['id'], $record, $this->uid, $this->userData);
-
-    return TRUE;
-  }
-
-  /**
-   * Get and decode the data expected during setup.
-   *
-   * @return null|string
-   *   The string if found, otherwise NULL;
-   *
-   * @throws \Drupal\encrypt\Exception\EncryptionMethodCanNotDecryptException
-   * @throws \Drupal\encrypt\Exception\EncryptException
-   */
-  public function getExpectedFieldData() {
-    $data = $this->getUserData($this->pluginDefinition['id'], 'test_data', $this->uid, $this->userData);
-    if (!empty($data['expected_field'])) {
-      return $this->decrypt(base64_decode($data['expected_field']));
-    }
-
-    return NULL;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getHelpLinks() {
-    return [];
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getSetupMessages() {
-    return [];
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getOverview(array $params) {
-    return [
-      'heading' => [
-        '#type' => 'html_tag',
-        '#tag' => 'h2',
-        '#value' => $this->t('TFA application'),
-      ],
-      'link' => [
-        '#theme' => 'links',
-        '#links' => [
-          'admin' => [
-            'title' => !$params['enabled'] ? $this->t('Set up test application') : $this->t('Reset test application'),
-            'url' => Url::fromRoute('tfa.validation.setup', [
-              'user' => $params['account']->id(),
-              'method' => $params['plugin_id'],
-            ]),
-          ],
-        ],
-      ],
-    ];
-  }
-
-}
diff --git a/tests/modules/tfa_test_plugins/src/Plugin/TfaValidation/TfaTestValidationPlugin.php b/tests/modules/tfa_test_plugins/src/Plugin/TfaValidation/TfaTestValidationPlugin.php
deleted file mode 100644
index 8735073..0000000
--- a/tests/modules/tfa_test_plugins/src/Plugin/TfaValidation/TfaTestValidationPlugin.php
+++ /dev/null
@@ -1,44 +0,0 @@
-<?php
-
-namespace Drupal\tfa_test_plugins\Plugin\TfaValidation;
-
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\tfa\Plugin\TfaBasePlugin;
-use Drupal\tfa\Plugin\TfaValidationInterface;
-
-/**
- * TFA Test Validation Plugin.
- *
- * @package Drupal\tfa_test_plugins
- *
- * @TfaValidation(
- *   id = "tfa_test_plugins_validation",
- *   label = @Translation("TFA Test Validation Plugin"),
- *   description = @Translation("TFA Test Validation Plugin"),
- *   setupPluginId = "tfa_test_plugins_validation_setup",
- * )
- */
-class TfaTestValidationPlugin extends TfaBasePlugin implements TfaValidationInterface {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getForm(array $form, FormStateInterface $form_state) {
-    return $form;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function validateForm(array $form, FormStateInterface $form_state) {
-    return TRUE;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function ready() {
-    return TRUE;
-  }
-
-}
diff --git a/tests/src/Functional/TfaConfigTest.php b/tests/src/Functional/TfaConfigTest.php
index 2de1e9b..a6379d3 100644
--- a/tests/src/Functional/TfaConfigTest.php
+++ b/tests/src/Functional/TfaConfigTest.php
@@ -5,7 +5,7 @@ namespace Drupal\Tests\tfa\Functional;
 /**
  * Tests the Tfa UI.
  *
- * @group Tfa
+ * @group tfa
  */
 class TfaConfigTest extends TfaTestBase {
   /**
diff --git a/tests/src/Functional/TfaHotpSetupPluginTest.php b/tests/src/Functional/TfaHotpSetupPluginTest.php
index 69475df..016e756 100644
--- a/tests/src/Functional/TfaHotpSetupPluginTest.php
+++ b/tests/src/Functional/TfaHotpSetupPluginTest.php
@@ -7,7 +7,7 @@ use ParagonIE\ConstantTime\Encoding;
 /**
  * TfaHotpSetup plugin test.
  *
- * @group Tfa
+ * @group tfa
  */
 class TfaHotpSetupPluginTest extends TfaTestBase {
 
@@ -28,7 +28,7 @@ class TfaHotpSetupPluginTest extends TfaTestBase {
   /**
    * Instance of the setup plugin for the $validationPluginId.
    *
-   * @var \Drupal\tfa\Plugin\TfaSetup\TfaHotpSetup
+   * @var \Drupal\tfa\Plugin\Tfa\TfaHotp
    */
   public $setupPlugin;
 
@@ -53,7 +53,7 @@ class TfaHotpSetupPluginTest extends TfaTestBase {
       'setup own tfa',
       'disable own tfa',
     ]);
-    $this->setupPlugin = \Drupal::service('plugin.manager.tfa.setup')->createInstance($this->validationPluginId . '_setup', ['uid' => $this->userAccount->id()]);
+    $this->setupPlugin = \Drupal::service('plugin.manager.tfa')->createInstance($this->validationPluginId, ['uid' => $this->userAccount->id()]);
     $this->drupalLogin($this->userAccount);
   }
 
diff --git a/tests/src/Functional/TfaHotpValidationPluginTest.php b/tests/src/Functional/TfaHotpValidationPluginTest.php
index 2821d08..84df600 100644
--- a/tests/src/Functional/TfaHotpValidationPluginTest.php
+++ b/tests/src/Functional/TfaHotpValidationPluginTest.php
@@ -7,7 +7,7 @@ use ParagonIE\ConstantTime\Encoding;
 /**
  * TfaHotpValidation plugin test.
  *
- * @group Tfa
+ * @group tfa
  */
 class TfaHotpValidationPluginTest extends TfaTestBase {
 
@@ -28,7 +28,7 @@ class TfaHotpValidationPluginTest extends TfaTestBase {
   /**
    * Instance of the validation plugin for the $validationPluginId.
    *
-   * @var \Drupal\tfa\Plugin\TfaValidation\TfaHotpValidation
+   * @var \Drupal\tfa\Plugin\Tfa\TfaHotp
    */
   public $validationPlugin;
 
@@ -60,7 +60,7 @@ class TfaHotpValidationPluginTest extends TfaTestBase {
       'setup own tfa',
       'disable own tfa',
     ]);
-    $this->validationPlugin = \Drupal::service('plugin.manager.tfa.validation')->createInstance($this->validationPluginId, ['uid' => $this->userAccount->id()]);
+    $this->validationPlugin = \Drupal::service('plugin.manager.tfa')->createInstance($this->validationPluginId, ['uid' => $this->userAccount->id()]);
     $this->drupalLogin($this->userAccount);
     $this->setupUserHotp();
     $this->drupalLogout();
diff --git a/tests/src/Functional/TfaLoginTest.php b/tests/src/Functional/TfaLoginTest.php
index 5ba8be6..27cad41 100644
--- a/tests/src/Functional/TfaLoginTest.php
+++ b/tests/src/Functional/TfaLoginTest.php
@@ -5,7 +5,7 @@ namespace Drupal\Tests\tfa\Functional;
 /**
  * Tests for the tfa login process.
  *
- * @group Tfa
+ * @group tfa
  */
 class TfaLoginTest extends TfaTestBase {
 
diff --git a/tests/src/Functional/TfaRecoveryCodePluginTest.php b/tests/src/Functional/TfaRecoveryCodePluginTest.php
index 15097f5..bcfd323 100644
--- a/tests/src/Functional/TfaRecoveryCodePluginTest.php
+++ b/tests/src/Functional/TfaRecoveryCodePluginTest.php
@@ -7,7 +7,7 @@ namespace Drupal\Tests\tfa\Functional;
  *
  * @group tfa
  *
- * @ingroup Tfa
+ * @ingroup tfa
  */
 class TfaRecoveryCodePluginTest extends TfaTestBase {
 
@@ -26,30 +26,16 @@ class TfaRecoveryCodePluginTest extends TfaTestBase {
   public $userAccount;
 
   /**
-   * Setup plugin manager.
+   * Tfa plugin manager.
    *
-   * @var \Drupal\tfa\TfaSetupPluginManager
-   */
-  public $tfaSetupManager;
-
-  /**
-   * Validation plugin manager.
-   *
-   * @var \Drupal\tfa\TfaValidationPluginManager
+   * @var \Drupal\tfa\TfaPluginManager
    */
   public $tfaValidationManager;
 
-  /**
-   * Instance of the setup plugin for the $validationPluginId.
-   *
-   * @var \Drupal\tfa\Plugin\TfaSetup\TfaRecoveryCodeSetup
-   */
-  public $setupPlugin;
-
   /**
    * Instance of the validation plugin for the $validationPluginId.
    *
-   * @var \Drupal\tfa\Plugin\TfaValidation\TfaRecoveryCode
+   * @var \Drupal\tfa\Plugin\Tfa\TfaRecoveryCode
    */
   public $validationPlugin;
 
@@ -75,10 +61,7 @@ class TfaRecoveryCodePluginTest extends TfaTestBase {
     $permissions = ['setup own tfa', 'disable own tfa'];
     $this->userAccount = $this->createUser($permissions);
 
-    $this->tfaSetupManager = \Drupal::service('plugin.manager.tfa.setup');
-    $this->setupPlugin = $this->tfaSetupManager->createInstance($this->validationPluginId . '_setup', ['uid' => $this->userAccount->id()]);
-
-    $this->tfaValidationManager = \Drupal::service('plugin.manager.tfa.validation');
+    $this->tfaValidationManager = \Drupal::service('plugin.manager.tfa');
     $this->validationPlugin = $this->tfaValidationManager->createInstance($this->validationPluginId, ['uid' => $this->userAccount->id()]);
   }
 
diff --git a/tests/src/Functional/TfaTestBase.php b/tests/src/Functional/TfaTestBase.php
index ffd862d..fa768d2 100644
--- a/tests/src/Functional/TfaTestBase.php
+++ b/tests/src/Functional/TfaTestBase.php
@@ -104,7 +104,7 @@ abstract class TfaTestBase extends BrowserTestBase {
 
     $edit = [
       'tfa_enabled' => TRUE,
-      'tfa_validate' => $validation_plugin_id,
+      'tfa_default_validation_plugin' => $validation_plugin_id,
       "tfa_allowed_validation_plugins[{$validation_plugin_id}]" => $validation_plugin_id,
       'encryption_profile' => $this->encryptionProfile->id(),
     ];
@@ -112,7 +112,7 @@ abstract class TfaTestBase extends BrowserTestBase {
     $this->submitForm($edit, 'Save configuration');
     $assert->statusCodeEquals(200);
     $assert->pageTextContains('The configuration options have been saved.');
-    $select_field_id = 'edit-tfa-validate';
+    $select_field_id = 'edit-tfa-default-validation-plugin';
     $option_field = $assert->optionExists($select_field_id, $validation_plugin_id);
     $result = $option_field->hasAttribute('selected');
     $this->assertTrue($result, "Option {$validation_plugin_id} for field {$select_field_id} is selected.");
diff --git a/tests/src/Functional/TfaTotpSetupPluginTest.php b/tests/src/Functional/TfaTotpSetupPluginTest.php
index 22f1b9e..acef2ac 100644
--- a/tests/src/Functional/TfaTotpSetupPluginTest.php
+++ b/tests/src/Functional/TfaTotpSetupPluginTest.php
@@ -7,7 +7,7 @@ use ParagonIE\ConstantTime\Encoding;
 /**
  * TfaTotpSetup plugin test.
  *
- * @group Tfa
+ * @group tfa
  */
 class TfaTotpSetupPluginTest extends TfaTestBase {
 
@@ -28,7 +28,7 @@ class TfaTotpSetupPluginTest extends TfaTestBase {
   /**
    * Instance of the setup plugin for the $validationPluginId.
    *
-   * @var \Drupal\tfa\Plugin\TfaSetup\TfaTotpSetup
+   * @var \Drupal\tfa\Plugin\Tfa\TfaTotp
    */
   public $setupPlugin;
 
@@ -53,7 +53,7 @@ class TfaTotpSetupPluginTest extends TfaTestBase {
       'setup own tfa',
       'disable own tfa',
     ]);
-    $this->setupPlugin = \Drupal::service('plugin.manager.tfa.setup')->createInstance($this->validationPluginId . '_setup', ['uid' => $this->userAccount->id()]);
+    $this->setupPlugin = \Drupal::service('plugin.manager.tfa')->createInstance($this->validationPluginId, ['uid' => $this->userAccount->id()]);
     $this->drupalLogin($this->userAccount);
   }
 
diff --git a/tests/src/Functional/TfaTotpValidationPluginTest.php b/tests/src/Functional/TfaTotpValidationPluginTest.php
index 0b3244a..7069c25 100644
--- a/tests/src/Functional/TfaTotpValidationPluginTest.php
+++ b/tests/src/Functional/TfaTotpValidationPluginTest.php
@@ -7,7 +7,7 @@ use ParagonIE\ConstantTime\Encoding;
 /**
  * TfaTotpValidation plugin test.
  *
- * @group Tfa
+ * @group tfa
  */
 class TfaTotpValidationPluginTest extends TfaTestBase {
 
@@ -28,7 +28,7 @@ class TfaTotpValidationPluginTest extends TfaTestBase {
   /**
    * Instance of the validation plugin for the $validationPluginId.
    *
-   * @var \Drupal\tfa\Plugin\TfaValidation\TfaTotpValidation
+   * @var \Drupal\tfa\Plugin\Tfa\TfaTotp
    */
   public $validationPlugin;
 
@@ -60,7 +60,7 @@ class TfaTotpValidationPluginTest extends TfaTestBase {
       'setup own tfa',
       'disable own tfa',
     ]);
-    $this->validationPlugin = \Drupal::service('plugin.manager.tfa.validation')->createInstance($this->validationPluginId, ['uid' => $this->userAccount->id()]);
+    $this->validationPlugin = \Drupal::service('plugin.manager.tfa')->createInstance($this->validationPluginId, ['uid' => $this->userAccount->id()]);
     $this->drupalLogin($this->userAccount);
     $this->setupUserTotp();
     $this->drupalLogout();
diff --git a/tests/src/Unit/Plugin/TfaValidation/TfaRecoveryCodeTest.php b/tests/src/Unit/Plugin/Tfa/TfaRecoveryCodeTest.php
similarity index 95%
rename from tests/src/Unit/Plugin/TfaValidation/TfaRecoveryCodeTest.php
rename to tests/src/Unit/Plugin/Tfa/TfaRecoveryCodeTest.php
index a570a78..12461b6 100644
--- a/tests/src/Unit/Plugin/TfaValidation/TfaRecoveryCodeTest.php
+++ b/tests/src/Unit/Plugin/Tfa/TfaRecoveryCodeTest.php
@@ -1,6 +1,6 @@
 <?php
 
-namespace Drupal\Tests\tfa\Unit\Plugin\TfaValidation;
+namespace Drupal\Tests\tfa\Unit\Plugin\Tfa;
 
 use Prophecy\PhpUnit\ProphecyTrait;
 use Drupal\Core\Config\ConfigFactoryInterface;
@@ -11,11 +11,11 @@ use Drupal\encrypt\EncryptionProfileInterface;
 use Drupal\encrypt\EncryptionProfileManagerInterface;
 use Drupal\encrypt\EncryptServiceInterface;
 use Drupal\Tests\UnitTestCase;
-use Drupal\tfa\Plugin\TfaValidation\TfaRecoveryCode;
+use Drupal\tfa\Plugin\Tfa\TfaRecoveryCode;
 use Drupal\user\UserDataInterface;
 
 /**
- * @coversDefaultClass \Drupal\tfa\Plugin\TfaValidation\TfaRecoveryCode
+ * @coversDefaultClass \Drupal\tfa\Plugin\Tfa\TfaRecoveryCode
  *
  * @group tfa
  */
@@ -92,7 +92,7 @@ class TfaRecoveryCodeTest extends UnitTestCase {
   /**
    * Helper method to construct the test fixture.
    *
-   * @return \Drupal\tfa\Plugin\TfaValidation\TfaRecoveryCode
+   * @return \Drupal\tfa\Plugin\Tfa\TfaRecoveryCode
    *   Recovery code.
    *
    * @throws \Exception
diff --git a/tests/src/Unit/TfaContextTest.php b/tests/src/Unit/TfaContextTest.php
index 76f9d7e..b665db5 100644
--- a/tests/src/Unit/TfaContextTest.php
+++ b/tests/src/Unit/TfaContextTest.php
@@ -8,11 +8,9 @@ use Drupal\Core\Config\ImmutableConfig;
 use Drupal\Tests\UnitTestCase;
 use Drupal\tfa\Plugin\TfaValidationInterface;
 use Drupal\tfa\TfaContext;
-use Drupal\tfa\TfaLoginPluginManager;
-use Drupal\tfa\TfaValidationPluginManager;
+use Drupal\tfa\TfaPluginManager;
 use Drupal\user\UserDataInterface;
 use Drupal\user\UserInterface;
-use Symfony\Component\HttpFoundation\Request;
 
 /**
  * @coversDefaultClass \Drupal\tfa\TfaContext
@@ -23,18 +21,11 @@ class TfaContextTest extends UnitTestCase {
   use ProphecyTrait;
 
   /**
-   * Validation plugin manager.
+   * Tfa plugin manager.
    *
-   * @var \Drupal\tfa\TfaValidationPluginManager
+   * @var \Drupal\tfa\TfaPluginManager
    */
-  protected $tfaValidationManager;
-
-  /**
-   * Login plugin manager.
-   *
-   * @var \Drupal\tfa\TfaLoginPluginManager
-   */
-  protected $tfaLoginManager;
+  protected $pluginManager;
 
   /**
    * The config factory.
@@ -64,13 +55,6 @@ class TfaContextTest extends UnitTestCase {
    */
   protected $userData;
 
-  /**
-   * Current request object.
-   *
-   * @var \Symfony\Component\HttpFoundation\Request
-   */
-  protected $request;
-
   /**
    * {@inheritdoc}
    */
@@ -79,8 +63,7 @@ class TfaContextTest extends UnitTestCase {
 
     // Setup default mocked services. These can be overridden by
     // re-instantiating them as needed prior to calling ::getFixture().
-    $this->tfaValidationManager = $this->prophesize(TfaValidationPluginManager::class)->reveal();
-    $this->tfaLoginManager = $this->prophesize(TfaLoginPluginManager::class)->reveal();
+    $this->pluginManager = $this->prophesize(TfaPluginManager::class)->reveal();
     $this->tfaSettings = $this->prophesize(ImmutableConfig::class)->reveal();
     $this->configFactory = $this->prophesize(ConfigFactoryInterface::class);
     $this->configFactory->get('tfa.settings')->willReturn($this->tfaSettings);
@@ -89,7 +72,6 @@ class TfaContextTest extends UnitTestCase {
     $this->user->id()->willReturn(3);
     $this->user = $this->user->reveal();
     $this->userData = $this->prophesize(UserDataInterface::class)->reveal();
-    $this->request = $this->prophesize(Request::class)->reveal();
   }
 
   /**
@@ -100,12 +82,10 @@ class TfaContextTest extends UnitTestCase {
    */
   protected function getFixture() {
     return new TfaContext(
-      $this->tfaValidationManager,
-      $this->tfaLoginManager,
+      $this->pluginManager,
       $this->configFactory,
       $this->user,
-      $this->userData,
-      $this->request
+      $this->userData
     );
   }
 
@@ -209,18 +189,20 @@ class TfaContextTest extends UnitTestCase {
     $this->configFactory = $config_factory->reveal();
     $validator = $this->prophesize(TfaValidationInterface::class);
     $validator->ready()->willReturn(TRUE);
-    $manager = $this->prophesize(TfaValidationPluginManager::class);
+    $manager = $this->prophesize(TfaPluginManager::class);
     $manager->createInstance('foo', ['uid' => 3])->willReturn($validator->reveal());
-    $this->tfaValidationManager = $manager->reveal();
+    $manager->getLoginDefinitions()->willReturn([]);
+    $this->pluginManager = $manager->reveal();
     $fixture = $this->getFixture();
     $this->assertTrue($fixture->isReady());
 
     // Plugin set, but not ready.
     $validator = $this->prophesize(TfaValidationInterface::class);
     $validator->ready()->willReturn(FALSE);
-    $manager = $this->prophesize(TfaValidationPluginManager::class);
+    $manager = $this->prophesize(TfaPluginManager::class);
     $manager->createInstance('foo', ['uid' => 3])->willReturn($validator->reveal());
-    $this->tfaValidationManager = $manager->reveal();
+    $manager->getLoginDefinitions()->willReturn([]);
+    $this->pluginManager = $manager->reveal();
     $fixture = $this->getFixture();
     $this->assertFalse($fixture->isReady());
   }
diff --git a/tfa.services.yml b/tfa.services.yml
index 44ae361..aef1364 100644
--- a/tfa.services.yml
+++ b/tfa.services.yml
@@ -1,19 +1,7 @@
 services:
-  plugin.manager.tfa.login:
-    class: Drupal\tfa\TfaLoginPluginManager
+  plugin.manager.tfa:
+    class: Drupal\tfa\TfaPluginManager
     parent: default_plugin_manager
-    arguments: ['@config.factory', '@user.data', '@encrypt.encryption_profile.manager','@encryption']
-  plugin.manager.tfa.send:
-    class: Drupal\tfa\TfaSendPluginManager
-    parent: default_plugin_manager
-  plugin.manager.tfa.validation:
-    class: Drupal\tfa\TfaValidationPluginManager
-    parent: default_plugin_manager
-    arguments: ['@config.factory', '@user.data', '@encrypt.encryption_profile.manager','@encryption']
-  plugin.manager.tfa.setup:
-    class: Drupal\tfa\TfaSetupPluginManager
-    parent: default_plugin_manager
-    arguments: ['@user.data', '@encrypt.encryption_profile.manager','@encryption']
   tfa.route_subscriber:
     class: Drupal\tfa\Routing\TfaRouteSubscriber
     tags:
