name : PWAuthOnWebAuthn.php
<?php
/**
 * @package   admintools
 * @copyright Copyright (c)2010-2025 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 3, or later
 */

namespace Akeeba\Plugin\System\AdminTools\Feature;

defined('_JEXEC') or die;

use Exception;
use Joomla\CMS\Application\CMSApplication;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Form\Form;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\User\User;
use Joomla\CMS\User\UserFactoryInterface;
use Joomla\CMS\User\UserHelper;
use Joomla\Database\ParameterType;
use Joomla\Plugin\System\Webauthn\CredentialRepository;
use Joomla\Registry\Registry;
use Joomla\Utilities\ArrayHelper;
use RuntimeException;
use Throwable;

class PWAuthOnWebAuthn extends Base
{
	/**
	 * Is this feature enabled?
	 *
	 * @return bool
	 */
	public function isEnabled()
	{
		if (!($this->app instanceof CMSApplication))
		{
			return false;
		}

		// This feature only applies to HTTPS sites (WebAuthn is not available on plain old HTTP).
		if (!Uri::getInstance()->isSsl())
		{
			return false;
		}

		return $this->wafParams->getValue('nopwonwebauthn', 0) != 0;
	}

	public function onUserLogin($response, $options = []): bool
	{
		$user = $this->getUserObject($response, $options);

		// If this is not a password login do nothing
		if (strtolower(($response['type'] ?? 'joomla')) != 'joomla')
		{
			return true;
		}

		// If this is not a blocked login do nothing
		if (!$this->isBlockedLogin($user))
		{
			return true;
		}

		// If the user does not have WebAuthn enabled do nothing
		if (!$this->hasWebAuthn($user))
		{
			return true;
		}

		// Logout the user and close the session before throwing the error (otherwise the user won't be logged out).
		$this->app->logout($user->id, []);
		$this->app->getSession()->close();

		// Throw error
		throw new RuntimeException(Text::_('JGLOBAL_AUTH_ACCESS_DENIED'), 403);
	}

	public function onContentPrepareForm(Form $form, $data): void
	{
		// This feature only applies to HTTPS sites.
		if (!Uri::getInstance()->isSsl())
		{
			return;
		}

		// This feature only applies if the WebAuthn plugin is enabled
		if (!PluginHelper::isEnabled('system', 'webauthn'))
		{
			return;
		}

		$name         = $form->getName();
		$allowedForms = [
			'com_users.user', 'com_users.profile', 'com_users.registration',
		];

		if (!\in_array($name, $allowedForms))
		{
			return;
		}

		// If we have no $data, it means that Joomla is simply loading the user profile without loading the data inside it
		// This means that we can't perform some checks on the user, simply attach our form
		if (!$data)
		{
			$method = strtoupper($this->input->getMethod());
			$option = $this->input->get->getCmd('option');
			$id     = $this->input->get->getInt('id');

			if ($method !== 'POST' || $option !== 'com_users' || empty ($id))
			{
				return;
			}

			$data['id'] = $id;
		}

		// Get the user object
		$user   = $this->getUserFromFormData($data);
		$mySelf = $this->app->getIdentity();

		// 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 (($mySelf->id != $user->id) && !$mySelf->authorise('core.edit', 'com_users'))
		{
			return;
		}

		if (!$this->needsUserProfileFields($user->id))
		{
			return;
		}

		// Add the fields to the form.
		Form::addFormPath(JPATH_PLUGINS . '/system/admintools/forms');
		$form->loadFile('pwloginonwebauthn', false);
	}

	public function onContentPrepareData($context, $data): void
	{
		if (
			!in_array($context, ['com_users.profile', 'com_users.user', 'com_users.registration'])
			|| !is_object($data) || !isset($data->id) || (($data->id ?: 0) <= 0)
		)
		{
			return;
		}

		try
		{
			$db     = $this->db;
			$key    = 'com_admintools.nopwonwebauthn';
			$userId = $data->id ?: 0;
			$query  = (method_exists($db, 'createQuery') ? $db->createQuery() : $db->getQuery(true))
				->select($db->quoteName('profile_value'))
				->from($db->quoteName('#__user_profiles'))
				->where($db->quoteName('user_id') . ' = ' . $db->quote($userId))
				->where($db->quoteName('profile_key') . ' = ' . $db->quote($key));
			$result = $db->setQuery($query)->loadResult() ?? 1;

			// Merge the profile data.
			$data->pwloginonwebauthn = [
				'nopwonwebauthn' => $result,
			];
		}
		catch (Throwable $e)
		{
			// Ignore exception
		}
	}

