[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/plugins/multifactorauth/totp/src/Extension/ -> Totp.php (source)

   1  <?php
   2  
   3  /**
   4   * @package     Joomla.Plugin
   5   * @subpackage  Multifactorauth.totp
   6   *
   7   * @copyright   (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
   8   * @license     GNU General Public License version 2 or later; see LICENSE.txt
   9   */
  10  
  11  namespace Joomla\Plugin\Multifactorauth\Totp\Extension;
  12  
  13  use Joomla\CMS\Encrypt\Totp as TotpHelper;
  14  use Joomla\CMS\Event\MultiFactor\Captive;
  15  use Joomla\CMS\Event\MultiFactor\GetMethod;
  16  use Joomla\CMS\Event\MultiFactor\GetSetup;
  17  use Joomla\CMS\Event\MultiFactor\SaveSetup;
  18  use Joomla\CMS\Event\MultiFactor\Validate;
  19  use Joomla\CMS\Factory;
  20  use Joomla\CMS\Language\Text;
  21  use Joomla\CMS\Plugin\CMSPlugin;
  22  use Joomla\CMS\Uri\Uri;
  23  use Joomla\CMS\User\User;
  24  use Joomla\CMS\User\UserFactoryInterface;
  25  use Joomla\Component\Users\Administrator\DataShape\CaptiveRenderOptions;
  26  use Joomla\Component\Users\Administrator\DataShape\MethodDescriptor;
  27  use Joomla\Component\Users\Administrator\DataShape\SetupRenderOptions;
  28  use Joomla\Component\Users\Administrator\Table\MfaTable;
  29  use Joomla\Event\SubscriberInterface;
  30  use Joomla\Input\Input;
  31  use RuntimeException;
  32  
  33  // phpcs:disable PSR1.Files.SideEffects
  34  \defined('_JEXEC') or die;
  35  // phpcs:enable PSR1.Files.SideEffects
  36  
  37  /**
  38   * Joomla! Multi-factor Authentication using Google Authenticator TOTP Plugin
  39   *
  40   * @since  3.2
  41   */
  42  class Totp extends CMSPlugin implements SubscriberInterface
  43  {
  44      /**
  45       * Affects constructor behavior. If true, language files will be loaded automatically.
  46       *
  47       * @var    boolean
  48       * @since  3.2
  49       */
  50      protected $autoloadLanguage = true;
  51  
  52      /**
  53       * The MFA Method name handled by this plugin
  54       *
  55       * @var   string
  56       * @since 4.2.0
  57       */
  58      private $mfaMethodName = 'totp';
  59  
  60      /**
  61       * Should I try to detect and register legacy event listeners?
  62       *
  63       * @var   boolean
  64       * @since 4.2.0
  65       *
  66       * @deprecated
  67       */
  68      protected $allowLegacyListeners = false;
  69  
  70      /**
  71       * Returns an array of events this subscriber will listen to.
  72       *
  73       * @return  array
  74       *
  75       * @since   4.2.0
  76       */
  77      public static function getSubscribedEvents(): array
  78      {
  79          return [
  80              'onUserMultifactorGetMethod' => 'onUserMultifactorGetMethod',
  81              'onUserMultifactorCaptive'   => 'onUserMultifactorCaptive',
  82              'onUserMultifactorGetSetup'  => 'onUserMultifactorGetSetup',
  83              'onUserMultifactorSaveSetup' => 'onUserMultifactorSaveSetup',
  84              'onUserMultifactorValidate'  => 'onUserMultifactorValidate',
  85          ];
  86      }
  87  
  88      /**
  89       * Gets the identity of this MFA Method
  90       *
  91       * @param   GetMethod  $event  The event we are handling
  92       *
  93       * @return  void
  94       * @since   4.2.0
  95       */
  96      public function onUserMultifactorGetMethod(GetMethod $event): void
  97      {
  98          $event->addResult(
  99              new MethodDescriptor(
 100                  [
 101                      'name'      => $this->mfaMethodName,
 102                      'display'   => Text::_('PLG_MULTIFACTORAUTH_TOTP_METHOD_TITLE'),
 103                      'shortinfo' => Text::_('PLG_MULTIFACTORAUTH_TOTP_SHORTINFO'),
 104                      'image'     => 'media/plg_multifactorauth_totp/images/totp.svg',
 105                  ]
 106              )
 107          );
 108      }
 109  
 110      /**
 111       * Returns the information which allows Joomla to render the Captive MFA page. This is the page
 112       * which appears right after you log in and asks you to validate your login with MFA.
 113       *
 114       * @param   Captive  $event  The event we are handling
 115       *
 116       * @return  void
 117       * @since   4.2.0
 118       */
 119      public function onUserMultifactorCaptive(Captive $event): void
 120      {
 121          /**
 122           * @var   MfaTable $record The record currently selected by the user.
 123           */
 124          $record = $event['record'];
 125  
 126          // Make sure we are actually meant to handle this Method
 127          if ($record->method !== $this->mfaMethodName) {
 128              return;
 129          }
 130  
 131          $event->addResult(
 132              new CaptiveRenderOptions(
 133                  [
 134                      // Custom HTML to display above the MFA form
 135                      'pre_message'      => Text::_('PLG_MULTIFACTORAUTH_TOTP_CAPTIVE_PROMPT'),
 136                      // How to render the MFA code field. "input" (HTML input element) or "custom" (custom HTML)
 137                      'field_type'       => 'input',
 138                      // The type attribute for the HTML input box. Typically "text" or "password". Use any HTML5 input type.
 139                      'input_type'       => 'text',
 140                      // The attributes for the HTML input box.
 141                      'input_attributes' => [
 142                          'pattern' => "{0,9}", 'maxlength' => "6", 'inputmode' => "numeric"
 143                      ],
 144                      // Placeholder text for the HTML input box. Leave empty if you don't need it.
 145                      'placeholder'      => '',
 146                      // Label to show above the HTML input box. Leave empty if you don't need it.
 147                      'label'            => Text::_('PLG_MULTIFACTORAUTH_TOTP_LBL_LABEL'),
 148                      // Custom HTML. Only used when field_type = custom.
 149                      'html'             => '',
 150                      // Custom HTML to display below the MFA form
 151                      'post_message'     => '',
 152                  ]
 153              )
 154          );
 155      }
 156  
 157      /**
 158       * Returns the information which allows Joomla to render the MFA setup page. This is the page
 159       * which allows the user to add or modify a MFA Method for their user account. If the record
 160       * does not correspond to your plugin return an empty array.
 161       *
 162       * @param   GetSetup  $event  The event we are handling
 163       *
 164       * @return  void
 165       * @since   4.2.0
 166       */
 167      public function onUserMultifactorGetSetup(GetSetup $event): void
 168      {
 169          /**
 170           * @var   MfaTable $record The record currently selected by the user.
 171           */
 172          $record = $event['record'];
 173  
 174          // Make sure we are actually meant to handle this Method
 175          if ($record->method !== $this->mfaMethodName) {
 176              return;
 177          }
 178  
 179          $totp = new TotpHelper();
 180  
 181          // Load the options from the record (if any)
 182          $options      = $this->decodeRecordOptions($record);
 183          $key          = $options['key'] ?? '';
 184          $session      = $this->getApplication()->getSession();
 185          $isConfigured = !empty($key);
 186  
 187          // If there's a key in the session use that instead.
 188          $sessionKey = $session->get('com_users.totp.key', null);
 189  
 190          if (!empty($sessionKey)) {
 191              $key = $sessionKey;
 192          }
 193  
 194          // If there's still no key in the options, generate one and save it in the session
 195          if (empty($key)) {
 196              $key = $totp->generateSecret();
 197              $session->set('com_users.totp.key', $key);
 198          }
 199  
 200          // Generate a QR code for the key
 201          $user     = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($record->user_id);
 202          $hostname = Uri::getInstance()->toString(['host']);
 203          $otpURL   = sprintf("otpauth://totp/%s@%s?secret=%s", $user->username, $hostname, $key);
 204          $document = $this->getApplication()->getDocument();
 205          $wam      = $document->getWebAssetManager();
 206  
 207          $document->addScriptOptions('plg_multifactorauth_totp.totp.qr', $otpURL);
 208  
 209          $wam->getRegistry()->addExtensionRegistryFile('plg_multifactorauth_totp');
 210          $wam->useScript('plg_multifactorauth_totp.setup');
 211  
 212          $event->addResult(
 213              new SetupRenderOptions(
 214                  [
 215                      'default_title'    => Text::_('PLG_MULTIFACTORAUTH_TOTP_METHOD_TITLE'),
 216                      'pre_message'      => Text::_('PLG_MULTIFACTORAUTH_TOTP_LBL_SETUP_INSTRUCTIONS'),
 217                      'table_heading'    => Text::_('PLG_MULTIFACTORAUTH_TOTP_LBL_SETUP_TABLE_HEADING'),
 218                      'tabular_data'     => [
 219                          '' => Text::_('PLG_MULTIFACTORAUTH_TOTP_LBL_SETUP_TABLE_SUBHEAD'),
 220                          Text::_('PLG_MULTIFACTORAUTH_TOTP_LBL_SETUP_TABLE_KEY')  => $key,
 221                          Text::_('PLG_MULTIFACTORAUTH_TOTP_LBL_SETUP_TABLE_QR')   => "<span id=\"users-mfa-totp-qrcode\" />",
 222                          Text::_('PLG_MULTIFACTORAUTH_TOTP_LBL_SETUP_TABLE_LINK')
 223                              => Text::sprintf('PLG_MULTIFACTORAUTH_TOTP_LBL_SETUP_TABLE_LINK_TEXT', $otpURL) .
 224                              '<br/><small>' . Text::_('PLG_MULTIFACTORAUTH_TOTP_LBL_SETUP_TABLE_LINK_NOTE') . '</small>',
 225                      ],
 226                      'hidden_data'   => [
 227                          'key' => $key,
 228                      ],
 229                      'input_type'       => $isConfigured ? 'hidden' : 'text',
 230                      'input_attributes' => [
 231                          'pattern' => "{0,9}", 'maxlength' => "6", 'inputmode' => "numeric"
 232                      ],
 233                      'input_value'      => '',
 234                      'placeholder'      => Text::_('PLG_MULTIFACTORAUTH_TOTP_LBL_SETUP_PLACEHOLDER'),
 235                      'label'            => Text::_('PLG_MULTIFACTORAUTH_TOTP_LBL_LABEL'),
 236                  ]
 237              )
 238          );
 239      }
 240  
 241      /**
 242       * Parse the input from the MFA setup page and return the configuration information to be saved to the database. If
 243       * the information is invalid throw a RuntimeException to signal the need to display the editor page again. The
 244       * message of the exception will be displayed to the user. If the record does not correspond to your plugin return
 245       * an empty array.
 246       *
 247       * @param   SaveSetup  $event  The event we are handling
 248       *
 249       * @return  void The configuration data to save to the database
 250       * @since   4.2.0
 251       */
 252      public function onUserMultifactorSaveSetup(SaveSetup $event): void
 253      {
 254          /**
 255           * @var   MfaTable $record The record currently selected by the user.
 256           * @var   Input    $input  The user input you are going to take into account.
 257           */
 258          $record = $event['record'];
 259          $input  = $event['input'];
 260  
 261          // Make sure we are actually meant to handle this Method
 262          if ($record->method != $this->mfaMethodName) {
 263              return;
 264          }
 265  
 266          // Load the options from the record (if any)
 267          $options    = $this->decodeRecordOptions($record);
 268          $optionsKey = $options['key'] ?? '';
 269          $key        = $optionsKey;
 270          $session    = $this->getApplication()->getSession();
 271  
 272          // If there is no key in the options fetch one from the session
 273          if (empty($key)) {
 274              $key = $session->get('com_users.totp.key', null);
 275          }
 276  
 277          // If there is still no key in the options throw an error
 278          if (empty($key)) {
 279              throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
 280          }
 281  
 282          /**
 283           * If the code is empty but the key already existed in $options someone is simply changing the title / default
 284           * Method status. We can allow this and stop checking anything else now.
 285           */
 286          $code = $input->getInt('code');
 287  
 288          if (empty($code) && !empty($optionsKey)) {
 289              $event->addResult($options);
 290  
 291              return;
 292          }
 293  
 294          // In any other case validate the submitted code
 295          $totp    = new TotpHelper();
 296          $isValid = $totp->checkCode($key, $code);
 297  
 298          if (!$isValid) {
 299              throw new RuntimeException(Text::_('PLG_MULTIFACTORAUTH_TOTP_ERR_VALIDATIONFAILED'), 500);
 300          }
 301  
 302          // The code is valid. Unset the key from the session.
 303          $session->set('com_users.totp.key', null);
 304  
 305          // Return the configuration to be serialized
 306          $event->addResult(
 307              [
 308                  'key' => $key,
 309              ]
 310          );
 311      }
 312  
 313      /**
 314       * Validates the Multi-factor Authentication code submitted by the user in the Multi-Factor
 315       * Authentication page. If the record does not correspond to your plugin return FALSE.
 316       *
 317       * @param   Validate  $event  The event we are handling
 318       *
 319       * @return  void
 320       * @since   4.2.0
 321       */
 322      public function onUserMultifactorValidate(Validate $event): void
 323      {
 324          /**
 325           * @var   MfaTable $record The MFA Method's record you're validating against
 326           * @var   User     $user   The user record
 327           * @var   string   $code   The submitted code
 328           */
 329          $record = $event['record'];
 330          $user   = $event['user'];
 331          $code   = $event['code'];
 332  
 333          // Make sure we are actually meant to handle this Method
 334          if ($record->method !== $this->mfaMethodName) {
 335              $event->addResult(false);
 336  
 337              return;
 338          }
 339  
 340          // Double check the MFA Method is for the correct user
 341          if ($user->id != $record->user_id) {
 342              $event->addResult(false);
 343  
 344              return;
 345          }
 346  
 347          // Load the options from the record (if any)
 348          $options = $this->decodeRecordOptions($record);
 349          $key     = $options['key'] ?? '';
 350  
 351          // If there is no key in the options throw an error
 352          if (empty($key)) {
 353              $event->addResult(false);
 354  
 355              return;
 356          }
 357  
 358          // Check the MFA code for validity
 359          $event->addResult((new TotpHelper())->checkCode($key, $code));
 360      }
 361  
 362      /**
 363       * Decodes the options from a record into an options object.
 364       *
 365       * @param   MfaTable  $record  The record to decode options for
 366       *
 367       * @return  array
 368       * @since   4.2.0
 369       */
 370      private function decodeRecordOptions(MfaTable $record): array
 371      {
 372          $options = [
 373              'key' => '',
 374          ];
 375  
 376          if (!empty($record->options)) {
 377              $recordOptions = $record->options;
 378  
 379              $options = array_merge($options, $recordOptions);
 380          }
 381  
 382          return $options;
 383      }
 384  }


Generated: Wed Sep 7 05:41:13 2022 Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer