add webauthn assertion

This commit is contained in:
Daniel Seifert 2022-10-23 22:42:32 +02:00
parent 911ff99c83
commit b725333e86
Signed by: DanielS
GPG Key ID: 6A513E13AEE66170
8 changed files with 203 additions and 26 deletions

View File

@ -28,14 +28,43 @@ class PublicKeyCredentials extends BaseModel implements PublicKeyCredentialSourc
$qb->createNamedParameter(bin2hex($publicKeyCredentialId)) $qb->createNamedParameter(bin2hex($publicKeyCredentialId))
), ),
$qb->expr()->eq( $qb->expr()->eq(
'shopid', 'oxshopid',
$qb->createNamedParameter(Registry::getConfig()->getShopId()) $qb->createNamedParameter(Registry::getConfig()->getShopId())
) )
) )
); );
$credential = $qb->execute()->fetchOne(); $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 public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array
@ -70,6 +99,10 @@ class PublicKeyCredentials extends BaseModel implements PublicKeyCredentialSourc
*/ */
public function saveCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource): void 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([ $this->assign([
'oxshopid' => Registry::getConfig()->getShopId(), 'oxshopid' => Registry::getConfig()->getShopId(),
'oxuserid' => $publicKeyCredentialSource->getUserHandle(), 'oxuserid' => $publicKeyCredentialSource->getUserHandle(),

View File

@ -10,6 +10,7 @@ use Nyholm\Psr7Server\ServerRequestCreator;
use OxidEsales\Eshop\Application\Model\User; use OxidEsales\Eshop\Application\Model\User;
use OxidEsales\Eshop\Core\Registry; use OxidEsales\Eshop\Core\Registry;
use Webauthn\PublicKeyCredentialCreationOptions; use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialRpEntity; use Webauthn\PublicKeyCredentialRpEntity;
use Webauthn\PublicKeyCredentialSource; use Webauthn\PublicKeyCredentialSource;
use Webauthn\Server; use Webauthn\Server;
@ -17,7 +18,7 @@ use Webauthn\Server;
class Webauthn class Webauthn
{ {
public const SESSION_CREATIONS_OPTIONS = 'd3WebAuthnCreationOptions'; public const SESSION_CREATIONS_OPTIONS = 'd3WebAuthnCreationOptions';
public const SESSION_USERENTITY = 'd3WebAuthnUserEntity'; public const SESSION_ASSERTION_OPTIONS = 'd3WebAuthnAssertionOptions';
public function getCreationOptions() public function getCreationOptions()
{ {
@ -26,8 +27,6 @@ class Webauthn
$user->load('oxdefaultadmin'); $user->load('oxdefaultadmin');
$userEntity = $user->d3GetWebauthnUserEntity(); $userEntity = $user->d3GetWebauthnUserEntity();
Registry::getSession()->setVariable(self::SESSION_USERENTITY, $userEntity);
$credentialSourceRepository = new PublicKeyCredentials(); $credentialSourceRepository = new PublicKeyCredentials();
$credentialSources = $credentialSourceRepository->findAllForUserEntity($userEntity); $credentialSources = $credentialSourceRepository->findAllForUserEntity($userEntity);
$excludeCredentials = array_map(function (PublicKeyCredentialSource $credential) { $excludeCredentials = array_map(function (PublicKeyCredentialSource $credential) {
@ -46,6 +45,35 @@ class Webauthn
return json_encode($publicKeyCredentialCreationOptions); 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 * @return Server
*/ */
@ -93,4 +121,46 @@ class Webauthn
die(); 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 "<hr>";
dumpvar(bin2hex(serialize($publicKeySource)));
dumpvar(unserialize(hex2bin(bin2hex(serialize($publicKeySource)))));
*/
dumpvar('successfully');
} catch (\Exception $e) {
dumpvar($e->getMessage());
dumpvar($e);
die();
}
}
} }

View File

@ -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--
<form id="webauthn" action="[{$oViewConf->getSelfActionLink()}]" method="post">
[{$oViewConf->getHiddenSid()}]
[{$oViewConf->getNavFormParams()}]
<input type="hidden" name="fnc" value="assertAuthn">
<input type="hidden" name="cl" value="[{$oViewConf->getActiveClassName()}]">
<input type="hidden" name="credential" value='credent'>
</form>
--B--
[{$smarty.block.parent}]

View File

@ -5,24 +5,6 @@
[{capture name="d3script"}] [{capture name="d3script"}]
var creationOptions = [{$creationOptions}]; var creationOptions = [{$creationOptions}];
createCredentials(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}] [{/capture}]
[{oxscript add=$smarty.capture.d3script}] [{oxscript add=$smarty.capture.d3script}]
--A-- --A--

View File

@ -24,8 +24,10 @@ namespace D3\Totp\Modules\Application\Component
namespace D3\Totp\Modules\Application\Controller namespace D3\Totp\Modules\Application\Controller
{ {
use OxidEsales\Eshop\Application\Controller\ContactController;
use OxidEsales\Eshop\Application\Controller\OrderController; use OxidEsales\Eshop\Application\Controller\OrderController;
use OxidEsales\Eshop\Application\Controller\PaymentController; use OxidEsales\Eshop\Application\Controller\PaymentController;
use OxidEsales\Eshop\Application\Controller\StartController;
use OxidEsales\Eshop\Application\Controller\UserController; use OxidEsales\Eshop\Application\Controller\UserController;
class d3_totp_UserController_parent extends 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_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 namespace D3\Totp\Modules\Application\Controller\Admin

View File

@ -0,0 +1,23 @@
<?php
namespace D3\Totp\Modules\Application\Controller;
use D3\Totp\Application\Model\Webauthn\Webauthn;
use OxidEsales\Eshop\Core\Registry;
class d3_totp_ContactController extends d3_totp_ContactController_parent
{
public function render()
{
$webAuthn = oxNew(Webauthn::class);
$this->addTplParam('requestOptions', $webAuthn->getRequestOptions());
return parent::render();
}
public function assertAuthn()
{
$webAuthn = oxNew(Webauthn::class);
$webAuthn->assertAuthn(Registry::getRequest()->getRequestEscapedParameter('credential'));
}
}

View File

@ -19,6 +19,7 @@ use D3\Totp\Application\Controller\d3_account_totp;
use D3\Totp\Application\Controller\d3totplogin; use D3\Totp\Application\Controller\d3totplogin;
use D3\Totp\Modules\Application\Component\d3_totp_UserComponent; 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\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_OrderController;
use D3\Totp\Modules\Application\Controller\d3_totp_PaymentController; use D3\Totp\Modules\Application\Controller\d3_totp_PaymentController;
use D3\Totp\Modules\Application\Controller\d3_totp_StartController; 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 D3\Totp\Setup as ModuleSetup;
use OxidEsales\Eshop\Application\Component\UserComponent; use OxidEsales\Eshop\Application\Component\UserComponent;
use OxidEsales\Eshop\Application\Controller\Admin\LoginController; use OxidEsales\Eshop\Application\Controller\Admin\LoginController;
use OxidEsales\Eshop\Application\Controller\ContactController;
use OxidEsales\Eshop\Application\Controller\OrderController; use OxidEsales\Eshop\Application\Controller\OrderController;
use OxidEsales\Eshop\Application\Controller\PaymentController; use OxidEsales\Eshop\Application\Controller\PaymentController;
use OxidEsales\Eshop\Application\Controller\StartController; use OxidEsales\Eshop\Application\Controller\StartController;
@ -68,7 +70,8 @@ $aModule = [
LoginController::class => d3_totp_LoginController::class, LoginController::class => d3_totp_LoginController::class,
Utils::class => d3_totp_utils::class, Utils::class => d3_totp_utils::class,
UserComponent::class => d3_totp_UserComponent::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' => [ 'controllers' => [
'd3user_totp' => d3user_totp::class, 'd3user_totp' => d3user_totp::class,
@ -109,5 +112,10 @@ $aModule = [
'block' => 'start_welcome_text', 'block' => 'start_welcome_text',
'file' => 'Application/views/blocks/page/shop/start_welcome_text.tpl', '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',
],
] ]
]; ];

View File

@ -19,7 +19,7 @@ const base64UrlDecode = (input) => {
return window.atob(input); return window.atob(input);
}; };
const preparePublicKeyOptions = (publicKey) => { const prepareOptions = (publicKey) => {
"use strict"; "use strict";
//Convert challenge from Base64Url string to Uint8Array //Convert challenge from Base64Url string to Uint8Array
publicKey.challenge = Uint8Array.from( publicKey.challenge = Uint8Array.from(
@ -70,6 +70,7 @@ const preparePublicKeyOptions = (publicKey) => {
return publicKey; return publicKey;
}; };
/** https://gist.github.com/jonleighton/958841 **/
function base64ArrayBuffer(arrayBuffer) { function base64ArrayBuffer(arrayBuffer) {
"use strict"; "use strict";
var base64 = '' var base64 = ''
@ -126,7 +127,7 @@ function base64ArrayBuffer(arrayBuffer) {
const createCredentials = (publicKey) => { const createCredentials = (publicKey) => {
"use strict"; "use strict";
preparePublicKeyOptions(publicKey); prepareOptions(publicKey);
navigator.credentials.create({publicKey: publicKey}) navigator.credentials.create({publicKey: publicKey})
.then(function (newCredentialInfo) { .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.
});
}