	public function onUserAfterSave($data, $isNew, $result, $error): void
	{
		$userId = ArrayHelper::getValue($data, 'id', 0, 'int');

		if ($userId <= 0 || count($data['pwloginonwebauthn'] ?: []) == 0)
		{
			return;
		}

		try
		{
			$db    = $this->db;
			$key   = 'com_admintools.nopwonwebauthn';
			$query = (method_exists($db, 'createQuery') ? $db->createQuery() : $db->getQuery(true))
				->delete($db->quoteName('#__user_profiles'))
				->where($db->quoteName('user_id') . ' = :user_id')
				->where($db->quoteName('profile_key') . ' = :key')
				->bind(':user_id', $userId)
				->bind(':key', $key);
			$db->setQuery($query)->execute();

			$o = (object) [
				'user_id'       => $userId,
				'profile_key'   => 'com_admintools.nopwonwebauthn',
				'profile_value' => $data['pwloginonwebauthn']['nopwonwebauthn'],
				'order'         => 1,
			];
			$db->insertObject('#__user_profiles', $o);
		}
		catch (Exception $e)
		{
			// Ignore
		}
	}

	public function onUserAfterDelete($user, $success, $msg): void
	{
		if (!$success)
		{
			return;
		}

		$userId = ArrayHelper::getValue($user, 'id', 0, 'int');

		if ($userId)
		{
			return;
		}

		try
		{
			$db    = $this->db;
			$key   = 'com_admintools.nopwonwebauthn';
			$query = (method_exists($db, 'createQuery') ? $db->createQuery() : $db->getQuery(true))
				->delete($db->quoteName('#__user_profiles'))
				->where($db->quoteName('user_id') . ' = :user_id')
				->where($db->quoteName('profile_key') . ' = :key')
				->bind(':user_id', $userId)
				->bind(':key', $key);
			$db->setQuery($query)->execute();
		}
		catch (Throwable $e)
		{
			// Ignore
		}
	}


	private function getUserObject($user, $options = [])
	{
		$instance = new User();

		if ($id = intval(UserHelper::getUserId($user['username'])))
		{
			$instance->load($id);

			return $instance;
		}

		$config           = ComponentHelper::getParams('com_users');
		$defaultUserGroup = $config->get('new_usertype', 2);

		$instance->id       = 0;
		$instance->name     = $user['fullname'];
		$instance->username = $user['username'];
		$instance->email    = $user['email']; // Result should contain an email (check)
		$instance->usertype = 'deprecated';
		$instance->groups   = [$defaultUserGroup];

		return $instance;
	}

	private function isBlockedLogin(User $user): bool
	{
		$isBackend = $user->authorise('core.login.admin', 0);
		$setting   = $this->wafParams->getValue('nopwonwebauthn', 0);

		// Allowed for all?
		if ($setting == 0)
		{
			return false;
		}

		// Disabled for all backend or frontend users?
		if (($isBackend && in_array($setting, [8, 9, 12])) || (!$isBackend && ($setting == 12)))
		{
			return true;
		}

		// In any other case we need to check the user's preference.
		return $this->getUserParam($user->id, 'com_admintools.nopwonwebauthn') == 0;
	}

	private function needsUserProfileFields(int $id): bool
	{
		$user      = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($id);
		$isBackend = $user->authorise('core.login.admin', 0);
		$setting   = $this->wafParams->getValue('nopwonwebauthn', 0);

		// Allowed for all?
		if ($setting == 0)
		{
			return false;
		}

		// Disabled for all backend or frontend users?
		if (($isBackend && in_array($setting, [8, 9, 12])) || (!$isBackend && ($setting == 12)))
		{
			return false;
		}

		// In any other case we need to check the user's preference.
		return true;
	}

	private function getUserParam(?int $id, string $key): int
	{
		$db    = $this->db;
		$query = (method_exists($db, 'createQuery') ? $db->createQuery() : $db->getQuery(true))
			->select($db->qn('profile_value'))
			->from($db->qn('#__user_profiles'))
			->where($db->qn('profile_key') . ' = :profileKey')
			->where($db->qn('user_id') . ' = :userId');

		$query->bind(':profileKey', $key, ParameterType::STRING);
		$query->bind(':userId', $id, ParameterType::INTEGER);

		return (int) ($db->setQuery($query)->loadResult() ?? 0);
	}

	private function hasWebAuthn(User $user): bool
	{
		// If the WebAuthn plugin is not enabled the user can't log into the site with WebAuthn (DUH!)
		if (!PluginHelper::isEnabled('system', 'webauthn'))
		{
			return false;
		}

		if (!class_exists(CredentialRepository::class))
		{
			return false;
		}

		try
		{
			$webAuthnCredentials = (new CredentialRepository($this->db))->getAll($user->id) ?: [];
		}
		catch (Throwable $e)
		{
			return false;
		}

		return count($webAuthnCredentials) > 0;
	}

	private function getUserFromFormData($data): ?User
	{
		$id = null;

		if (\is_array($data))
		{
			$id = isset($data['id']) ? $data['id'] : null;
		}
		elseif (\is_object($data) && ($data instanceof Registry))
		{
			$id = $data->get('id');
		}
		elseif (\is_object($data))
		{
			$id = isset($data->id) ? $data->id : null;
		}

		$user = empty($id) ? $this->app->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;
	}

}

© 2025 Cubjrnet7