[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/plugins/multifactorauth/webauthn/src/Helper/ -> Credentials.php (source)

   1  <?php
   2  
   3  /**
   4   * @package     Joomla.Plugin
   5   * @subpackage  Multifactorauth.webauthn
   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\Webauthn\Helper;
  12  
  13  use Exception;
  14  use Joomla\CMS\Application\CMSApplication;
  15  use Joomla\CMS\Factory;
  16  use Joomla\CMS\Language\Text;
  17  use Joomla\CMS\Uri\Uri;
  18  use Joomla\CMS\User\User;
  19  use Joomla\CMS\User\UserFactoryInterface;
  20  use Joomla\Plugin\Multifactorauth\Webauthn\CredentialRepository;
  21  use Joomla\Plugin\Multifactorauth\Webauthn\Hotfix\Server;
  22  use Joomla\Session\SessionInterface;
  23  use Laminas\Diactoros\ServerRequestFactory;
  24  use ReflectionClass;
  25  use RuntimeException;
  26  use Webauthn\AttestedCredentialData;
  27  use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs;
  28  use Webauthn\AuthenticatorSelectionCriteria;
  29  use Webauthn\PublicKeyCredentialCreationOptions;
  30  use Webauthn\PublicKeyCredentialDescriptor;
  31  use Webauthn\PublicKeyCredentialRequestOptions;
  32  use Webauthn\PublicKeyCredentialRpEntity;
  33  use Webauthn\PublicKeyCredentialSource;
  34  use Webauthn\PublicKeyCredentialUserEntity;
  35  
  36  // phpcs:disable PSR1.Files.SideEffects
  37  \defined('_JEXEC') or die;
  38  // phpcs:enable PSR1.Files.SideEffects
  39  
  40  /**
  41   * Helper class to aid in credentials creation (link an authenticator to a user account)
  42   *
  43   * @since 4.2.0
  44   */
  45  abstract class Credentials
  46  {
  47      /**
  48       * Authenticator registration step 1: create a public key for credentials attestation.
  49       *
  50       * The result is a JSON string which can be used in Javascript code with navigator.credentials.create().
  51       *
  52       * @param   User   $user   The Joomla user to create the public key for
  53       *
  54       * @return  string
  55       * @throws  Exception  On error
  56       * @since   4.2.0
  57       */
  58      public static function requestAttestation(User $user): string
  59      {
  60          $publicKeyCredentialCreationOptions = self::getWebauthnServer($user->id)
  61              ->generatePublicKeyCredentialCreationOptions(
  62                  self::getUserEntity($user),
  63                  PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
  64                  self::getPubKeyDescriptorsForUser($user),
  65                  new AuthenticatorSelectionCriteria(
  66                      AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE,
  67                      false,
  68                      AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_PREFERRED
  69                  ),
  70                  new AuthenticationExtensionsClientInputs()
  71              );
  72  
  73          // Save data in the session
  74          $session = Factory::getApplication()->getSession();
  75  
  76          $session->set(
  77              'plg_multifactorauth_webauthn.publicKeyCredentialCreationOptions',
  78              base64_encode(serialize($publicKeyCredentialCreationOptions))
  79          );
  80          $session->set('plg_multifactorauth_webauthn.registration_user_id', $user->id);
  81  
  82          return json_encode($publicKeyCredentialCreationOptions, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
  83      }
  84  
  85      /**
  86       * Authenticator registration step 2: verify the credentials attestation by the authenticator
  87       *
  88       * This returns the attested credential data on success.
  89       *
  90       * An exception will be returned on error. Also, under very rare conditions, you may receive NULL instead of
  91       * attested credential data which means that something was off in the returned data from the browser.
  92       *
  93       * @param   string   $data   The JSON-encoded data returned by the browser during the authentication flow
  94       *
  95       * @return  AttestedCredentialData|null
  96       * @throws  Exception  When something does not check out
  97       * @since   4.2.0
  98       */
  99      public static function verifyAttestation(string $data): ?PublicKeyCredentialSource
 100      {
 101          $session = Factory::getApplication()->getSession();
 102  
 103          // Retrieve the PublicKeyCredentialCreationOptions object created earlier and perform sanity checks
 104          $encodedOptions = $session->get('plg_multifactorauth_webauthn.publicKeyCredentialCreationOptions', null);
 105  
 106          if (empty($encodedOptions)) {
 107              throw new RuntimeException(Text::_('PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_CREATE_NO_PK'));
 108          }
 109  
 110          try {
 111              $publicKeyCredentialCreationOptions = unserialize(base64_decode($encodedOptions));
 112          } catch (Exception $e) {
 113              $publicKeyCredentialCreationOptions = null;
 114          }
 115  
 116          if (!is_object($publicKeyCredentialCreationOptions) || !($publicKeyCredentialCreationOptions instanceof PublicKeyCredentialCreationOptions)) {
 117              throw new RuntimeException(Text::_('PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_CREATE_NO_PK'));
 118          }
 119  
 120          // Retrieve the stored user ID and make sure it's the same one in the request.
 121          $storedUserId = $session->get('plg_multifactorauth_webauthn.registration_user_id', 0);
 122          $myUser       = Factory::getApplication()->getIdentity()
 123              ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0);
 124          $myUserId     = $myUser->id;
 125  
 126          if (($myUser->guest) || ($myUserId != $storedUserId)) {
 127              throw new RuntimeException(Text::_('PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_CREATE_INVALID_USER'));
 128          }
 129  
 130          return self::getWebauthnServer($myUser->id)->loadAndCheckAttestationResponse(
 131              base64_decode($data),
 132              $publicKeyCredentialCreationOptions,
 133              ServerRequestFactory::fromGlobals()
 134          );
 135      }
 136  
 137      /**
 138       * Authentication step 1: create a challenge for key verification
 139       *
 140       * @param   int  $userId  The user ID to create a WebAuthn PK for
 141       *
 142       * @return  string
 143       * @throws  Exception  On error
 144       * @since   4.2.0
 145       */
 146      public static function requestAssertion(int $userId): string
 147      {
 148          $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId);
 149  
 150          $publicKeyCredentialRequestOptions = self::getWebauthnServer($userId)
 151              ->generatePublicKeyCredentialRequestOptions(
 152                  PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED,
 153                  self::getPubKeyDescriptorsForUser($user)
 154              );
 155  
 156          // Save in session. This is used during the verification stage to prevent replay attacks.
 157          /** @var SessionInterface $session */
 158          $session = Factory::getApplication()->getSession();
 159          $session->set('plg_multifactorauth_webauthn.publicKeyCredentialRequestOptions', base64_encode(serialize($publicKeyCredentialRequestOptions)));
 160          $session->set('plg_multifactorauth_webauthn.userHandle', $userId);
 161          $session->set('plg_multifactorauth_webauthn.userId', $userId);
 162  
 163          // Return the JSON encoded data to the caller
 164          return json_encode($publicKeyCredentialRequestOptions, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
 165      }
 166  
 167      /**
 168       * Authentication step 2: Checks if the browser's response to our challenge is valid.
 169       *
 170       * @param   string   $response   Base64-encoded response
 171       *
 172       * @return  void
 173       * @throws  Exception  When something does not check out.
 174       * @since   4.2.0
 175       */
 176      public static function verifyAssertion(string $response): void
 177      {
 178          /** @var SessionInterface $session */
 179          $session = Factory::getApplication()->getSession();
 180  
 181          $encodedPkOptions = $session->get('plg_multifactorauth_webauthn.publicKeyCredentialRequestOptions', null);
 182          $userHandle       = $session->get('plg_multifactorauth_webauthn.userHandle', null);
 183          $userId           = $session->get('plg_multifactorauth_webauthn.userId', null);
 184  
 185          $session->set('plg_multifactorauth_webauthn.publicKeyCredentialRequestOptions', null);
 186          $session->set('plg_multifactorauth_webauthn.userHandle', null);
 187          $session->set('plg_multifactorauth_webauthn.userId', null);
 188  
 189          if (empty($userId)) {
 190              throw new RuntimeException(Text::_('PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST'));
 191          }
 192  
 193          // Make sure the user exists
 194          $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId);
 195  
 196          if ($user->id != $userId) {
 197              throw new RuntimeException(Text::_('PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST'));
 198          }
 199  
 200          // Make sure the user is ourselves (we cannot perform MFA on behalf of another user!)
 201          $currentUser = Factory::getApplication()->getIdentity()
 202              ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0);
 203  
 204          if ($currentUser->id != $userId) {
 205              throw new RuntimeException(Text::_('PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST'));
 206          }
 207  
 208          // Make sure the public key credential request options in the session are valid
 209          $serializedOptions                 = base64_decode($encodedPkOptions);
 210          $publicKeyCredentialRequestOptions = unserialize($serializedOptions);
 211  
 212          if (
 213              !is_object($publicKeyCredentialRequestOptions)
 214              || empty($publicKeyCredentialRequestOptions)
 215              || !($publicKeyCredentialRequestOptions instanceof PublicKeyCredentialRequestOptions)
 216          ) {
 217              throw new RuntimeException(Text::_('PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST'));
 218          }
 219  
 220          // Unserialize the browser response data
 221          $data = base64_decode($response);
 222  
 223          self::getWebauthnServer($user->id)->loadAndCheckAssertionResponse(
 224              $data,
 225              $publicKeyCredentialRequestOptions,
 226              self::getUserEntity($user),
 227              ServerRequestFactory::fromGlobals()
 228          );
 229      }
 230  
 231      /**
 232       * Get the user's avatar (through Gravatar)
 233       *
 234       * @param   User   $user   The Joomla user object
 235       * @param   int    $size   The dimensions of the image to fetch (default: 64 pixels)
 236       *
 237       * @return  string  The URL to the user's avatar
 238       *
 239       * @since 4.2.0
 240       */
 241      private static function getAvatar(User $user, int $size = 64)
 242      {
 243          $scheme    = Uri::getInstance()->getScheme();
 244          $subdomain = ($scheme == 'https') ? 'secure' : 'www';
 245  
 246          return sprintf('%s://%s.gravatar.com/avatar/%s.jpg?s=%u&d=mm', $scheme, $subdomain, md5($user->email), $size);
 247      }
 248  
 249      /**
 250       * Get a WebAuthn user entity for a Joomla user
 251       *
 252       * @param   User   $user  The user to get an entity for
 253       *
 254       * @return  PublicKeyCredentialUserEntity
 255       * @since   4.2.0
 256       */
 257      private static function getUserEntity(User $user): PublicKeyCredentialUserEntity
 258      {
 259          return new PublicKeyCredentialUserEntity(
 260              $user->username,
 261              $user->id,
 262              $user->name,
 263              self::getAvatar($user, 64)
 264          );
 265      }
 266  
 267      /**
 268       * Get the WebAuthn library server object
 269       *
 270       * @param   int|null  $userId  The user ID holding the list of valid authenticators
 271       *
 272       * @return  Server
 273       * @since   4.2.0
 274       */
 275      private static function getWebauthnServer(?int $userId): Server
 276      {
 277          /** @var CMSApplication $app */
 278          try {
 279              $app      = Factory::getApplication();
 280              $siteName = $app->get('sitename');
 281          } catch (Exception $e) {
 282              $siteName = 'Joomla! Site';
 283          }
 284  
 285          // Credentials repository
 286          $repository = new CredentialRepository($userId);
 287  
 288          // Relaying Party -- Our site
 289          $rpEntity = new PublicKeyCredentialRpEntity(
 290              $siteName ?? 'Joomla! Site',
 291              Uri::getInstance()->toString(['host']),
 292              ''
 293          );
 294  
 295          $refClass       = new ReflectionClass(Server::class);
 296          $refConstructor = $refClass->getConstructor();
 297          $params         = $refConstructor->getParameters();
 298  
 299          if (count($params) === 3) {
 300              // WebAuthn library 2, 3
 301              $server = new Server($rpEntity, $repository, null);
 302          } else {
 303              // WebAuthn library 4 (based on the deprecated comments in library version 3)
 304              $server = new Server($rpEntity, $repository);
 305          }
 306  
 307          // Ed25519 is only available with libsodium
 308          if (!function_exists('sodium_crypto_sign_seed_keypair')) {
 309              $server->setSelectedAlgorithms(['RS256', 'RS512', 'PS256', 'PS512', 'ES256', 'ES512']);
 310          }
 311  
 312          return $server;
 313      }
 314  
 315      /**
 316       * Returns an array of the PK credential descriptors (registered authenticators) for the given user.
 317       *
 318       * @param   User   $user  The user to get the descriptors for
 319       *
 320       * @return  PublicKeyCredentialDescriptor[]
 321       * @since   4.2.0
 322       */
 323      private static function getPubKeyDescriptorsForUser(User $user): array
 324      {
 325          $userEntity  = self::getUserEntity($user);
 326          $repository  = new CredentialRepository($user->id);
 327          $descriptors = [];
 328          $records     = $repository->findAllForUserEntity($userEntity);
 329  
 330          foreach ($records as $record) {
 331              $descriptors[] = $record->getPublicKeyCredentialDescriptor();
 332          }
 333  
 334          return $descriptors;
 335      }
 336  }


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