[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/plugins/api-authentication/token/src/Extension/ -> Token.php (source)

   1  <?php
   2  
   3  /**
   4   * @package     Joomla.Plugin
   5   * @subpackage  Apiauthentication.token
   6   *
   7   * @copyright   (C) 2020 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\ApiAuthentication\Token\Extension;
  12  
  13  use Joomla\CMS\Authentication\Authentication;
  14  use Joomla\CMS\Crypt\Crypt;
  15  use Joomla\CMS\Plugin\CMSPlugin;
  16  use Joomla\CMS\User\UserFactoryInterface;
  17  use Joomla\Component\Plugins\Administrator\Model\PluginModel;
  18  use Joomla\Database\DatabaseAwareTrait;
  19  use Joomla\Database\ParameterType;
  20  use Joomla\Event\DispatcherInterface;
  21  use Joomla\Filter\InputFilter;
  22  
  23  // phpcs:disable PSR1.Files.SideEffects
  24  \defined('_JEXEC') or die;
  25  // phpcs:enable PSR1.Files.SideEffects
  26  
  27  /**
  28   * Joomla Token Authentication plugin
  29   *
  30   * @since  4.0.0
  31   */
  32  final class Token extends CMSPlugin
  33  {
  34      use DatabaseAwareTrait;
  35  
  36      /**
  37       * The prefix of the user profile keys, without the dot.
  38       *
  39       * @var    string
  40       * @since  4.0.0
  41       */
  42      private $profileKeyPrefix = 'joomlatoken';
  43  
  44      /**
  45       * Allowed HMAC algorithms for the token
  46       *
  47       * @var    string[]
  48       * @since  4.0.0
  49       */
  50      private $allowedAlgos = ['sha256', 'sha512'];
  51  
  52      /**
  53       * The user factory
  54       *
  55       * @var    UserFactoryInterface
  56       * @since  4.2.0
  57       */
  58      private $userFactory;
  59  
  60      /**
  61       * The input filter
  62       *
  63       * @var    InputFilter
  64       * @since  4.2.0
  65       */
  66      private $filter;
  67  
  68      /**
  69       * Constructor.
  70       *
  71       * @param   DispatcherInterface   $dispatcher   The dispatcher
  72       * @param   array                 $config       An optional associative array of configuration settings
  73       * @param   UserFactoryInterface  $userFactory  The user factory
  74       * @param   InputFilter           $filter       The input filter
  75       *
  76       * @since   4.2.0
  77       */
  78      public function __construct(DispatcherInterface $dispatcher, array $config, UserFactoryInterface $userFactory, InputFilter $filter)
  79      {
  80          parent::__construct($dispatcher, $config);
  81  
  82          $this->userFactory = $userFactory;
  83          $this->filter      = $filter;
  84      }
  85  
  86      /**
  87       * This method should handle any authentication and report back to the subject
  88       *
  89       * @param   array   $credentials  Array holding the user credentials
  90       * @param   array   $options      Array of extra options
  91       * @param   object  $response     Authentication response object
  92       *
  93       * @return  void
  94       *
  95       * @since   4.0.0
  96       */
  97      public function onUserAuthenticate($credentials, $options, &$response): void
  98      {
  99          // Default response is authentication failure.
 100          $response->type          = 'Token';
 101          $response->status        = Authentication::STATUS_FAILURE;
 102          $response->error_message = $this->getApplication()->getLanguage()->_('JGLOBAL_AUTH_FAIL');
 103  
 104          /**
 105           * First look for an HTTP Authorization header with the following format:
 106           * Authorization: Bearer <token>
 107           * Do keep in mind that Bearer is **case-sensitive**. Whitespace between Bearer and the
 108           * token, as well as any whitespace following the token is discarded.
 109           */
 110          $authHeader  = $this->getApplication()->input->server->get('HTTP_AUTHORIZATION', '', 'string');
 111          $tokenString = '';
 112  
 113          // Apache specific fixes. See https://github.com/symfony/symfony/issues/19693
 114          if (
 115              empty($authHeader) && \PHP_SAPI === 'apache2handler'
 116              && function_exists('apache_request_headers') && apache_request_headers() !== false
 117          ) {
 118              $apacheHeaders = array_change_key_case(apache_request_headers(), CASE_LOWER);
 119  
 120              if (array_key_exists('authorization', $apacheHeaders)) {
 121                  $authHeader = $this->filter->clean($apacheHeaders['authorization'], 'STRING');
 122              }
 123          }
 124  
 125          if (substr($authHeader, 0, 7) == 'Bearer ') {
 126              $parts       = explode(' ', $authHeader, 2);
 127              $tokenString = trim($parts[1]);
 128              $tokenString = $this->filter->clean($tokenString, 'BASE64');
 129          }
 130  
 131          if (empty($tokenString)) {
 132              $tokenString = $this->getApplication()->input->server->get('HTTP_X_JOOMLA_TOKEN', '', 'string');
 133          }
 134  
 135          // No token: authentication failure
 136          if (empty($tokenString)) {
 137              return;
 138          }
 139  
 140          // The token is a base64 encoded string. Make sure we can decode it.
 141          $authString = @base64_decode($tokenString);
 142  
 143          if (empty($authString) || (strpos($authString, ':') === false)) {
 144              return;
 145          }
 146  
 147          /**
 148           * Deconstruct the decoded token string to its three discrete parts: algorithm, user ID and
 149           * HMAC of the token string saved in the database.
 150           */
 151          $parts = explode(':', $authString, 3);
 152  
 153          if (count($parts) != 3) {
 154              return;
 155          }
 156  
 157          list($algo, $userId, $tokenHMAC) = $parts;
 158  
 159          /**
 160           * Verify the HMAC algorithm requested in the token string is allowed
 161           */
 162          $allowedAlgo = in_array($algo, $this->allowedAlgos);
 163  
 164          /**
 165           * Make sure the user ID is an integer
 166           */
 167          $userId = (int) $userId;
 168  
 169          /**
 170           * Calculate the reference token data HMAC
 171           */
 172          try {
 173              $siteSecret = $this->getApplication()->get('secret');
 174          } catch (\Exception $e) {
 175              return;
 176          }
 177  
 178          // An empty secret! What kind of monster are you?!
 179          if (empty($siteSecret)) {
 180              return;
 181          }
 182  
 183          $referenceTokenData = $this->getTokenSeedForUser($userId);
 184          $referenceTokenData = empty($referenceTokenData) ? '' : $referenceTokenData;
 185          $referenceTokenData = base64_decode($referenceTokenData);
 186          $referenceHMAC      = hash_hmac($algo, $referenceTokenData, $siteSecret);
 187  
 188          // Is the token enabled?
 189          $enabled = $this->isTokenEnabledForUser($userId);
 190  
 191          // Do the tokens match? Use a timing safe string comparison to prevent timing attacks.
 192          $hashesMatch = Crypt::timingSafeCompare($referenceHMAC, $tokenHMAC);
 193  
 194          // Is the user in the allowed user groups?
 195          $inAllowedUserGroups = $this->isInAllowedUserGroup($userId);
 196  
 197          /**
 198           * Can we log in?
 199           *
 200           * DO NOT concatenate in a single line. Due to boolean short-circuit evaluation it might
 201           * make timing attacks possible. Using separate lines of code with the previously calculated
 202           * boolean value to the right hand side forces PHP to evaluate the conditions in
 203           * approximately constant time.
 204           */
 205  
 206          // We need non-empty reference token data (the user must have configured a token)
 207          $canLogin = !empty($referenceTokenData);
 208  
 209          // The token must be enabled
 210          $canLogin = $enabled && $canLogin;
 211  
 212          // The token hash must be calculated with an allowed algorithm
 213          $canLogin = $allowedAlgo && $canLogin;
 214  
 215          // The token HMAC hash coming into the request and our reference must match.
 216          $canLogin = $hashesMatch && $canLogin;
 217  
 218          // The user must belong in the allowed user groups
 219          $canLogin = $inAllowedUserGroups && $canLogin;
 220  
 221          /**
 222           * DO NOT try to be smart and do an early return when either of the individual conditions
 223           * are not met. There's a reason we only return after checking all three conditions: it
 224           * prevents timing attacks.
 225           */
 226          if (!$canLogin) {
 227              return;
 228          }
 229  
 230          // Get the actual user record
 231          $user = $this->userFactory->loadUserById($userId);
 232  
 233          // Disallow login for blocked, inactive or password reset required users
 234          if ($user->block || !empty(trim($user->activation)) || $user->requireReset) {
 235              $response->status = Authentication::STATUS_DENIED;
 236  
 237              return;
 238          }
 239  
 240          // Update the response to indicate successful login
 241          $response->status        = Authentication::STATUS_SUCCESS;
 242          $response->error_message = '';
 243          $response->username      = $user->username;
 244          $response->email         = $user->email;
 245          $response->fullname      = $user->name;
 246          $response->timezone      = $user->get('timezone');
 247          $response->language      = $user->get('language');
 248      }
 249  
 250      /**
 251       * Retrieve the token seed string for the given user ID.
 252       *
 253       * @param   int  $userId  The numeric user ID to return the token seed string for.
 254       *
 255       * @return  string|null  Null if there is no token configured or the user doesn't exist.
 256       * @since   4.0.0
 257       */
 258      private function getTokenSeedForUser(int $userId): ?string
 259      {
 260          try {
 261              $db    = $this->getDatabase();
 262              $query = $db->getQuery(true)
 263                  ->select($db->quoteName('profile_value'))
 264                  ->from($db->quoteName('#__user_profiles'))
 265                  ->where($db->quoteName('profile_key') . ' = :profileKey')
 266                  ->where($db->quoteName('user_id') . ' = :userId');
 267  
 268              $profileKey = $this->profileKeyPrefix . '.token';
 269              $query->bind(':profileKey', $profileKey, ParameterType::STRING);
 270              $query->bind(':userId', $userId, ParameterType::INTEGER);
 271  
 272              return $db->setQuery($query)->loadResult();
 273          } catch (\Exception $e) {
 274              return null;
 275          }
 276      }
 277  
 278      /**
 279       * Is the token enabled for a given user ID? If the user does not exist or has no token it
 280       * returns false.
 281       *
 282       * @param   int  $userId  The User ID to check whether the token is enabled on their account.
 283       *
 284       * @return  boolean
 285       * @since   4.0.0
 286       */
 287      private function isTokenEnabledForUser(int $userId): bool
 288      {
 289          try {
 290              $db    = $this->getDatabase();
 291              $query = $db->getQuery(true)
 292                  ->select($db->quoteName('profile_value'))
 293                  ->from($db->quoteName('#__user_profiles'))
 294                  ->where($db->quoteName('profile_key') . ' = :profileKey')
 295                  ->where($db->quoteName('user_id') . ' = :userId');
 296  
 297              $profileKey = $this->profileKeyPrefix . '.enabled';
 298              $query->bind(':profileKey', $profileKey, ParameterType::STRING);
 299              $query->bind(':userId', $userId, ParameterType::INTEGER);
 300  
 301              $value = $db->setQuery($query)->loadResult();
 302  
 303              return $value == 1;
 304          } catch (\Exception $e) {
 305              return false;
 306          }
 307      }
 308  
 309      /**
 310       * Retrieves a configuration parameter of a different plugin than the current one.
 311       *
 312       * @param   string  $folder  Plugin folder
 313       * @param   string  $plugin  Plugin name
 314       * @param   string  $param   Parameter name
 315       * @param   null    $default Default value, in case the parameter is missing
 316       *
 317       * @return  mixed
 318       * @since   4.0.0
 319       */
 320      private function getPluginParameter(string $folder, string $plugin, string $param, $default = null)
 321      {
 322          /** @var PluginModel $model */
 323          $model = $this->getApplication()->bootComponent('plugins')
 324              ->getMVCFactory()->createModel('Plugin', 'Administrator', ['ignore_request' => true]);
 325  
 326          $pluginObject = $model->getItem(['folder' => $folder, 'element' => $plugin]);
 327  
 328          if (!\is_object($pluginObject) || !$pluginObject->enabled || !\array_key_exists($param, $pluginObject->params)) {
 329              return $default;
 330          }
 331  
 332          return $pluginObject->params[$param];
 333      }
 334  
 335      /**
 336       * Get the configured user groups which are allowed to have access to tokens.
 337       *
 338       * @return  int[]
 339       * @since   4.0.0
 340       */
 341      private function getAllowedUserGroups(): array
 342      {
 343          $userGroups = $this->getPluginParameter('user', 'token', 'allowedUserGroups', [8]);
 344  
 345          if (empty($userGroups)) {
 346              return [];
 347          }
 348  
 349          if (!is_array($userGroups)) {
 350              $userGroups = [$userGroups];
 351          }
 352  
 353          return $userGroups;
 354      }
 355  
 356      /**
 357       * Is the user with the given ID in the allowed User Groups with access to tokens?
 358       *
 359       * @param   int  $userId  The user ID to check
 360       *
 361       * @return  boolean  False when doesn't belong to allowed user groups, user not found, or guest
 362       * @since   4.0.0
 363       */
 364      private function isInAllowedUserGroup($userId)
 365      {
 366          $allowedUserGroups = $this->getAllowedUserGroups();
 367  
 368          $user = $this->userFactory->loadUserById($userId);
 369  
 370          if ($user->id != $userId) {
 371              return false;
 372          }
 373  
 374          if ($user->guest) {
 375              return false;
 376          }
 377  
 378          // No specifically allowed user groups: allow ALL user groups.
 379          if (empty($allowedUserGroups)) {
 380              return true;
 381          }
 382  
 383          $groups       = $user->getAuthorisedGroups();
 384          $intersection = array_intersect($groups, $allowedUserGroups);
 385  
 386          return !empty($intersection);
 387      }
 388  }


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