2022-10-26 00:02:55 +02:00
|
|
|
<?php
|
|
|
|
|
2022-11-04 22:02:44 +01:00
|
|
|
/**
|
2022-11-04 22:45:47 +01:00
|
|
|
* For the full copyright and license information, please view the LICENSE
|
|
|
|
* file that was distributed with this source code.
|
2022-11-04 22:02:44 +01:00
|
|
|
*
|
2022-11-04 22:45:47 +01:00
|
|
|
* https://www.d3data.de
|
2022-11-04 22:02:44 +01:00
|
|
|
*
|
|
|
|
* @copyright (C) D3 Data Development (Inh. Thomas Dartsch)
|
2022-11-04 22:45:47 +01:00
|
|
|
* @author D3 Data Development - Daniel Seifert <info@shopmodule.com>
|
|
|
|
* @link https://www.oxidmodule.com
|
2022-11-04 22:02:44 +01:00
|
|
|
*/
|
|
|
|
|
2022-10-26 00:02:55 +02:00
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
namespace D3\Webauthn\Application\Model;
|
|
|
|
|
2022-10-31 00:11:06 +01:00
|
|
|
use Assert\AssertionFailedException;
|
2022-12-08 00:28:53 +01:00
|
|
|
use D3\TestingTools\Production\IsMockable;
|
2022-10-26 00:02:55 +02:00
|
|
|
use D3\Webauthn\Application\Model\Credential\PublicKeyCredential;
|
|
|
|
use D3\Webauthn\Application\Model\Credential\PublicKeyCredentialList;
|
2022-11-03 13:18:02 +01:00
|
|
|
use D3\Webauthn\Application\Model\Exceptions\WebauthnException;
|
|
|
|
use D3\Webauthn\Application\Model\Exceptions\WebauthnGetException;
|
2022-10-26 22:27:25 +02:00
|
|
|
use D3\Webauthn\Modules\Application\Model\d3_User_Webauthn;
|
2022-10-31 00:11:06 +01:00
|
|
|
use Doctrine\DBAL\Driver\Exception as DoctrineDriverException;
|
|
|
|
use Doctrine\DBAL\Exception as DoctrineException;
|
|
|
|
use Exception;
|
2022-10-26 00:02:55 +02:00
|
|
|
use Nyholm\Psr7\Factory\Psr17Factory;
|
|
|
|
use Nyholm\Psr7Server\ServerRequestCreator;
|
|
|
|
use OxidEsales\Eshop\Application\Model\User;
|
2022-12-08 00:28:53 +01:00
|
|
|
use OxidEsales\Eshop\Core\Config;
|
|
|
|
use OxidEsales\Eshop\Core\Session;
|
|
|
|
use OxidEsales\Eshop\Core\UtilsView;
|
2022-10-31 00:11:06 +01:00
|
|
|
use Psr\Container\ContainerExceptionInterface;
|
|
|
|
use Psr\Container\NotFoundExceptionInterface;
|
2023-01-21 13:50:18 +01:00
|
|
|
use Psr\Log\LoggerInterface;
|
2022-12-08 00:28:53 +01:00
|
|
|
use Throwable;
|
2022-10-26 00:02:55 +02:00
|
|
|
use Webauthn\PublicKeyCredentialCreationOptions;
|
2022-12-08 00:28:53 +01:00
|
|
|
use Webauthn\PublicKeyCredentialDescriptor;
|
2022-10-26 00:02:55 +02:00
|
|
|
use Webauthn\PublicKeyCredentialRequestOptions;
|
|
|
|
use Webauthn\PublicKeyCredentialSource;
|
|
|
|
use Webauthn\Server;
|
|
|
|
|
|
|
|
class Webauthn
|
|
|
|
{
|
2022-12-08 00:28:53 +01:00
|
|
|
use IsMockable;
|
|
|
|
|
2022-10-26 00:02:55 +02:00
|
|
|
public const SESSION_CREATIONS_OPTIONS = 'd3WebAuthnCreationOptions';
|
|
|
|
public const SESSION_ASSERTION_OPTIONS = 'd3WebAuthnAssertionOptions';
|
|
|
|
|
2022-11-04 22:02:44 +01:00
|
|
|
/**
|
|
|
|
* @return bool
|
|
|
|
*/
|
2022-10-31 00:11:06 +01:00
|
|
|
public function isAvailable(): bool
|
2022-10-26 10:15:49 +02:00
|
|
|
{
|
2022-10-28 15:02:28 +02:00
|
|
|
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' || // is HTTPS
|
|
|
|
!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https' ||
|
|
|
|
!empty($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] == 'on' ||
|
2022-11-04 22:02:44 +01:00
|
|
|
in_array($_SERVER['REMOTE_ADDR'], ['127.0.0.1', '::1']) || // is localhost
|
2022-12-13 22:24:33 +01:00
|
|
|
(isset($_SERVER['REMOTE_ADDR']) && preg_match('/.*\.localhost$/mi', $_SERVER['REMOTE_ADDR'])) // localhost is TLD
|
2022-10-26 10:15:49 +02:00
|
|
|
) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-11-03 13:18:02 +01:00
|
|
|
$e = oxNew(WebauthnException::class, 'D3_WEBAUTHN_ERR_UNSECURECONNECTION');
|
2023-01-21 13:50:18 +01:00
|
|
|
d3GetOxidDIC()->get('d3ox.webauthn.'.LoggerInterface::class)->info($e->getDetailedErrorMessage());
|
|
|
|
d3GetOxidDIC()->get('d3ox.webauthn.'.UtilsView::class)->addErrorToDisplay($e);
|
2022-10-27 14:52:20 +02:00
|
|
|
|
2022-10-26 10:15:49 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2022-10-26 00:02:55 +02:00
|
|
|
/**
|
2022-10-31 00:11:06 +01:00
|
|
|
* @param User $user
|
2022-11-04 22:02:44 +01:00
|
|
|
* @return string
|
2022-10-31 00:11:06 +01:00
|
|
|
* @throws ContainerExceptionInterface
|
|
|
|
* @throws DoctrineDriverException
|
|
|
|
* @throws DoctrineException
|
|
|
|
* @throws NotFoundExceptionInterface
|
2022-10-26 00:02:55 +02:00
|
|
|
*/
|
2022-11-04 22:02:44 +01:00
|
|
|
public function getCreationOptions(User $user): string
|
2022-10-26 00:02:55 +02:00
|
|
|
{
|
2023-01-21 13:50:18 +01:00
|
|
|
d3GetOxidDIC()->set(UserEntity::class.'.args.user', $user);
|
2023-01-28 23:41:39 +01:00
|
|
|
/** @var UserEntity $userEntity */
|
2023-01-21 13:50:18 +01:00
|
|
|
$userEntity = d3GetOxidDIC()->get(UserEntity::class);
|
2022-10-26 00:02:55 +02:00
|
|
|
|
2022-12-08 00:28:53 +01:00
|
|
|
$publicKeyCredentialCreationOptions = $this->getServer()->generatePublicKeyCredentialCreationOptions(
|
2022-10-26 00:02:55 +02:00
|
|
|
$userEntity,
|
|
|
|
PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
|
2022-12-08 00:28:53 +01:00
|
|
|
$this->getExistingCredentials($userEntity)
|
2022-10-26 00:02:55 +02:00
|
|
|
);
|
|
|
|
|
2023-01-21 13:50:18 +01:00
|
|
|
d3GetOxidDIC()->get('d3ox.webauthn.'.Session::class)
|
2022-12-08 00:28:53 +01:00
|
|
|
->setVariable(self::SESSION_CREATIONS_OPTIONS, $publicKeyCredentialCreationOptions);
|
2022-10-26 00:02:55 +02:00
|
|
|
|
2022-12-08 00:28:53 +01:00
|
|
|
$json = $this->jsonEncode($publicKeyCredentialCreationOptions);
|
2022-11-04 22:02:44 +01:00
|
|
|
|
|
|
|
if ($json === false) {
|
|
|
|
throw oxNew(Exception::class, "can't encode creation options");
|
|
|
|
}
|
|
|
|
|
|
|
|
return $json;
|
2022-10-26 00:02:55 +02:00
|
|
|
}
|
|
|
|
|
2022-10-31 00:11:06 +01:00
|
|
|
/**
|
2022-12-08 00:28:53 +01:00
|
|
|
* @param UserEntity $userEntity
|
|
|
|
* @return PublicKeyCredentialDescriptor[]
|
2022-10-31 00:11:06 +01:00
|
|
|
* @throws DoctrineDriverException
|
|
|
|
* @throws DoctrineException
|
|
|
|
*/
|
2022-12-08 00:28:53 +01:00
|
|
|
public function getExistingCredentials(UserEntity $userEntity): array
|
2022-10-26 00:02:55 +02:00
|
|
|
{
|
|
|
|
// Get the list of authenticators associated to the user
|
2022-12-12 23:41:07 +01:00
|
|
|
/** @var PublicKeyCredentialList $credentialList */
|
2023-01-21 13:50:18 +01:00
|
|
|
$credentialList = d3GetOxidDIC()->get(PublicKeyCredentialList::class);
|
2022-10-28 00:45:32 +02:00
|
|
|
$credentialSources = $credentialList->findAllForUserEntity($userEntity);
|
2022-10-26 00:02:55 +02:00
|
|
|
|
|
|
|
// Convert the Credential Sources into Public Key Credential Descriptors
|
2022-12-08 00:28:53 +01:00
|
|
|
return array_map(function (PublicKeyCredentialSource $credential) {
|
2022-10-26 00:02:55 +02:00
|
|
|
return $credential->getPublicKeyCredentialDescriptor();
|
|
|
|
}, $credentialSources);
|
2022-12-08 00:28:53 +01:00
|
|
|
}
|
2022-10-26 00:02:55 +02:00
|
|
|
|
2022-12-08 00:28:53 +01:00
|
|
|
/**
|
|
|
|
* @param PublicKeyCredentialCreationOptions|PublicKeyCredentialRequestOptions $creationOptions
|
|
|
|
* @return false|string
|
|
|
|
*/
|
|
|
|
protected function jsonEncode($creationOptions)
|
|
|
|
{
|
2022-12-13 22:24:33 +01:00
|
|
|
return json_encode($creationOptions, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
2022-12-08 00:28:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param string $userId
|
|
|
|
* @return string
|
|
|
|
* @throws DoctrineDriverException
|
|
|
|
* @throws DoctrineException
|
|
|
|
*/
|
|
|
|
public function getRequestOptions(string $userId): string
|
|
|
|
{
|
|
|
|
/** @var d3_User_Webauthn $user */
|
2023-01-21 13:50:18 +01:00
|
|
|
$user = d3GetOxidDIC()->get('d3ox.webauthn.'.User::class);
|
2022-12-08 00:28:53 +01:00
|
|
|
$user->load($userId);
|
2023-01-21 13:50:18 +01:00
|
|
|
d3GetOxidDIC()->set(UserEntity::class.'.args.user', $user);
|
2023-01-28 23:41:39 +01:00
|
|
|
/** @var UserEntity $userEntity */
|
2023-01-21 13:50:18 +01:00
|
|
|
$userEntity = d3GetOxidDIC()->get(UserEntity::class);
|
2022-10-26 00:02:55 +02:00
|
|
|
|
|
|
|
// We generate the set of options.
|
2022-12-08 00:28:53 +01:00
|
|
|
$publicKeyCredentialRequestOptions = $this->getServer()->generatePublicKeyCredentialRequestOptions(
|
2022-10-26 00:02:55 +02:00
|
|
|
PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED, // Default value
|
2022-12-08 00:28:53 +01:00
|
|
|
$this->getExistingCredentials($userEntity)
|
2022-10-26 00:02:55 +02:00
|
|
|
);
|
|
|
|
|
2023-01-21 13:50:18 +01:00
|
|
|
d3GetOxidDIC()->get('d3ox.webauthn.'.Session::class)
|
2022-12-08 00:28:53 +01:00
|
|
|
->setVariable(self::SESSION_ASSERTION_OPTIONS, $publicKeyCredentialRequestOptions);
|
2022-10-26 00:02:55 +02:00
|
|
|
|
2022-12-08 00:28:53 +01:00
|
|
|
$json = $this->jsonEncode($publicKeyCredentialRequestOptions);
|
2022-11-04 22:02:44 +01:00
|
|
|
|
|
|
|
if ($json === false) {
|
|
|
|
throw oxNew(Exception::class, "can't encode request options");
|
|
|
|
}
|
|
|
|
|
|
|
|
return $json;
|
2022-10-26 00:02:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return Server
|
|
|
|
*/
|
2022-12-08 00:28:53 +01:00
|
|
|
protected function getServer(): Server
|
2022-10-26 00:02:55 +02:00
|
|
|
{
|
2023-01-21 13:50:18 +01:00
|
|
|
$server = $this->getServerObject();
|
2023-01-28 23:41:39 +01:00
|
|
|
/** @var LoggerInterface $logger */
|
|
|
|
$logger = d3GetOxidDIC()->get('d3ox.webauthn.'.LoggerInterface::class);
|
|
|
|
$server->setLogger($logger);
|
2022-11-01 23:42:25 +01:00
|
|
|
return $server;
|
2022-10-26 00:02:55 +02:00
|
|
|
}
|
|
|
|
|
2022-11-02 16:38:43 +01:00
|
|
|
/**
|
2022-12-08 00:28:53 +01:00
|
|
|
* @param string $credential
|
2022-11-02 16:38:43 +01:00
|
|
|
* @param string|null $keyName
|
|
|
|
*
|
2022-12-08 00:28:53 +01:00
|
|
|
* @throws AssertionFailedException
|
2022-11-02 16:38:43 +01:00
|
|
|
* @throws DoctrineDriverException
|
|
|
|
* @throws DoctrineException
|
2022-12-08 00:28:53 +01:00
|
|
|
* @throws Throwable
|
2022-11-02 16:38:43 +01:00
|
|
|
*/
|
2022-11-04 22:02:44 +01:00
|
|
|
public function saveAuthn(string $credential, string $keyName = null): void
|
2022-10-26 00:02:55 +02:00
|
|
|
{
|
2022-11-02 16:38:43 +01:00
|
|
|
$psr17Factory = new Psr17Factory();
|
|
|
|
$creator = new ServerRequestCreator(
|
|
|
|
$psr17Factory,
|
|
|
|
$psr17Factory,
|
|
|
|
$psr17Factory,
|
|
|
|
$psr17Factory
|
|
|
|
);
|
|
|
|
$serverRequest = $creator->fromGlobals();
|
|
|
|
|
|
|
|
$publicKeyCredentialSource = $this->getServer()->loadAndCheckAttestationResponse(
|
|
|
|
html_entity_decode($credential),
|
2023-01-21 13:50:18 +01:00
|
|
|
d3GetOxidDIC()->get('d3ox.webauthn.'.Session::class)->getVariable(self::SESSION_CREATIONS_OPTIONS),
|
2022-11-02 16:38:43 +01:00
|
|
|
$serverRequest
|
|
|
|
);
|
|
|
|
|
2023-01-25 21:35:14 +01:00
|
|
|
/** @var PublicKeyCredential $pkCredential */
|
2023-01-21 13:50:18 +01:00
|
|
|
$pkCredential = d3GetOxidDIC()->get(PublicKeyCredential::class);
|
2022-11-02 16:38:43 +01:00
|
|
|
$pkCredential->saveCredentialSource($publicKeyCredentialSource, $keyName);
|
2022-10-26 00:02:55 +02:00
|
|
|
}
|
|
|
|
|
2022-10-31 00:11:06 +01:00
|
|
|
/**
|
|
|
|
* @param string $response
|
2022-11-02 16:38:43 +01:00
|
|
|
*
|
2022-10-31 00:11:06 +01:00
|
|
|
* @return bool
|
2022-11-03 13:18:02 +01:00
|
|
|
* @throws WebauthnException
|
2022-10-31 00:11:06 +01:00
|
|
|
*/
|
|
|
|
public function assertAuthn(string $response): bool
|
2022-10-26 00:02:55 +02:00
|
|
|
{
|
2022-10-28 00:45:32 +02:00
|
|
|
$psr17Factory = new Psr17Factory();
|
|
|
|
$creator = new ServerRequestCreator(
|
|
|
|
$psr17Factory,
|
|
|
|
$psr17Factory,
|
|
|
|
$psr17Factory,
|
|
|
|
$psr17Factory
|
|
|
|
);
|
|
|
|
$serverRequest = $creator->fromGlobals();
|
2022-10-26 00:02:55 +02:00
|
|
|
|
2022-12-08 00:28:53 +01:00
|
|
|
$userEntity = $this->getUserEntityFrom($this->getSavedUserIdFromSession());
|
2022-10-26 00:02:55 +02:00
|
|
|
|
2022-11-03 13:18:02 +01:00
|
|
|
try {
|
2022-11-04 22:02:44 +01:00
|
|
|
$this->getServer()->loadAndCheckAssertionResponse(
|
2022-12-13 22:24:33 +01:00
|
|
|
html_entity_decode($response),
|
2023-01-21 13:50:18 +01:00
|
|
|
d3GetOxidDIC()->get('d3ox.webauthn.'.Session::class)
|
2022-12-13 22:24:33 +01:00
|
|
|
->getVariable(self::SESSION_ASSERTION_OPTIONS),
|
2022-11-04 22:02:44 +01:00
|
|
|
$userEntity,
|
|
|
|
$serverRequest
|
|
|
|
);
|
2022-11-03 13:18:02 +01:00
|
|
|
} catch (AssertionFailedException $e) {
|
|
|
|
/** @var WebauthnGetException $exc */
|
|
|
|
$exc = oxNew(WebauthnGetException::class, $e->getMessage(), 0, $e);
|
|
|
|
throw $exc;
|
|
|
|
}
|
2022-10-30 00:27:11 +02:00
|
|
|
|
|
|
|
return true;
|
2022-10-26 00:02:55 +02:00
|
|
|
}
|
2022-10-26 22:27:25 +02:00
|
|
|
|
2022-12-08 00:28:53 +01:00
|
|
|
/**
|
|
|
|
* @param $userId
|
|
|
|
* @return UserEntity
|
|
|
|
*/
|
|
|
|
protected function getUserEntityFrom($userId): UserEntity
|
|
|
|
{
|
|
|
|
/** @var User $user */
|
2023-01-21 13:50:18 +01:00
|
|
|
$user = d3GetOxidDIC()->get('d3ox.webauthn.'.User::class);
|
2022-12-08 00:28:53 +01:00
|
|
|
$user->load($userId);
|
2022-12-12 23:41:07 +01:00
|
|
|
|
2023-01-21 13:50:18 +01:00
|
|
|
d3GetOxidDIC()->set(UserEntity::class.'.args.user', $user);
|
2023-01-28 23:41:39 +01:00
|
|
|
/** @var UserEntity $userEntity */
|
|
|
|
$userEntity = d3GetOxidDIC()->get(UserEntity::class);
|
|
|
|
|
|
|
|
return $userEntity;
|
2022-12-08 00:28:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return string|null
|
|
|
|
*/
|
|
|
|
protected function getSavedUserIdFromSession(): ?string
|
|
|
|
{
|
2023-01-21 13:50:18 +01:00
|
|
|
$session = d3GetOxidDIC()->get('d3ox.webauthn.'.Session::class);
|
2022-12-08 00:28:53 +01:00
|
|
|
|
|
|
|
return $this->isAdmin() ?
|
|
|
|
$session->getVariable(WebauthnConf::WEBAUTHN_ADMIN_SESSION_CURRENTUSER) :
|
|
|
|
$session->getVariable(WebauthnConf::WEBAUTHN_SESSION_CURRENTUSER);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
public function isAdmin(): bool
|
|
|
|
{
|
|
|
|
return isAdmin();
|
|
|
|
}
|
|
|
|
|
2022-10-26 22:27:25 +02:00
|
|
|
/**
|
2022-10-28 00:45:32 +02:00
|
|
|
* @param $userId
|
2022-10-26 22:27:25 +02:00
|
|
|
* @return bool
|
2022-10-31 00:11:06 +01:00
|
|
|
* @throws ContainerExceptionInterface
|
|
|
|
* @throws DoctrineDriverException
|
|
|
|
* @throws DoctrineException
|
|
|
|
* @throws NotFoundExceptionInterface
|
2022-10-26 22:27:25 +02:00
|
|
|
*/
|
|
|
|
public function isActive($userId): bool
|
|
|
|
{
|
2023-01-21 13:50:18 +01:00
|
|
|
return !d3GetOxidDIC()->get('d3ox.webauthn.'.Config::class)
|
2022-12-08 00:28:53 +01:00
|
|
|
->getConfigParam(WebauthnConf::GLOBAL_SWITCH)
|
2023-01-21 13:50:18 +01:00
|
|
|
&& !d3GetOxidDIC()->get('d3ox.webauthn.'.Session::class)
|
2022-12-08 00:28:53 +01:00
|
|
|
->getVariable(WebauthnConf::GLOBAL_SWITCH)
|
2022-10-26 22:27:25 +02:00
|
|
|
&& $this->UserUseWebauthn($userId);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param $userId
|
|
|
|
* @return bool
|
2022-10-31 00:11:06 +01:00
|
|
|
* @throws ContainerExceptionInterface
|
|
|
|
* @throws DoctrineDriverException
|
|
|
|
* @throws DoctrineException
|
|
|
|
* @throws NotFoundExceptionInterface
|
2022-10-26 22:27:25 +02:00
|
|
|
*/
|
|
|
|
public function UserUseWebauthn($userId): bool
|
|
|
|
{
|
2022-12-08 00:28:53 +01:00
|
|
|
$entity = $this->getUserEntityFrom($userId);
|
2022-10-31 00:11:06 +01:00
|
|
|
|
2022-11-04 22:02:44 +01:00
|
|
|
/** @var PublicKeyCredentialList $credentialList */
|
2023-01-21 13:50:18 +01:00
|
|
|
$credentialList = d3GetOxidDIC()->get(PublicKeyCredentialList::class);
|
2022-10-31 00:11:06 +01:00
|
|
|
$list = $credentialList->findAllForUserEntity($entity);
|
2022-10-26 22:27:25 +02:00
|
|
|
|
|
|
|
return is_array($list) && count($list);
|
|
|
|
}
|
2023-01-21 13:50:18 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @return Server
|
|
|
|
*/
|
|
|
|
protected function getServerObject()
|
|
|
|
{
|
|
|
|
/** @var RelyingPartyEntity $rpEntity */
|
|
|
|
$rpEntity = d3GetOxidDIC()->get(RelyingPartyEntity::class);
|
|
|
|
|
|
|
|
return oxNew(
|
|
|
|
Server::class,
|
|
|
|
$rpEntity,
|
|
|
|
d3GetOxidDIC()->get(PublicKeyCredentialList::class)
|
|
|
|
);
|
|
|
|
}
|
2022-12-13 22:24:33 +01:00
|
|
|
}
|