[ Index ] |
PHP Cross Reference of Joomla 4.2.2 documentation |
[Summary view] [Print] [Text view]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Sep 7 05:41:13 2022 | Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer |