[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/plugins/system/webauthn/src/PluginTraits/ -> AjaxHandlerLogin.php (source)

   1  <?php
   2  
   3  /**
   4   * @package     Joomla.Plugin
   5   * @subpackage  System.Webauthn
   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\System\Webauthn\PluginTraits;
  12  
  13  use Exception;
  14  use Joomla\CMS\Authentication\Authentication;
  15  use Joomla\CMS\Authentication\AuthenticationResponse;
  16  use Joomla\CMS\Event\Plugin\System\Webauthn\AjaxLogin;
  17  use Joomla\CMS\Factory;
  18  use Joomla\CMS\Language\Text;
  19  use Joomla\CMS\Log\Log;
  20  use Joomla\CMS\Plugin\PluginHelper;
  21  use Joomla\CMS\Uri\Uri;
  22  use Joomla\CMS\User\User;
  23  use Joomla\CMS\User\UserFactoryInterface;
  24  use RuntimeException;
  25  use Throwable;
  26  
  27  // phpcs:disable PSR1.Files.SideEffects
  28  \defined('_JEXEC') or die;
  29  // phpcs:enable PSR1.Files.SideEffects
  30  
  31  /**
  32   * Ajax handler for akaction=login
  33   *
  34   * Verifies the response received from the browser and logs in the user
  35   *
  36   * @since  4.0.0
  37   */
  38  trait AjaxHandlerLogin
  39  {
  40      /**
  41       * Returns the public key set for the user and a unique challenge in a Public Key Credential Request encoded as
  42       * JSON.
  43       *
  44       * @param   AjaxLogin  $event  The event we are handling
  45       *
  46       * @return  void
  47       *
  48       * @since   4.0.0
  49       */
  50      public function onAjaxWebauthnLogin(AjaxLogin $event): void
  51      {
  52          $session   = $this->getApplication()->getSession();
  53          $returnUrl = $session->get('plg_system_webauthn.returnUrl', Uri::base());
  54          $userId    = $session->get('plg_system_webauthn.userId', 0);
  55  
  56          try {
  57              $credentialRepository = $this->authenticationHelper->getCredentialsRepository();
  58  
  59              // No user ID: no username was provided and the resident credential refers to an unknown user handle. DIE!
  60              if (empty($userId)) {
  61                  Log::add('Cannot determine the user ID', Log::NOTICE, 'webauthn.system');
  62  
  63                  throw new RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST'));
  64              }
  65  
  66              // Do I have a valid user?
  67              $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId);
  68  
  69              if ($user->id != $userId) {
  70                  $message = sprintf('User #%d does not exist', $userId);
  71                  Log::add($message, Log::NOTICE, 'webauthn.system');
  72  
  73                  throw new RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST'));
  74              }
  75  
  76              // Validate the authenticator response and get the user handle
  77              $userHandle           = $this->getUserHandleFromResponse($user);
  78  
  79              if (is_null($userHandle)) {
  80                  Log::add('Cannot retrieve the user handle from the request; the browser did not assert our request.', Log::NOTICE, 'webauthn.system');
  81  
  82                  throw new RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST'));
  83              }
  84  
  85              // Does the user handle match the user ID? This should never trigger by definition of the login check.
  86              $validUserHandle = $credentialRepository->getHandleFromUserId($userId);
  87  
  88              if ($userHandle != $validUserHandle) {
  89                  $message = sprintf('Invalid user handle; expected %s, got %s', $validUserHandle, $userHandle);
  90                  Log::add($message, Log::NOTICE, 'webauthn.system');
  91  
  92                  throw new RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST'));
  93              }
  94  
  95              // Make sure the user exists
  96              $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId);
  97  
  98              if ($user->id != $userId) {
  99                  $message = sprintf('Invalid user ID; expected %d, got %d', $userId, $user->id);
 100                  Log::add($message, Log::NOTICE, 'webauthn.system');
 101  
 102                  throw new RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST'));
 103              }
 104  
 105              // Login the user
 106              Log::add("Logging in the user", Log::INFO, 'webauthn.system');
 107              $this->loginUser((int) $userId);
 108          } catch (Throwable $e) {
 109              $session->set('plg_system_webauthn.publicKeyCredentialRequestOptions', null);
 110  
 111              $response                = $this->getAuthenticationResponseObject();
 112              $response->status        = Authentication::STATUS_UNKNOWN;
 113              $response->error_message = $e->getMessage();
 114  
 115              Log::add(sprintf("Received login failure. Message: %s", $e->getMessage()), Log::ERROR, 'webauthn.system');
 116  
 117              // This also enqueues the login failure message for display after redirection. Look for JLog in that method.
 118              $this->processLoginFailure($response, null, 'system');
 119          } finally {
 120              /**
 121               * This code needs to run no matter if the login succeeded or failed. It prevents replay attacks and takes
 122               * the user back to the page they started from.
 123               */
 124  
 125              // Remove temporary information for security reasons
 126              $session->set('plg_system_webauthn.publicKeyCredentialRequestOptions', null);
 127              $session->set('plg_system_webauthn.returnUrl', null);
 128              $session->set('plg_system_webauthn.userId', null);
 129  
 130              // Redirect back to the page we were before.
 131              $this->getApplication()->redirect($returnUrl);
 132          }
 133      }
 134  
 135      /**
 136       * Logs in a user to the site, bypassing the authentication plugins.
 137       *
 138       * @param   int   $userId   The user ID to log in
 139       *
 140       * @return  void
 141       * @throws  Exception
 142       * @since   4.2.0
 143       */
 144      private function loginUser(int $userId): void
 145      {
 146          // Trick the class auto-loader into loading the necessary classes
 147          class_exists('Joomla\\CMS\\Authentication\\Authentication', true);
 148  
 149          // Fake a successful login message
 150          $isAdmin = $this->getApplication()->isClient('administrator');
 151          $user    = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId);
 152  
 153          // Does the user account have a pending activation?
 154          if (!empty($user->activation)) {
 155              throw new RuntimeException(Text::_('JGLOBAL_AUTH_ACCESS_DENIED'));
 156          }
 157  
 158          // Is the user account blocked?
 159          if ($user->block) {
 160              throw new RuntimeException(Text::_('JGLOBAL_AUTH_ACCESS_DENIED'));
 161          }
 162  
 163          $statusSuccess = Authentication::STATUS_SUCCESS;
 164  
 165          $response                = $this->getAuthenticationResponseObject();
 166          $response->status        = $statusSuccess;
 167          $response->username      = $user->username;
 168          $response->fullname      = $user->name;
 169          $response->error_message = '';
 170          $response->language      = $user->getParam('language');
 171          $response->type          = 'Passwordless';
 172  
 173          if ($isAdmin) {
 174              $response->language = $user->getParam('admin_language');
 175          }
 176  
 177          /**
 178           * Set up the login options.
 179           *
 180           * The 'remember' element forces the use of the Remember Me feature when logging in with Webauthn, as the
 181           * users would expect.
 182           *
 183           * The 'action' element is actually required by plg_user_joomla. It is the core ACL action the logged in user
 184           * must be allowed for the login to succeed. Please note that front-end and back-end logins use a different
 185           * action. This allows us to provide the WebAuthn button on both front- and back-end and be sure that if a
 186           * used with no backend access tries to use it to log in Joomla! will just slap him with an error message about
 187           * insufficient privileges - the same thing that'd happen if you tried to use your front-end only username and
 188           * password in a back-end login form.
 189           */
 190          $options = [
 191              'remember' => true,
 192              'action'   => 'core.login.site',
 193          ];
 194  
 195          if ($isAdmin) {
 196              $options['action'] = 'core.login.admin';
 197          }
 198  
 199          // Run the user plugins. They CAN block login by returning boolean false and setting $response->error_message.
 200          PluginHelper::importPlugin('user');
 201          $eventClassName = self::getEventClassByEventName('onUserLogin');
 202          $event          = new $eventClassName('onUserLogin', [(array) $response, $options]);
 203          $result         = $this->getApplication()->getDispatcher()->dispatch($event->getName(), $event);
 204          $results        = !isset($result['result']) || \is_null($result['result']) ? [] : $result['result'];
 205  
 206          // If there is no boolean FALSE result from any plugin the login is successful.
 207          if (in_array(false, $results, true) === false) {
 208              // Set the user in the session, letting Joomla! know that we are logged in.
 209              $this->getApplication()->getSession()->set('user', $user);
 210  
 211              // Trigger the onUserAfterLogin event
 212              $options['user']         = $user;
 213              $options['responseType'] = $response->type;
 214  
 215              // The user is successfully logged in. Run the after login events
 216              $eventClassName = self::getEventClassByEventName('onUserAfterLogin');
 217              $event          = new $eventClassName('onUserAfterLogin', [$options]);
 218              $this->getApplication()->getDispatcher()->dispatch($event->getName(), $event);
 219  
 220              return;
 221          }
 222  
 223          // If we are here the plugins marked a login failure. Trigger the onUserLoginFailure Event.
 224          $eventClassName = self::getEventClassByEventName('onUserLoginFailure');
 225          $event          = new $eventClassName('onUserLoginFailure', [(array) $response]);
 226          $this->getApplication()->getDispatcher()->dispatch($event->getName(), $event);
 227  
 228          // Log the failure
 229          Log::add($response->error_message, Log::WARNING, 'jerror');
 230  
 231          // Throw an exception to let the caller know that the login failed
 232          throw new RuntimeException($response->error_message);
 233      }
 234  
 235      /**
 236       * Returns a (blank) Joomla! authentication response
 237       *
 238       * @return  AuthenticationResponse
 239       *
 240       * @since   4.2.0
 241       */
 242      private function getAuthenticationResponseObject(): AuthenticationResponse
 243      {
 244          // Force the class auto-loader to load the JAuthentication class
 245          class_exists('Joomla\\CMS\\Authentication\\Authentication', true);
 246  
 247          return new AuthenticationResponse();
 248      }
 249  
 250      /**
 251       * Have Joomla! process a login failure
 252       *
 253       * @param   AuthenticationResponse   $response   The Joomla! auth response object
 254       *
 255       * @return  boolean
 256       *
 257       * @since   4.2.0
 258       */
 259      private function processLoginFailure(AuthenticationResponse $response): bool
 260      {
 261          // Import the user plugin group.
 262          PluginHelper::importPlugin('user');
 263  
 264          // Trigger onUserLoginFailure Event.
 265          Log::add('Calling onUserLoginFailure plugin event', Log::INFO, 'plg_system_webauthn');
 266  
 267          $eventClassName = self::getEventClassByEventName('onUserLoginFailure');
 268          $event          = new $eventClassName('onUserLoginFailure', [(array) $response]);
 269          $this->getApplication()->getDispatcher()->dispatch($event->getName(), $event);
 270  
 271          // If status is success, any error will have been raised by the user plugin
 272          $expectedStatus = Authentication::STATUS_SUCCESS;
 273  
 274          if ($response->status !== $expectedStatus) {
 275              Log::add('The login failure has been logged in Joomla\'s error log', Log::INFO, 'webauthn.system');
 276  
 277              // Everything logged in the 'jerror' category ends up being enqueued in the application message queue.
 278              Log::add($response->error_message, Log::WARNING, 'jerror');
 279          } else {
 280              $message = 'A login failure was caused by a third party user plugin but it did not return any' .
 281                  'further information.';
 282              Log::add($message, Log::WARNING, 'webauthn.system');
 283          }
 284  
 285          return false;
 286      }
 287  
 288      /**
 289       * Validate the authenticator response sent to us by the browser.
 290       *
 291       * @param   User  $user  The user we are trying to log in.
 292       *
 293       * @return  string|null  The user handle or null
 294       *
 295       * @throws  Exception
 296       * @since   4.2.0
 297       */
 298      private function getUserHandleFromResponse(User $user): ?string
 299      {
 300          // Retrieve data from the request and session
 301          $pubKeyCredentialSource = $this->authenticationHelper->validateAssertionResponse(
 302              $this->getApplication()->input->getBase64('data', ''),
 303              $user
 304          );
 305  
 306          return $pubKeyCredentialSource ? $pubKeyCredentialSource->getUserHandle() : null;
 307      }
 308  }


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