webauthn/src/Application/Model/Webauthn.php

311 lines
10 KiB
PHP
Raw Normal View History

<?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
*/
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;
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;
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;
2022-12-08 00:28:53 +01:00
use Throwable;
use Webauthn\PublicKeyCredentialCreationOptions;
2022-12-08 00:28:53 +01:00
use Webauthn\PublicKeyCredentialDescriptor;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialSource;
use Webauthn\Server;
class Webauthn
{
2022-12-08 00:28:53 +01:00
use IsMockable;
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
{
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-08 00:28:53 +01:00
(isset($_SERVER['REMOTE_ADDR']) && preg_match('/.*\.localhost$/mi', $_SERVER['REMOTE_ADDR']) ) // localhost is TLD
) {
return true;
}
2022-11-03 13:18:02 +01:00
$e = oxNew(WebauthnException::class, 'D3_WEBAUTHN_ERR_UNSECURECONNECTION');
2022-12-08 00:28:53 +01:00
$this->d3GetMockableLogger()->info($e->getDetailedErrorMessage());
$this->d3GetMockableRegistryObject(UtilsView::class)->addErrorToDisplay($e);
return false;
}
/**
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-11-04 22:02:44 +01:00
public function getCreationOptions(User $user): string
{
2022-12-08 00:28:53 +01:00
$userEntity = $this->d3GetMockableOxNewObject(UserEntity::class, $user);
2022-12-08 00:28:53 +01:00
$publicKeyCredentialCreationOptions = $this->getServer()->generatePublicKeyCredentialCreationOptions(
$userEntity,
PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
2022-12-08 00:28:53 +01:00
$this->getExistingCredentials($userEntity)
);
2022-12-08 00:28:53 +01:00
$this->d3GetMockableRegistryObject(Session::class)
->setVariable(self::SESSION_CREATIONS_OPTIONS, $publicKeyCredentialCreationOptions);
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-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
{
// Get the list of authenticators associated to the user
2022-12-12 23:41:07 +01:00
/** @var PublicKeyCredentialList $credentialList */
2022-12-08 00:28:53 +01:00
$credentialList = $this->d3GetMockableOxNewObject(PublicKeyCredentialList::class);
$credentialSources = $credentialList->findAllForUserEntity($userEntity);
// Convert the Credential Sources into Public Key Credential Descriptors
2022-12-08 00:28:53 +01:00
return array_map(function (PublicKeyCredentialSource $credential) {
return $credential->getPublicKeyCredentialDescriptor();
}, $credentialSources);
2022-12-08 00:28:53 +01:00
}
2022-12-08 00:28:53 +01:00
/**
* @param PublicKeyCredentialCreationOptions|PublicKeyCredentialRequestOptions $creationOptions
* @return false|string
*/
protected function jsonEncode($creationOptions)
{
return json_encode($creationOptions,JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
}
/**
* @param string $userId
* @return string
* @throws DoctrineDriverException
* @throws DoctrineException
*/
public function getRequestOptions(string $userId): string
{
/** @var d3_User_Webauthn $user */
$user = $this->d3GetMockableOxNewObject(User::class);
$user->load($userId);
$userEntity = $this->d3GetMockableOxNewObject(UserEntity::class, $user);
// We generate the set of options.
2022-12-08 00:28:53 +01:00
$publicKeyCredentialRequestOptions = $this->getServer()->generatePublicKeyCredentialRequestOptions(
PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED, // Default value
2022-12-08 00:28:53 +01:00
$this->getExistingCredentials($userEntity)
);
2022-12-08 00:28:53 +01:00
$this->d3GetMockableRegistryObject(Session::class)
->setVariable(self::SESSION_ASSERTION_OPTIONS, $publicKeyCredentialRequestOptions);
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;
}
/**
* @return Server
*/
2022-12-08 00:28:53 +01:00
protected function getServer(): Server
{
2022-11-04 22:02:44 +01:00
/** @var RelyingPartyEntity $rpEntity */
2022-12-08 00:28:53 +01:00
$rpEntity = $this->d3GetMockableOxNewObject(RelyingPartyEntity::class);
2022-11-04 22:02:44 +01:00
/** @var Server $server */
2022-12-08 00:28:53 +01:00
$server = $this->d3GetMockableOxNewObject(
Server::class,
$rpEntity,
$this->d3GetMockableOxNewObject(PublicKeyCredentialList::class)
);
$server->setLogger($this->d3GetMockableLogger());
2022-11-01 23:42:25 +01:00
return $server;
}
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-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),
2022-12-08 00:28:53 +01:00
$this->d3GetMockableRegistryObject(Session::class)->getVariable(self::SESSION_CREATIONS_OPTIONS),
2022-11-02 16:38:43 +01:00
$serverRequest
);
2022-12-08 00:28:53 +01:00
$pkCredential = $this->d3GetMockableOxNewObject(PublicKeyCredential::class);
2022-11-02 16:38:43 +01:00
$pkCredential->saveCredentialSource($publicKeyCredentialSource, $keyName);
}
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
{
$psr17Factory = new Psr17Factory();
$creator = new ServerRequestCreator(
$psr17Factory,
$psr17Factory,
$psr17Factory,
$psr17Factory
);
$serverRequest = $creator->fromGlobals();
2022-12-08 00:28:53 +01:00
$userEntity = $this->getUserEntityFrom($this->getSavedUserIdFromSession());
2022-11-03 13:18:02 +01:00
try {
2022-11-04 22:02:44 +01:00
$this->getServer()->loadAndCheckAssertionResponse(
html_entity_decode( $response ),
2022-12-08 00:28:53 +01:00
$this->d3GetMockableRegistryObject(Session::class)
->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 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 */
$user = $this->d3GetMockableOxNewObject(User::class);
$user->load($userId);
2022-12-12 23:41:07 +01:00
2022-12-08 00:28:53 +01:00
return $this->d3GetMockableOxNewObject(UserEntity::class, $user);
}
/**
* @return string|null
*/
protected function getSavedUserIdFromSession(): ?string
{
$session = $this->d3GetMockableRegistryObject(Session::class);
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
/**
* @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
{
2022-12-08 00:28:53 +01:00
return !$this->d3GetMockableRegistryObject(Config::class)
->getConfigParam(WebauthnConf::GLOBAL_SWITCH)
&& !$this->d3GetMockableRegistryObject(Session::class)
->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 */
2022-12-08 00:28:53 +01:00
$credentialList = $this->d3GetMockableOxNewObject(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);
}
}