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], | ||||
|                 ] | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										435
									
								
								plugins/multifactorauth/webauthn/src/Extension/Webauthn.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										435
									
								
								plugins/multifactorauth/webauthn/src/Extension/Webauthn.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,435 @@ | ||||
| <?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\Extension; | ||||
|  | ||||
| use Joomla\CMS\Event\MultiFactor\Captive; | ||||
| use Joomla\CMS\Event\MultiFactor\GetMethod; | ||||
| use Joomla\CMS\Event\MultiFactor\GetSetup; | ||||
| use Joomla\CMS\Event\MultiFactor\SaveSetup; | ||||
| use Joomla\CMS\Event\MultiFactor\Validate; | ||||
| use Joomla\CMS\Language\Text; | ||||
| use Joomla\CMS\Plugin\CMSPlugin; | ||||
| use Joomla\CMS\Plugin\PluginHelper; | ||||
| use Joomla\CMS\Uri\Uri; | ||||
| use Joomla\CMS\User\User; | ||||
| use Joomla\CMS\User\UserFactoryAwareTrait; | ||||
| use Joomla\Component\Users\Administrator\DataShape\CaptiveRenderOptions; | ||||
| use Joomla\Component\Users\Administrator\DataShape\MethodDescriptor; | ||||
| use Joomla\Component\Users\Administrator\DataShape\SetupRenderOptions; | ||||
| use Joomla\Component\Users\Administrator\Table\MfaTable; | ||||
| use Joomla\Event\SubscriberInterface; | ||||
| use Joomla\Input\Input; | ||||
| use Joomla\Plugin\Multifactorauth\Webauthn\Helper\Credentials; | ||||
| use Webauthn\PublicKeyCredentialRequestOptions; | ||||
|  | ||||
| // phpcs:disable PSR1.Files.SideEffects | ||||
| \defined('_JEXEC') or die; | ||||
| // phpcs:enable PSR1.Files.SideEffects | ||||
|  | ||||
| /** | ||||
|  * Joomla Multi-factor Authentication plugin for WebAuthn | ||||
|  * | ||||
|  * @since 4.2.0 | ||||
|  */ | ||||
| class Webauthn extends CMSPlugin implements SubscriberInterface | ||||
| { | ||||
|     use UserFactoryAwareTrait; | ||||
|  | ||||
|     /** | ||||
|      * Auto-load the plugin's language files | ||||
|      * | ||||
|      * @var    boolean | ||||
|      * @since  4.2.0 | ||||
|      */ | ||||
|     protected $autoloadLanguage = true; | ||||
|  | ||||
|     /** | ||||
|      * The MFA Method name handled by this plugin | ||||
|      * | ||||
|      * @var   string | ||||
|      * @since  4.2.0 | ||||
|      */ | ||||
|     private $mfaMethodName = 'webauthn'; | ||||
|  | ||||
|     /** | ||||
|      * Returns an array of events this subscriber will listen to. | ||||
|      * | ||||
|      * @return  array | ||||
|      * | ||||
|      * @since  4.2.0 | ||||
|      */ | ||||
|     public static function getSubscribedEvents(): array | ||||
|     { | ||||
|         return [ | ||||
|             'onUserMultifactorGetMethod' => 'onUserMultifactorGetMethod', | ||||
|             'onUserMultifactorCaptive'   => 'onUserMultifactorCaptive', | ||||
|             'onUserMultifactorGetSetup'  => 'onUserMultifactorGetSetup', | ||||
|             'onUserMultifactorSaveSetup' => 'onUserMultifactorSaveSetup', | ||||
|             'onUserMultifactorValidate'  => 'onUserMultifactorValidate', | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Gets the identity of this MFA Method | ||||
|      * | ||||
|      * @param   GetMethod  $event  The event we are handling | ||||
|      * | ||||
|      * @return  void | ||||
|      * @since   4.2.0 | ||||
|      */ | ||||
|     public function onUserMultifactorGetMethod(GetMethod $event): void | ||||
|     { | ||||
|         $event->addResult( | ||||
|             new MethodDescriptor( | ||||
|                 [ | ||||
|                     'name'               => $this->mfaMethodName, | ||||
|                     'display'            => Text::_('PLG_MULTIFACTORAUTH_WEBAUTHN_LBL_DISPLAYEDAS'), | ||||
|                     'shortinfo'          => Text::_('PLG_MULTIFACTORAUTH_WEBAUTHN_LBL_SHORTINFO'), | ||||
|                     'image'              => 'media/plg_multifactorauth_webauthn/images/passkeys.svg', | ||||
|                     'allowMultiple'      => true, | ||||
|                     'allowEntryBatching' => true, | ||||
|                 ] | ||||
|             ) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns the information which allows Joomla to render the MFA setup page. This is the page | ||||
|      * which allows the user to add or modify a MFA Method for their user account. If the record | ||||
|      * does not correspond to your plugin return an empty array. | ||||
|      * | ||||
|      * @param   GetSetup  $event  The event we are handling | ||||
|      * | ||||
|      * @return  void | ||||
|      * @throws  \Exception | ||||
|      * @since   4.2.0 | ||||
|      */ | ||||
|     public function onUserMultifactorGetSetup(GetSetup $event): void | ||||
|     { | ||||
|         /** | ||||
|          * @var   MfaTable $record The record currently selected by the user. | ||||
|          */ | ||||
|         $record = $event['record']; | ||||
|  | ||||
|         // Make sure we are actually meant to handle this Method | ||||
|         if ($record->method != $this->mfaMethodName) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Get some values assuming that we are NOT setting up U2F (the key is already registered) | ||||
|         $submitClass = ''; | ||||
|         $submitIcon  = 'icon icon-ok'; | ||||
|         $submitText  = 'JSAVE'; | ||||
|         $preMessage  = Text::_('PLG_MULTIFACTORAUTH_WEBAUTHN_LBL_CONFIGURED'); | ||||
|         $type        = 'input'; | ||||
|         $html        = ''; | ||||
|         $hiddenData  = []; | ||||
|  | ||||
|         /** | ||||
|          * If there are no authenticators set up yet I need to show a different message and take a different action when | ||||
|          * my user clicks the submit button. | ||||
|          */ | ||||
|         if (!\is_array($record->options) || empty($record->options['credentialId'] ?? '')) { | ||||
|             $document = $this->getApplication()->getDocument(); | ||||
|             $wam      = $document->getWebAssetManager(); | ||||
|             $wam->getRegistry()->addExtensionRegistryFile('plg_multifactorauth_webauthn'); | ||||
|  | ||||
|             $layoutPath = PluginHelper::getLayoutPath('multifactorauth', 'webauthn'); | ||||
|             ob_start(); | ||||
|             include $layoutPath; | ||||
|             $html = ob_get_clean(); | ||||
|             $type = 'custom'; | ||||
|  | ||||
|             // Load JS translations | ||||
|             Text::script('PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_NOTAVAILABLE_HEAD'); | ||||
|  | ||||
|             $document->addScriptOptions('com_users.pagetype', 'setup', false); | ||||
|  | ||||
|             // Save the WebAuthn request to the session | ||||
|             $user                    = $this->getApplication()->getIdentity() ?: $this->getUserFactory()->loadUserById(0); | ||||
|             $hiddenData['pkRequest'] = base64_encode(Credentials::requestAttestation($user)); | ||||
|  | ||||
|             // Special button handling | ||||
|             $submitClass = "multifactorauth_webauthn_setup"; | ||||
|             $submitIcon  = 'icon icon-lock'; | ||||
|             $submitText  = 'PLG_MULTIFACTORAUTH_WEBAUTHN_LBL_REGISTERKEY'; | ||||
|  | ||||
|             // Message to display | ||||
|             $preMessage = Text::sprintf( | ||||
|                 'PLG_MULTIFACTORAUTH_WEBAUTHN_LBL_INSTRUCTIONS', | ||||
|                 Text::_($submitText) | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         $event->addResult( | ||||
|             new SetupRenderOptions( | ||||
|                 [ | ||||
|                     'default_title' => Text::_('PLG_MULTIFACTORAUTH_WEBAUTHN_LBL_DISPLAYEDAS'), | ||||
|                     'pre_message'   => $preMessage, | ||||
|                     'hidden_data'   => $hiddenData, | ||||
|                     'field_type'    => $type, | ||||
|                     'input_type'    => 'hidden', | ||||
|                     'html'          => $html, | ||||
|                     'show_submit'   => true, | ||||
|                     'submit_class'  => $submitClass, | ||||
|                     'submit_icon'   => $submitIcon, | ||||
|                     'submit_text'   => $submitText, | ||||
|                 ] | ||||
|             ) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Parse the input from the MFA setup page and return the configuration information to be saved to the database. If | ||||
|      * the information is invalid throw a RuntimeException to signal the need to display the editor page again. The | ||||
|      * message of the exception will be displayed to the user. If the record does not correspond to your plugin return | ||||
|      * an empty array. | ||||
|      * | ||||
|      * @param   SaveSetup  $event  The event we are handling | ||||
|      * | ||||
|      * @return  void The configuration data to save to the database | ||||
|      * @since   4.2.0 | ||||
|      */ | ||||
|     public function onUserMultifactorSaveSetup(SaveSetup $event): void | ||||
|     { | ||||
|         /** | ||||
|          * @var   MfaTable $record The record currently selected by the user. | ||||
|          * @var   Input    $input  The user input you are going to take into account. | ||||
|          */ | ||||
|         $record = $event['record']; | ||||
|         $input  = $event['input']; | ||||
|  | ||||
|         // Make sure we are actually meant to handle this Method | ||||
|         if ($record->method != $this->mfaMethodName) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Editing an existing authenticator: only the title is saved | ||||
|         if (\is_array($record->options) && !empty($record->options['credentialId'] ?? '')) { | ||||
|             $event->addResult($record->options); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         $code                = $input->get('code', null, 'base64'); | ||||
|         $session             = $this->getApplication()->getSession(); | ||||
|         $registrationRequest = $session->get('plg_multifactorauth_webauthn.publicKeyCredentialCreationOptions', null); | ||||
|  | ||||
|         // If there was no registration request BUT there is a registration response throw an error | ||||
|         if (empty($registrationRequest) && !empty($code)) { | ||||
|             throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); | ||||
|         } | ||||
|  | ||||
|         // If there is no registration request (and there isn't a registration response) we are just saving the title. | ||||
|         if (empty($registrationRequest)) { | ||||
|             $event->addResult($record->options); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // In any other case try to authorize the registration | ||||
|         try { | ||||
|             $publicKeyCredentialSource = Credentials::verifyAttestation($code); | ||||
|         } catch (\Exception $err) { | ||||
|             throw new \RuntimeException($err->getMessage(), 403); | ||||
|         } finally { | ||||
|             // Unset the request data from the session. | ||||
|             $session->set('plg_multifactorauth_webauthn.publicKeyCredentialCreationOptions', null); | ||||
|             $session->set('plg_multifactorauth_webauthn.registration_user_id', null); | ||||
|         } | ||||
|  | ||||
|         // Return the configuration to be serialized | ||||
|         $event->addResult( | ||||
|             [ | ||||
|                 'credentialId' => base64_encode($publicKeyCredentialSource->getAttestedCredentialData()->getCredentialId()), | ||||
|                 'pubkeysource' => json_encode($publicKeyCredentialSource), | ||||
|                 'counter'      => 0, | ||||
|             ] | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns the information which allows Joomla to render the Captive MFA page. This is the page | ||||
|      * which appears right after you log in and asks you to validate your login with MFA. | ||||
|      * | ||||
|      * @param   Captive  $event  The event we are handling | ||||
|      * | ||||
|      * @return  void | ||||
|      * @throws \Exception | ||||
|      * @since   4.2.0 | ||||
|      */ | ||||
|     public function onUserMultifactorCaptive(Captive $event): void | ||||
|     { | ||||
|         /** | ||||
|          * @var   MfaTable $record The record currently selected by the user. | ||||
|          */ | ||||
|         $record = $event['record']; | ||||
|  | ||||
|         // Make sure we are actually meant to handle this Method | ||||
|         if ($record->method != $this->mfaMethodName) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * The following code looks stupid. An explanation is in order. | ||||
|          * | ||||
|          * What we normally want to do is save the authentication data returned by getAuthenticateData into the session. | ||||
|          * This is what is sent to the authenticator through the Javascript API and signed. The signature is posted back | ||||
|          * to the form as the "code" which is read by onUserMultifactorauthValidate. That Method will read the authentication | ||||
|          * data from the session and pass it along with the key registration data (from the database) and the | ||||
|          * authentication response (the "code" submitted in the form) to the WebAuthn library for validation. | ||||
|          * | ||||
|          * Validation will work as long as the challenge recorded in the encrypted AUTHENTICATION RESPONSE matches, upon | ||||
|          * decryption, the challenge recorded in the AUTHENTICATION DATA. | ||||
|          * | ||||
|          * I observed that for whatever stupid reason the browser was sometimes sending TWO requests to the server's | ||||
|          * Captive login page but only rendered the FIRST. This meant that the authentication data sent to the key had | ||||
|          * already been overwritten in the session by the "invisible" second request. As a result the challenge would | ||||
|          * not match and we'd get a validation error. | ||||
|          * | ||||
|          * The code below will attempt to read the authentication data from the session first. If it exists it will NOT | ||||
|          * try to replace it (technically it replaces it with a copy of the same data - same difference!). If nothing | ||||
|          * exists in the session, however, it WILL store the (random seeded) result of the getAuthenticateData Method. | ||||
|          * Therefore the first request to the Captive login page will store a new set of authentication data whereas the | ||||
|          * second, "invisible", request will just reuse the same data as the first request, fixing the observed issue in | ||||
|          * a way that doesn't compromise security. | ||||
|          * | ||||
|          * In case you are wondering, yes, the data is removed from the session in the onUserMultifactorauthValidate Method. | ||||
|          * In fact it's the first thing we do after reading it, preventing constant reuse of the same set of challenges. | ||||
|          * | ||||
|          * That was fun to debug - for "poke your eyes with a rusty fork" values of fun. | ||||
|          */ | ||||
|  | ||||
|         $session          = $this->getApplication()->getSession(); | ||||
|         $pkOptionsEncoded = $session->get('plg_multifactorauth_webauthn.publicKeyCredentialRequestOptions', null); | ||||
|  | ||||
|         $force = $this->getApplication()->getInput()->getInt('force', 0); | ||||
|  | ||||
|         try { | ||||
|             if ($force) { | ||||
|                 throw new \RuntimeException('Expected exception (good): force a new key request'); | ||||
|             } | ||||
|  | ||||
|             if (empty($pkOptionsEncoded)) { | ||||
|                 throw new \RuntimeException('Expected exception (good): we do not have a pending key request'); | ||||
|             } | ||||
|  | ||||
|             $serializedOptions = base64_decode($pkOptionsEncoded); | ||||
|             $pkOptions         = unserialize($serializedOptions); | ||||
|  | ||||
|             if (!\is_object($pkOptions) || empty($pkOptions) || !($pkOptions instanceof PublicKeyCredentialRequestOptions)) { | ||||
|                 throw new \RuntimeException('The pending key request is corrupt; a new one will be created'); | ||||
|             } | ||||
|  | ||||
|             $pkRequest = json_encode($pkOptions, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); | ||||
|         } catch (\Exception $e) { | ||||
|             $pkRequest = Credentials::requestAssertion($record->user_id); | ||||
|         } | ||||
|  | ||||
|         $document = $this->getApplication()->getDocument(); | ||||
|         $wam      = $document->getWebAssetManager(); | ||||
|         $wam->getRegistry()->addExtensionRegistryFile('plg_multifactorauth_webauthn'); | ||||
|  | ||||
|         try { | ||||
|             $document->addScriptOptions('com_users.authData', base64_encode($pkRequest), false); | ||||
|             $layoutPath = PluginHelper::getLayoutPath('multifactorauth', 'webauthn'); | ||||
|             ob_start(); | ||||
|             include $layoutPath; | ||||
|             $html = ob_get_clean(); | ||||
|         } catch (\Exception $e) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Load JS translations | ||||
|         Text::script('PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_NOTAVAILABLE_HEAD'); | ||||
|         Text::script('PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_NO_STORED_CREDENTIAL'); | ||||
|  | ||||
|         $document->addScriptOptions('com_users.pagetype', 'validate', false); | ||||
|  | ||||
|         $event->addResult( | ||||
|             new CaptiveRenderOptions( | ||||
|                 [ | ||||
|                     'pre_message' => Text::sprintf( | ||||
|                         'PLG_MULTIFACTORAUTH_WEBAUTHN_LBL_INSTRUCTIONS', | ||||
|                         Text::_('PLG_MULTIFACTORAUTH_WEBAUTHN_LBL_VALIDATEKEY') | ||||
|                     ), | ||||
|                     'field_type'         => 'custom', | ||||
|                     'input_type'         => 'hidden', | ||||
|                     'placeholder'        => '', | ||||
|                     'label'              => '', | ||||
|                     'html'               => $html, | ||||
|                     'post_message'       => '', | ||||
|                     'hide_submit'        => false, | ||||
|                     'submit_icon'        => 'icon icon-lock', | ||||
|                     'submit_text'        => 'PLG_MULTIFACTORAUTH_WEBAUTHN_LBL_VALIDATEKEY', | ||||
|                     'allowEntryBatching' => true, | ||||
|                 ] | ||||
|             ) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Validates the Multi-factor Authentication code submitted by the user in the Multi-Factor | ||||
|      * Authentication page. If the record does not correspond to your plugin return FALSE. | ||||
|      * | ||||
|      * @param   Validate  $event  The event we are handling | ||||
|      * | ||||
|      * @return  void | ||||
|      * @since   4.2.0 | ||||
|      */ | ||||
|     public function onUserMultifactorValidate(Validate $event): void | ||||
|     { | ||||
|         // This method is only available on HTTPS | ||||
|         if (Uri::getInstance()->getScheme() !== 'https') { | ||||
|             $event->addResult(false); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * @var   MfaTable $record The MFA Method's record you're validating against | ||||
|          * @var   User     $user   The user record | ||||
|          * @var   string   $code   The submitted code | ||||
|          */ | ||||
|         $record = $event['record']; | ||||
|         $user   = $event['user']; | ||||
|         $code   = $event['code']; | ||||
|  | ||||
|         // Make sure we are actually meant to handle this Method | ||||
|         if ($record->method != $this->mfaMethodName) { | ||||
|             $event->addResult(false); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Double check the MFA Method is for the correct user | ||||
|         if ($user->id != $record->user_id) { | ||||
|             $event->addResult(false); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             Credentials::verifyAssertion($code); | ||||
|         } catch (\Exception $e) { | ||||
|             try { | ||||
|                 $this->getApplication()->enqueueMessage($e->getMessage(), 'error'); | ||||
|             } catch (\Exception $e) { | ||||
|             } | ||||
|  | ||||
|             $event->addResult(false); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         $event->addResult(true); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										332
									
								
								plugins/multifactorauth/webauthn/src/Helper/Credentials.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										332
									
								
								plugins/multifactorauth/webauthn/src/Helper/Credentials.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,332 @@ | ||||
| <?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\Helper; | ||||
|  | ||||
| use Joomla\CMS\Application\CMSApplication; | ||||
| use Joomla\CMS\Factory; | ||||
| use Joomla\CMS\Language\Text; | ||||
| use Joomla\CMS\Uri\Uri; | ||||
| use Joomla\CMS\User\User; | ||||
| use Joomla\CMS\User\UserFactoryInterface; | ||||
| use Joomla\CMS\WebAuthn\Server; | ||||
| use Joomla\Plugin\Multifactorauth\Webauthn\CredentialRepository; | ||||
| use Joomla\Session\SessionInterface; | ||||
| use Laminas\Diactoros\ServerRequestFactory; | ||||
| use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs; | ||||
| use Webauthn\AuthenticatorSelectionCriteria; | ||||
| use Webauthn\PublicKeyCredentialCreationOptions; | ||||
| use Webauthn\PublicKeyCredentialDescriptor; | ||||
| use Webauthn\PublicKeyCredentialRequestOptions; | ||||
| use Webauthn\PublicKeyCredentialRpEntity; | ||||
| use Webauthn\PublicKeyCredentialSource; | ||||
| use Webauthn\PublicKeyCredentialUserEntity; | ||||
|  | ||||
| // phpcs:disable PSR1.Files.SideEffects | ||||
| \defined('_JEXEC') or die; | ||||
| // phpcs:enable PSR1.Files.SideEffects | ||||
|  | ||||
| /** | ||||
|  * Helper class to aid in credentials creation (link an authenticator to a user account) | ||||
|  * | ||||
|  * @since 4.2.0 | ||||
|  */ | ||||
| abstract class Credentials | ||||
| { | ||||
|     /** | ||||
|      * Authenticator registration step 1: create a public key for credentials attestation. | ||||
|      * | ||||
|      * The result is a JSON string which can be used in Javascript code with navigator.credentials.create(). | ||||
|      * | ||||
|      * @param   User   $user   The Joomla user to create the public key for | ||||
|      * | ||||
|      * @return  string | ||||
|      * @throws  \Exception  On error | ||||
|      * @since   4.2.0 | ||||
|      */ | ||||
|     public static function requestAttestation(User $user): string | ||||
|     { | ||||
|         $publicKeyCredentialCreationOptions = self::getWebauthnServer($user->id) | ||||
|             ->generatePublicKeyCredentialCreationOptions( | ||||
|                 self::getUserEntity($user), | ||||
|                 PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE, | ||||
|                 self::getPubKeyDescriptorsForUser($user), | ||||
|                 new AuthenticatorSelectionCriteria( | ||||
|                     AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE, | ||||
|                     false, | ||||
|                     AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_PREFERRED | ||||
|                 ), | ||||
|                 new AuthenticationExtensionsClientInputs() | ||||
|             ); | ||||
|  | ||||
|         // Save data in the session | ||||
|         $session = Factory::getApplication()->getSession(); | ||||
|  | ||||
|         $session->set( | ||||
|             'plg_multifactorauth_webauthn.publicKeyCredentialCreationOptions', | ||||
|             base64_encode(serialize($publicKeyCredentialCreationOptions)) | ||||
|         ); | ||||
|         $session->set('plg_multifactorauth_webauthn.registration_user_id', $user->id); | ||||
|  | ||||
|         return json_encode($publicKeyCredentialCreationOptions, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Authenticator registration step 2: verify the credentials attestation by the authenticator | ||||
|      * | ||||
|      * This returns the attested credential data on success. | ||||
|      * | ||||
|      * An exception will be returned on error. Also, under very rare conditions, you may receive NULL instead of | ||||
|      * attested credential data which means that something was off in the returned data from the browser. | ||||
|      * | ||||
|      * @param   string   $data   The JSON-encoded data returned by the browser during the authentication flow | ||||
|      * | ||||
|      * @return  ?PublicKeyCredentialSource | ||||
|      * @throws  \Exception  When something does not check out | ||||
|      * @since   4.2.0 | ||||
|      */ | ||||
|     public static function verifyAttestation(string $data): ?PublicKeyCredentialSource | ||||
|     { | ||||
|         $session = Factory::getApplication()->getSession(); | ||||
|  | ||||
|         // Retrieve the PublicKeyCredentialCreationOptions object created earlier and perform sanity checks | ||||
|         $encodedOptions = $session->get('plg_multifactorauth_webauthn.publicKeyCredentialCreationOptions', null); | ||||
|  | ||||
|         if (empty($encodedOptions)) { | ||||
|             throw new \RuntimeException(Text::_('PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_CREATE_NO_PK')); | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             $publicKeyCredentialCreationOptions = unserialize(base64_decode($encodedOptions)); | ||||
|         } catch (\Exception $e) { | ||||
|             $publicKeyCredentialCreationOptions = null; | ||||
|         } | ||||
|  | ||||
|         if (!\is_object($publicKeyCredentialCreationOptions) || !($publicKeyCredentialCreationOptions instanceof PublicKeyCredentialCreationOptions)) { | ||||
|             throw new \RuntimeException(Text::_('PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_CREATE_NO_PK')); | ||||
|         } | ||||
|  | ||||
|         // Retrieve the stored user ID and make sure it's the same one in the request. | ||||
|         $storedUserId = $session->get('plg_multifactorauth_webauthn.registration_user_id', 0); | ||||
|         $myUser       = Factory::getApplication()->getIdentity() | ||||
|             ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); | ||||
|         $myUserId     = $myUser->id; | ||||
|  | ||||
|         if (($myUser->guest) || ($myUserId != $storedUserId)) { | ||||
|             throw new \RuntimeException(Text::_('PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_CREATE_INVALID_USER')); | ||||
|         } | ||||
|  | ||||
|         return self::getWebauthnServer($myUser->id)->loadAndCheckAttestationResponse( | ||||
|             base64_decode($data), | ||||
|             $publicKeyCredentialCreationOptions, | ||||
|             ServerRequestFactory::fromGlobals() | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Authentication step 1: create a challenge for key verification | ||||
|      * | ||||
|      * @param   int  $userId  The user ID to create a WebAuthn PK for | ||||
|      * | ||||
|      * @return  string | ||||
|      * @throws  \Exception  On error | ||||
|      * @since   4.2.0 | ||||
|      */ | ||||
|     public static function requestAssertion(int $userId): string | ||||
|     { | ||||
|         $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); | ||||
|  | ||||
|         $publicKeyCredentialRequestOptions = self::getWebauthnServer($userId) | ||||
|             ->generatePublicKeyCredentialRequestOptions( | ||||
|                 PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED, | ||||
|                 self::getPubKeyDescriptorsForUser($user) | ||||
|             ); | ||||
|  | ||||
|         // Save in session. This is used during the verification stage to prevent replay attacks. | ||||
|         /** @var SessionInterface $session */ | ||||
|         $session = Factory::getApplication()->getSession(); | ||||
|         $session->set('plg_multifactorauth_webauthn.publicKeyCredentialRequestOptions', base64_encode(serialize($publicKeyCredentialRequestOptions))); | ||||
|         $session->set('plg_multifactorauth_webauthn.userHandle', $userId); | ||||
|         $session->set('plg_multifactorauth_webauthn.userId', $userId); | ||||
|  | ||||
|         // Return the JSON encoded data to the caller | ||||
|         return json_encode($publicKeyCredentialRequestOptions, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Authentication step 2: Checks if the browser's response to our challenge is valid. | ||||
|      * | ||||
|      * @param   string   $response   Base64-encoded response | ||||
|      * | ||||
|      * @return  void | ||||
|      * @throws  \Exception  When something does not check out. | ||||
|      * @since   4.2.0 | ||||
|      */ | ||||
|     public static function verifyAssertion(string $response): void | ||||
|     { | ||||
|         /** @var SessionInterface $session */ | ||||
|         $session = Factory::getApplication()->getSession(); | ||||
|  | ||||
|         $encodedPkOptions = $session->get('plg_multifactorauth_webauthn.publicKeyCredentialRequestOptions', null); | ||||
|         $userHandle       = $session->get('plg_multifactorauth_webauthn.userHandle', null); | ||||
|         $userId           = $session->get('plg_multifactorauth_webauthn.userId', null); | ||||
|  | ||||
|         $session->set('plg_multifactorauth_webauthn.publicKeyCredentialRequestOptions', null); | ||||
|         $session->set('plg_multifactorauth_webauthn.userHandle', null); | ||||
|         $session->set('plg_multifactorauth_webauthn.userId', null); | ||||
|  | ||||
|         if (empty($userId)) { | ||||
|             throw new \RuntimeException(Text::_('PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST')); | ||||
|         } | ||||
|  | ||||
|         // Make sure the user exists | ||||
|         $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); | ||||
|  | ||||
|         if ($user->id != $userId) { | ||||
|             throw new \RuntimeException(Text::_('PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST')); | ||||
|         } | ||||
|  | ||||
|         // Make sure the user is ourselves (we cannot perform MFA on behalf of another user!) | ||||
|         $currentUser = Factory::getApplication()->getIdentity() | ||||
|             ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); | ||||
|  | ||||
|         if ($currentUser->id != $userId) { | ||||
|             throw new \RuntimeException(Text::_('PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST')); | ||||
|         } | ||||
|  | ||||
|         // Make sure the public key credential request options in the session are valid | ||||
|         $serializedOptions                 = base64_decode($encodedPkOptions); | ||||
|         $publicKeyCredentialRequestOptions = unserialize($serializedOptions); | ||||
|  | ||||
|         if ( | ||||
|             !\is_object($publicKeyCredentialRequestOptions) | ||||
|             || empty($publicKeyCredentialRequestOptions) | ||||
|             || !($publicKeyCredentialRequestOptions instanceof PublicKeyCredentialRequestOptions) | ||||
|         ) { | ||||
|             throw new \RuntimeException(Text::_('PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST')); | ||||
|         } | ||||
|  | ||||
|         // Unserialize the browser response data | ||||
|         $data = base64_decode($response); | ||||
|  | ||||
|         self::getWebauthnServer($user->id)->loadAndCheckAssertionResponse( | ||||
|             $data, | ||||
|             $publicKeyCredentialRequestOptions, | ||||
|             self::getUserEntity($user), | ||||
|             ServerRequestFactory::fromGlobals() | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get the user's avatar (through Gravatar) | ||||
|      * | ||||
|      * @param   User   $user   The Joomla user object | ||||
|      * @param   int    $size   The dimensions of the image to fetch (default: 64 pixels) | ||||
|      * | ||||
|      * @return  string  The URL to the user's avatar | ||||
|      * | ||||
|      * @since 4.2.0 | ||||
|      */ | ||||
|     private static function getAvatar(User $user, int $size = 64) | ||||
|     { | ||||
|         $scheme    = Uri::getInstance()->getScheme(); | ||||
|         $subdomain = ($scheme == 'https') ? 'secure' : 'www'; | ||||
|  | ||||
|         return \sprintf('%s://%s.gravatar.com/avatar/%s.jpg?s=%u&d=mm', $scheme, $subdomain, md5($user->email), $size); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get a WebAuthn user entity for a Joomla user | ||||
|      * | ||||
|      * @param   User   $user  The user to get an entity for | ||||
|      * | ||||
|      * @return  PublicKeyCredentialUserEntity | ||||
|      * @since   4.2.0 | ||||
|      */ | ||||
|     private static function getUserEntity(User $user): PublicKeyCredentialUserEntity | ||||
|     { | ||||
|         return new PublicKeyCredentialUserEntity( | ||||
|             $user->username, | ||||
|             $user->id, | ||||
|             $user->name, | ||||
|             self::getAvatar($user, 64) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get the WebAuthn library server object | ||||
|      * | ||||
|      * @param   int|null  $userId  The user ID holding the list of valid authenticators | ||||
|      * | ||||
|      * @return  Server | ||||
|      * @since   4.2.0 | ||||
|      */ | ||||
|     private static function getWebauthnServer(?int $userId): Server | ||||
|     { | ||||
|         /** @var CMSApplication $app */ | ||||
|         try { | ||||
|             $app      = Factory::getApplication(); | ||||
|             $siteName = $app->get('sitename'); | ||||
|         } catch (\Exception $e) { | ||||
|             $siteName = 'Joomla! Site'; | ||||
|         } | ||||
|  | ||||
|         // Credentials repository | ||||
|         $repository = new CredentialRepository($userId); | ||||
|  | ||||
|         // Relaying Party -- Our site | ||||
|         $rpEntity = new PublicKeyCredentialRpEntity( | ||||
|             $siteName ?? 'Joomla! Site', | ||||
|             Uri::getInstance()->toString(['host']), | ||||
|             '' | ||||
|         ); | ||||
|  | ||||
|         $refClass       = new \ReflectionClass(Server::class); | ||||
|         $refConstructor = $refClass->getConstructor(); | ||||
|         $params         = $refConstructor->getParameters(); | ||||
|  | ||||
|         if (\count($params) === 3) { | ||||
|             // WebAuthn library 2, 3 | ||||
|             $server = new Server($rpEntity, $repository, null); | ||||
|         } else { | ||||
|             // WebAuthn library 4 (based on the deprecated comments in library version 3) | ||||
|             $server = new Server($rpEntity, $repository); | ||||
|         } | ||||
|  | ||||
|         // Ed25519 is only available with libsodium | ||||
|         if (!\function_exists('sodium_crypto_sign_seed_keypair')) { | ||||
|             $server->setSelectedAlgorithms(['RS256', 'RS512', 'PS256', 'PS512', 'ES256', 'ES512']); | ||||
|         } | ||||
|  | ||||
|         return $server; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns an array of the PK credential descriptors (registered authenticators) for the given user. | ||||
|      * | ||||
|      * @param   User   $user  The user to get the descriptors for | ||||
|      * | ||||
|      * @return  PublicKeyCredentialDescriptor[] | ||||
|      * @since   4.2.0 | ||||
|      */ | ||||
|     private static function getPubKeyDescriptorsForUser(User $user): array | ||||
|     { | ||||
|         $userEntity  = self::getUserEntity($user); | ||||
|         $repository  = new CredentialRepository($user->id); | ||||
|         $descriptors = []; | ||||
|         $records     = $repository->findAllForUserEntity($userEntity); | ||||
|  | ||||
|         foreach ($records as $record) { | ||||
|             $descriptors[] = $record->getPublicKeyCredentialDescriptor(); | ||||
|         } | ||||
|  | ||||
|         return $descriptors; | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user