diff --git a/src/Application/Model/Webauthn/PublicKeyCredentials.php b/src/Application/Model/Webauthn/PublicKeyCredentials.php index 837dfbc..bdd2b4a 100644 --- a/src/Application/Model/Webauthn/PublicKeyCredentials.php +++ b/src/Application/Model/Webauthn/PublicKeyCredentials.php @@ -28,14 +28,43 @@ class PublicKeyCredentials extends BaseModel implements PublicKeyCredentialSourc $qb->createNamedParameter(bin2hex($publicKeyCredentialId)) ), $qb->expr()->eq( - 'shopid', + 'oxshopid', $qb->createNamedParameter(Registry::getConfig()->getShopId()) ) ) ); $credential = $qb->execute()->fetchOne(); - return strlen($credential) ? hex2bin(unserialize($credential)) : null; + if (!strlen($credential)) { + return null; + } + + $credential = unserialize(hex2bin($credential)); + + return $credential instanceof PublicKeyCredentialSource ? $credential : null; + } + + public function getIdByCredentialId(string $publicKeyCredentialId): ?string + { + /** @var QueryBuilder $qb */ + $qb = ContainerFactory::getInstance()->getContainer()->get(QueryBuilderFactoryInterface::class)->create(); + $qb->select('oxid') + ->from($this->getViewName()) + ->where( + $qb->expr()->and( + $qb->expr()->eq( + 'credid_hex', + $qb->createNamedParameter(bin2hex($publicKeyCredentialId)) + ), + $qb->expr()->eq( + 'oxshopid', + $qb->createNamedParameter(Registry::getConfig()->getShopId()) + ) + ) + ); + $oxid = $qb->execute()->fetchOne(); + + return strlen($oxid) ? $oxid : null; } public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array @@ -70,6 +99,10 @@ class PublicKeyCredentials extends BaseModel implements PublicKeyCredentialSourc */ public function saveCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource): void { + // will saved on every successfully assertion, set id to prevent duplicated database entries + $id = $this->getIdByCredentialId($publicKeyCredentialSource->getPublicKeyCredentialId()); + $this->setId($id); + $this->assign([ 'oxshopid' => Registry::getConfig()->getShopId(), 'oxuserid' => $publicKeyCredentialSource->getUserHandle(), diff --git a/src/Application/Model/Webauthn/Webauthn.php b/src/Application/Model/Webauthn/Webauthn.php index 23d2278..c77a68e 100644 --- a/src/Application/Model/Webauthn/Webauthn.php +++ b/src/Application/Model/Webauthn/Webauthn.php @@ -10,6 +10,7 @@ use Nyholm\Psr7Server\ServerRequestCreator; use OxidEsales\Eshop\Application\Model\User; use OxidEsales\Eshop\Core\Registry; use Webauthn\PublicKeyCredentialCreationOptions; +use Webauthn\PublicKeyCredentialRequestOptions; use Webauthn\PublicKeyCredentialRpEntity; use Webauthn\PublicKeyCredentialSource; use Webauthn\Server; @@ -17,7 +18,7 @@ use Webauthn\Server; class Webauthn { public const SESSION_CREATIONS_OPTIONS = 'd3WebAuthnCreationOptions'; - public const SESSION_USERENTITY = 'd3WebAuthnUserEntity'; + public const SESSION_ASSERTION_OPTIONS = 'd3WebAuthnAssertionOptions'; public function getCreationOptions() { @@ -26,8 +27,6 @@ class Webauthn $user->load('oxdefaultadmin'); $userEntity = $user->d3GetWebauthnUserEntity(); - Registry::getSession()->setVariable(self::SESSION_USERENTITY, $userEntity); - $credentialSourceRepository = new PublicKeyCredentials(); $credentialSources = $credentialSourceRepository->findAllForUserEntity($userEntity); $excludeCredentials = array_map(function (PublicKeyCredentialSource $credential) { @@ -46,6 +45,35 @@ class Webauthn return json_encode($publicKeyCredentialCreationOptions); } + public function getRequestOptions() + { + /** @var d3_totp_user $user */ + $user = oxNew(User::class); + $user->load('oxdefaultadmin'); + $userEntity = $user->d3GetWebauthnUserEntity(); + + // Get the list of authenticators associated to the user + $credentialSourceRepository = oxNew(PublicKeyCredentials::class); + $credentialSources = $credentialSourceRepository->findAllForUserEntity($userEntity); + + // Convert the Credential Sources into Public Key Credential Descriptors + $allowedCredentials = array_map(function (PublicKeyCredentialSource $credential) { + return $credential->getPublicKeyCredentialDescriptor(); + }, $credentialSources); + + $server = $this->getServer(); + + // We generate the set of options. + $publicKeyCredentialRequestOptions = $server->generatePublicKeyCredentialRequestOptions( + PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED, // Default value + $allowedCredentials + ); + + Registry::getSession()->setVariable(self::SESSION_ASSERTION_OPTIONS, $publicKeyCredentialRequestOptions); + + return json_encode($publicKeyCredentialRequestOptions); + } + /** * @return Server */ @@ -93,4 +121,46 @@ class Webauthn die(); } } + + public function assertAuthn(string $response) + { + try { + $psr17Factory = new Psr17Factory(); + $creator = new ServerRequestCreator( + $psr17Factory, + $psr17Factory, + $psr17Factory, + $psr17Factory + ); + $serverRequest = $creator->fromGlobals(); + + /** @var d3_totp_user $user */ + $user = oxNew(User::class); + $user->load('oxdefaultadmin'); + $userEntity = $user->d3GetWebauthnUserEntity(); + + $publicKeySource = $this->getServer()->loadAndCheckAssertionResponse( + html_entity_decode($response), + Registry::getSession()->getVariable(self::SESSION_ASSERTION_OPTIONS), + $userEntity, + $serverRequest + ); +/* + dumpvar($publicKeySource); + dumpvar(serialize($publicKeySource)); + dumpvar(unserialize(serialize($publicKeySource))); + echo "
"; + dumpvar(bin2hex(serialize($publicKeySource))); + dumpvar(unserialize(hex2bin(bin2hex(serialize($publicKeySource))))); +*/ + + dumpvar('successfully'); + + } catch (\Exception $e) { + dumpvar($e->getMessage()); + dumpvar($e); + + die(); + } + } } \ No newline at end of file diff --git a/src/Application/views/blocks/page/info/d3webauthn.tpl b/src/Application/views/blocks/page/info/d3webauthn.tpl new file mode 100644 index 0000000..6f1fe2c --- /dev/null +++ b/src/Application/views/blocks/page/info/d3webauthn.tpl @@ -0,0 +1,18 @@ +[{oxscript include=$oViewConf->getModuleUrl('d3totp', 'out/src/js/index.js')}] + +[{capture name="d3script"}] + var creationOptions = [{$requestOptions}]; + requestCredentials(creationOptions); +[{/capture}] +[{oxscript add=$smarty.capture.d3script}] +--A-- +
+ [{$oViewConf->getHiddenSid()}] + [{$oViewConf->getNavFormParams()}] + + + +
+--B-- + +[{$smarty.block.parent}] \ No newline at end of file diff --git a/src/Application/views/blocks/page/shop/start_welcome_text.tpl b/src/Application/views/blocks/page/shop/start_welcome_text.tpl index c473b35..95b5605 100644 --- a/src/Application/views/blocks/page/shop/start_welcome_text.tpl +++ b/src/Application/views/blocks/page/shop/start_welcome_text.tpl @@ -5,24 +5,6 @@ [{capture name="d3script"}] var creationOptions = [{$creationOptions}]; createCredentials(creationOptions); - [{*** - // Import the tool(s) ou need - import {useRegistration} from '@web-auth/webauthn-helper'; - - // We want to register new authenticators - const register = useRegistration({ - actionUrl: '/api/register', - optionsUrl: '/api/register/options' - }); - - register({ - username: 'FOO4', - displayName: 'baR' - }) - .then((response)=> console.log('Registration success')) - .catch((error)=> console.log('Registration failure')) - ; - ***}] [{/capture}] [{oxscript add=$smarty.capture.d3script}] --A-- diff --git a/src/IntelliSenseHelper.php b/src/IntelliSenseHelper.php index bab0639..daaff78 100644 --- a/src/IntelliSenseHelper.php +++ b/src/IntelliSenseHelper.php @@ -24,8 +24,10 @@ namespace D3\Totp\Modules\Application\Component namespace D3\Totp\Modules\Application\Controller { + use OxidEsales\Eshop\Application\Controller\ContactController; use OxidEsales\Eshop\Application\Controller\OrderController; use OxidEsales\Eshop\Application\Controller\PaymentController; + use OxidEsales\Eshop\Application\Controller\StartController; use OxidEsales\Eshop\Application\Controller\UserController; class d3_totp_UserController_parent extends UserController @@ -39,6 +41,14 @@ namespace D3\Totp\Modules\Application\Controller class d3_totp_OrderController_parent extends OrderController { } + + class d3_totp_StartController_parent extends StartController + { + } + + class d3_totp_ContactController_parent extends ContactController + { + } } namespace D3\Totp\Modules\Application\Controller\Admin diff --git a/src/Modules/Application/Controller/d3_totp_ContactController.php b/src/Modules/Application/Controller/d3_totp_ContactController.php new file mode 100644 index 0000000..9afd5ba --- /dev/null +++ b/src/Modules/Application/Controller/d3_totp_ContactController.php @@ -0,0 +1,23 @@ +addTplParam('requestOptions', $webAuthn->getRequestOptions()); + + return parent::render(); + } + + public function assertAuthn() + { + $webAuthn = oxNew(Webauthn::class); + $webAuthn->assertAuthn(Registry::getRequest()->getRequestEscapedParameter('credential')); + } +} \ No newline at end of file diff --git a/src/metadata.php b/src/metadata.php index 2ef7be6..e285866 100644 --- a/src/metadata.php +++ b/src/metadata.php @@ -19,6 +19,7 @@ use D3\Totp\Application\Controller\d3_account_totp; use D3\Totp\Application\Controller\d3totplogin; use D3\Totp\Modules\Application\Component\d3_totp_UserComponent; use D3\Totp\Modules\Application\Controller\Admin\d3_totp_LoginController; +use D3\Totp\Modules\Application\Controller\d3_totp_ContactController; use D3\Totp\Modules\Application\Controller\d3_totp_OrderController; use D3\Totp\Modules\Application\Controller\d3_totp_PaymentController; use D3\Totp\Modules\Application\Controller\d3_totp_StartController; @@ -28,6 +29,7 @@ use D3\Totp\Modules\Core\d3_totp_utils; use D3\Totp\Setup as ModuleSetup; use OxidEsales\Eshop\Application\Component\UserComponent; use OxidEsales\Eshop\Application\Controller\Admin\LoginController; +use OxidEsales\Eshop\Application\Controller\ContactController; use OxidEsales\Eshop\Application\Controller\OrderController; use OxidEsales\Eshop\Application\Controller\PaymentController; use OxidEsales\Eshop\Application\Controller\StartController; @@ -68,7 +70,8 @@ $aModule = [ LoginController::class => d3_totp_LoginController::class, Utils::class => d3_totp_utils::class, UserComponent::class => d3_totp_UserComponent::class, - StartController::class => d3_totp_StartController::class + StartController::class => d3_totp_StartController::class, + ContactController::class => d3_totp_ContactController::class ], 'controllers' => [ 'd3user_totp' => d3user_totp::class, @@ -109,5 +112,10 @@ $aModule = [ 'block' => 'start_welcome_text', 'file' => 'Application/views/blocks/page/shop/start_welcome_text.tpl', ], + [ + 'template' => 'page/info/contact.tpl', + 'block' => 'd3webauthn', + 'file' => 'Application/views/blocks/page/info/d3webauthn.tpl', + ], ] ]; diff --git a/src/out/src/js/index.js b/src/out/src/js/index.js index d62a4f8..7983019 100644 --- a/src/out/src/js/index.js +++ b/src/out/src/js/index.js @@ -19,7 +19,7 @@ const base64UrlDecode = (input) => { return window.atob(input); }; -const preparePublicKeyOptions = (publicKey) => { +const prepareOptions = (publicKey) => { "use strict"; //Convert challenge from Base64Url string to Uint8Array publicKey.challenge = Uint8Array.from( @@ -70,6 +70,7 @@ const preparePublicKeyOptions = (publicKey) => { return publicKey; }; +/** https://gist.github.com/jonleighton/958841 **/ function base64ArrayBuffer(arrayBuffer) { "use strict"; var base64 = '' @@ -126,7 +127,7 @@ function base64ArrayBuffer(arrayBuffer) { const createCredentials = (publicKey) => { "use strict"; - preparePublicKeyOptions(publicKey); + prepareOptions(publicKey); navigator.credentials.create({publicKey: publicKey}) .then(function (newCredentialInfo) { @@ -150,3 +151,35 @@ const createCredentials = (publicKey) => { }); } +const requestCredentials = (publicKey) => { + "use strict"; +console.log('--AB--'); + + prepareOptions(publicKey); + + navigator.credentials.get({publicKey: publicKey}) + .then(function (authenticateInfo) { +console.log(authenticateInfo); + // Send authenticate info to server for verification. + var cred = { + id: authenticateInfo.id, + rawId: base64ArrayBuffer(authenticateInfo.rawId), + response: { + authenticatorData: base64ArrayBuffer(authenticateInfo.response.authenticatorData), + signature: base64ArrayBuffer(authenticateInfo.response.signature), + userHandle: authenticateInfo.response.userHandle, + clientDataJSON: base64ArrayBuffer(authenticateInfo.response.clientDataJSON) + }, + type: authenticateInfo.type + }; +console.log(cred); + document.getElementById('webauthn').credential.value = JSON.stringify(cred); + document.getElementById('webauthn').submit(); + }).catch(function (err) { + console.log('--2--'); + console.log('WebAuthn auth: ' + err); + // No acceptable authenticator or user refused consent. Handle appropriately. + }); + +} +