<?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;
// Protect from unauthorized access
\defined('_JEXEC') or die();
use Exception;
use Joomla\CMS\Application\CMSApplication;
use Joomla\CMS\Factory;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\User\UserHelper;
use Joomla\Plugin\System\Webauthn\CredentialRepository;
use Joomla\Plugin\System\Webauthn\Helper\Joomla;
use Throwable;
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialSource;
use Webauthn\PublicKeyCredentialUserEntity;
/**
* 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.
*
* @return string A JSON-encoded object or JSON-encoded false if the username is invalid or no credentials stored
*
* @throws Exception
*
* @since 4.0.0
*/
public function onAjaxWebauthnChallenge()
{
// Load the language files
$this->loadLanguage();
// Initialize objects
/** @var CMSApplication $app */
$app = Factory::getApplication();
$input = $app->input;
$repository = new CredentialRepository;
// Retrieve data from the request
$username = $input->getUsername('username', '');
$returnUrl = base64_encode(
Joomla::getSessionVar('returnUrl', Uri::current(), 'plg_system_webauthn')
);
$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();
}
Joomla::setSessionVar('returnUrl', $returnUrl, 'plg_system_webauthn');
// Do I have a username?
if (empty($username))
{
return json_encode(false);
}
// Is the username valid?
try
{
$userId = UserHelper::getUserId($username);
}
catch (Exception $e)
{
$userId = 0;
}
if ($userId <= 0)
{
return json_encode(false);
}
// Load the saved credentials into an array of PublicKeyCredentialDescriptor objects
try
{
$userEntity = new PublicKeyCredentialUserEntity(
'', $repository->getHandleFromUserId($userId), ''
);
$credentials = $repository->findAllForUserEntity($userEntity);
}
catch (Exception $e)
{
return json_encode(false);
}
// No stored credentials?
if (empty($credentials))
{
return json_encode(false);
}
$registeredPublicKeyCredentialDescriptors = [];
/** @var PublicKeyCredentialSource $record */
foreach ($credentials as $record)
{
try
{
$registeredPublicKeyCredentialDescriptors[] = $record->getPublicKeyCredentialDescriptor();
}
catch (Throwable $e)
{
continue;
}
}
// Extensions
$extensions = new AuthenticationExtensionsClientInputs;
// Public Key Credential Request Options
$publicKeyCredentialRequestOptions = new PublicKeyCredentialRequestOptions(
random_bytes(32),
60000,
Uri::getInstance()->toString(['host']),
$registeredPublicKeyCredentialDescriptors,
PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED,
$extensions
);
// Save in session. This is used during the verification stage to prevent replay attacks.
Joomla::setSessionVar(
'publicKeyCredentialRequestOptions',
base64_encode(serialize($publicKeyCredentialRequestOptions)),
'plg_system_webauthn'
);
Joomla::setSessionVar(
'userHandle',
$repository->getHandleFromUserId($userId),
'plg_system_webauthn'
);
Joomla::setSessionVar('userId', $userId, 'plg_system_webauthn');
// Return the JSON encoded data to the caller
return json_encode(
$publicKeyCredentialRequestOptions,
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
);
}
}