[ 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\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 }
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 |