primo commit
This commit is contained in:
256
plugins/multifactorauth/webauthn/src/CredentialRepository.php
Normal file
256
plugins/multifactorauth/webauthn/src/CredentialRepository.php
Normal file
@ -0,0 +1,256 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Plugin
|
||||
* @subpackage Multifactorauth.webauthn
|
||||
*
|
||||
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Plugin\Multifactorauth\Webauthn;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
|
||||
use Joomla\CMS\User\UserFactoryInterface;
|
||||
use Joomla\Component\Users\Administrator\Helper\Mfa as MfaHelper;
|
||||
use Joomla\Component\Users\Administrator\Table\MfaTable;
|
||||
use Webauthn\AttestationStatement\AttestationStatement;
|
||||
use Webauthn\AttestedCredentialData;
|
||||
use Webauthn\PublicKeyCredentialDescriptor;
|
||||
use Webauthn\PublicKeyCredentialSource;
|
||||
use Webauthn\PublicKeyCredentialSourceRepository;
|
||||
use Webauthn\PublicKeyCredentialUserEntity;
|
||||
use Webauthn\TrustPath\EmptyTrustPath;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Implementation of the credentials repository for the WebAuthn library.
|
||||
*
|
||||
* Important assumption: interaction with Webauthn through the library is only performed for the currently logged in
|
||||
* user. Therefore all Methods which take a credential ID work by checking the Joomla MFA records of the current
|
||||
* user only. This is a necessity. The records are stored encrypted, therefore we cannot do a partial search in the
|
||||
* table. We have to load the records, decrypt them and inspect them. We cannot do that for thousands of records but
|
||||
* we CAN do that for the few records each user has under their account.
|
||||
*
|
||||
* This behavior can be changed by passing a user ID in the constructor of the class.
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
class CredentialRepository implements PublicKeyCredentialSourceRepository
|
||||
{
|
||||
/**
|
||||
* The user ID we will operate with
|
||||
*
|
||||
* @var integer
|
||||
* @since 4.2.0
|
||||
*/
|
||||
private $userId = 0;
|
||||
|
||||
/**
|
||||
* CredentialRepository constructor.
|
||||
*
|
||||
* @param int $userId The user ID this repository will be working with.
|
||||
*
|
||||
* @throws \Exception
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function __construct(int $userId = 0)
|
||||
{
|
||||
if (empty($userId)) {
|
||||
$user = Factory::getApplication()->getIdentity()
|
||||
?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0);
|
||||
|
||||
$userId = $user->id;
|
||||
}
|
||||
|
||||
$this->userId = $userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a WebAuthn record given a credential ID
|
||||
*
|
||||
* @param string $publicKeyCredentialId The public credential ID to look for
|
||||
*
|
||||
* @return PublicKeyCredentialSource|null
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function findOneByCredentialId(string $publicKeyCredentialId): ?PublicKeyCredentialSource
|
||||
{
|
||||
$publicKeyCredentialUserEntity = new PublicKeyCredentialUserEntity('', $this->userId, '', '');
|
||||
$credentials = $this->findAllForUserEntity($publicKeyCredentialUserEntity);
|
||||
|
||||
foreach ($credentials as $record) {
|
||||
if ($record->getAttestedCredentialData()->getCredentialId() != $publicKeyCredentialId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all WebAuthn entries given a user entity
|
||||
*
|
||||
* @param PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity The user entity to search by
|
||||
*
|
||||
* @return array|PublicKeyCredentialSource[]
|
||||
* @throws \Exception
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array
|
||||
{
|
||||
if (empty($publicKeyCredentialUserEntity)) {
|
||||
$userId = $this->userId;
|
||||
} else {
|
||||
$userId = $publicKeyCredentialUserEntity->getId();
|
||||
}
|
||||
|
||||
$return = [];
|
||||
|
||||
$results = MfaHelper::getUserMfaRecords($userId);
|
||||
|
||||
if (\count($results) < 1) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
/** @var MfaTable $result */
|
||||
foreach ($results as $result) {
|
||||
$options = $result->options;
|
||||
|
||||
if (!\is_array($options) || empty($options)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($options['attested']) && !isset($options['pubkeysource'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($options['attested']) && \is_string($options['attested'])) {
|
||||
$options['attested'] = json_decode($options['attested'], true);
|
||||
|
||||
$return[$result->id] = $this->attestedCredentialToPublicKeyCredentialSource(
|
||||
AttestedCredentialData::createFromArray($options['attested']),
|
||||
$userId
|
||||
);
|
||||
} elseif (isset($options['pubkeysource']) && \is_string($options['pubkeysource'])) {
|
||||
$options['pubkeysource'] = json_decode($options['pubkeysource'], true);
|
||||
$return[$result->id] = PublicKeyCredentialSource::createFromArray($options['pubkeysource']);
|
||||
} elseif (isset($options['pubkeysource']) && \is_array($options['pubkeysource'])) {
|
||||
$return[$result->id] = PublicKeyCredentialSource::createFromArray($options['pubkeysource']);
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a legacy AttestedCredentialData object stored in the database into a PublicKeyCredentialSource object.
|
||||
*
|
||||
* This makes several assumptions which can be problematic and the reason why the WebAuthn library version 2 moved
|
||||
* away from attested credentials to public key credential sources:
|
||||
*
|
||||
* - The credential is always of the public key type (that's safe as the only option supported)
|
||||
* - You can access it with any kind of authenticator transport: USB, NFC, Internal or Bluetooth LE (possibly
|
||||
* dangerous)
|
||||
* - There is no attestations (generally safe since browsers don't seem to support attestation yet)
|
||||
* - There is no trust path (generally safe since browsers don't seem to provide one)
|
||||
* - No counter was stored (dangerous since it can lead to replay attacks).
|
||||
*
|
||||
* @param AttestedCredentialData $record Legacy attested credential data object
|
||||
* @param int $userId User ID we are getting the credential source for
|
||||
*
|
||||
* @return PublicKeyCredentialSource
|
||||
* @since 4.2.0
|
||||
*/
|
||||
private function attestedCredentialToPublicKeyCredentialSource(AttestedCredentialData $record, int $userId): PublicKeyCredentialSource
|
||||
{
|
||||
return new PublicKeyCredentialSource(
|
||||
$record->getCredentialId(),
|
||||
PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY,
|
||||
[
|
||||
PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_USB,
|
||||
PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_NFC,
|
||||
PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_INTERNAL,
|
||||
PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_BLE,
|
||||
],
|
||||
AttestationStatement::TYPE_NONE,
|
||||
new EmptyTrustPath(),
|
||||
$record->getAaguid(),
|
||||
$record->getCredentialPublicKey(),
|
||||
$userId,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a WebAuthn record
|
||||
*
|
||||
* @param PublicKeyCredentialSource $publicKeyCredentialSource The record to save
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function saveCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource): void
|
||||
{
|
||||
// I can only create or update credentials for the user this class was created for
|
||||
if ($publicKeyCredentialSource->getUserHandle() != $this->userId) {
|
||||
throw new \RuntimeException('Cannot create or update WebAuthn credentials for a different user.', 403);
|
||||
}
|
||||
|
||||
// Do I have an existing record for this credential?
|
||||
$recordId = null;
|
||||
$publicKeyCredentialUserEntity = new PublicKeyCredentialUserEntity('', $this->userId, '', '');
|
||||
$credentials = $this->findAllForUserEntity($publicKeyCredentialUserEntity);
|
||||
|
||||
foreach ($credentials as $id => $record) {
|
||||
if ($record->getAttestedCredentialData()->getCredentialId() != $publicKeyCredentialSource->getAttestedCredentialData()->getCredentialId()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$recordId = $id;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Create or update a record
|
||||
/** @var MVCFactoryInterface $factory */
|
||||
$factory = Factory::getApplication()->bootComponent('com_users')->getMVCFactory();
|
||||
/** @var MfaTable $mfaTable */
|
||||
$mfaTable = $factory->createTable('Mfa', 'Administrator');
|
||||
|
||||
if ($recordId) {
|
||||
$mfaTable->load($recordId);
|
||||
|
||||
$options = $mfaTable->options;
|
||||
|
||||
if (isset($options['attested'])) {
|
||||
unset($options['attested']);
|
||||
}
|
||||
|
||||
$options['pubkeysource'] = $publicKeyCredentialSource;
|
||||
$mfaTable->save(
|
||||
[
|
||||
'options' => $options,
|
||||
]
|
||||
);
|
||||
} else {
|
||||
$mfaTable->reset();
|
||||
$mfaTable->save(
|
||||
[
|
||||
'user_id' => $this->userId,
|
||||
'title' => 'WebAuthn auto-save',
|
||||
'method' => 'webauthn',
|
||||
'default' => 0,
|
||||
'options' => ['pubkeysource' => $publicKeyCredentialSource],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user