[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/plugins/multifactorauth/webauthn/src/ -> CredentialRepository.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;
  12  
  13  use Joomla\CMS\Factory;
  14  use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
  15  use Joomla\CMS\User\UserFactoryInterface;
  16  use Joomla\Component\Users\Administrator\Helper\Mfa as MfaHelper;
  17  use Joomla\Component\Users\Administrator\Table\MfaTable;
  18  use RuntimeException;
  19  use Webauthn\AttestationStatement\AttestationStatement;
  20  use Webauthn\AttestedCredentialData;
  21  use Webauthn\PublicKeyCredentialDescriptor;
  22  use Webauthn\PublicKeyCredentialSource;
  23  use Webauthn\PublicKeyCredentialSourceRepository;
  24  use Webauthn\PublicKeyCredentialUserEntity;
  25  use Webauthn\TrustPath\EmptyTrustPath;
  26  
  27  // phpcs:disable PSR1.Files.SideEffects
  28  \defined('_JEXEC') or die;
  29  // phpcs:enable PSR1.Files.SideEffects
  30  
  31  /**
  32   * Implementation of the credentials repository for the WebAuthn library.
  33   *
  34   * Important assumption: interaction with Webauthn through the library is only performed for the currently logged in
  35   * user. Therefore all Methods which take a credential ID work by checking the Joomla MFA records of the current
  36   * user only. This is a necessity. The records are stored encrypted, therefore we cannot do a partial search in the
  37   * table. We have to load the records, decrypt them and inspect them. We cannot do that for thousands of records but
  38   * we CAN do that for the few records each user has under their account.
  39   *
  40   * This behavior can be changed by passing a user ID in the constructor of the class.
  41   *
  42   * @since 4.2.0
  43   */
  44  class CredentialRepository implements PublicKeyCredentialSourceRepository
  45  {
  46      /**
  47       * The user ID we will operate with
  48       *
  49       * @var   integer
  50       * @since 4.2.0
  51       */
  52      private $userId = 0;
  53  
  54      /**
  55       * CredentialRepository constructor.
  56       *
  57       * @param   int  $userId  The user ID this repository will be working with.
  58       *
  59       * @throws \Exception
  60       * @since 4.2.0
  61       */
  62      public function __construct(int $userId = 0)
  63      {
  64          if (empty($userId)) {
  65              $user = Factory::getApplication()->getIdentity()
  66                  ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0);
  67  
  68              $userId = $user->id;
  69          }
  70  
  71          $this->userId = $userId;
  72      }
  73  
  74      /**
  75       * Finds a WebAuthn record given a credential ID
  76       *
  77       * @param   string  $publicKeyCredentialId  The public credential ID to look for
  78       *
  79       * @return  PublicKeyCredentialSource|null
  80       * @since   4.2.0
  81       */
  82      public function findOneByCredentialId(string $publicKeyCredentialId): ?PublicKeyCredentialSource
  83      {
  84          $publicKeyCredentialUserEntity = new PublicKeyCredentialUserEntity('', $this->userId, '', '');
  85          $credentials                   = $this->findAllForUserEntity($publicKeyCredentialUserEntity);
  86  
  87          foreach ($credentials as $record) {
  88              if ($record->getAttestedCredentialData()->getCredentialId() != $publicKeyCredentialId) {
  89                  continue;
  90              }
  91  
  92              return $record;
  93          }
  94  
  95          return null;
  96      }
  97  
  98      /**
  99       * Find all WebAuthn entries given a user entity
 100       *
 101       * @param   PublicKeyCredentialUserEntity  $publicKeyCredentialUserEntity The user entity to search by
 102       *
 103       * @return  array|PublicKeyCredentialSource[]
 104       * @throws  \Exception
 105       * @since   4.2.0
 106       */
 107      public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array
 108      {
 109          if (empty($publicKeyCredentialUserEntity)) {
 110              $userId = $this->userId;
 111          } else {
 112              $userId = $publicKeyCredentialUserEntity->getId();
 113          }
 114  
 115          $return = [];
 116  
 117          $results = MfaHelper::getUserMfaRecords($userId);
 118  
 119          if (count($results) < 1) {
 120              return $return;
 121          }
 122  
 123          /** @var MfaTable $result */
 124          foreach ($results as $result) {
 125              $options = $result->options;
 126  
 127              if (!is_array($options) || empty($options)) {
 128                  continue;
 129              }
 130  
 131              if (!isset($options['attested']) && !isset($options['pubkeysource'])) {
 132                  continue;
 133              }
 134  
 135              if (isset($options['attested']) && is_string($options['attested'])) {
 136                  $options['attested'] = json_decode($options['attested'], true);
 137  
 138                  $return[$result->id] = $this->attestedCredentialToPublicKeyCredentialSource(
 139                      AttestedCredentialData::createFromArray($options['attested']),
 140                      $userId
 141                  );
 142              } elseif (isset($options['pubkeysource']) && is_string($options['pubkeysource'])) {
 143                  $options['pubkeysource'] = json_decode($options['pubkeysource'], true);
 144                  $return[$result->id]     = PublicKeyCredentialSource::createFromArray($options['pubkeysource']);
 145              } elseif (isset($options['pubkeysource']) && is_array($options['pubkeysource'])) {
 146                  $return[$result->id] = PublicKeyCredentialSource::createFromArray($options['pubkeysource']);
 147              }
 148          }
 149  
 150          return $return;
 151      }
 152  
 153      /**
 154       * Converts a legacy AttestedCredentialData object stored in the database into a PublicKeyCredentialSource object.
 155       *
 156       * This makes several assumptions which can be problematic and the reason why the WebAuthn library version 2 moved
 157       * away from attested credentials to public key credential sources:
 158       *
 159       * - The credential is always of the public key type (that's safe as the only option supported)
 160       * - You can access it with any kind of authenticator transport: USB, NFC, Internal or Bluetooth LE (possibly
 161       * dangerous)
 162       * - There is no attestations (generally safe since browsers don't seem to support attestation yet)
 163       * - There is no trust path (generally safe since browsers don't seem to provide one)
 164       * - No counter was stored (dangerous since it can lead to replay attacks).
 165       *
 166       * @param   AttestedCredentialData  $record  Legacy attested credential data object
 167       * @param   int                     $userId  User ID we are getting the credential source for
 168       *
 169       * @return  PublicKeyCredentialSource
 170       * @since   4.2.0
 171       */
 172      private function attestedCredentialToPublicKeyCredentialSource(AttestedCredentialData $record, int $userId): PublicKeyCredentialSource
 173      {
 174          return new PublicKeyCredentialSource(
 175              $record->getCredentialId(),
 176              PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY,
 177              [
 178                  PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_USB,
 179                  PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_NFC,
 180                  PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_INTERNAL,
 181                  PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_BLE,
 182              ],
 183              AttestationStatement::TYPE_NONE,
 184              new EmptyTrustPath(),
 185              $record->getAaguid(),
 186              $record->getCredentialPublicKey(),
 187              $userId,
 188              0
 189          );
 190      }
 191  
 192      /**
 193       * Save a WebAuthn record
 194       *
 195       * @param   PublicKeyCredentialSource  $publicKeyCredentialSource  The record to save
 196       *
 197       * @return  void
 198       * @throws  \Exception
 199       * @since   4.2.0
 200       */
 201      public function saveCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource): void
 202      {
 203          // I can only create or update credentials for the user this class was created for
 204          if ($publicKeyCredentialSource->getUserHandle() != $this->userId) {
 205              throw new RuntimeException('Cannot create or update WebAuthn credentials for a different user.', 403);
 206          }
 207  
 208          // Do I have an existing record for this credential?
 209          $recordId                      = null;
 210          $publicKeyCredentialUserEntity = new PublicKeyCredentialUserEntity('', $this->userId, '', '');
 211          $credentials                   = $this->findAllForUserEntity($publicKeyCredentialUserEntity);
 212  
 213          foreach ($credentials as $id => $record) {
 214              if ($record->getAttestedCredentialData()->getCredentialId() != $publicKeyCredentialSource->getAttestedCredentialData()->getCredentialId()) {
 215                  continue;
 216              }
 217  
 218              $recordId = $id;
 219  
 220              break;
 221          }
 222  
 223          // Create or update a record
 224          /** @var MVCFactoryInterface $factory */
 225          $factory = Factory::getApplication()->bootComponent('com_users')->getMVCFactory();
 226          /** @var MfaTable $mfaTable */
 227          $mfaTable = $factory->createTable('Mfa', 'Administrator');
 228  
 229          if ($recordId) {
 230              $mfaTable->load($recordId);
 231  
 232              $options = $mfaTable->options;
 233  
 234              if (isset($options['attested'])) {
 235                  unset($options['attested']);
 236              }
 237  
 238              $options['pubkeysource'] = $publicKeyCredentialSource;
 239              $mfaTable->save(
 240                  [
 241                      'options' => $options
 242                  ]
 243              );
 244          } else {
 245              $mfaTable->reset();
 246              $mfaTable->save(
 247                  [
 248                      'user_id' => $this->userId,
 249                      'title'   => 'WebAuthn auto-save',
 250                      'method'  => 'webauthn',
 251                      'default' => 0,
 252                      'options' => ['pubkeysource' => $publicKeyCredentialSource],
 253                  ]
 254              );
 255          }
 256      }
 257  }


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