primo commit
This commit is contained in:
		
							
								
								
									
										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