[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/plugins/authentication/cookie/ -> cookie.php (source)

   1  <?php
   2  
   3  /**
   4   * @package     Joomla.Plugin
   5   * @subpackage  Authentication.cookie
   6   *
   7   * @copyright   (C) 2013 Open Source Matters, Inc. <https://www.joomla.org>
   8   * @license     GNU General Public License version 2 or later; see LICENSE.txt
   9  
  10   * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace
  11   */
  12  
  13  use Joomla\CMS\Authentication\Authentication;
  14  use Joomla\CMS\Filter\InputFilter;
  15  use Joomla\CMS\Language\Text;
  16  use Joomla\CMS\Log\Log;
  17  use Joomla\CMS\Plugin\CMSPlugin;
  18  use Joomla\CMS\User\User;
  19  use Joomla\CMS\User\UserHelper;
  20  
  21  // phpcs:disable PSR1.Files.SideEffects
  22  \defined('_JEXEC') or die;
  23  // phpcs:enable PSR1.Files.SideEffects
  24  
  25  /**
  26   * Joomla Authentication plugin
  27   *
  28   * @since  3.2
  29   * @note   Code based on http://jaspan.com/improved_persistent_login_cookie_best_practice
  30   *         and http://fishbowl.pastiche.org/2004/01/19/persistent_login_cookie_best_practice/
  31   */
  32  class PlgAuthenticationCookie extends CMSPlugin
  33  {
  34      /**
  35       * Application object
  36       *
  37       * @var    \Joomla\CMS\Application\CMSApplication
  38       * @since  3.2
  39       */
  40      protected $app;
  41  
  42      /**
  43       * Database object
  44       *
  45       * @var    \Joomla\Database\DatabaseDriver
  46       * @since  3.2
  47       */
  48      protected $db;
  49  
  50      /**
  51       * Reports the privacy related capabilities for this plugin to site administrators.
  52       *
  53       * @return  array
  54       *
  55       * @since   3.9.0
  56       */
  57      public function onPrivacyCollectAdminCapabilities()
  58      {
  59          $this->loadLanguage();
  60  
  61          return array(
  62              Text::_('PLG_AUTHENTICATION_COOKIE') => array(
  63                  Text::_('PLG_AUTHENTICATION_COOKIE_PRIVACY_CAPABILITY_COOKIE'),
  64              ),
  65          );
  66      }
  67  
  68      /**
  69       * This method should handle any authentication and report back to the subject
  70       *
  71       * @param   array   $credentials  Array holding the user credentials
  72       * @param   array   $options      Array of extra options
  73       * @param   object  &$response    Authentication response object
  74       *
  75       * @return  boolean
  76       *
  77       * @since   3.2
  78       */
  79      public function onUserAuthenticate($credentials, $options, &$response)
  80      {
  81          // No remember me for admin
  82          if ($this->app->isClient('administrator')) {
  83              return false;
  84          }
  85  
  86          // Get cookie
  87          $cookieName  = 'joomla_remember_me_' . UserHelper::getShortHashedUserAgent();
  88          $cookieValue = $this->app->input->cookie->get($cookieName);
  89  
  90          // Try with old cookieName (pre 3.6.0) if not found
  91          if (!$cookieValue) {
  92              $cookieName  = UserHelper::getShortHashedUserAgent();
  93              $cookieValue = $this->app->input->cookie->get($cookieName);
  94          }
  95  
  96          if (!$cookieValue) {
  97              return false;
  98          }
  99  
 100          $cookieArray = explode('.', $cookieValue);
 101  
 102          // Check for valid cookie value
 103          if (count($cookieArray) !== 2) {
 104              // Destroy the cookie in the browser.
 105              $this->app->input->cookie->set($cookieName, '', 1, $this->app->get('cookie_path', '/'), $this->app->get('cookie_domain', ''));
 106              Log::add('Invalid cookie detected.', Log::WARNING, 'error');
 107  
 108              return false;
 109          }
 110  
 111          $response->type = 'Cookie';
 112  
 113          // Filter series since we're going to use it in the query
 114          $filter = new InputFilter();
 115          $series = $filter->clean($cookieArray[1], 'ALNUM');
 116          $now    = time();
 117  
 118          // Remove expired tokens
 119          $query = $this->db->getQuery(true)
 120              ->delete($this->db->quoteName('#__user_keys'))
 121              ->where($this->db->quoteName('time') . ' < :now')
 122              ->bind(':now', $now);
 123  
 124          try {
 125              $this->db->setQuery($query)->execute();
 126          } catch (RuntimeException $e) {
 127              // We aren't concerned with errors from this query, carry on
 128          }
 129  
 130          // Find the matching record if it exists.
 131          $query = $this->db->getQuery(true)
 132              ->select($this->db->quoteName(['user_id', 'token', 'series', 'time']))
 133              ->from($this->db->quoteName('#__user_keys'))
 134              ->where($this->db->quoteName('series') . ' = :series')
 135              ->where($this->db->quoteName('uastring') . ' = :uastring')
 136              ->order($this->db->quoteName('time') . ' DESC')
 137              ->bind(':series', $series)
 138              ->bind(':uastring', $cookieName);
 139  
 140          try {
 141              $results = $this->db->setQuery($query)->loadObjectList();
 142          } catch (RuntimeException $e) {
 143              $response->status = Authentication::STATUS_FAILURE;
 144  
 145              return false;
 146          }
 147  
 148          if (count($results) !== 1) {
 149              // Destroy the cookie in the browser.
 150              $this->app->input->cookie->set($cookieName, '', 1, $this->app->get('cookie_path', '/'), $this->app->get('cookie_domain', ''));
 151              $response->status = Authentication::STATUS_FAILURE;
 152  
 153              return false;
 154          }
 155  
 156          // We have a user with one cookie with a valid series and a corresponding record in the database.
 157          if (!UserHelper::verifyPassword($cookieArray[0], $results[0]->token)) {
 158              /*
 159               * This is a real attack!
 160               * Either the series was guessed correctly or a cookie was stolen and used twice (once by attacker and once by victim).
 161               * Delete all tokens for this user!
 162               */
 163              $query = $this->db->getQuery(true)
 164                  ->delete($this->db->quoteName('#__user_keys'))
 165                  ->where($this->db->quoteName('user_id') . ' = :userid')
 166                  ->bind(':userid', $results[0]->user_id);
 167  
 168              try {
 169                  $this->db->setQuery($query)->execute();
 170              } catch (RuntimeException $e) {
 171                  // Log an alert for the site admin
 172                  Log::add(
 173                      sprintf('Failed to delete cookie token for user %s with the following error: %s', $results[0]->user_id, $e->getMessage()),
 174                      Log::WARNING,
 175                      'security'
 176                  );
 177              }
 178  
 179              // Destroy the cookie in the browser.
 180              $this->app->input->cookie->set($cookieName, '', 1, $this->app->get('cookie_path', '/'), $this->app->get('cookie_domain', ''));
 181  
 182              // Issue warning by email to user and/or admin?
 183              Log::add(Text::sprintf('PLG_AUTHENTICATION_COOKIE_ERROR_LOG_LOGIN_FAILED', $results[0]->user_id), Log::WARNING, 'security');
 184              $response->status = Authentication::STATUS_FAILURE;
 185  
 186              return false;
 187          }
 188  
 189          // Make sure there really is a user with this name and get the data for the session.
 190          $query = $this->db->getQuery(true)
 191              ->select($this->db->quoteName(['id', 'username', 'password']))
 192              ->from($this->db->quoteName('#__users'))
 193              ->where($this->db->quoteName('username') . ' = :userid')
 194              ->where($this->db->quoteName('requireReset') . ' = 0')
 195              ->bind(':userid', $results[0]->user_id);
 196  
 197          try {
 198              $result = $this->db->setQuery($query)->loadObject();
 199          } catch (RuntimeException $e) {
 200              $response->status = Authentication::STATUS_FAILURE;
 201  
 202              return false;
 203          }
 204  
 205          if ($result) {
 206              // Bring this in line with the rest of the system
 207              $user = User::getInstance($result->id);
 208  
 209              // Set response data.
 210              $response->username = $result->username;
 211              $response->email    = $user->email;
 212              $response->fullname = $user->name;
 213              $response->password = $result->password;
 214              $response->language = $user->getParam('language');
 215  
 216              // Set response status.
 217              $response->status        = Authentication::STATUS_SUCCESS;
 218              $response->error_message = '';
 219          } else {
 220              $response->status        = Authentication::STATUS_FAILURE;
 221              $response->error_message = Text::_('JGLOBAL_AUTH_NO_USER');
 222          }
 223      }
 224  
 225      /**
 226       * We set the authentication cookie only after login is successfully finished.
 227       * We set a new cookie either for a user with no cookies or one
 228       * where the user used a cookie to authenticate.
 229       *
 230       * @param   array  $options  Array holding options
 231       *
 232       * @return  boolean  True on success
 233       *
 234       * @since   3.2
 235       */
 236      public function onUserAfterLogin($options)
 237      {
 238          // No remember me for admin
 239          if ($this->app->isClient('administrator')) {
 240              return false;
 241          }
 242  
 243          if (isset($options['responseType']) && $options['responseType'] === 'Cookie') {
 244              // Logged in using a cookie
 245              $cookieName = 'joomla_remember_me_' . UserHelper::getShortHashedUserAgent();
 246  
 247              // We need the old data to get the existing series
 248              $cookieValue = $this->app->input->cookie->get($cookieName);
 249  
 250              // Try with old cookieName (pre 3.6.0) if not found
 251              if (!$cookieValue) {
 252                  $oldCookieName = UserHelper::getShortHashedUserAgent();
 253                  $cookieValue   = $this->app->input->cookie->get($oldCookieName);
 254  
 255                  // Destroy the old cookie in the browser
 256                  $this->app->input->cookie->set($oldCookieName, '', 1, $this->app->get('cookie_path', '/'), $this->app->get('cookie_domain', ''));
 257              }
 258  
 259              $cookieArray = explode('.', $cookieValue);
 260  
 261              // Filter series since we're going to use it in the query
 262              $filter = new InputFilter();
 263              $series = $filter->clean($cookieArray[1], 'ALNUM');
 264          } elseif (!empty($options['remember'])) {
 265              // Remember checkbox is set
 266              $cookieName = 'joomla_remember_me_' . UserHelper::getShortHashedUserAgent();
 267  
 268              // Create a unique series which will be used over the lifespan of the cookie
 269              $unique     = false;
 270              $errorCount = 0;
 271  
 272              do {
 273                  $series = UserHelper::genRandomPassword(20);
 274                  $query  = $this->db->getQuery(true)
 275                      ->select($this->db->quoteName('series'))
 276                      ->from($this->db->quoteName('#__user_keys'))
 277                      ->where($this->db->quoteName('series') . ' = :series')
 278                      ->bind(':series', $series);
 279  
 280                  try {
 281                      $results = $this->db->setQuery($query)->loadResult();
 282  
 283                      if ($results === null) {
 284                          $unique = true;
 285                      }
 286                  } catch (RuntimeException $e) {
 287                      $errorCount++;
 288  
 289                      // We'll let this query fail up to 5 times before giving up, there's probably a bigger issue at this point
 290                      if ($errorCount === 5) {
 291                          return false;
 292                      }
 293                  }
 294              } while ($unique === false);
 295          } else {
 296              return false;
 297          }
 298  
 299          // Get the parameter values
 300          $lifetime = $this->params->get('cookie_lifetime', 60) * 24 * 60 * 60;
 301          $length   = $this->params->get('key_length', 16);
 302  
 303          // Generate new cookie
 304          $token       = UserHelper::genRandomPassword($length);
 305          $cookieValue = $token . '.' . $series;
 306  
 307          // Overwrite existing cookie with new value
 308          $this->app->input->cookie->set(
 309              $cookieName,
 310              $cookieValue,
 311              time() + $lifetime,
 312              $this->app->get('cookie_path', '/'),
 313              $this->app->get('cookie_domain', ''),
 314              $this->app->isHttpsForced(),
 315              true
 316          );
 317  
 318          $query = $this->db->getQuery(true);
 319  
 320          if (!empty($options['remember'])) {
 321              $future = (time() + $lifetime);
 322  
 323              // Create new record
 324              $query
 325                  ->insert($this->db->quoteName('#__user_keys'))
 326                  ->set($this->db->quoteName('user_id') . ' = :userid')
 327                  ->set($this->db->quoteName('series') . ' = :series')
 328                  ->set($this->db->quoteName('uastring') . ' = :uastring')
 329                  ->set($this->db->quoteName('time') . ' = :time')
 330                  ->bind(':userid', $options['user']->username)
 331                  ->bind(':series', $series)
 332                  ->bind(':uastring', $cookieName)
 333                  ->bind(':time', $future);
 334          } else {
 335              // Update existing record with new token
 336              $query
 337                  ->update($this->db->quoteName('#__user_keys'))
 338                  ->where($this->db->quoteName('user_id') . ' = :userid')
 339                  ->where($this->db->quoteName('series') . ' = :series')
 340                  ->where($this->db->quoteName('uastring') . ' = :uastring')
 341                  ->bind(':userid', $options['user']->username)
 342                  ->bind(':series', $series)
 343                  ->bind(':uastring', $cookieName);
 344          }
 345  
 346          $hashedToken = UserHelper::hashPassword($token);
 347  
 348          $query->set($this->db->quoteName('token') . ' = :token')
 349              ->bind(':token', $hashedToken);
 350  
 351          try {
 352              $this->db->setQuery($query)->execute();
 353          } catch (RuntimeException $e) {
 354              return false;
 355          }
 356  
 357          return true;
 358      }
 359  
 360      /**
 361       * This is where we delete any authentication cookie when a user logs out
 362       *
 363       * @param   array  $options  Array holding options (length, timeToExpiration)
 364       *
 365       * @return  boolean  True on success
 366       *
 367       * @since   3.2
 368       */
 369      public function onUserAfterLogout($options)
 370      {
 371          // No remember me for admin
 372          if ($this->app->isClient('administrator')) {
 373              return false;
 374          }
 375  
 376          $cookieName  = 'joomla_remember_me_' . UserHelper::getShortHashedUserAgent();
 377          $cookieValue = $this->app->input->cookie->get($cookieName);
 378  
 379          // There are no cookies to delete.
 380          if (!$cookieValue) {
 381              return true;
 382          }
 383  
 384          $cookieArray = explode('.', $cookieValue);
 385  
 386          // Filter series since we're going to use it in the query
 387          $filter = new InputFilter();
 388          $series = $filter->clean($cookieArray[1], 'ALNUM');
 389  
 390          // Remove the record from the database
 391          $query = $this->db->getQuery(true)
 392              ->delete($this->db->quoteName('#__user_keys'))
 393              ->where($this->db->quoteName('series') . ' = :series')
 394              ->bind(':series', $series);
 395  
 396          try {
 397              $this->db->setQuery($query)->execute();
 398          } catch (RuntimeException $e) {
 399              // We aren't concerned with errors from this query, carry on
 400          }
 401  
 402          // Destroy the cookie
 403          $this->app->input->cookie->set($cookieName, '', 1, $this->app->get('cookie_path', '/'), $this->app->get('cookie_domain', ''));
 404  
 405          return true;
 406      }
 407  }


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