File manager - Edit - /home/opticamezl/www/newok/PluginTraits.tar
Back
EventReturnAware.php 0000644 00000002077 15175244111 0010525 0 ustar 00 <?php /** * @package Joomla.Plugin * @subpackage System.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\System\Webauthn\PluginTraits; use Joomla\Event\Event; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Utility trait to facilitate returning data from event handlers. * * @since 4.2.0 */ trait EventReturnAware { /** * Adds a result value to an event * * @param Event $event The event we were processing * @param mixed $value The value to append to the event's results * * @return void * @since 4.2.0 */ private function returnFromEvent(Event $event, $value = null): void { $result = $event->getArgument('result') ?: []; if (!is_array($result)) { $result = [$result]; } $result[] = $value; $event->setArgument('result', $result); } } UserDeletion.php 0000644 00000004215 15175244111 0007662 0 ustar 00 <?php /** * @package Joomla.Plugin * @subpackage System.Webauthn * * @copyright (C) 2020 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\Plugin\System\Webauthn\PluginTraits; use Joomla\CMS\Factory; use Joomla\CMS\Log\Log; use Joomla\Database\DatabaseInterface; use Joomla\Event\Event; use Joomla\Utilities\ArrayHelper; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Delete all WebAuthn credentials for a particular user * * @since 4.0.0 */ trait UserDeletion { /** * Remove all passwordless credential information for the given user ID. * * This method is called after user data is deleted from the database. * * @param Event $event The event we are handling * * @return void * * @since 4.0.0 */ public function onUserAfterDelete(Event $event): void { /** * @var array $user Holds the user data * @var bool $success True if user was successfully stored in the database * @var string|null $msg Message */ [$user, $success, $msg] = array_values($event->getArguments()); if (!$success) { $this->returnFromEvent($event, true); } $userId = ArrayHelper::getValue($user, 'id', 0, 'int'); if ($userId) { Log::add("Removing WebAuthn Passwordless Login information for deleted user #{$userId}", Log::DEBUG, 'webauthn.system'); /** @var DatabaseInterface $db */ $db = Factory::getContainer()->get(DatabaseInterface::class); $query = $db->getQuery(true) ->delete($db->quoteName('#__webauthn_credentials')) ->where($db->quoteName('user_id') . ' = :userId') ->bind(':userId', $userId); try { $db->setQuery($query)->execute(); } catch (\Exception $e) { // Don't worry if this fails } $this->returnFromEvent($event, true); } } } AjaxHandlerLogin.php 0000644 00000030476 15175244111 0010442 0 ustar 00 <?php /** * @package Joomla.Plugin * @subpackage System.Webauthn * * @copyright (C) 2020 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\Plugin\System\Webauthn\PluginTraits; use Exception; use Joomla\CMS\Authentication\Authentication; use Joomla\CMS\Authentication\AuthenticationResponse; use Joomla\CMS\Event\Plugin\System\Webauthn\AjaxLogin; use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; use Joomla\CMS\Log\Log; use Joomla\CMS\Plugin\PluginHelper; use Joomla\CMS\Uri\Uri; use Joomla\CMS\User\User; use Joomla\CMS\User\UserFactoryInterface; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Ajax handler for akaction=login * * Verifies the response received from the browser and logs in the user * * @since 4.0.0 */ trait AjaxHandlerLogin { /** * Returns the public key set for the user and a unique challenge in a Public Key Credential Request encoded as * JSON. * * @param AjaxLogin $event The event we are handling * * @return void * * @since 4.0.0 */ public function onAjaxWebauthnLogin(AjaxLogin $event): void { $session = $this->getApplication()->getSession(); $returnUrl = $session->get('plg_system_webauthn.returnUrl', Uri::base()); $userId = $session->get('plg_system_webauthn.userId', 0); try { $credentialRepository = $this->authenticationHelper->getCredentialsRepository(); // No user ID: no username was provided and the resident credential refers to an unknown user handle. DIE! if (empty($userId)) { Log::add('Cannot determine the user ID', Log::NOTICE, 'webauthn.system'); throw new \RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST')); } // Do I have a valid user? $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); if ($user->id != $userId) { $message = sprintf('User #%d does not exist', $userId); Log::add($message, Log::NOTICE, 'webauthn.system'); throw new \RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST')); } // Validate the authenticator response and get the user handle $userHandle = $this->getUserHandleFromResponse($user); if (is_null($userHandle)) { Log::add('Cannot retrieve the user handle from the request; the browser did not assert our request.', Log::NOTICE, 'webauthn.system'); throw new \RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST')); } // Does the user handle match the user ID? This should never trigger by definition of the login check. $validUserHandle = $credentialRepository->getHandleFromUserId($userId); if ($userHandle != $validUserHandle) { $message = sprintf('Invalid user handle; expected %s, got %s', $validUserHandle, $userHandle); Log::add($message, Log::NOTICE, 'webauthn.system'); throw new \RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST')); } // Make sure the user exists $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); if ($user->id != $userId) { $message = sprintf('Invalid user ID; expected %d, got %d', $userId, $user->id); Log::add($message, Log::NOTICE, 'webauthn.system'); throw new \RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST')); } // Login the user Log::add("Logging in the user", Log::INFO, 'webauthn.system'); $this->loginUser((int) $userId); } catch (\Throwable $e) { $session->set('plg_system_webauthn.publicKeyCredentialRequestOptions', null); $response = $this->getAuthenticationResponseObject(); $response->status = Authentication::STATUS_UNKNOWN; $response->error_message = $e->getMessage(); Log::add(sprintf("Received login failure. Message: %s", $e->getMessage()), Log::ERROR, 'webauthn.system'); // This also enqueues the login failure message for display after redirection. Look for JLog in that method. $this->processLoginFailure($response, null, 'system'); } finally { /** * This code needs to run no matter if the login succeeded or failed. It prevents replay attacks and takes * the user back to the page they started from. */ // Remove temporary information for security reasons $session->set('plg_system_webauthn.publicKeyCredentialRequestOptions', null); $session->set('plg_system_webauthn.returnUrl', null); $session->set('plg_system_webauthn.userId', null); // Redirect back to the page we were before. $this->getApplication()->redirect($returnUrl); } } /** * Logs in a user to the site, bypassing the authentication plugins. * * @param int $userId The user ID to log in * * @return void * @throws \Exception * @since 4.2.0 */ private function loginUser(int $userId): void { // Trick the class auto-loader into loading the necessary classes class_exists('Joomla\\CMS\\Authentication\\Authentication', true); // Fake a successful login message $isAdmin = $this->getApplication()->isClient('administrator'); $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); // Does the user account have a pending activation? if (!empty($user->activation)) { throw new \RuntimeException(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); } // Is the user account blocked? if ($user->block) { throw new \RuntimeException(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); } $statusSuccess = Authentication::STATUS_SUCCESS; $response = $this->getAuthenticationResponseObject(); $response->status = $statusSuccess; $response->username = $user->username; $response->fullname = $user->name; $response->error_message = ''; $response->language = $user->getParam('language'); $response->type = 'Passwordless'; if ($isAdmin) { $response->language = $user->getParam('admin_language'); } /** * Set up the login options. * * The 'remember' element forces the use of the Remember Me feature when logging in with Webauthn, as the * users would expect. * * The 'action' element is actually required by plg_user_joomla. It is the core ACL action the logged in user * must be allowed for the login to succeed. Please note that front-end and back-end logins use a different * action. This allows us to provide the WebAuthn button on both front- and back-end and be sure that if a * used with no backend access tries to use it to log in Joomla! will just slap him with an error message about * insufficient privileges - the same thing that'd happen if you tried to use your front-end only username and * password in a back-end login form. */ $options = [ 'remember' => true, 'action' => 'core.login.site', ]; if ($isAdmin) { $options['action'] = 'core.login.admin'; } // Run the user plugins. They CAN block login by returning boolean false and setting $response->error_message. PluginHelper::importPlugin('user'); $eventClassName = self::getEventClassByEventName('onUserLogin'); $event = new $eventClassName('onUserLogin', [(array) $response, $options]); $result = $this->getApplication()->getDispatcher()->dispatch($event->getName(), $event); $results = !isset($result['result']) || \is_null($result['result']) ? [] : $result['result']; // If there is no boolean FALSE result from any plugin the login is successful. if (in_array(false, $results, true) === false) { // Set the user in the session, letting Joomla! know that we are logged in. $this->getApplication()->getSession()->set('user', $user); // Trigger the onUserAfterLogin event $options['user'] = $user; $options['responseType'] = $response->type; // The user is successfully logged in. Run the after login events $eventClassName = self::getEventClassByEventName('onUserAfterLogin'); $event = new $eventClassName('onUserAfterLogin', [$options]); $this->getApplication()->getDispatcher()->dispatch($event->getName(), $event); return; } // If we are here the plugins marked a login failure. Trigger the onUserLoginFailure Event. $eventClassName = self::getEventClassByEventName('onUserLoginFailure'); $event = new $eventClassName('onUserLoginFailure', [(array) $response]); $this->getApplication()->getDispatcher()->dispatch($event->getName(), $event); // Log the failure Log::add($response->error_message, Log::WARNING, 'jerror'); // Throw an exception to let the caller know that the login failed throw new \RuntimeException($response->error_message); } /** * Returns a (blank) Joomla! authentication response * * @return AuthenticationResponse * * @since 4.2.0 */ private function getAuthenticationResponseObject(): AuthenticationResponse { // Force the class auto-loader to load the JAuthentication class class_exists('Joomla\\CMS\\Authentication\\Authentication', true); return new AuthenticationResponse(); } /** * Have Joomla! process a login failure * * @param AuthenticationResponse $response The Joomla! auth response object * * @return boolean * * @since 4.2.0 */ private function processLoginFailure(AuthenticationResponse $response): bool { // Import the user plugin group. PluginHelper::importPlugin('user'); // Trigger onUserLoginFailure Event. Log::add('Calling onUserLoginFailure plugin event', Log::INFO, 'plg_system_webauthn'); $eventClassName = self::getEventClassByEventName('onUserLoginFailure'); $event = new $eventClassName('onUserLoginFailure', [(array) $response]); $this->getApplication()->getDispatcher()->dispatch($event->getName(), $event); // If status is success, any error will have been raised by the user plugin $expectedStatus = Authentication::STATUS_SUCCESS; if ($response->status !== $expectedStatus) { Log::add('The login failure has been logged in Joomla\'s error log', Log::INFO, 'webauthn.system'); // Everything logged in the 'jerror' category ends up being enqueued in the application message queue. Log::add($response->error_message, Log::WARNING, 'jerror'); } else { $message = 'A login failure was caused by a third party user plugin but it did not return any' . 'further information.'; Log::add($message, Log::WARNING, 'webauthn.system'); } return false; } /** * Validate the authenticator response sent to us by the browser. * * @param User $user The user we are trying to log in. * * @return string|null The user handle or null * * @throws \Exception * @since 4.2.0 */ private function getUserHandleFromResponse(User $user): ?string { // Retrieve data from the request and session $pubKeyCredentialSource = $this->authenticationHelper->validateAssertionResponse( $this->getApplication()->getInput()->getBase64('data', ''), $user ); return $pubKeyCredentialSource ? $pubKeyCredentialSource->getUserHandle() : null; } } AjaxHandlerSaveLabel.php 0000644 00000004705 15175244111 0011224 0 ustar 00 <?php /** * @package Joomla.Plugin * @subpackage System.Webauthn * * @copyright (C) 2020 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\Plugin\System\Webauthn\PluginTraits; use Joomla\CMS\Event\Plugin\System\Webauthn\AjaxSaveLabel; use Joomla\CMS\User\User; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Ajax handler for akaction=savelabel * * Stores a new label for a security key * * @since 4.0.0 */ trait AjaxHandlerSaveLabel { /** * Handle the callback to rename an authenticator * * @param AjaxSaveLabel $event The event we are handling * * @return void * * @since 4.0.0 */ public function onAjaxWebauthnSavelabel(AjaxSaveLabel $event): void { // Initialize objects $input = $this->getApplication()->getInput(); $repository = $this->authenticationHelper->getCredentialsRepository(); // Retrieve data from the request $credentialId = $input->getBase64('credential_id', ''); $newLabel = $input->getString('new_label', ''); // Is this a valid credential? if (empty($credentialId)) { $event->addResult(false); return; } $credentialId = base64_decode($credentialId); if (empty($credentialId) || !$repository->has($credentialId)) { $event->addResult(false); return; } // Make sure I am editing my own key try { $credentialHandle = $repository->getUserHandleFor($credentialId); $user = $this->getApplication()->getIdentity() ?? new User(); $myHandle = $repository->getHandleFromUserId($user->id); } catch (\Exception $e) { $event->addResult(false); return; } if ($credentialHandle !== $myHandle) { $event->addResult(false); return; } // Make sure the new label is not empty if (empty($newLabel)) { $event->addResult(false); return; } // Save the new label try { $repository->setLabel($credentialId, $newLabel); } catch (\Exception $e) { $event->addResult(false); return; } $event->addResult(true); } } AdditionalLoginButtons.php 0000644 00000014216 15175244111 0011702 0 ustar 00 <?php /** * @package Joomla.Plugin * @subpackage System.Webauthn * * @copyright (C) 2020 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\Plugin\System\Webauthn\PluginTraits; use Joomla\CMS\Application\CMSApplication; use Joomla\CMS\Document\HtmlDocument; use Joomla\CMS\Helper\AuthenticationHelper; use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\Uri\Uri; use Joomla\CMS\User\UserHelper; use Joomla\Event\Event; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Inserts Webauthn buttons into login modules * * @since 4.0.0 */ trait AdditionalLoginButtons { /** * Do I need to inject buttons? Automatically detected (i.e. disabled if I'm already logged * in). * * @var boolean|null * @since 4.0.0 */ protected $allowButtonDisplay = null; /** * Have I already injected CSS and JavaScript? Prevents double inclusion of the same files. * * @var boolean * @since 4.0.0 */ private $injectedCSSandJS = false; /** * Creates additional login buttons * * @param Event $event The event we are handling * * @return void * * @see AuthenticationHelper::getLoginButtons() * * @since 4.0.0 */ public function onUserLoginButtons(Event $event): void { /** @var string $form The HTML ID of the form we are enclosed in */ [$form] = array_values($event->getArguments()); // If we determined we should not inject a button return early if (!$this->mustDisplayButton()) { return; } // Load necessary CSS and Javascript files $this->addLoginCSSAndJavascript(); // Unique ID for this button (allows display of multiple modules on the page) $randomId = 'plg_system_webauthn-' . UserHelper::genRandomPassword(12) . '-' . UserHelper::genRandomPassword(8); // Get local path to image $image = HTMLHelper::_('image', 'plg_system_webauthn/webauthn.svg', '', '', true, true); // If you can't find the image then skip it $image = $image ? JPATH_ROOT . substr($image, \strlen(Uri::root(true))) : ''; // Extract image if it exists $image = file_exists($image) ? file_get_contents($image) : ''; $this->returnFromEvent($event, [ [ 'label' => 'PLG_SYSTEM_WEBAUTHN_LOGIN_LABEL', 'tooltip' => 'PLG_SYSTEM_WEBAUTHN_LOGIN_DESC', 'id' => $randomId, 'data-webauthn-form' => $form, 'svg' => $image, 'class' => 'plg_system_webauthn_login_button', ], ]); } /** * Should I allow this plugin to add a WebAuthn login button? * * @return boolean * * @since 4.0.0 */ private function mustDisplayButton(): bool { // We must have a valid application if (!($this->getApplication() instanceof CMSApplication)) { return false; } // This plugin only applies to the frontend and administrator applications if (!$this->getApplication()->isClient('site') && !$this->getApplication()->isClient('administrator')) { return false; } // We must have a valid user if (empty($this->getApplication()->getIdentity())) { return false; } if (\is_null($this->allowButtonDisplay)) { $this->allowButtonDisplay = false; /** * Do not add a WebAuthn login button if we are already logged in */ if (!$this->getApplication()->getIdentity()->guest) { return false; } /** * Only display a button on HTML output */ try { $document = $this->getApplication()->getDocument(); } catch (\Exception $e) { $document = null; } if (!($document instanceof HtmlDocument)) { return false; } /** * WebAuthn only works on HTTPS. This is a security-related limitation of the W3C Web Authentication * specification, not an issue with this plugin :) */ if (!Uri::getInstance()->isSsl()) { return false; } // All checks passed; we should allow displaying a WebAuthn login button $this->allowButtonDisplay = true; } return $this->allowButtonDisplay; } /** * Injects the WebAuthn CSS and Javascript for frontend logins, but only once per page load. * * @return void * * @since 4.0.0 */ private function addLoginCSSAndJavascript(): void { if ($this->injectedCSSandJS) { return; } // Set the "don't load again" flag $this->injectedCSSandJS = true; /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->getApplication()->getDocument()->getWebAssetManager(); if (!$wa->assetExists('style', 'plg_system_webauthn.button')) { $wa->registerStyle('plg_system_webauthn.button', 'plg_system_webauthn/button.css'); } if (!$wa->assetExists('script', 'plg_system_webauthn.login')) { $wa->registerScript('plg_system_webauthn.login', 'plg_system_webauthn/login.js', [], ['defer' => true], ['core']); } $wa->useStyle('plg_system_webauthn.button') ->useScript('plg_system_webauthn.login'); // Load language strings client-side Text::script('PLG_SYSTEM_WEBAUTHN_ERR_CANNOT_FIND_USERNAME'); Text::script('PLG_SYSTEM_WEBAUTHN_ERR_EMPTY_USERNAME'); Text::script('PLG_SYSTEM_WEBAUTHN_ERR_INVALID_USERNAME'); // Store the current URL as the default return URL after login (or failure) $this->getApplication()->getSession()->set('plg_system_webauthn.returnUrl', Uri::current()); } } AjaxHandlerInitCreate.php 0000644 00000003371 15175244111 0011413 0 ustar 00 <?php /** * @package Joomla.Plugin * @subpackage System.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\System\Webauthn\PluginTraits; use Joomla\CMS\Event\Plugin\System\Webauthn\AjaxInitCreate; use Joomla\CMS\Factory; use Joomla\CMS\User\User; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Ajax handler for akaction=initcreate * * Returns the Public Key Creation Options to start the attestation ceremony on the browser. * * @since 4.2.0 */ trait AjaxHandlerInitCreate { /** * Returns the Public Key Creation Options to start the attestation ceremony on the browser. * * @param AjaxInitCreate $event The event we are handling * * @return void * @throws \Exception * @since 4.2.0 */ public function onAjaxWebauthnInitcreate(AjaxInitCreate $event): void { // Make sure I have a valid user $user = Factory::getApplication()->getIdentity(); if (!($user instanceof User) || $user->guest) { $event->addResult(new \stdClass()); return; } // I need the server to have either GMP or BCComp support to attest new authenticators if (function_exists('gmp_intval') === false && function_exists('bccomp') === false) { $event->addResult(new \stdClass()); return; } $session = $this->getApplication()->getSession(); $session->set('plg_system_webauthn.registration_user_id', $user->id); $event->addResult($this->authenticationHelper->getPubKeyCreationOptions($user)); } } AjaxHandlerChallenge.php 0000644 00000005551 15175244111 0011250 0 ustar 00 <?php /** * @package Joomla.Plugin * @subpackage System.Webauthn * * @copyright (C) 2020 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\Plugin\System\Webauthn\PluginTraits; use Joomla\CMS\Event\Plugin\System\Webauthn\AjaxChallenge; use Joomla\CMS\Factory; use Joomla\CMS\Uri\Uri; use Joomla\CMS\User\User; use Joomla\CMS\User\UserFactoryInterface; use Joomla\CMS\User\UserHelper; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Ajax handler for akaction=challenge * * Generates the public key and challenge which is used by the browser when logging in with Webauthn. This is the bit * which prevents tampering with the login process and replay attacks. * * @since 4.0.0 */ trait AjaxHandlerChallenge { /** * Returns the public key set for the user and a unique challenge in a Public Key Credential Request encoded as * JSON. * * @param AjaxChallenge $event The event we are handling * * @return void * * @throws \Exception * @since 4.0.0 */ public function onAjaxWebauthnChallenge(AjaxChallenge $event): void { // Initialize objects $session = $this->getApplication()->getSession(); $input = $this->getApplication()->getInput(); // Retrieve data from the request $username = $input->getUsername('username', ''); $returnUrl = base64_encode( $session->get('plg_system_webauthn.returnUrl', Uri::current()) ); $returnUrl = $input->getBase64('returnUrl', $returnUrl); $returnUrl = base64_decode($returnUrl); // For security reasons the post-login redirection URL must be internal to the site. if (!Uri::isInternal($returnUrl)) { // If the URL wasn't internal redirect to the site's root. $returnUrl = Uri::base(); } $session->set('plg_system_webauthn.returnUrl', $returnUrl); // Do I have a username? if (empty($username)) { $event->addResult(false); return; } // Is the username valid? try { $userId = UserHelper::getUserId($username) ?: 0; $myUser = $userId ? Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId) : new User(); } catch (\Exception $e) { $myUser = new User(); $userId = 0; } $publicKeyCredentialRequestOptions = $this->authenticationHelper->getPubkeyRequestOptions($myUser); $session->set('plg_system_webauthn.userId', $userId); // Return the JSON encoded data to the caller $event->addResult(json_encode($publicKeyCredentialRequestOptions, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); } } AjaxHandlerCreate.php 0000644 00000011234 15175244111 0010564 0 ustar 00 <?php /** * @package Joomla.Plugin * @subpackage System.Webauthn * * @copyright (C) 2020 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\Plugin\System\Webauthn\PluginTraits; use Joomla\CMS\Event\Plugin\System\Webauthn\AjaxCreate; use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; use Joomla\CMS\Layout\FileLayout; use Joomla\CMS\User\UserFactoryInterface; use Webauthn\PublicKeyCredentialSource; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Ajax handler for akaction=create * * Handles the browser postback for the credentials creation flow * * @since 4.0.0 */ trait AjaxHandlerCreate { /** * Handle the callback to add a new WebAuthn authenticator * * @param AjaxCreate $event The event we are handling * * @return void * * @throws \Exception * @since 4.0.0 */ public function onAjaxWebauthnCreate(AjaxCreate $event): void { /** * Fundamental sanity check: this callback is only allowed after a Public Key has been created server-side and * the user it was created for matches the current user. * * This is also checked in the validateAuthenticationData() so why check here? In case we have the wrong user * I need to fail early with a Joomla error page instead of falling through the code and possibly displaying * someone else's Webauthn configuration thus mitigating a major privacy and security risk. So, please, DO NOT * remove this sanity check! */ $session = $this->getApplication()->getSession(); $storedUserId = $session->get('plg_system_webauthn.registration_user_id', 0); $thatUser = empty($storedUserId) ? Factory::getApplication()->getIdentity() : Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($storedUserId); $myUser = Factory::getApplication()->getIdentity(); if ($thatUser->guest || ($thatUser->id != $myUser->id)) { // Unset the session variables used for registering authenticators (security precaution). $session->set('plg_system_webauthn.registration_user_id', null); $session->set('plg_system_webauthn.publicKeyCredentialCreationOptions', null); // Politely tell the presumed hacker trying to abuse this callback to go away. throw new \RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_INVALID_USER')); } // Get the credentials repository object. It's outside the try-catch because I also need it to display the GUI. $credentialRepository = $this->authenticationHelper->getCredentialsRepository(); // Try to validate the browser data. If there's an error I won't save anything and pass the message to the GUI. try { $input = $this->getApplication()->getInput(); // Retrieve the data sent by the device $data = $input->get('data', '', 'raw'); $publicKeyCredentialSource = $this->authenticationHelper->validateAttestationResponse($data); if (!\is_object($publicKeyCredentialSource) || !($publicKeyCredentialSource instanceof PublicKeyCredentialSource)) { throw new \RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_NO_ATTESTED_DATA')); } $credentialRepository->saveCredentialSource($publicKeyCredentialSource); } catch (\Exception $e) { $error = $e->getMessage(); $publicKeyCredentialSource = null; } // Unset the session variables used for registering authenticators (security precaution). $session->set('plg_system_webauthn.registration_user_id', null); $session->set('plg_system_webauthn.publicKeyCredentialCreationOptions', null); // Render the GUI and return it $layoutParameters = [ 'user' => $thatUser, 'allow_add' => $thatUser->id == $myUser->id, 'credentials' => $credentialRepository->getAll($thatUser->id), 'knownAuthenticators' => $this->authenticationHelper->getKnownAuthenticators(), 'attestationSupport' => $this->authenticationHelper->hasAttestationSupport(), ]; if (isset($error) && !empty($error)) { $layoutParameters['error'] = $error; } $layout = new FileLayout('plugins.system.webauthn.manage', JPATH_SITE . '/plugins/system/webauthn/layout'); $event->addResult($layout->render($layoutParameters)); } } UserProfileFields.php 0000644 00000016172 15175244111 0010653 0 ustar 00 <?php /** * @package Joomla.Plugin * @subpackage System.Webauthn * * @copyright (C) 2020 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\Plugin\System\Webauthn\PluginTraits; use Joomla\CMS\Factory; use Joomla\CMS\Form\Form; use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\Log\Log; use Joomla\CMS\Uri\Uri; use Joomla\CMS\User\User; use Joomla\CMS\User\UserFactoryInterface; use Joomla\Event\Event; use Joomla\Plugin\System\Webauthn\Extension\Webauthn; use Joomla\Registry\Registry; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Add extra fields in the User Profile page. * * This class only injects the custom form fields. The actual interface is rendered through * JFormFieldWebauthn. * * @see JFormFieldWebauthn::getInput() * * @since 4.0.0 */ trait UserProfileFields { /** * User object derived from the displayed user profile data. * * This is required to display the number and names of authenticators already registered when * the user displays the profile view page. * * @var User|null * @since 4.0.0 */ private static $userFromFormData = null; /** * HTMLHelper method to render the WebAuthn user profile field in the profile view page. * * Instead of showing a nonsensical "Website default" label next to the field, this method * displays the number and names of authenticators already registered by the user. * * This static method is set up for use in the onContentPrepareData method of this plugin. * * @param mixed $value Ignored. The WebAuthn profile field is virtual, it doesn't have a * stored value. We only use it as a proxy to render a sub-form. * * @return string * @since 4.0.0 */ public static function renderWebauthnProfileField($value): string { if (\is_null(self::$userFromFormData)) { return ''; } /** @var Webauthn $plugin */ $plugin = Factory::getApplication()->bootPlugin('webauthn', 'system'); $credentialRepository = $plugin->getAuthenticationHelper()->getCredentialsRepository(); $credentials = $credentialRepository->getAll(self::$userFromFormData->id); $authenticators = array_map( function (array $credential) { return $credential['label']; }, $credentials ); return Text::plural('PLG_SYSTEM_WEBAUTHN_FIELD_N_AUTHENTICATORS_REGISTERED', \count($authenticators), implode(', ', $authenticators)); } /** * Adds additional fields to the user editing form * * @param Event $event The event we are handling * * @return void * * @throws \Exception * @since 4.0.0 */ public function onContentPrepareForm(Event $event) { /** * @var Form $form The form to be altered. * @var mixed $data The associated data for the form. */ [$form, $data] = array_values($event->getArguments()); $name = $form->getName(); $allowedForms = [ 'com_admin.profile', 'com_users.user', 'com_users.profile', 'com_users.registration', ]; if (!\in_array($name, $allowedForms)) { return; } // This feature only applies in the site and administrator applications if ( !$this->getApplication()->isClient('site') && !$this->getApplication()->isClient('administrator') ) { return; } // This feature only applies to HTTPS sites. if (!Uri::getInstance()->isSsl()) { return; } // Get the user object $user = $this->getUserFromData($data); // Make sure the loaded user is the correct one if (\is_null($user)) { return; } // Make sure I am either editing myself OR I am a Super User if (!$this->canEditUser($user)) { return; } // Add the fields to the form. if ($name !== 'com_users.registration') { Log::add('Injecting WebAuthn Passwordless Login fields in user profile edit page', Log::DEBUG, 'webauthn.system'); Form::addFormPath(JPATH_PLUGINS . '/' . $this->_type . '/' . $this->_name . '/forms'); $form->loadFile('webauthn', false); } } /** * @param Event $event The event we are handling * * @return void * * @throws \Exception * @since 4.0.0 */ public function onContentPrepareData(Event $event): void { /** * @var string|null $context The context for the data * @var array|object|null $data An object or array containing the data for the form. */ [$context, $data] = array_values($event->getArguments()); if (!\in_array($context, ['com_users.profile', 'com_users.user'])) { return; } self::$userFromFormData = $this->getUserFromData($data); if (!HTMLHelper::isRegistered('users.webauthnWebauthn')) { HTMLHelper::register('users.webauthn', [__CLASS__, 'renderWebauthnProfileField']); } } /** * Get the user object based on the ID found in the provided user form data * * @param array|object|null $data The user form data * * @return User|null A user object or null if no match is found * * @throws \Exception * @since 4.0.0 */ private function getUserFromData($data): ?User { $id = null; if (\is_array($data)) { $id = $data['id'] ?? null; } elseif (\is_object($data) && ($data instanceof Registry)) { $id = $data->get('id'); } elseif (\is_object($data)) { $id = $data->id ?? null; } $user = empty($id) ? Factory::getApplication()->getIdentity() : Factory::getContainer() ->get(UserFactoryInterface::class) ->loadUserById($id); // Make sure the loaded user is the correct one if ($user->id != $id) { return null; } return $user; } /** * Is the current user allowed to edit the WebAuthn configuration of $user? * * To do so I must either be editing my own account OR I have to be a Super User. * * @param ?User $user The user you want to know if we're allowed to edit * * @return boolean * * @since 4.2.0 */ private function canEditUser(?User $user = null): bool { // I can edit myself, but Guests can't have passwordless logins associated if (empty($user) || $user->guest) { return true; } // Get the currently logged in used $myUser = $this->getApplication()->getIdentity() ?? new User(); // I can edit myself. If I'm a Super user I can edit other users too. return ($myUser->id == $user->id) || $myUser->authorise('core.admin'); } } AjaxHandlerDelete.php 0000644 00000004274 15175244111 0010571 0 ustar 00 <?php /** * @package Joomla.Plugin * @subpackage System.Webauthn * * @copyright (C) 2020 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\Plugin\System\Webauthn\PluginTraits; use Joomla\CMS\Event\Plugin\System\Webauthn\AjaxDelete; use Joomla\CMS\User\User; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Ajax handler for akaction=delete * * Deletes a security key * * @since 4.0.0 */ trait AjaxHandlerDelete { /** * Handle the callback to remove an authenticator * * @param AjaxDelete $event The event we are handling * * @return void * @since 4.0.0 */ public function onAjaxWebauthnDelete(AjaxDelete $event): void { // Initialize objects $input = $this->getApplication()->getInput(); $repository = $this->authenticationHelper->getCredentialsRepository(); // Retrieve data from the request $credentialId = $input->getBase64('credential_id', ''); // Is this a valid credential? if (empty($credentialId)) { $event->addResult(false); return; } $credentialId = base64_decode($credentialId); if (empty($credentialId) || !$repository->has($credentialId)) { $event->addResult(false); return; } // Make sure I am editing my own key try { $user = $this->getApplication()->getIdentity() ?? new User(); $credentialHandle = $repository->getUserHandleFor($credentialId); $myHandle = $repository->getHandleFromUserId($user->id); } catch (\Exception $e) { $event->addResult(false); return; } if ($credentialHandle !== $myHandle) { $event->addResult(false); return; } // Delete the record try { $repository->remove($credentialId); } catch (\Exception $e) { $event->addResult(false); return; } $event->addResult(true); } } AjaxHandler.php 0000644 00000015531 15175244111 0007444 0 ustar 00 <?php /** * @package Joomla.Plugin * @subpackage System.Webauthn * * @copyright (C) 2020 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\Plugin\System\Webauthn\PluginTraits; use Joomla\CMS\Application\CMSApplication; use Joomla\CMS\Event\AbstractEvent; use Joomla\CMS\Event\GenericEvent; use Joomla\CMS\Event\Plugin\System\Webauthn\Ajax; use Joomla\CMS\Event\Plugin\System\Webauthn\Ajax as PlgSystemWebauthnAjax; use Joomla\CMS\Event\Plugin\System\Webauthn\AjaxChallenge as PlgSystemWebauthnAjaxChallenge; use Joomla\CMS\Event\Plugin\System\Webauthn\AjaxCreate as PlgSystemWebauthnAjaxCreate; use Joomla\CMS\Event\Plugin\System\Webauthn\AjaxDelete as PlgSystemWebauthnAjaxDelete; use Joomla\CMS\Event\Plugin\System\Webauthn\AjaxInitCreate as PlgSystemWebauthnAjaxInitCreate; use Joomla\CMS\Event\Plugin\System\Webauthn\AjaxLogin as PlgSystemWebauthnAjaxLogin; use Joomla\CMS\Event\Plugin\System\Webauthn\AjaxSaveLabel as PlgSystemWebauthnAjaxSaveLabel; use Joomla\CMS\Event\Result\ResultAwareInterface; use Joomla\CMS\Language\Text; use Joomla\CMS\Log\Log; use Joomla\CMS\Uri\Uri; use Joomla\Event\Event; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Allows the plugin to handle AJAX requests in the backend of the site, where com_ajax is not * available when we are not logged in. * * @since 4.0.0 */ trait AjaxHandler { /** * Processes the callbacks from the passwordless login views. * * Note: this method is called from Joomla's com_ajax or, in the case of backend logins, * through the special onAfterInitialize handler we have created to work around com_ajax usage * limitations in the backend. * * @param Event $event The event we are handling * * @return void * * @throws \Exception * @since 4.0.0 */ public function onAjaxWebauthn(Ajax $event): void { $input = $this->getApplication()->getInput(); // Get the return URL from the session $returnURL = $this->getApplication()->getSession()->get('plg_system_webauthn.returnUrl', Uri::base()); $result = null; try { Log::add("Received AJAX callback.", Log::DEBUG, 'webauthn.system'); if (!($this->getApplication() instanceof CMSApplication)) { Log::add("This is not a CMS application", Log::NOTICE, 'webauthn.system'); return; } $akaction = $input->getCmd('akaction'); if (!$this->getApplication()->checkToken('request')) { throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR')); } // Empty action? No bueno. if (empty($akaction)) { throw new \RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_AJAX_INVALIDACTION')); } // Call the plugin event onAjaxWebauthnSomething where Something is the akaction param. /** @var AbstractEvent|ResultAwareInterface $triggerEvent */ $eventName = 'onAjaxWebauthn' . ucfirst($akaction); switch ($eventName) { case 'onAjaxWebauthn': $eventClass = PlgSystemWebauthnAjax::class; break; case 'onAjaxWebauthnChallenge': $eventClass = PlgSystemWebauthnAjaxChallenge::class; break; case 'onAjaxWebauthnCreate': $eventClass = PlgSystemWebauthnAjaxCreate::class; break; case 'onAjaxWebauthnDelete': $eventClass = PlgSystemWebauthnAjaxDelete::class; break; case 'onAjaxWebauthnInitcreate': $eventClass = PlgSystemWebauthnAjaxInitCreate::class; break; case 'onAjaxWebauthnLogin': $eventClass = PlgSystemWebauthnAjaxLogin::class; break; case 'onAjaxWebauthnSavelabel': $eventClass = PlgSystemWebauthnAjaxSaveLabel::class; break; default: $eventClass = GenericEvent::class; break; } $triggerEvent = new $eventClass($eventName, []); $result = $this->getApplication()->getDispatcher()->dispatch($eventName, $triggerEvent); $results = ($result instanceof ResultAwareInterface) ? ($result['result'] ?? []) : []; $result = array_reduce( $results, function ($carry, $result) { return $carry ?? $result; }, null ); } catch (\Exception $e) { Log::add("Callback failure, redirecting to $returnURL.", Log::DEBUG, 'webauthn.system'); $this->getApplication()->getSession()->set('plg_system_webauthn.returnUrl', null); $this->getApplication()->enqueueMessage($e->getMessage(), 'error'); $this->getApplication()->redirect($returnURL); return; } if (!\is_null($result)) { switch ($input->getCmd('encoding', 'json')) { case 'raw': Log::add("Callback complete, returning raw response.", Log::DEBUG, 'webauthn.system'); echo $result; break; case 'redirect': $modifiers = ''; if (isset($result['message'])) { $type = $result['type'] ?? 'info'; $this->getApplication()->enqueueMessage($result['message'], $type); $modifiers = " and setting a system message of type $type"; } if (isset($result['url'])) { Log::add("Callback complete, performing redirection to {$result['url']}{$modifiers}.", Log::DEBUG, 'webauthn.system'); $this->getApplication()->redirect($result['url']); } Log::add("Callback complete, performing redirection to {$result}{$modifiers}.", Log::DEBUG, 'webauthn.system'); $this->getApplication()->redirect($result); return; default: Log::add("Callback complete, returning JSON.", Log::DEBUG, 'webauthn.system'); echo json_encode($result); break; } $this->getApplication()->close(200); } Log::add("Null response from AJAX callback, redirecting to $returnURL", Log::DEBUG, 'webauthn.system'); $this->getApplication()->getSession()->set('plg_system_webauthn.returnUrl', null); $this->getApplication()->redirect($returnURL); } }
| ver. 1.4 |
Github
|
.
| PHP 8.3.23 | Generation time: 0 |
proxy
|
phpinfo
|
Settings