commit b5b3117fec3da394c4c3e70a19e962431f4b3461 Author: Daniel Seifert Date: Mon Oct 24 22:24:40 2022 +0200 add shop controllers diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..62edc07 --- /dev/null +++ b/composer.json @@ -0,0 +1,46 @@ +{ + "name": "d3/oxwebauthn", + "description": "Webauthn / FIDO2 module for OXID eShop.", + "type": "oxideshop-module", + "keywords": [ + "oxid", + "modules", + "eShop", + "d3", + "webauthn", + "FIDO2", + "CTAP", + "public key" + ], + "authors": [ + { + "name": "D3 Data Development (Inh. Thomas Dartsch)", + "email": "info@shopmodule.com", + "homepage": "https://www.d3data.de", + "role": "Owner" + } + ], + "homepage": "https://www.oxidmodule.com/", + "license": [ + "GPL-3.0-or-later" + ], + "extra": { + "oxideshop": { + "source-directory": "/src", + "target-directory": "d3/oxwebauthn" + } + }, + "require": { + "php": ">=5.6", + "oxid-esales/oxideshop-ce": "6.10 - 6.12", + "web-auth/webauthn-lib": "^3.3", + "ext-json": "*", + "ext-soap": "*", + "ext-PDO": "*" + }, + "autoload": { + "psr-4": { + "D3\\Webauthn\\": "../../../source/modules/d3/oxwebauthn" + } + } +} diff --git a/src/Application/Controller/Admin/d3user_webauthn.php b/src/Application/Controller/Admin/d3user_webauthn.php new file mode 100755 index 0000000..e604ac2 --- /dev/null +++ b/src/Application/Controller/Admin/d3user_webauthn.php @@ -0,0 +1,174 @@ + + * @link http://www.oxidmodule.com + */ + +namespace D3\Webauthn\Application\Controller\Admin; + +use D3\Webauthn\Application\Model\Credential\d3PublicKeyCredentialList; +use D3\Webauthn\Application\Model\d3webauthn; +use D3\Webauthn\Application\Model\Webauthn\d3PublicKeyCredentialUserEntity; +use D3\Webauthn\Modules\Application\Model\d3_User_Webauthn; +use Exception; +use OxidEsales\Eshop\Application\Controller\Admin\AdminDetailsController; +use OxidEsales\Eshop\Application\Model\User; +use OxidEsales\Eshop\Core\Exception\DatabaseConnectionException; +use OxidEsales\Eshop\Core\Exception\DatabaseErrorException; +use OxidEsales\Eshop\Core\Registry; +use OxidEsales\Eshop\Core\UtilsView; + +class d3user_webauthn extends AdminDetailsController +{ + protected $_sSaveError = null; + + protected $_sThisTemplate = 'd3user_webauthn.tpl'; + + /** + * @return string + * @throws DatabaseConnectionException + * @throws DatabaseErrorException + */ + public function render() + { + parent::render(); + + $soxId = $this->getEditObjectId(); + + if (isset($soxId) && $soxId != "-1") { + /** @var d3_User_Webauthn $oUser */ + $oUser = $this->getUserObject(); + if ($oUser->load($soxId)) { + $this->addTplParam("oxid", $oUser->getId()); + } else { + $this->addTplParam("oxid", '-1'); + } + $this->addTplParam("edit", $oUser); + } + + if ($this->_sSaveError) { + $this->addTplParam("sSaveError", $this->_sSaveError); + } + + $this->setAuthnRegister(); + + return $this->_sThisTemplate; + } + + /** + * @throws DatabaseConnectionException + * @throws DatabaseErrorException + */ + public function setAuthnRegister() + { + $publicKeyCredentialCreationOptions = $this->getWebauthnObject()->setAuthnRegister($this->getEditObjectId()); + $this->addTplParam( + 'webauthn_publickey_register', + json_encode($publicKeyCredentialCreationOptions, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) + ); + } + + /** + * @param $userId + * @return d3PublicKeyCredentialList|object + * @throws DatabaseConnectionException + * @throws DatabaseErrorException + */ + public function getCredentialList($userId) + { + $credentialList = oxNew(d3PublicKeyCredentialList::class); + + $oUser = $this->getUserObject(); + $oUser->load($userId); + if ($oUser && $oUser->getId()) { + /** @var d3PublicKeyCredentialUserEntity $userEntity */ + $userEntity = oxNew(d3PublicKeyCredentialUserEntity::class, $oUser); + $credentialList->loadAllForUserEntity($userEntity); + } + + return $credentialList; + } + + /** + * @return User + */ + public function getUserObject() + { + return oxNew(User::class); + } + + /** + * @return d3webauthn + */ + public function getWebauthnObject() + { + return oxNew(d3webauthn::class); + } + + public function registerNewKey() + { + $this->getWebauthnObject()->registerNewKey(Registry::getRequest()->getRequestParameter('authn')); + } + + /** + * @throws Exception + */ + public function save() + { + parent::save(); + + $aParams = Registry::getRequest()->getRequestEscapedParameter("editval"); + + try { + /** @var d3webauthn $oWebauthn */ + $oWebauthn = $this->getWebauthnObject(); +/* + if ($oWebauthn->checkIfAlreadyExist($this->getEditObjectId())) { + $oException = oxNew(StandardException::class, 'D3_TOTP_ALREADY_EXIST'); + throw $oException; + }; + + if ($aParams['d3totp__oxid']) { + $oWebauthn->load($aParams['d3totp__oxid']); + } else { + $aParams['d3totp__usetotp'] = 1; + $seed = Registry::getRequest()->getRequestEscapedParameter("secret"); + $otp = Registry::getRequest()->getRequestEscapedParameter("otp"); + + $oWebauthn->saveSecret($seed); + $oWebauthn->assign($aParams); + $oWebauthn->verify($otp, $seed); + $oWebauthn->setId(); + } + $oWebauthn->save(); +*/ + } catch (Exception $oExcp) { + $this->_sSaveError = $oExcp->getMessage(); + } + } + + /** + * @throws DatabaseConnectionException + */ + public function delete() + { + $aParams = Registry::getRequest()->getRequestEscapedParameter("editval"); + + /** @var d3webauthn $oWebauthn */ + $oWebauthn = $this->getWebauthnObject(); + if ($aParams['d3totp__oxid']) { + $oWebauthn->load($aParams['d3totp__oxid']); + $oWebauthn->delete(); + Registry::get(UtilsView::class)->addErrorToDisplay('D3_TOTP_REGISTERDELETED'); + } + } +} \ No newline at end of file diff --git a/src/Application/Controller/d3_account_webauthn.php b/src/Application/Controller/d3_account_webauthn.php new file mode 100755 index 0000000..9ba9a9d --- /dev/null +++ b/src/Application/Controller/d3_account_webauthn.php @@ -0,0 +1,105 @@ + + * @link http://www.oxidmodule.com + */ + +namespace D3\Webauthn\Application\Controller; + +use D3\Webauthn\Application\Model\Credential\d3PublicKeyCredential; +use D3\Webauthn\Application\Model\Credential\d3PublicKeyCredentialList; +use D3\Webauthn\Application\Model\d3webauthn; +use D3\Webauthn\Application\Model\Webauthn\d3PublicKeyCredentialUserEntity; +use OxidEsales\Eshop\Application\Controller\AccountController; +use OxidEsales\Eshop\Core\Exception\DatabaseConnectionException; +use OxidEsales\Eshop\Core\Exception\DatabaseErrorException; +use OxidEsales\Eshop\Core\Registry; + +class d3_account_webauthn extends AccountController +{ + protected $_sThisTemplate = 'd3_account_webauthn.tpl'; + + /** + * @return string + * @throws DatabaseConnectionException + * @throws DatabaseErrorException + */ + public function render() + { + if (Registry::getRequest()->getRequestEscapedParameter('error')) { +dumpvar(Registry::getRequest()->getRequestEscapedParameter('error')); + Registry::getUtilsView()->addErrorToDisplay('error occured'); + } + + $sRet = parent::render(); + + // is logged in ? + $oUser = $this->getUser(); + if (!$oUser) { + return $this->_sThisTemplate = $this->_sThisLoginTemplate; + } + + $this->addTplParam('user', $this->getUser()); + + $this->setAuthnRegister(); + + return $sRet; + } + + /** + * @return d3PublicKeyCredentialList|object + * @throws DatabaseConnectionException + * @throws DatabaseErrorException + */ + public function getCredentialList() + { + $credentialList = oxNew(d3PublicKeyCredentialList::class); + + $oUser = $this->getUser(); + if ($oUser) { + /** @var d3PublicKeyCredentialUserEntity $userEntity */ + $userEntity = oxNew(d3PublicKeyCredentialUserEntity::class, $oUser); + $credentialList->loadAllForUserEntity($userEntity); + } + + return $credentialList; + } + + /** + * @throws DatabaseConnectionException + * @throws DatabaseErrorException + */ + public function setAuthnRegister() + { + $webauthn = oxNew(d3webauthn::class); + $publicKeyCredentialCreationOptions = $webauthn->setAuthnRegister('36944b76d6e583fe2.12734046'); + + $this->addTplParam( + 'webauthn_publickey_register', + json_encode($publicKeyCredentialCreationOptions, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) + ); + } + + public function registerNewKey() + { + $webauthn = oxNew(d3webauthn::class); + $webauthn->registerNewKey(Registry::getRequest()->getRequestParameter('authn')); + } + + public function deleteKey() + { + if (Registry::getRequest()->getRequestEscapedParameter('oxid')) { + $credential = oxNew(d3PublicKeyCredential::class); + $credential->delete(Registry::getRequest()->getRequestEscapedParameter('oxid')); + } + } +} \ No newline at end of file diff --git a/src/Application/Controller/d3webauthnlogin.php b/src/Application/Controller/d3webauthnlogin.php new file mode 100755 index 0000000..5a042ae --- /dev/null +++ b/src/Application/Controller/d3webauthnlogin.php @@ -0,0 +1,121 @@ + + * @link http://www.oxidmodule.com + */ + +namespace D3\Webauthn\Application\Controller; + +use D3\Webauthn\Application\Model\d3webauthn; +use D3\Webauthn\Application\Model\d3webauthn_conf; +use OxidEsales\Eshop\Application\Controller\FrontendController; +use OxidEsales\Eshop\Core\Exception\DatabaseConnectionException; +use OxidEsales\Eshop\Core\Exception\DatabaseErrorException; +use OxidEsales\Eshop\Core\Registry; +use OxidEsales\Eshop\Core\Utils; + +class d3webauthnlogin extends FrontendController +{ + protected $_sThisTemplate = 'd3webauthnlogin.tpl'; + + /** + * @return null + * @throws DatabaseConnectionException + * @throws DatabaseErrorException + */ + public function render() + { + + if (Registry::getSession()->hasVariable(d3webauthn_conf::WEBAUTHN_SESSION_AUTH) || + false == Registry::getSession()->hasVariable(d3webauthn_conf::WEBAUTHN_SESSION_CURRENTUSER) + ) { + $this->getUtils()->redirect('index.php?cl=start', true, 302); + if (false == defined('OXID_PHP_UNIT')) { + // @codeCoverageIgnoreStart + exit; + // @codeCoverageIgnoreEnd + } + } + + $this->generateCredentialRequest(); + + $this->addTplParam('navFormParams', Registry::getSession()->getVariable(d3webauthn_conf::WEBAUTHN_SESSION_NAVFORMPARAMS)); + + return parent::render(); + } + + /** + * @throws DatabaseConnectionException + * @throws DatabaseErrorException + */ + public function generateCredentialRequest() + { + $auth = Registry::getSession()->getSession()->getVariable(d3webauthn_conf::WEBAUTHN_SESSION_CURRENTUSER); + $webauthn = oxNew(d3webauthn::class); + $publicKeyCredentialRequestOptions = $webauthn->getCredentialRequestOptions($auth); + + $this->addTplParam( + 'webauthn_publickey_login', + $publicKeyCredentialRequestOptions + ); + } + + /** + * @return Utils + */ + public function getUtils() + { + return Registry::getUtils(); + } + + public function getPreviousClass() + { + return Registry::getSession()->getVariable(d3webauthn_conf::WEBAUTHN_SESSION_CURRENTCLASS); + } + + public function previousClassIsOrderStep() + { + $sClassKey = Registry::getSession()->getVariable(d3webauthn_conf::WEBAUTHN_SESSION_CURRENTCLASS); + $resolvedClass = Registry::getControllerClassNameResolver()->getClassNameById($sClassKey); + $resolvedClass = $resolvedClass ? $resolvedClass : 'start'; + + /** @var FrontendController $oController */ + $oController = oxNew($resolvedClass); + return $oController->getIsOrderStep(); + } + + /** + * @return bool + */ + public function getIsOrderStep() + { + return $this->previousClassIsOrderStep(); + } + + /** + * Returns Bread Crumb - you are here page1/page2/page3... + * + * @return array + */ + public function getBreadCrumb() + { + $aPaths = []; + $aPath = []; + $iBaseLanguage = Registry::getLang()->getBaseLanguage(); + $aPath['title'] = Registry::getLang()->translateString('D3_WEBAUTHN_BREADCRUMB', $iBaseLanguage, false); + $aPath['link'] = $this->getLink(); + + $aPaths[] = $aPath; + + return $aPaths; + } +} \ No newline at end of file diff --git a/src/Application/Model/Credential/d3MetadataStatementRepository.php b/src/Application/Model/Credential/d3MetadataStatementRepository.php new file mode 100755 index 0000000..68c12f4 --- /dev/null +++ b/src/Application/Model/Credential/d3MetadataStatementRepository.php @@ -0,0 +1,29 @@ + + * @link http://www.oxidmodule.com + */ + +namespace D3\Webauthn\Application\Model\Credential; + +use Webauthn\MetadataService\MetadataStatement; +use Webauthn\MetadataService\MetadataStatementRepository; + +class d3MetadataStatementRepository implements MetadataStatementRepository +{ + public function findOneByAAGUID(string $aaguid): ?MetadataStatement + { + return new MetadataStatement(); + } +} \ No newline at end of file diff --git a/src/Application/Model/Credential/d3PublicKeyCredential.php b/src/Application/Model/Credential/d3PublicKeyCredential.php new file mode 100755 index 0000000..e715034 --- /dev/null +++ b/src/Application/Model/Credential/d3PublicKeyCredential.php @@ -0,0 +1,153 @@ + + * @link http://www.oxidmodule.com + */ + +namespace D3\Webauthn\Application\Model\Credential; + +use OxidEsales\Eshop\Core\DatabaseProvider; +use OxidEsales\Eshop\Core\Exception\DatabaseConnectionException; +use OxidEsales\Eshop\Core\Model\BaseModel; +use OxidEsales\Eshop\Core\Registry; + +class d3PublicKeyCredential extends BaseModel +{ + protected $_sCoreTable = 'd3PublicKeyCredential'; + + public function __construct() + { + $this->init($this->getCoreTableName()); + + parent::__construct(); + } + + public function d3SetName($name) + { + $this->assign(['name' => $name]); + } + + public function d3GetName() + { + return $this->getFieldData('name'); + } + + public function d3SetCredentialId($credentialId) + { + $this->assign(['credentialid' => $credentialId]); + } + + public function d3GetCredentialId() + { + return $this->__get($this->_getFieldLongName('credentialid'))->rawValue; + } + + public function d3SetType($type) + { + $this->assign(['Type' => $type]); + } + + public function d3GetType() + { + return $this->getFieldData('Type'); + } + + public function d3SetTransports($transports) + { + $this->assign(['Transports' => base64_encode(serialize($transports))]); + } + + public function d3GetTransports() + { + return unserialize(base64_decode($this->getFieldData('Transports'))); + } + + public function d3SetAttestationType($attestationType) + { + $this->assign(['AttestationType' => $attestationType]); + } + + public function d3GetAttestationType() + { + return $this->getFieldData('AttestationType'); + } + + public function d3SetTrustPath($trustPath) + { + $this->assign(['TrustPath' => base64_encode(serialize($trustPath))]); + } + + public function d3GetTrustPath() + { + return unserialize(base64_decode($this->getFieldData('TrustPath'))); + } + + public function d3SetAaguid($aaguid) + { + $this->assign(['Aaguid' => base64_encode(serialize($aaguid))]); + } + + public function d3GetAaguid() + { + return unserialize(base64_decode($this->getFieldData('Aaguid'))); + } + + public function d3SetPublicKey($publicKey) + { + $this->assign(['PublicKey' => $publicKey]); + } + + public function d3GetPublicKey() + { + return $this->__get($this->_getFieldLongName('PublicKey'))->rawValue; + } + + public function d3SetUserHandle($userHandle) + { + $this->assign(['UserHandle' => $userHandle]); + } + + public function d3GetUserHandle() + { + return $this->getFieldData('UserHandle'); + } + + public function d3SetCounter($count) + { + $this->assign(['Counter' => $count]); + } + + public function d3GetCounter() + { + return $this->getFieldData('Counter'); + } + + /** + * @param string $publicKeyCredentialId + * @return |null + * @throws DatabaseConnectionException + */ + public function loadByCredentialId(string $publicKeyCredentialId) + { + if (Registry::getRequest()->getRequestEscapedParameter('fnc') == 'checkregister') { + return null; + } + + $oDb = DatabaseProvider::getDb(DatabaseProvider::FETCH_MODE_ASSOC); + $q = "SELECT oxid FROM ".$this->getViewName()." WHERE CredentialId = ".$oDb->quote($publicKeyCredentialId); + $id = $oDb->getOne($q); + $this->load($id); + } + +} \ No newline at end of file diff --git a/src/Application/Model/Credential/d3PublicKeyCredentialList.php b/src/Application/Model/Credential/d3PublicKeyCredentialList.php new file mode 100755 index 0000000..cccfa09 --- /dev/null +++ b/src/Application/Model/Credential/d3PublicKeyCredentialList.php @@ -0,0 +1,53 @@ + + * @link http://www.oxidmodule.com + */ + +namespace D3\Webauthn\Application\Model\Credential; + +use OxidEsales\Eshop\Core\DatabaseProvider; +use OxidEsales\Eshop\Core\Exception\DatabaseConnectionException; +use OxidEsales\Eshop\Core\Exception\DatabaseErrorException; +use OxidEsales\Eshop\Core\Model\ListModel; +use Webauthn\PublicKeyCredentialUserEntity; + +class d3PublicKeyCredentialList extends ListModel +{ + protected $_sObjectsInListName = d3PublicKeyCredential::class; + + public function __construct() + { + parent::__construct(d3PublicKeyCredential::class); + } + + /** + * @param PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity + * @throws DatabaseConnectionException + * @throws DatabaseErrorException + */ + public function loadAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity) + { + $q = "SELECT oxid FROM ".$this->getBaseObject()->getViewName()." WHERE UserHandle = ".DatabaseProvider::getDb(DatabaseProvider::FETCH_MODE_ASSOC)->quote($publicKeyCredentialUserEntity->getId()); + $idList = DatabaseProvider::getDb(DatabaseProvider::FETCH_MODE_ASSOC)->getAll($q); + + if ($idList && is_iterable($idList)) { + foreach ($idList as $id) { + $credential = oxNew($this->_sObjectsInListName); + $credential->load($id['oxid']); + $this->offsetSet($credential->getId(), $credential); + } + } + } +} \ No newline at end of file diff --git a/src/Application/Model/Exceptions/d3webauthnExceptionAbstract.php b/src/Application/Model/Exceptions/d3webauthnExceptionAbstract.php new file mode 100755 index 0000000..851ddfe --- /dev/null +++ b/src/Application/Model/Exceptions/d3webauthnExceptionAbstract.php @@ -0,0 +1,25 @@ + + * @link http://www.oxidmodule.com + */ + +namespace D3\Webauthn\Application\Model\Exceptions; + +use OxidEsales\Eshop\Core\Exception\StandardException; + +abstract class d3webauthnExceptionAbstract extends StandardException +{ + +} \ No newline at end of file diff --git a/src/Application/Model/Exceptions/d3webauthnMissingPublicKeyCredentialRequestOptions.php b/src/Application/Model/Exceptions/d3webauthnMissingPublicKeyCredentialRequestOptions.php new file mode 100755 index 0000000..59c64a6 --- /dev/null +++ b/src/Application/Model/Exceptions/d3webauthnMissingPublicKeyCredentialRequestOptions.php @@ -0,0 +1,36 @@ + + * @link http://www.oxidmodule.com + */ + +namespace D3\Webauthn\Application\Model\Exceptions; + +use Exception; +use OxidEsales\Eshop\Core\Exception\StandardException; + +class d3webauthnMissingPublicKeyCredentialRequestOptions extends d3webauthnExceptionAbstract +{ + /** + * Default constructor + * + * @param string $sMessage exception message + * @param integer $iCode exception code + * @param Exception|null $previous previous exception + */ + public function __construct($sMessage = "D3_WEBAUTHN_ERROR_MISSINGPKC", $iCode = 0, Exception $previous = null) + { + parent::__construct($sMessage, $iCode, $previous); + } +} diff --git a/src/Application/Model/Exceptions/d3webauthnWrongAuthException.php b/src/Application/Model/Exceptions/d3webauthnWrongAuthException.php new file mode 100755 index 0000000..db9dcd5 --- /dev/null +++ b/src/Application/Model/Exceptions/d3webauthnWrongAuthException.php @@ -0,0 +1,35 @@ + + * @link http://www.oxidmodule.com + */ + +namespace D3\Webauthn\Application\Model\Exceptions; + +use Exception; + +class d3webauthnWrongAuthException extends d3webauthnExceptionAbstract +{ + /** + * Default constructor + * + * @param string $sMessage exception message + * @param integer $iCode exception code + * @param Exception|null $previous previous exception + */ + public function __construct($sMessage = "D3_WEBAUTHN_ERROR_UNVALID", $iCode = 0, Exception $previous = null) + { + parent::__construct($sMessage, $iCode, $previous); + } +} diff --git a/src/Application/Model/Webauthn/d3PublicKeyCredentialRpEntity.php b/src/Application/Model/Webauthn/d3PublicKeyCredentialRpEntity.php new file mode 100755 index 0000000..b04c96d --- /dev/null +++ b/src/Application/Model/Webauthn/d3PublicKeyCredentialRpEntity.php @@ -0,0 +1,34 @@ + + * @link http://www.oxidmodule.com + */ + +declare(strict_types=1); + +namespace D3\Webauthn\Application\Model\Webauthn; + +use OxidEsales\Eshop\Application\Model\Shop; +use Webauthn\PublicKeyCredentialRpEntity; + +class d3PublicKeyCredentialRpEntity extends PublicKeyCredentialRpEntity +{ + public function __construct(Shop $shop) + { + parent::__construct( + $shop->getFieldData('oxname'), + $_SERVER['HTTP_HOST'] + ); + } +} \ No newline at end of file diff --git a/src/Application/Model/Webauthn/d3PublicKeyCredentialSource.php b/src/Application/Model/Webauthn/d3PublicKeyCredentialSource.php new file mode 100755 index 0000000..bb90f21 --- /dev/null +++ b/src/Application/Model/Webauthn/d3PublicKeyCredentialSource.php @@ -0,0 +1,74 @@ + + * @link http://www.oxidmodule.com + */ + +namespace D3\Webauthn\Application\Model\Webauthn; + +use D3\Webauthn\Application\Model\Credential\d3PublicKeyCredential; +use Webauthn\PublicKeyCredentialSource; + +class d3PublicKeyCredentialSource extends PublicKeyCredentialSource +{ + /** + * @throws \Exception + */ + public function saveCredential() + { + $credential = oxNew(d3PublicKeyCredential::class); + $credential->d3SetName(date('Y-m-d H:i:s')); + $credential->d3SetCredentialId($this->getPublicKeyCredentialId()); + $credential->d3SetType($this->getType()); + $credential->d3SetTransports($this->getTransports()); + $credential->d3SetAttestationType($this->getAttestationType()); + $credential->d3SetTrustPath($this->getTrustPath()); + $credential->d3SetAaguid($this->getAaguid()); + $credential->d3SetPublicKey($this->getCredentialPublicKey()); + $credential->d3SetUserHandle($this->getUserHandle()); + $credential->d3SetCounter($this->getCounter()); + + $credential->save(); + } + + public static function createFromd3PublicKeyCredential(d3PublicKeyCredential $publicKeyCredential): self + { + return new self( + $publicKeyCredential->d3GetCredentialId(), + $publicKeyCredential->d3GetType(), + $publicKeyCredential->d3GetTransports(), + $publicKeyCredential->d3GetAttestationType(), + $publicKeyCredential->d3GetTrustPath(), + $publicKeyCredential->d3GetAaguid(), + $publicKeyCredential->d3GetPublicKey(), + $publicKeyCredential->d3GetUserHandle(), + $publicKeyCredential->d3GetCounter() + ); + } + + public static function createFromPublicKeyCredentialSource(publicKeyCredentialSource $publicKeyCredential): self + { + return new self( + $publicKeyCredential->getPublicKeyCredentialId(), + $publicKeyCredential->getType(), + $publicKeyCredential->getTransports(), + $publicKeyCredential->getAttestationType(), + $publicKeyCredential->getTrustPath(), + $publicKeyCredential->getAaguid(), + $publicKeyCredential->getCredentialPublicKey(), + $publicKeyCredential->getUserHandle(), + $publicKeyCredential->getCounter() + ); + } +} \ No newline at end of file diff --git a/src/Application/Model/Webauthn/d3PublicKeyCredentialSourceRepository.php b/src/Application/Model/Webauthn/d3PublicKeyCredentialSourceRepository.php new file mode 100755 index 0000000..e6c572d --- /dev/null +++ b/src/Application/Model/Webauthn/d3PublicKeyCredentialSourceRepository.php @@ -0,0 +1,88 @@ + + * @link http://www.oxidmodule.com + */ + +declare(strict_types=1); + +namespace D3\Webauthn\Application\Model\Webauthn; + +use D3\Webauthn\Application\Model\Credential\d3PublicKeyCredential; +use D3\Webauthn\Application\Model\Credential\d3PublicKeyCredentialList; +use Exception; +use OxidEsales\Eshop\Core\Exception\DatabaseConnectionException; +use OxidEsales\Eshop\Core\Exception\DatabaseErrorException; +use OxidEsales\Eshop\Core\Registry; +use Webauthn\PublicKeyCredentialSource; +use Webauthn\PublicKeyCredentialSourceRepository; +use Webauthn\PublicKeyCredentialUserEntity; + +class d3PublicKeyCredentialSourceRepository implements PublicKeyCredentialSourceRepository +{ + /** + * @param string $publicKeyCredentialId + * @return PublicKeyCredentialSource|null + * @throws DatabaseConnectionException + */ + public function findOneByCredentialId(string $publicKeyCredentialId): ?PublicKeyCredentialSource + { + if (Registry::getRequest()->getRequestEscapedParameter('fnc') == 'checkregister') { + return null; + } + + $credential = oxNew(d3PublicKeyCredential::class); + $credential->loadByCredentialId($publicKeyCredentialId); + + return $credential->getId() ? + d3PublicKeyCredentialSource::createFromd3PublicKeyCredential($credential) : + null; + } + + /** + * @param PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity + * @return array + * @throws DatabaseConnectionException + * @throws DatabaseErrorException + */ + public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array + { + $sourceList = []; + + $credentialList = oxNew(d3PublicKeyCredentialList::class); + $credentialList->loadAllForUserEntity($publicKeyCredentialUserEntity); + + /** @var d3PublicKeyCredential $credential */ + foreach ($credentialList->getArray() as $credential) { + $sourceList[$credential->getId()] = d3PublicKeyCredentialSource::createFromd3PublicKeyCredential($credential); + }; + + return $sourceList; + } + + /** + * @param PublicKeyCredentialSource $publicKeyCredentialSource + * @throws Exception + */ + public function saveCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource): void + { + $publicKeyCredentialSource = d3PublicKeyCredentialSource::createFromPublicKeyCredentialSource($publicKeyCredentialSource); + + if ($this->findOneByCredentialId($publicKeyCredentialSource->getPublicKeyCredentialId())) { + // increase counter + } else { + $publicKeyCredentialSource->saveCredential(); + } + } +} diff --git a/src/Application/Model/Webauthn/d3PublicKeyCredentialUserEntity.php b/src/Application/Model/Webauthn/d3PublicKeyCredentialUserEntity.php new file mode 100755 index 0000000..dd89463 --- /dev/null +++ b/src/Application/Model/Webauthn/d3PublicKeyCredentialUserEntity.php @@ -0,0 +1,33 @@ + + * @link http://www.oxidmodule.com + */ + +namespace D3\Webauthn\Application\Model\Webauthn; + +use OxidEsales\Eshop\Application\Model\User; +use Webauthn\PublicKeyCredentialUserEntity; + +class d3PublicKeyCredentialUserEntity extends publicKeyCredentialUserEntity +{ + public function __construct(User $user) + { + parent::__construct( + strtolower($user->getFieldData('oxfname').'.'.$user->getFieldData('oxlname')), + $user->getId(), + $user->getFieldData('oxfname').', '.$user->getFieldData('oxlname') + ); + } +} \ No newline at end of file diff --git a/src/Application/Model/d3webauthn.php b/src/Application/Model/d3webauthn.php new file mode 100755 index 0000000..2cf58e8 --- /dev/null +++ b/src/Application/Model/d3webauthn.php @@ -0,0 +1,334 @@ + + * @link http://www.oxidmodule.com + */ + +namespace D3\Webauthn\Application\Model; + +use Assert\InvalidArgumentException; +use D3\Webauthn\Application\Model\Credential\d3MetadataStatementRepository; +use D3\Webauthn\Application\Model\Exceptions\d3webauthnWrongAuthException; +use D3\Webauthn\Application\Model\Exceptions\d3webauthnMissingPublicKeyCredentialRequestOptions; +use D3\Webauthn\Application\Model\Webauthn\d3PublicKeyCredentialRpEntity; +use D3\Webauthn\Application\Model\Webauthn\d3PublicKeyCredentialSourceRepository; +use D3\Webauthn\Application\Model\Webauthn\d3PublicKeyCredentialUserEntity; +use Nyholm\Psr7\Factory\Psr17Factory; +use Nyholm\Psr7Server\ServerRequestCreator; +use OxidEsales\Eshop\Application\Model\User; +use OxidEsales\Eshop\Core\Database\Adapter\DatabaseInterface; +use OxidEsales\Eshop\Core\DatabaseProvider; +use OxidEsales\Eshop\Core\Exception\DatabaseConnectionException; +use OxidEsales\Eshop\Core\Exception\DatabaseErrorException; +use OxidEsales\Eshop\Core\Model\BaseModel; +use OxidEsales\Eshop\Core\Registry; +use Webauthn\PublicKeyCredentialCreationOptions; +use Webauthn\PublicKeyCredentialRequestOptions; +use Webauthn\Server; + +class d3webauthn extends BaseModel +{ + public $tableName = 'd3PublicKeyCredential'; + protected $_sCoreTable = 'd3PublicKeyCredential'; + public $userId; + + /** + * d3webauthn constructor. + */ + public function __construct() + { + $this->init($this->tableName); + + return parent::__construct(); + } + + /** + * @param $userId + * @throws DatabaseConnectionException + */ + public function loadByUserId($userId) + { + $this->userId = $userId; + $oDb = $this->d3GetDb(); + + if ($userId && $oDb->getOne("SHOW TABLES LIKE '".$this->tableName."'")) { + $query = "SELECT oxid FROM ".$this->getViewName().' WHERE UserHandle = '.$oDb->quote($userId).' LIMIT 1'; + $this->load($oDb->getOne($query)); + } + } + + /** + * @return DatabaseInterface + * @throws DatabaseConnectionException + */ + public function d3GetDb() + { + return DatabaseProvider::getDb(DatabaseProvider::FETCH_MODE_ASSOC); + } + + /** + * @return User + */ + public function getUser() + { + $userId = $this->userId ? $this->userId : $this->getFieldData('UserHandle'); + + $user = $this->d3GetUser(); + $user->load($userId); + return $user; + } + + /** + * @return User + */ + public function d3GetUser() + { + return oxNew(User::class); + } + + /** + * @return bool + */ + public function isActive() + { + return false == Registry::getConfig()->getConfigParam('blDisableWebauthnGlobally') + && $this->UserUseWebauthn(); + } + + /** + * @return bool + */ + public function UserUseWebauthn() + { + return strlen($this->getId()) + && strlen($this->__get($this->_getFieldLongName('publickey'))->rawValue); + } + + /** + * @param $auth + * @return false|string|null + * @throws DatabaseConnectionException + * @throws DatabaseErrorException + */ + public function getCredentialRequestOptions($auth) + { + $this->loadByUserId($auth); + + $requestOptions = null; + + if ($auth + && $this->isActive() + && false == Registry::getSession()->getVariable(d3webauthn_conf::WEBAUTHN_SESSION_AUTH) + ) { + /** @var d3PublicKeyCredentialRpEntity $rpEntity */ + $rpEntity = oxNew(d3PublicKeyCredentialRpEntity::class, Registry::getConfig()->getActiveShop()); + + $publicKeyCredentialSourceRepository = oxNew(d3PublicKeyCredentialSourceRepository::class); + + $server = new Server( + $rpEntity, + $publicKeyCredentialSourceRepository, + new d3MetadataStatementRepository() + ); + + $user = $this->getUser(); + $userEntity = new d3PublicKeyCredentialUserEntity($user); +dumpvar($userEntity); + $allowedCredentials = []; + $credentialSourceRepository = oxNew(d3PublicKeyCredentialSourceRepository::class); + /** @var d3PublicKeyCredentialSource $credentialSource */ + foreach ($credentialSourceRepository->findAllForUserEntity($userEntity) as $credentialSource) { + $allowedCredentials[] = $credentialSource->getPublicKeyCredentialDescriptor(); + } + + // We generate the set of options. + $publicKeyCredentialRequestOptions = $server->generatePublicKeyCredentialRequestOptions( + PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED, // Default value + $allowedCredentials + ); +dumpvar($publicKeyCredentialRequestOptions); + $requestOptions = json_encode($publicKeyCredentialRequestOptions, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + + Registry::getSession()->setVariable(d3webauthn_conf::WEBAUTHN_LOGIN_OBJECT, $publicKeyCredentialRequestOptions); + + // set auth as secured parameter; + Registry::getSession()->setVariable("auth", $auth); + } + + return $requestOptions; + } + + /** + * @param $webauth + * @return bool + * @throws d3webauthnWrongAuthException + * @throws d3webauthnMissingPublicKeyCredentialRequestOptions + */ + public function verify($webauth) + { + $blVerify = false; + // Retrieve the Options passed to the device + $publicKeyCredentialRequestOptions = Registry::getSession()->getVariable(d3webauthn_conf::WEBAUTHN_LOGIN_OBJECT); + + if (!$publicKeyCredentialRequestOptions) { + $oException = oxNew(d3webauthnMissingPublicKeyCredentialRequestOptions::class); + throw $oException; + } + + $psr17Factory = new Psr17Factory(); + $creator = new ServerRequestCreator( + $psr17Factory, // ServerRequestFactory + $psr17Factory, // UriFactory + $psr17Factory, // UploadedFileFactory + $psr17Factory // StreamFactory + ); + + $serverRequest = $creator->fromGlobals(); + + $publicKeyCredentialSourceRepository = oxNew(d3PublicKeyCredentialSourceRepository::class); + + $server = new Server( + new d3PublicKeyCredentialRpEntity(Registry::getConfig()->getActiveShop()), + $publicKeyCredentialSourceRepository, + new d3MetadataStatementRepository() + ); + + $user = $this->getUser(); + $userEntity = new d3PublicKeyCredentialUserEntity($user); + + try { + $server->loadAndCheckAssertionResponse( + $webauth, + $publicKeyCredentialRequestOptions, // The options you stored during the previous step + $userEntity, // The user entity + $serverRequest // The PSR-7 request + ); + $blVerify = true; + + Registry::getSession()->deleteVariable(d3webauthn_conf::WEBAUTHN_LOGIN_OBJECT); + //If everything is fine, this means the user has correctly been authenticated using the + // authenticator defined in $publicKeyCredentialSource + } catch(InvalidArgumentException $exception) { +// ToDo + $oException = oxNew(d3webauthnWrongAuthException::class); + Registry::getUtilsView()->addErrorToDisplay($oException); + // write to log + //dumpvar(openssl_error_string()); + //dumpvar($exception); + } + + if (false == $blVerify) { + $oException = oxNew(d3webauthnWrongAuthException::class); + throw $oException; + } + + return $blVerify; + } + + /** + * @param $sUserId + * @return PublicKeyCredentialCreationOptions + * @throws DatabaseConnectionException + * @throws DatabaseErrorException + */ + public function setAuthnRegister($sUserId) + { + $rpEntity = oxNew(d3PublicKeyCredentialRpEntity::class, Registry::getConfig()->getActiveShop()); + + $publicKeyCredentialSourceRepository = oxNew(d3PublicKeyCredentialSourceRepository::class); + + $server = new Server( + $rpEntity, + $publicKeyCredentialSourceRepository, + new d3MetadataStatementRepository() + ); + /* + if (!($user = Registry::getSession()->getUser())) { + $e = oxNew(\Exception::class, 'no user loaded'); + throw $e; + } + */ + $user = oxNew(User::class); + $user->load($sUserId); + + $userEntity = new d3PublicKeyCredentialUserEntity($user); + + $excludedCredentials = []; + $credentialSourceRepository = oxNew(d3PublicKeyCredentialSourceRepository::class); + foreach ($credentialSourceRepository->findAllForUserEntity($userEntity) as $credentialSource) { + $excludedCredentials[] = $credentialSource->getPublicKeyCredentialDescriptor(); + } + + $publicKeyCredentialCreationOptions = $server->generatePublicKeyCredentialCreationOptions( + $userEntity, + PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE, + $excludedCredentials + ); + + if (!Registry::getSession()->isSessionStarted()) { + Registry::getSession()->start(); + } + Registry::getSession()->setVariable('authnobject', $publicKeyCredentialCreationOptions); + + return $publicKeyCredentialCreationOptions; + } + + /** + * @param $request + */ + public function registerNewKey($request) + { + /** @var PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions */ + $publicKeyCredentialCreationOptions = Registry::getSession()->getVariable('authnobject'); + + // Retrieve de data sent by the device + $data = base64_decode($request, true); + + $psr17Factory = new Psr17Factory(); + $creator = new ServerRequestCreator( + $psr17Factory, // ServerRequestFactory + $psr17Factory, // UriFactory + $psr17Factory, // UploadedFileFactory + $psr17Factory // StreamFactory + ); + + $serverRequest = $creator->fromGlobals(); + + /*** register ***/ + $rpEntity = oxNew(d3PublicKeyCredentialRpEntity::class, Registry::getConfig()->getActiveShop()); + + $publicKeyCredentialSourceRepository = oxNew(d3PublicKeyCredentialSourceRepository::class); + + $server = new Server( + $rpEntity, + $publicKeyCredentialSourceRepository, + new d3MetadataStatementRepository() + ); + + try { + $publicKeyCredentialSource = $server->loadAndCheckAttestationResponse( + $data, + $publicKeyCredentialCreationOptions, // The options you stored during the previous step + $serverRequest // The PSR-7 request + ); + + // The user entity and the public key credential source can now be stored using their repository + // The Public Key Credential Source repository must implement Webauthn\PublicKeyCredentialSourceRepository +// ToDo: is counter set and why will not save in case of login? + $publicKeyCredentialSourceRepository->saveCredentialSource($publicKeyCredentialSource); + + } catch(\Exception $exception) { + dumpvar($exception); + } + dumpvar('registered'); + } +} \ No newline at end of file diff --git a/src/Application/Model/d3webauthn_conf.php b/src/Application/Model/d3webauthn_conf.php new file mode 100755 index 0000000..8e9b5f8 --- /dev/null +++ b/src/Application/Model/d3webauthn_conf.php @@ -0,0 +1,25 @@ + + * @link http://www.oxidmodule.com + */ + +namespace D3\Webauthn\Application\Model; + +class d3webauthn_conf +{ + const WEBAUTHN_SESSION_AUTH = 'webauthn_auth'; + const WEBAUTHN_LOGIN_OBJECT = 'authnloginobject'; + const WEBAUTHN_SESSION_CURRENTUSER = 'd3webauthnCurrentUser'; + const WEBAUTHN_SESSION_CURRENTCLASS = 'd3webauthnCurrentClass'; + const WEBAUTHN_SESSION_NAVFORMPARAMS = 'd3webauthnNavFormParams'; +} \ No newline at end of file diff --git a/src/Application/translations/de/d3webauthn_lang.php b/src/Application/translations/de/d3webauthn_lang.php new file mode 100755 index 0000000..d658425 --- /dev/null +++ b/src/Application/translations/de/d3webauthn_lang.php @@ -0,0 +1,20 @@ + 'UTF-8', + 'D3_WEBAUTHN_ACCOUNT' => 'd3 Webauthn', + + 'WEBAUTHN_INPUT_HELP' => 'Bitte mit Hardwareschlüssel authentisieren.', + 'WEBAUTHN_CANCEL_LOGIN' => 'Anmeldung abbrechen', + 'D3_WEBAUTHN_BREADCRUMB' => 'Passwortloses Anmelden', + + 'D3_WEBAUTHN_ACCOUNT_TYPE0' => 'nur Passwort', + 'D3_WEBAUTHN_ACCOUNT_TYPE1' => 'nur Auth-Stick', + 'D3_WEBAUTHN_ACCOUNT_TYPE2' => 'nur Auth-Stick, Passwort als Alternative', + 'D3_WEBAUTHN_ACCOUNT_TYPE3' => 'Auth-Stick und Passwort in Kombination', +); diff --git a/src/Application/views/admin/blocks/d3webauthn_login_admin_login_form.tpl b/src/Application/views/admin/blocks/d3webauthn_login_admin_login_form.tpl new file mode 100755 index 0000000..78ff1dc --- /dev/null +++ b/src/Application/views/admin/blocks/d3webauthn_login_admin_login_form.tpl @@ -0,0 +1,85 @@ +[{if $request_webauthn}] + [{$oViewConf->getHiddenSid()}] + + + + + + [{if $Errors.default|@count}] + [{include file="inc_error.tpl" Errorlist=$Errors.default}] + [{/if}] + +
+
+ [{include file=$oViewConf->getModulePath('d3webauthn', 'out/img/fingerprint.svg')}] +
+
[{oxmultilang ident="WEBAUTHN_INPUT_HELP"}]
+
+ + [{* prevent cancel button (1st button) action when form is sent via Enter key *}] + + + + + [{capture name="webauthn_login"}] + function arrayToBase64String(a) { + return btoa(String.fromCharCode(...a)); + } + + function base64url2base64(input) { + input = input + .replace(/=/g, "") + .replace(/-/g, '+') + .replace(/_/g, '/'); + + const pad = input.length % 4; + if(pad) { + if(pad === 1) { + throw new Error('InvalidLengthError: Input base64url string is the wrong length to determine padding'); + } + input += new Array(5-pad).join('='); + } + + return input; + } + + let publicKey = [{$webauthn_publickey_login}]; + + publicKey.challenge = Uint8Array.from(window.atob(base64url2base64(publicKey.challenge)), function(c){return c.charCodeAt(0);}); + if (publicKey.allowCredentials) { + publicKey.allowCredentials = publicKey.allowCredentials.map(function(data) { + data.id = Uint8Array.from(window.atob(base64url2base64(data.id)), function(c){return c.charCodeAt(0);}); + return data; + }); + } + + navigator.credentials.get({ 'publicKey': publicKey }).then(function(data){ + let publicKeyCredential = { + id: data.id, + type: data.type, + rawId: arrayToBase64String(new Uint8Array(data.rawId)), + response: { + authenticatorData: arrayToBase64String(new Uint8Array(data.response.authenticatorData)), + clientDataJSON: arrayToBase64String(new Uint8Array(data.response.clientDataJSON)), + signature: arrayToBase64String(new Uint8Array(data.response.signature)), + userHandle: data.response.userHandle ? arrayToBase64String(new Uint8Array(data.response.userHandle)) : null + } + }; + document.getElementById('keyauth').value = btoa(JSON.stringify(publicKeyCredential)); + document.getElementById('login').submit(); + }) + .catch(function(error){ + // alert('Open your browser console!'); + console.log('FAIL', error); + }); + [{/capture}] + [{oxscript add=$smarty.capture.webauthn_login}] + [{oxscript}] + + [{oxstyle include=$oViewConf->getModuleUrl('d3webauthn', 'out/admin/src/css/d3webauthnlogin.css')}] + [{oxstyle}] +[{else}] + [{$smarty.block.parent}] +[{/if}] \ No newline at end of file diff --git a/src/Application/views/admin/de/d3webauthn_lang.php b/src/Application/views/admin/de/d3webauthn_lang.php new file mode 100755 index 0000000..0c1f3d8 --- /dev/null +++ b/src/Application/views/admin/de/d3webauthn_lang.php @@ -0,0 +1,53 @@ + + * @link http://www.oxidmodule.com + */ + +$sLangName = "Deutsch"; + +$aLang = [ + 'charset' => 'UTF-8', + + 'D3_WEBAUTHN_ERROR_UNVALID' => 'Der verwendete Schlüssel ist ungültig oder kann nicht geprüft werden.', + 'D3_WEBAUTHN_ERROR_MISSINGPKC' => 'Keine prüfbaren Anfrageoptionen gespeichert. Bitte führen Sie die Anmeldung noch einmal durch bzw. wenden sich an den Betreiber.', + 'WEBAUTHN_INPUT_HELP' => 'Bitte mit Hardwareschlüssel authentisieren.', + 'WEBAUTHN_CANCEL_LOGIN' => 'Anmeldung abbrechen', +/* + 'd3mxuser_totp' => '2-Faktor-Authentisierung', + + 'D3_TOTP_REGISTERNEW' => 'neue Registrierung erstellen', + 'D3_TOTP_QRCODE' => 'QR-Code', + 'D3_TOTP_QRCODE_HELP' => 'Scannen Sie diesen QR-Code mit Ihrer Authentisierungs-App, um dieses Benutzerkonto dort zu hinterlegen.', + 'D3_TOTP_SECRET' => 'QR-Code kann nicht gescannt werden?', + 'D3_TOTP_SECRET_HELP' => 'Setzen Sie keine App ein, die den QR-Code scannen kann, können Sie diese Zeichenkette auch in Ihr Authentisierungstool kopieren. Stellen Sie bitte die Passwortlänge auf 6 Zeichen und das Zeitinterval auf 30 Sekunden ein.', + 'D3_TOTP_CURROTP' => 'Bestätigung mit Einmalpasswort', + 'D3_TOTP_CURROTP_HELP' => 'Haben Sie dieses Kundenkonto in Ihrer Authentisierungs-App registriert, generieren Sie damit ein Einmalpasswort, tragen Sie es hier ein und senden das Formular direkt darauf hin ab.', + + 'D3_TOTP_REGISTEREXIST' => 'vorhandene Registrierung', + 'D3_TOTP_REGISTERDELETE' => 'Registrierung löschen', + 'D3_TOTP_REGISTERDELETE_DESC' => 'Um die Registrierung zu ändern, löschen Sie diese bitte vorerst. Sie können sofort im Anschluss eine neue Registrierung anlegen.
Wenn Sie die Registrierung löschen, ist das Konto nicht mehr durch die Zwei-Faktor-Authentisierung geschützt.', + 'D3_TOTP_REGISTERDELETED' => 'Die Registrierung wurde gelöscht.', + + 'D3_TOTP_BACKUPCODES' => 'Backupcodes', + 'D3_TOTP_BACKUPCODES_DESC' => 'Mit diesen Backupcodes können Sie sich anmelden, wenn die Generierung des Einmalpasswortes nicht möglich ist (z.B. Gerät verloren oder neu installiert). Sie können dann die Einstellungen zur Verwendung der 2-Faktor-Authentisierung ändern oder einen neuen Zugang erstellen. Speichern Sie sich diese Codes bitte in diesem Moment sicher ab. Nach Verlassen dieser Seite können diese Codes nicht erneut angezeigt werden.', + 'D3_TOTP_AVAILBACKUPCODECOUNT' => 'noch %1$s Backupcode(s) verfügbar', + 'D3_TOTP_AVAILBACKUPCODECOUNT_DESC' => 'Um neue Backupcodes zu erstellen, löschen Sie die bestehende Registrierung und legen diese bitte neu an.', + + 'D3_TOTP_SAVE' => 'Speichern', + + 'D3_TOTP_ERROR_UNVALID' => 'Das Einmalpasswort ist ungültig.', + 'D3_TOTP_ALREADY_EXIST' => 'Die Registrierung wurde schon gespeichert.', +*/ +]; diff --git a/src/Application/views/admin/en/d3webauthn_lang.php b/src/Application/views/admin/en/d3webauthn_lang.php new file mode 100755 index 0000000..26c8b46 --- /dev/null +++ b/src/Application/views/admin/en/d3webauthn_lang.php @@ -0,0 +1,53 @@ + + * @link http://www.oxidmodule.com + */ + +$sLangName = "English"; + +$aLang = [ + 'charset' => 'UTF-8', + + 'D3_WEBAUTHN_ERROR_UNVALID' => 'Der verwendete Schlüssel ist ungültig oder kann nicht geprüft werden.', + 'D3_WEBAUTHN_ERROR_MISSINGPKC' => 'Keine prüfbaren Anfrageoptionen gespeichert. Bitte führen Sie die Anmeldung noch einmal durch bzw. wenden sich an den Betreiber.', + 'WEBAUTHN_INPUT_HELP' => 'Please authenticate with hardware key.', + 'WEBAUTHN_CANCEL_LOGIN' => 'Anmeldung abbrechen', +/* + 'd3mxuser_totp' => '2-Faktor-Authentisierung', + + 'D3_TOTP_REGISTERNEW' => 'neue Registrierung erstellen', + 'D3_TOTP_QRCODE' => 'QR-Code', + 'D3_TOTP_QRCODE_HELP' => 'Scannen Sie diesen QR-Code mit Ihrer Authentisierungs-App, um dieses Benutzerkonto dort zu hinterlegen.', + 'D3_TOTP_SECRET' => 'QR-Code kann nicht gescannt werden?', + 'D3_TOTP_SECRET_HELP' => 'Setzen Sie keine App ein, die den QR-Code scannen kann, können Sie diese Zeichenkette auch in Ihr Authentisierungstool kopieren. Stellen Sie bitte die Passwortlänge auf 6 Zeichen und das Zeitinterval auf 30 Sekunden ein.', + 'D3_TOTP_CURROTP' => 'Bestätigung mit Einmalpasswort', + 'D3_TOTP_CURROTP_HELP' => 'Haben Sie dieses Kundenkonto in Ihrer Authentisierungs-App registriert, generieren Sie damit ein Einmalpasswort, tragen Sie es hier ein und senden das Formular direkt darauf hin ab.', + + 'D3_TOTP_REGISTEREXIST' => 'vorhandene Registrierung', + 'D3_TOTP_REGISTERDELETE' => 'Registrierung löschen', + 'D3_TOTP_REGISTERDELETE_DESC' => 'Um die Registrierung zu ändern, löschen Sie diese bitte vorerst. Sie können sofort im Anschluss eine neue Registrierung anlegen.
Wenn Sie die Registrierung löschen, ist das Konto nicht mehr durch die Zwei-Faktor-Authentisierung geschützt.', + 'D3_TOTP_REGISTERDELETED' => 'Die Registrierung wurde gelöscht.', + + 'D3_TOTP_BACKUPCODES' => 'Backupcodes', + 'D3_TOTP_BACKUPCODES_DESC' => 'Mit diesen Backupcodes können Sie sich anmelden, wenn die Generierung des Einmalpasswortes nicht möglich ist (z.B. Gerät verloren oder neu installiert). Sie können dann die Einstellungen zur Verwendung der 2-Faktor-Authentisierung ändern oder einen neuen Zugang erstellen. Speichern Sie sich diese Codes bitte in diesem Moment sicher ab. Nach Verlassen dieser Seite können diese Codes nicht erneut angezeigt werden.', + 'D3_TOTP_AVAILBACKUPCODECOUNT' => 'noch %1$s Backupcode(s) verfügbar', + 'D3_TOTP_AVAILBACKUPCODECOUNT_DESC' => 'Um neue Backupcodes zu erstellen, löschen Sie die bestehende Registrierung und legen diese bitte neu an.', + + 'D3_TOTP_SAVE' => 'Speichern', + + 'D3_TOTP_ERROR_UNVALID' => 'Das Einmalpasswort ist ungültig.', + 'D3_TOTP_ALREADY_EXIST' => 'Die Registrierung wurde schon gespeichert.', +*/ +]; diff --git a/src/Application/views/admin/tpl/d3user_webauthn.tpl b/src/Application/views/admin/tpl/d3user_webauthn.tpl new file mode 100755 index 0000000..af64e9b --- /dev/null +++ b/src/Application/views/admin/tpl/d3user_webauthn.tpl @@ -0,0 +1,179 @@ +[{include file="headitem.tpl" title="GENERAL_ADMIN_TITLE"|oxmultilangassign}] + +[{*assign var="webauthn" value=$edit->d3GetWebauthn()}]*}] +[{assign var="userid" value=$edit->getId()}] +[{*$webauthn->loadByUserId($userid)*}] + +[{if $readonly}] + [{assign var="readonly" value="readonly disabled"}] +[{else}] + [{assign var="readonly" value=""}] +[{/if}] + + + +
+ [{$oViewConf->getHiddenSid()}] + + +
+ +
+ [{$oViewConf->getHiddenSid()}] + + + + + + +[{* + + *}] + + [{if $sSaveError}] + + + + + +
[{oxmultilang ident=$sSaveError}]
+ [{/if}] + + [{capture name="javascripts"}] + function arrayToBase64String(a) { + return btoa(String.fromCharCode(...a)); + } + + function base64url2base64(input) { + input = input + .replace(/=/g, "") + .replace(/-/g, '+') + .replace(/_/g, '/'); + + const pad = input.length % 4; + if(pad) { + if(pad === 1) { + throw new Error('InvalidLengthError: Input base64url string is the wrong length to determine padding'); + } + input += new Array(5-pad).join('='); + } + + return input; + } + + function authnregister() { + + try { + let publicKey = [{$webauthn_publickey_register}]; +console.log('71'); + publicKey.challenge = Uint8Array.from(window.atob(base64url2base64(publicKey.challenge)), function(c){return c.charCodeAt(0);}); + publicKey.user.id = Uint8Array.from(window.atob(publicKey.user.id), function(c){return c.charCodeAt(0);}); +console.log('74'); + if (publicKey.excludeCredentials) { + publicKey.excludeCredentials = publicKey.excludeCredentials.map(function(data) { + data.id = Uint8Array.from(window.atob(base64url2base64(data.id)), function(c){return c.charCodeAt(0);}); + return data; + }); + } + + console.log('81'); + navigator.credentials.create({ 'publicKey': publicKey }).then(function(data){ +console.log('83'); + let publicKeyCredential = { + id: data.id, + type: data.type, + rawId: arrayToBase64String(new Uint8Array(data.rawId)), + response: { + clientDataJSON: arrayToBase64String(new Uint8Array(data.response.clientDataJSON)), + attestationObject: arrayToBase64String(new Uint8Array(data.response.attestationObject)) + } + }; +console.log('92'); + document.getElementById('fncname').value = 'registerNewKey'; +console.log('94'); + document.getElementById('authnvalue').value = btoa(JSON.stringify(publicKeyCredential)); +console.log('96'); + }).catch(function(error){ + console.log(error); + // document.getElementById('errorvalue').value = btoa(JSON.stringify(error)); + // document.getElementById('myedit').submit(); + }); + + } + catch (e) { + console.log(e); + } + + } + + function deleteItem(id) { + document.getElementById('fncname').value = 'deleteKey'; + document.getElementById('oxidvalue').value = id; + document.getElementById('actionform').submit(); + } + + function toggle(elementId) { + $("#" + elementId).toggle(); + } + [{/capture}] + [{oxscript add=$smarty.capture.javascripts}] + + + [{if $oxid && $oxid != '-1'}] + + + + + + + +
+ + [{block name="user_d3user_totp_form1"}] + + + + + + + [{/block}] +
+

[{oxmultilang ident="D3_TOTP_REGISTERNEW"}]

+
+ +
+
+ + [{block name="user_d3user_totp_form2"}] + + + + [{foreach from=$oView->getCredentialList($userid) item="credential"}] + + + + + [{/foreach}] + [{/block}] +
+

registered keys

+
+ + + + [{$credential->d3GetName()}] (last used: XX) + + +
+
+ [{/if}] +
+ +[{include file="bottomnaviitem.tpl"}] +[{include file="bottomitem.tpl"}] \ No newline at end of file diff --git a/src/Application/views/blocks/page/account/inc/account_menu.tpl b/src/Application/views/blocks/page/account/inc/account_menu.tpl new file mode 100755 index 0000000..744d7da --- /dev/null +++ b/src/Application/views/blocks/page/account/inc/account_menu.tpl @@ -0,0 +1,4 @@ +[{$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 new file mode 100755 index 0000000..d5a627b --- /dev/null +++ b/src/Application/views/blocks/page/shop/start_welcome_text.tpl @@ -0,0 +1,95 @@ +[{if $webauthn_publickey_register}] + [{capture name="webauthn_register"}] + function arrayToBase64String(a) { + return btoa(String.fromCharCode(...a)); + } + + function base64url2base64(input) { + input = input + .replace(/=/g, "") + .replace(/-/g, '+') + .replace(/_/g, '/'); + + const pad = input.length % 4; + if(pad) { + if(pad === 1) { + throw new Error('InvalidLengthError: Input base64url string is the wrong length to determine padding'); + } + input += new Array(5-pad).join('='); + } + + return input; + } + + function authnregister() { + let publicKey = [{$webauthn_publickey_register}]; + + publicKey.challenge = Uint8Array.from(window.atob(base64url2base64(publicKey.challenge)), function(c){return c.charCodeAt(0);}); + publicKey.user.id = Uint8Array.from(window.atob(publicKey.user.id), function(c){return c.charCodeAt(0);}); + if (publicKey.excludeCredentials) { + publicKey.excludeCredentials = publicKey.excludeCredentials.map(function(data) { + data.id = Uint8Array.from(window.atob(base64url2base64(data.id)), function(c){return c.charCodeAt(0);}); + return data; + }); + } + + navigator.credentials.create({ 'publicKey': publicKey }) + .then(function(data){ + let publicKeyCredential = { + id: data.id, + type: data.type, + rawId: arrayToBase64String(new Uint8Array(data.rawId)), + response: { + clientDataJSON: arrayToBase64String(new Uint8Array(data.response.clientDataJSON)), + attestationObject: arrayToBase64String(new Uint8Array(data.response.attestationObject)) + } + }; + window.location = 'index.php?cl=start&fnc=checkregister&authn='+btoa(JSON.stringify(publicKeyCredential)); + }).catch(function(error){ + //alert('Open your browser console!'); + console.log('FAIL', error); + } + ); + } + [{/capture}] + [{oxscript add=$smarty.capture.webauthn_register}] + + + [{capture name="webauthn_login"}] + function authnlogin() { + let publicKey = [{$webauthn_publickey_login}]; + + publicKey.challenge = Uint8Array.from(window.atob(base64url2base64(publicKey.challenge)), function(c){return c.charCodeAt(0);}); + if (publicKey.allowCredentials) { + publicKey.allowCredentials = publicKey.allowCredentials.map(function(data) { + data.id = Uint8Array.from(window.atob(base64url2base64(data.id)), function(c){return c.charCodeAt(0);}); + return data; + }); + } + + navigator.credentials.get({ 'publicKey': publicKey }) + .then(function(data){ + let publicKeyCredential = { + id: data.id, + type: data.type, + rawId: arrayToBase64String(new Uint8Array(data.rawId)), + response: { + authenticatorData: arrayToBase64String(new Uint8Array(data.response.authenticatorData)), + clientDataJSON: arrayToBase64String(new Uint8Array(data.response.clientDataJSON)), + signature: arrayToBase64String(new Uint8Array(data.response.signature)), + userHandle: data.response.userHandle ? arrayToBase64String(new Uint8Array(data.response.userHandle)) : null + } + }; + window.location = 'index.php?cl=start&fnc=checklogin&authn='+btoa(JSON.stringify(publicKeyCredential)); + }) + .catch(function(error){ + // alert('Open your browser console!'); + console.log('FAIL', error); + }); + } + [{/capture}] + [{oxscript add=$smarty.capture.webauthn_login}] + +[{/if}] + +[{$smarty.block.parent}] \ No newline at end of file diff --git a/src/Application/views/tpl/d3_account_webauthn.tpl b/src/Application/views/tpl/d3_account_webauthn.tpl new file mode 100755 index 0000000..e435edb --- /dev/null +++ b/src/Application/views/tpl/d3_account_webauthn.tpl @@ -0,0 +1,256 @@ +[{capture append="oxidBlock_content"}] + + [{capture name="javascripts"}] + function arrayToBase64String(a) { + return btoa(String.fromCharCode(...a)); + } + + function base64url2base64(input) { + input = input + .replace(/=/g, "") + .replace(/-/g, '+') + .replace(/_/g, '/'); + + const pad = input.length % 4; + if(pad) { + if(pad === 1) { + throw new Error('InvalidLengthError: Input base64url string is the wrong length to determine padding'); + } + input += new Array(5-pad).join('='); + } + + return input; + } + + function authnregister() { + let publicKey = [{$webauthn_publickey_register}]; + + publicKey.challenge = Uint8Array.from(window.atob(base64url2base64(publicKey.challenge)), function(c){return c.charCodeAt(0);}); + publicKey.user.id = Uint8Array.from(window.atob(publicKey.user.id), function(c){return c.charCodeAt(0);}); + if (publicKey.excludeCredentials) { + publicKey.excludeCredentials = publicKey.excludeCredentials.map(function(data) { + data.id = Uint8Array.from(window.atob(base64url2base64(data.id)), function(c){return c.charCodeAt(0);}); + return data; + }); + } + + navigator.credentials.create({ 'publicKey': publicKey }).then(function(data){ + let publicKeyCredential = { + id: data.id, + type: data.type, + rawId: arrayToBase64String(new Uint8Array(data.rawId)), + response: { + clientDataJSON: arrayToBase64String(new Uint8Array(data.response.clientDataJSON)), + attestationObject: arrayToBase64String(new Uint8Array(data.response.attestationObject)) + } + }; + document.getElementById('fncname').value = 'registerNewKey'; + document.getElementById('authnvalue').value = btoa(JSON.stringify(publicKeyCredential)); + document.getElementById('actionform').submit(); + }).catch(function(error){ + document.getElementById('errorvalue').value = btoa(JSON.stringify(error)); + document.getElementById('actionform').submit(); + }); + } + + function deleteItem(id) { + document.getElementById('fncname').value = 'deleteKey'; + document.getElementById('oxidvalue').value = id; + document.getElementById('actionform').submit(); + } + + function toggle(elementId) { + $("#" + elementId).toggle(); + } + [{/capture}] + [{oxscript add=$smarty.capture.javascripts}] + +

[{oxmultilang ident="D3_WEBAUTHN_ACCOUNT"}]

+ +
+ +
+ +
+
settings
+
+
+
+ +
+
+
+ Sicherheit +
+
+
+
+
+ +
+
+
+ Sicherheit +
+
+
+
+
+ +
+
+
+ Sicherheit +
+
+
+
+
+ +
+
+
+ Sicherheit +
+
+
+
+
+ +
+
registration
+
+ +
+
+ +
+
registered keys
+
+
+ [{foreach from=$oView->getCredentialList() item="credential"}] + + [{$credential->d3GetName()}] (last used: XX) + + + [{/foreach}] +
+
+
+ + [{if 1 == 0 && false == $totp->getId()}] +
+
+ [{oxmultilang ident="D3_TOTP_REGISTERNEW"}] +
+
+
+
+ [{oxmultilang ident="D3_TOTP_QRCODE"}]  +
+
+ [{$totp->getQrCodeElement()}] +
+
+

+ [{oxmultilang ident="D3_TOTP_QRCODE_HELP"}] +

+ +
+ +
+
+ +
+
+ +
+
+

+ [{oxmultilang ident="D3_TOTP_SECRET_HELP"}] +

+ +
+ +
+
+ +
+
+ +
+
+

+ [{oxmultilang ident="D3_TOTP_CURROTP_HELP"}] +

+
+
+ [{/if}] + + [{if 1 == 0 && $totp->getId()}] + [{block name="d3_account_totp_deletenotes"}] +
+
+ [{oxmultilang ident="D3_TOTP_REGISTEREXIST"}] +
+
+ [{oxmultilang ident="D3_TOTP_REGISTERDELETE_DESC"}] +
+
+ [{/block}] + + [{block name="d3_account_totp_backupcodes"}] +
+
+ [{oxmultilang ident="D3_TOTP_BACKUPCODES"}] +
+
+ [{if $oView->getBackupCodes()}] + [{block name="d3_account_totp_backupcodes_list"}] + + + [{/block}] + [{else}] + [{block name="d3_account_totp_backupcodes_info"}] + [{oxmultilang ident="D3_TOTP_AVAILBACKUPCODECOUNT" args=$oView->getAvailableBackupCodeCount()}]
+ [{oxmultilang ident="D3_TOTP_AVAILBACKUPCODECOUNT_DESC"}] + [{/block}] + [{/if}] +
+
+ [{/block}] + [{/if}] +[{* +

+ +

+ + [{/block}] +*}] +[{/capture}] + +[{capture append="oxidBlock_sidebar"}] + [{include file="page/account/inc/account_menu.tpl" active_link="d3webauthn"}] +[{/capture}] +[{include file="layout/page.tpl" sidebar="Left"}] \ No newline at end of file diff --git a/src/Application/views/tpl/d3webauthnlogin.tpl b/src/Application/views/tpl/d3webauthnlogin.tpl new file mode 100755 index 0000000..b56f8b3 --- /dev/null +++ b/src/Application/views/tpl/d3webauthnlogin.tpl @@ -0,0 +1,112 @@ +[{capture append="oxidBlock_content"}] + [{assign var="template_title" value=""}] + + [{if $oView->previousClassIsOrderStep()}] + [{* ordering steps *}] + [{include file="page/checkout/inc/steps.tpl" active=2}] + [{/if}] + +
+
+
+ [{$oViewConf->getHiddenSid()}] + + + + + [{$navFormParams}] + + [{if $Errors.default|@count}] + [{include file="inc_error.tpl" Errorlist=$Errors.default}] + [{/if}] + +
+
+ [{include file=$oViewConf->getModulePath('d3webauthn', 'out/img/fingerprint.svg')}] +
+
[{oxmultilang ident="WEBAUTHN_INPUT_HELP"}]
+
+ + [{* prevent cancel button (1st button) action when form is sent via Enter key *}] + + +
+
+ [{$oViewConf->getHiddenSid()}] + + + + [{$navFormParams}] + + + +
+
+
+ + [{if $webauthn_publickey_login}] + [{capture name="webauthn_login"}] + function arrayToBase64String(a) { + return btoa(String.fromCharCode(...a)); + } + + function base64url2base64(input) { + input = input + .replace(/=/g, "") + .replace(/-/g, '+') + .replace(/_/g, '/'); + + const pad = input.length % 4; + if(pad) { + if(pad === 1) { + throw new Error('InvalidLengthError: Input base64url string is the wrong length to determine padding'); + } + input += new Array(5-pad).join('='); + } + + return input; + } + + let publicKey = [{$webauthn_publickey_login}]; + + publicKey.challenge = Uint8Array.from(window.atob(base64url2base64(publicKey.challenge)), function(c){return c.charCodeAt(0);}); + if (publicKey.allowCredentials) { + publicKey.allowCredentials = publicKey.allowCredentials.map(function(data) { + data.id = Uint8Array.from(window.atob(base64url2base64(data.id)), function(c){return c.charCodeAt(0);}); + return data; + }); + } + + navigator.credentials.get({ 'publicKey': publicKey }).then(function(data){ + let publicKeyCredential = { + id: data.id, + type: data.type, + rawId: arrayToBase64String(new Uint8Array(data.rawId)), + response: { + authenticatorData: arrayToBase64String(new Uint8Array(data.response.authenticatorData)), + clientDataJSON: arrayToBase64String(new Uint8Array(data.response.clientDataJSON)), + signature: arrayToBase64String(new Uint8Array(data.response.signature)), + userHandle: data.response.userHandle ? arrayToBase64String(new Uint8Array(data.response.userHandle)) : null + } + }; + document.getElementById('keyauth').value = btoa(JSON.stringify(publicKeyCredential)); + document.getElementById('webauthnlogin').submit(); + }) + .catch(function(error){ + // alert('Open your browser console!'); + console.log('FAIL', error); + }); + [{/capture}] + [{oxscript add=$smarty.capture.webauthn_login}] + [{oxscript}] + [{/if}] + + [{oxstyle include=$oViewConf->getModuleUrl('d3webauthn', 'out/flow/src/css/d3webauthnlogin.css')}] + [{oxstyle}] + + [{insert name="oxid_tracker" title=$template_title}] +[{/capture}] + +[{include file="layout/page.tpl"}] \ No newline at end of file diff --git a/src/IntelliSenseHelper.php b/src/IntelliSenseHelper.php new file mode 100755 index 0000000..8cfc133 --- /dev/null +++ b/src/IntelliSenseHelper.php @@ -0,0 +1,61 @@ + + * @link http://www.oxidmodule.com + */ + +namespace D3\Webauthn\Modules\Application\Component +{ + use OxidEsales\Eshop\Application\Component\UserComponent; + + class d3_webauthn_UserComponent_parent extends UserComponent {} +} + +namespace D3\Webauthn\Modules\Application\Controller +{ + + 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_StartController_Webauthn_parent extends StartController {} + + class d3_webauthn_UserController_parent extends UserController {} + + class d3_webauthn_OrderController_parent extends OrderController {} + + class d3_webauthn_PaymentController_parent extends PaymentController {} +} + +namespace D3\Webauthn\Modules\Application\Controller\Admin +{ + use OxidEsales\Eshop\Application\Controller\Admin\LoginController; + + class d3_LoginController_Webauthn_parent extends LoginController {} +} + +namespace D3\Webauthn\Modules\Application\Model +{ + use OxidEsales\Eshop\Application\Model\User; + + class d3_User_Webauthn_parent extends User {} +} + +namespace D3\Webauthn\Modules\Core +{ + use OxidEsales\Eshop\Core\Utils; + + class d3_webauthn_utils_parent extends Utils {} +} \ No newline at end of file diff --git a/src/Modules/Application/Component/d3_webauthn_UserComponent.php b/src/Modules/Application/Component/d3_webauthn_UserComponent.php new file mode 100755 index 0000000..070b307 --- /dev/null +++ b/src/Modules/Application/Component/d3_webauthn_UserComponent.php @@ -0,0 +1,204 @@ + + * @link http://www.oxidmodule.com + */ + +namespace D3\Webauthn\Modules\Application\Component; + +use D3\Webauthn\Application\Model\d3webauthn; +use D3\Webauthn\Application\Model\d3webauthn_conf; +use D3\Webauthn\Application\Model\Exceptions\d3webauthnMissingPublicKeyCredentialRequestOptions; +use D3\Webauthn\Application\Model\Exceptions\d3webauthnWrongAuthException; +use D3\Webauthn\Modules\Application\Model\d3_User_Webauthn; +use Doctrine\DBAL\DBALException; +use OxidEsales\Eshop\Application\Model\User; +use OxidEsales\Eshop\Core\DatabaseProvider; +use OxidEsales\Eshop\Core\Exception\DatabaseConnectionException; +use OxidEsales\Eshop\Core\Registry; +use OxidEsales\Eshop\Core\Session; +use OxidEsales\Eshop\Core\UtilsView; + +class d3_webauthn_UserComponent extends d3_webauthn_UserComponent_parent +{ + /** + * @return string|void + * @throws DBALException + * @throws DatabaseConnectionException + */ + public function login_noredirect() + { + $sUser = Registry::getRequest()->getRequestParameter('lgn_usr'); + $oUser = oxNew(User::class); + $q = "SELECT * FROM ".$oUser->getViewName()." WHERE oxusername = ? and oxshopid = ?"; + $userId = DatabaseProvider::getDb()->getOne( + $q, + array($sUser, Registry::getConfig()->getActiveShop()->getId()) + ); + + if ($sUser) { + $webauthn = $this->d3GetWebauthnObject(); + $webauthn->loadByUserId($userId); + if ($webauthn->isActive() + && false == Registry::getSession()->getVariable(d3webauthn_conf::WEBAUTHN_SESSION_AUTH) + ) { + Registry::getSession()->setVariable( + d3webauthn_conf::WEBAUTHN_SESSION_CURRENTCLASS, + $this->getParent()->getClassKey() != 'd3webauthnlogin' ? $this->getParent()->getClassKey() : 'start'); + Registry::getSession()->setVariable(d3webauthn_conf::WEBAUTHN_SESSION_CURRENTUSER, $oUser->getId()); + Registry::getSession()->setVariable( + d3webauthn_conf::WEBAUTHN_SESSION_NAVFORMPARAMS, + $this->getParent()->getViewConfig()->getNavFormParams() + ); + + //$oUser->d3templogout(); + + return "d3webauthnlogin"; + } + } + + parent::login_noredirect(); + + /** @var d3_User_Webauthn $oUser */ +/* + $oUser = $this->getUser(); + + if ($oUser && $oUser->getId()) { + $webauthn = $this->d3GetWebauthnObject(); + $webauthn->loadByUserId($oUser->getId()); + + if ($webauthn->isActive() + && false == Registry::getSession()->getVariable(d3webauthn_conf::WEBAUTHN_SESSION_AUTH) + ) { + Registry::getSession()->setVariable( + d3webauthn_conf::WEBAUTHN_SESSION_CURRENTCLASS, + $this->getParent()->getClassKey() != 'd3webauthnlogin' ? $this->getParent()->getClassKey() : 'start'); + Registry::getSession()->setVariable(d3webauthn_conf::WEBAUTHN_SESSION_CURRENTUSER, $oUser->getId()); + Registry::getSession()->setVariable( + d3webauthn_conf::WEBAUTHN_SESSION_NAVFORMPARAMS, + $this->getParent()->getViewConfig()->getNavFormParams() + ); + + $oUser->d3templogout(); + + return "d3webauthnlogin"; + } + } +*/ + } + + /** + * @return d3webauthn + */ + public function d3GetWebauthnObject() + { + return oxNew(d3webauthn::class); + } + + /** + * @return bool|string + * @throws DatabaseConnectionException + * @throws d3webauthnMissingPublicKeyCredentialRequestOptions + */ + public function checkWebauthnlogin() + { + $sWebauth = base64_decode(Registry::getRequest()->getRequestParameter('keyauth')); + + $sUserId = Registry::getSession()->getVariable(d3webauthn_conf::WEBAUTHN_SESSION_CURRENTUSER); + $oUser = oxNew(User::class); + $oUser->load($sUserId); + + $webauthn = $this->d3GetWebauthnObject(); + $webauthn->loadByUserId($sUserId); + + try { + if (false == $this->isNoWebauthnOrNoLogin($webauthn) && $this->hasValidWebauthn($sWebauth, $webauthn)) { + $this->d3WebauthnRelogin($oUser, $sWebauth); + $this->d3WebauthnClearSessionVariables(); + + return false; + } + } catch (d3webauthnWrongAuthException $oEx) { + $this->d3GetUtilsView()->addErrorToDisplay($oEx, false, false, "", 'd3webauthnlogin'); + } + + return 'd3webauthnlogin'; + } + + /** + * @return UtilsView + */ + public function d3GetUtilsView() + { + return Registry::getUtilsView(); + } + + public function cancelWebauthnLogin() + { + $this->d3WebauthnClearSessionVariables(); + + return false; + } + + /** + * @param d3webauthn $webauthn + * @return bool + */ + public function isNoWebauthnOrNoLogin($webauthn) + { + return false == $this->d3GetSession()->getVariable("auth") + || false == $webauthn->isActive(); + } + + /** + * @param string $sWebauth + * @param d3webauthn $webauthn + * @return bool + * @throws d3webauthnMissingPublicKeyCredentialRequestOptions + * @throws d3webauthnWrongAuthException + */ + public function hasValidWebauthn($sWebauth, $webauthn) + { + return Registry::getSession()->getVariable(d3webauthn_conf::WEBAUTHN_SESSION_AUTH) || + ( + $sWebauth && $webauthn->verify($sWebauth) + ); + } + + /** + * @param User $oUser + * @param $sWebauthn + */ + public function d3WebauthnRelogin(User $oUser, $sWebauthn) + { + $this->d3GetSession()->setVariable(d3webauthn_conf::WEBAUTHN_SESSION_AUTH, $sWebauthn); + $this->d3GetSession()->setVariable('usr', $oUser->getId()); + $this->setUser(null); + $this->setLoginStatus(USER_LOGIN_SUCCESS); + $this->_afterLogin($oUser); + } + + public function d3WebauthnClearSessionVariables() + { + $this->d3GetSession()->deleteVariable(d3webauthn_conf::WEBAUTHN_SESSION_CURRENTCLASS); + $this->d3GetSession()->deleteVariable(d3webauthn_conf::WEBAUTHN_SESSION_CURRENTUSER); + $this->d3GetSession()->deleteVariable(d3webauthn_conf::WEBAUTHN_SESSION_NAVFORMPARAMS); + } + + /** + * @return Session + */ + public function d3GetSession() + { + return Registry::getSession(); + } +} \ No newline at end of file diff --git a/src/Modules/Application/Controller/Admin/d3_LoginController_Webauthn.php b/src/Modules/Application/Controller/Admin/d3_LoginController_Webauthn.php new file mode 100755 index 0000000..824954d --- /dev/null +++ b/src/Modules/Application/Controller/Admin/d3_LoginController_Webauthn.php @@ -0,0 +1,148 @@ + + * @link http://www.oxidmodule.com + */ + +namespace D3\Webauthn\Modules\Application\Controller\Admin; + +use D3\Webauthn\Application\Model\d3webauthn; +use D3\Webauthn\Application\Model\d3webauthn_conf; +use D3\Webauthn\Application\Model\Exceptions\d3WebauthnExceptionAbstract; +use D3\Webauthn\Application\Model\Exceptions\d3webauthnMissingPublicKeyCredentialRequestOptions; +use D3\Webauthn\Application\Model\Exceptions\d3webauthnWrongAuthException; +use OxidEsales\Eshop\Application\Model\User; +use OxidEsales\Eshop\Core\Exception\DatabaseConnectionException; +use OxidEsales\Eshop\Core\Exception\DatabaseErrorException; +use OxidEsales\Eshop\Core\Registry; +use OxidEsales\Eshop\Core\Session; +use OxidEsales\Eshop\Core\UtilsView; + +class d3_LoginController_Webauthn extends d3_LoginController_Webauthn_parent +{ + /** + * @return string + * @throws DatabaseConnectionException + * @throws DatabaseErrorException + */ + public function render() + { + $auth = $this->d3GetSession()->getVariable("auth"); + + $return = parent::render(); + + if ($auth) { + $webauthn = $this->d3GetWebauthnObject(); + $publicKeyCredentialRequestOptions = $webauthn->getCredentialRequestOptions($auth); + + $this->addTplParam( + 'webauthn_publickey_login', + $publicKeyCredentialRequestOptions + ); + + $this->addTplParam('request_webauthn', true); + } + + return $return; + } + + /** + * @return d3webauthn + */ + public function d3GetWebauthnObject() + { + return oxNew(d3webauthn::class); + } + + /** + * @return UtilsView + */ + public function d3GetUtilsView() + { + return Registry::getUtilsView(); + } + + /** + * @return Session + */ + public function d3GetSession() + { + return Registry::getSession(); + } + + /** + * @return mixed|string + * @throws DatabaseConnectionException + */ + public function checklogin() + { + //$sWebauth = Registry::getRequest()->getRequestEscapedParameter('keyauth'); + $sWebauth = base64_decode(Registry::getRequest()->getRequestParameter('keyauth')); + + $webauthn = $this->d3GetWebauthnObject(); + $webauthn->loadByUserId(Registry::getSession()->getVariable("auth")); + + $return = 'login'; + + try { + if ($this->isNoWebauthnOrNoLogin($webauthn)) { + $return = parent::checklogin(); + } elseif ($this->hasValidWebauthn($sWebauth, $webauthn)) { + $this->d3GetSession()->setVariable(d3webauthn_conf::WEBAUTHN_SESSION_AUTH, $sWebauth); + $return = "admin_start"; + } + } catch (d3webauthnExceptionAbstract $oEx) { + $this->d3GetUtilsView()->addErrorToDisplay($oEx); + } + + return $return; + } + + /** + * @param d3webauthn $webauthn + * @return bool + */ + public function isNoWebauthnOrNoLogin($webauthn) + { + return false == $this->d3GetSession()->getVariable("auth") + || false == $webauthn->isActive(); + } + + /** + * @param string $sWebauth + * @param d3webauthn $webauthn + * @return bool + * @throws d3webauthnMissingPublicKeyCredentialRequestOptions + * @throws d3webauthnWrongAuthException + */ + public function hasValidWebauthn($sWebauth, $webauthn) + { + return Registry::getSession()->getVariable(d3webauthn_conf::WEBAUTHN_SESSION_AUTH) || + ( + $sWebauth && $webauthn->verify($sWebauth) + ); + } + + public function d3WebauthnCancelLogin() + { + $oUser = $this->d3GetUserObject(); + $oUser->logout(); + } + + /** + * @return User + */ + public function d3GetUserObject() + { + return oxNew(User::class); + } +} \ No newline at end of file diff --git a/src/Modules/Application/Controller/d3_StartController_Webauthn.php b/src/Modules/Application/Controller/d3_StartController_Webauthn.php new file mode 100755 index 0000000..46b6009 --- /dev/null +++ b/src/Modules/Application/Controller/d3_StartController_Webauthn.php @@ -0,0 +1,214 @@ + + * @link http://www.oxidmodule.com + */ + +namespace D3\Webauthn\Modules\Application\Controller; + +use D3\Webauthn\Application\Model\Credential\d3MetadataStatementRepository; +use D3\Webauthn\Application\Model\Webauthn\d3PublicKeyCredentialRpEntity; +use D3\Webauthn\Application\Model\Webauthn\d3PublicKeyCredentialSourceRepository; +use D3\Webauthn\Application\Model\Webauthn\d3PublicKeyCredentialUserEntity; +use Nyholm\Psr7\Factory\Psr17Factory; +use Nyholm\Psr7Server\ServerRequestCreator; +use OxidEsales\Eshop\Application\Model\User; +use OxidEsales\Eshop\Core\Exception\DatabaseConnectionException; +use OxidEsales\Eshop\Core\Exception\DatabaseErrorException; +use OxidEsales\Eshop\Core\Registry; +use Webauthn\PublicKeyCredentialCreationOptions; +use Webauthn\PublicKeyCredentialRequestOptions; +use Webauthn\Server; + +class d3_StartController_Webauthn extends d3_StartController_Webauthn_parent +{ + /** + * @return string + * @throws DatabaseConnectionException + * @throws DatabaseErrorException + */ + public function render() + { + if (!Registry::getRequest()->getRequestEscapedParameter('authn')) { + /*** register ***/ + $rpEntity = oxNew(d3PublicKeyCredentialRpEntity::class, Registry::getConfig()->getActiveShop()); + + $publicKeyCredentialSourceRepository = oxNew(d3PublicKeyCredentialSourceRepository::class); + + $server = new Server( + $rpEntity, + $publicKeyCredentialSourceRepository, + new d3MetadataStatementRepository() + ); +/* + if (!($user = Registry::getSession()->getUser())) { + $e = oxNew(\Exception::class, 'no user loaded'); + throw $e; + } +*/ + $user = oxNew(User::class); + //$user->load('oxdefaultadmin'); + $user->load('36944b76d6e583fe2.12734046'); + + $userEntity = new d3PublicKeyCredentialUserEntity($user); + + $excludedCredentials = []; + $credentialSourceRepository = oxNew(d3PublicKeyCredentialSourceRepository::class); + foreach ($credentialSourceRepository->findAllForUserEntity($userEntity) as $credentialSource) { + $excludedCredentials[] = $credentialSource->getPublicKeyCredentialDescriptor(); + } + + $publicKeyCredentialCreationOptions = $server->generatePublicKeyCredentialCreationOptions( + $userEntity, + PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE, + $excludedCredentials + ); + + $this->addTplParam( + 'webauthn_publickey_register', + json_encode($publicKeyCredentialCreationOptions, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) + ); + + if (!Registry::getSession()->isSessionStarted()) { + Registry::getSession()->start(); + } + Registry::getSession()->setVariable('authnobject', $publicKeyCredentialCreationOptions); + + /*** login ***/ + + $allowedCredentials = []; + $credentialSourceRepository = oxNew(d3PublicKeyCredentialSourceRepository::class); + foreach ($credentialSourceRepository->findAllForUserEntity($userEntity) as $credentialSource) { + $allowedCredentials[] = $credentialSource->getPublicKeyCredentialDescriptor(); + } + + // We generate the set of options. + $publicKeyCredentialRequestOptions = $server->generatePublicKeyCredentialRequestOptions( + PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED, // Default value + $allowedCredentials + ); + + $this->addTplParam( + 'webauthn_publickey_login', + json_encode($publicKeyCredentialRequestOptions, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) + ); + + Registry::getSession()->setVariable('authnloginobject', $publicKeyCredentialRequestOptions); + } + + $return = parent::render(); + + return $return; + } + + public function checkregister() + { + // Retrieve the PublicKeyCredentialCreationOptions object created earlier + /** @var PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions */ + $publicKeyCredentialCreationOptions = Registry::getSession()->getVariable('authnobject'); + + // Retrieve de data sent by the device + $data = base64_decode(Registry::getRequest()->getRequestParameter('authn'), true); + + $psr17Factory = new Psr17Factory(); + $creator = new ServerRequestCreator( + $psr17Factory, // ServerRequestFactory + $psr17Factory, // UriFactory + $psr17Factory, // UploadedFileFactory + $psr17Factory // StreamFactory + ); + + $serverRequest = $creator->fromGlobals(); + + /*** register ***/ + $rpEntity = oxNew(d3PublicKeyCredentialRpEntity::class, Registry::getConfig()->getActiveShop()); + + $publicKeyCredentialSourceRepository = oxNew(d3PublicKeyCredentialSourceRepository::class); + + $server = new Server( + $rpEntity, + $publicKeyCredentialSourceRepository, + new d3MetadataStatementRepository() + ); + + try { + $publicKeyCredentialSource = $server->loadAndCheckAttestationResponse( + $data, + $publicKeyCredentialCreationOptions, // The options you stored during the previous step + $serverRequest // The PSR-7 request + ); + + // The user entity and the public key credential source can now be stored using their repository + // The Public Key Credential Source repository must implement Webauthn\PublicKeyCredentialSourceRepository + $publicKeyCredentialSourceRepository->saveCredentialSource($publicKeyCredentialSource); + + } catch(\Exception $exception) { + dumpvar($exception); + } + dumpvar('registered'); + } + + public function checklogin() + { + // Retrieve the Options passed to the device + $publicKeyCredentialRequestOptions = Registry::getSession()->getVariable('authnloginobject'); + + if (!$publicKeyCredentialRequestOptions) { + return; + } + + $psr17Factory = new Psr17Factory(); + $creator = new ServerRequestCreator( + $psr17Factory, // ServerRequestFactory + $psr17Factory, // UriFactory + $psr17Factory, // UploadedFileFactory + $psr17Factory // StreamFactory + ); + + $serverRequest = $creator->fromGlobals(); + + // Retrieve de data sent by the device + $data = base64_decode(Registry::getRequest()->getRequestParameter('authn')); + + $publicKeyCredentialSourceRepository = oxNew(d3PublicKeyCredentialSourceRepository::class); + + $server = new Server( + new d3PublicKeyCredentialRpEntity(Registry::getConfig()->getActiveShop()), + $publicKeyCredentialSourceRepository, + new d3MetadataStatementRepository() + ); + + $user = oxNew(User::class); + //$user->load('oxdefaultadmin'); + $user->load('36944b76d6e583fe2.12734046'); + + $userEntity = new d3PublicKeyCredentialUserEntity($user); + + try { + $publicKeyCredentialSource = $server->loadAndCheckAssertionResponse( + $data, + $publicKeyCredentialRequestOptions, // The options you stored during the previous step + $userEntity, // The user entity + $serverRequest // The PSR-7 request + ); + + //If everything is fine, this means the user has correctly been authenticated using the + // authenticator defined in $publicKeyCredentialSource + } catch(\Throwable $exception) { + dumpvar(openssl_error_string()); + dumpvar($exception); + } + + dumpvar('logged in'); + + } +} \ No newline at end of file diff --git a/src/Modules/Application/Controller/d3_webauthn_OrderController.php b/src/Modules/Application/Controller/d3_webauthn_OrderController.php new file mode 100755 index 0000000..e4ad8b7 --- /dev/null +++ b/src/Modules/Application/Controller/d3_webauthn_OrderController.php @@ -0,0 +1,22 @@ + + * @link http://www.oxidmodule.com + */ + +namespace D3\Webauthn\Modules\Application\Controller; + + +class d3_webauthn_OrderController extends d3_webauthn_OrderController_parent +{ + use d3_webauthn_getUserTrait; +} \ No newline at end of file diff --git a/src/Modules/Application/Controller/d3_webauthn_PaymentController.php b/src/Modules/Application/Controller/d3_webauthn_PaymentController.php new file mode 100755 index 0000000..bc16dac --- /dev/null +++ b/src/Modules/Application/Controller/d3_webauthn_PaymentController.php @@ -0,0 +1,22 @@ + + * @link http://www.oxidmodule.com + */ + +namespace D3\Webauthn\Modules\Application\Controller; + + +class d3_webauthn_PaymentController extends d3_webauthn_PaymentController_parent +{ + use d3_webauthn_getUserTrait; +} \ No newline at end of file diff --git a/src/Modules/Application/Controller/d3_webauthn_UserController.php b/src/Modules/Application/Controller/d3_webauthn_UserController.php new file mode 100755 index 0000000..3a67ee0 --- /dev/null +++ b/src/Modules/Application/Controller/d3_webauthn_UserController.php @@ -0,0 +1,22 @@ + + * @link http://www.oxidmodule.com + */ + +namespace D3\Webauthn\Modules\Application\Controller; + + +class d3_webauthn_UserController extends d3_webauthn_UserController_parent +{ + use d3_webauthn_getUserTrait; +} \ No newline at end of file diff --git a/src/Modules/Application/Controller/d3_webauthn_getUserTrait.php b/src/Modules/Application/Controller/d3_webauthn_getUserTrait.php new file mode 100755 index 0000000..f99d96a --- /dev/null +++ b/src/Modules/Application/Controller/d3_webauthn_getUserTrait.php @@ -0,0 +1,66 @@ + + * @link http://www.oxidmodule.com + */ + +namespace D3\Webauthn\Modules\Application\Controller; + +use D3\Webauthn\Application\Model\d3webauthn; +use D3\Webauthn\Application\Model\d3webauthn_conf; +use Doctrine\DBAL\DBALException; +use OxidEsales\Eshop\Application\Model\User; +use OxidEsales\Eshop\Core\Exception\DatabaseConnectionException; +use OxidEsales\Eshop\Core\Registry; +use OxidEsales\Eshop\Core\Session; + +trait d3_webauthn_getUserTrait +{ + /** + * @return bool|object|User + * @throws DatabaseConnectionException + * @throws DBALException + */ + public function getUser() + { + $oUser = parent::getUser(); + + if ($oUser && $oUser->getId()) { + $webauthn = $this->d3GetWebauthnpObject(); + $webauthn->loadByUserId($oUser->getId()); + + if ($webauthn->isActive() + && false == $this->d3GetSessionObject()->getVariable(d3webauthn_conf::WEBAUTHN_SESSION_AUTH) + ) { + return false; + } + } + + return $oUser; + } + + /** + * @return d3webauthn + */ + public function d3GetWebauthnpObject() + { + return oxNew(d3webauthn::class); + } + + /** + * @return Session + */ + public function d3GetSessionObject() + { + return Registry::getSession(); + } +} \ No newline at end of file diff --git a/src/Modules/Application/Model/d3_User_Webauthn.php b/src/Modules/Application/Model/d3_User_Webauthn.php new file mode 100755 index 0000000..47d83a7 --- /dev/null +++ b/src/Modules/Application/Model/d3_User_Webauthn.php @@ -0,0 +1,63 @@ + + * @link http://www.oxidmodule.com + */ + +namespace D3\Webauthn\Modules\Application\Model; + +use D3\Webauthn\Application\Model\d3webauthn; +use D3\Webauthn\Application\Model\d3webauthn_conf; +use OxidEsales\Eshop\Core\Registry; + +class d3_User_Webauthn extends d3_User_Webauthn_parent +{ + public function logout() + { + $return = parent::logout(); + + Registry::getSession()->deleteVariable(d3webauthn_conf::WEBAUTHN_SESSION_AUTH); + Registry::getSession()->deleteVariable(d3webauthn_conf::WEBAUTHN_LOGIN_OBJECT); + Registry::getSession()->deleteVariable(d3webauthn_conf::WEBAUTHN_SESSION_CURRENTUSER); + Registry::getSession()->deleteVariable(d3webauthn_conf::WEBAUTHN_SESSION_CURRENTCLASS); + Registry::getSession()->deleteVariable(d3webauthn_conf::WEBAUTHN_SESSION_NAVFORMPARAMS); + + return $return; + } + + public function d3templogout() + { + $varname = Registry::getSession()->getVariable(d3webauthn_conf::WEBAUTHN_SESSION_AUTH); + $object = Registry::getSession()->getVariable(d3webauthn_conf::WEBAUTHN_LOGIN_OBJECT); + $currentUser = Registry::getSession()->getVariable(d3webauthn_conf::WEBAUTHN_SESSION_CURRENTUSER); + $currentClass = Registry::getSession()->getVariable(d3webauthn_conf::WEBAUTHN_SESSION_CURRENTCLASS); + $navFormParams = Registry::getSession()->getVariable(d3webauthn_conf::WEBAUTHN_SESSION_NAVFORMPARAMS); + + $return = $this->logout(); + + Registry::getSession()->setVariable(d3webauthn_conf::WEBAUTHN_SESSION_AUTH, $varname); + Registry::getSession()->setVariable(d3webauthn_conf::WEBAUTHN_LOGIN_OBJECT, $object); + Registry::getSession()->setVariable(d3webauthn_conf::WEBAUTHN_SESSION_CURRENTUSER, $currentUser); + Registry::getSession()->setVariable(d3webauthn_conf::WEBAUTHN_SESSION_CURRENTCLASS, $currentClass); + Registry::getSession()->setVariable(d3webauthn_conf::WEBAUTHN_SESSION_NAVFORMPARAMS, $navFormParams); + + return $return; + } + + /** + * @return d3webauthn + */ + public function d3getWebauthn() + { + return oxNew(d3webauthn::class); + } +} \ No newline at end of file diff --git a/src/Modules/Core/d3_webauthn_utils.php b/src/Modules/Core/d3_webauthn_utils.php new file mode 100755 index 0000000..01a1605 --- /dev/null +++ b/src/Modules/Core/d3_webauthn_utils.php @@ -0,0 +1,69 @@ + + * @link http://www.oxidmodule.com + */ + +namespace D3\Webauthn\Modules\Core; + +use D3\Webauthn\Application\Model\d3webauthn; +use D3\Webauthn\Application\Model\d3webauthn_conf; +use Doctrine\DBAL\DBALException; +use OxidEsales\Eshop\Core\Exception\DatabaseConnectionException; +use OxidEsales\Eshop\Core\Registry; +use OxidEsales\Eshop\Core\Session; + +class d3_webauthn_utils extends d3_webauthn_utils_parent +{ + /** + * @return bool + * @throws DBALException + * @throws DatabaseConnectionException + */ + public function checkAccessRights() + { + $blAuth = parent::checkAccessRights(); + + $userID = $this->d3GetSessionObject()->getVariable("auth"); + $webauthnAuth = (bool) $this->d3GetSessionObject()->getVariable(d3webauthn_conf::WEBAUTHN_SESSION_AUTH); + /** @var d3webauthn $webauthn */ + $webauthn = $this->d3GetWebauthnObject(); + $webauthn->loadByUserId($userID); + + if ($blAuth && $webauthn->isActive() && false === $webauthnAuth) { + $this->redirect('index.php?cl=login', true, 302); + if (false == defined('OXID_PHP_UNIT')) { + // @codeCoverageIgnoreStart + exit; + // @codeCoverageIgnoreEnd + } + } + + return $blAuth; + } + + /** + * @return Session + */ + public function d3GetSessionObject() + { + return Registry::getSession(); + } + + /** + * @return d3webauthn + */ + public function d3GetWebauthnObject() + { + return oxNew(d3webauthn::class); + } +} \ No newline at end of file diff --git a/src/Setup/Events.php b/src/Setup/Events.php new file mode 100755 index 0000000..4a0290e --- /dev/null +++ b/src/Setup/Events.php @@ -0,0 +1,48 @@ + + * @link http://www.oxidmodule.com + */ + +namespace D3\Webauthn\Setup; + +use D3\ModCfg\Application\Model\Exception\d3ShopCompatibilityAdapterException; +use D3\ModCfg\Application\Model\Install\d3install; +use Doctrine\DBAL\DBALException; +use OxidEsales\Eshop\Core\Exception\DatabaseConnectionException; +use OxidEsales\Eshop\Core\Exception\DatabaseErrorException; +use OxidEsales\Eshop\Core\Exception\StandardException; +use OxidEsales\Eshop\Core\Exception\SystemComponentException; + +class Events +{ + /** + * @throws d3ShopCompatibilityAdapterException + * @throws DBALException + * @throws DatabaseConnectionException + * @throws DatabaseErrorException + * @throws StandardException + * @throws SystemComponentException + */ + public static function onActivate() + { + if (class_exists(d3install::class)) { + d3install::checkUpdateStart(); + } + } + + public static function onDeactivate() + { + } +} \ No newline at end of file diff --git a/src/Setup/Installation.php b/src/Setup/Installation.php new file mode 100755 index 0000000..5359b50 --- /dev/null +++ b/src/Setup/Installation.php @@ -0,0 +1,246 @@ + + * @link http://www.oxidmodule.com + */ + +namespace D3\Webauthn\Setup; + +use D3\ModCfg\Application\Model\d3database; +use D3\ModCfg\Application\Model\Install\d3install_updatebase; +use Doctrine\DBAL\DBALException; +use OxidEsales\Eshop\Core\DatabaseProvider; +use OxidEsales\Eshop\Core\Exception\ConnectionException; +use OxidEsales\Eshop\Core\Exception\DatabaseConnectionException; +use OxidEsales\Eshop\Core\Exception\DatabaseErrorException; + +class Installation extends d3install_updatebase +{ + protected $_aUpdateMethods = array( + array('check' => 'doesPublicKeyCredentialTableNotExist', + 'do' => 'addPublicKeyCredentialTable'), + array('check' => 'checkFields', + 'do' => 'fixFields'), + array('check' => 'checkIndizes', + 'do' => 'fixIndizes'), + array('check' => 'checkSEONotExists', + 'do' => 'addSEO'), + ); + + public $aMultiLangTables = array(); + + public $aFields = array( + 'OXID' => array( + 'sTableName' => 'd3PublicKeyCredential', + 'sFieldName' => 'OXID', + 'sType' => 'CHAR(32)', + 'blNull' => false, + 'sDefault' => false, + 'sComment' => '', + 'sExtra' => '', + 'blMultilang' => false, + ), + 'NAME' => array( + 'sTableName' => 'd3PublicKeyCredential', + 'sFieldName' => 'Name', + 'sType' => 'VARCHAR(255)', + 'blNull' => false, + 'sDefault' => false, + 'sComment' => '', + 'sExtra' => '', + 'blMultilang' => false, + ), + 'CREDENTIALID' => array( + 'sTableName' => 'd3PublicKeyCredential', + 'sFieldName' => 'CredentialId', + 'sType' => 'BINARY(48)', + 'blNull' => false, + 'sDefault' => false, + 'sComment' => '', + 'sExtra' => '', + 'blMultilang' => false, + ), + 'TYPE' => array( + 'sTableName' => 'd3PublicKeyCredential', + 'sFieldName' => 'Type', + 'sType' => 'CHAR(20)', + 'blNull' => false, + 'sDefault' => false, + 'sComment' => '', + 'sExtra' => '', + 'blMultilang' => false, + ), + 'TRANSPORTS' => array( + 'sTableName' => 'd3PublicKeyCredential', + 'sFieldName' => 'Transports', + 'sType' => 'VARCHAR(255)', + 'blNull' => false, + 'sDefault' => false, + 'sComment' => '', + 'sExtra' => '', + 'blMultilang' => false, + ), + 'ATTESTATIONTYPE' => array( + 'sTableName' => 'd3PublicKeyCredential', + 'sFieldName' => 'AttestationType', + 'sType' => 'CHAR(100)', + 'blNull' => false, + 'sDefault' => false, + 'sComment' => '', + 'sExtra' => '', + 'blMultilang' => false, + ), + 'TRUSTPATH' => array( + 'sTableName' => 'd3PublicKeyCredential', + 'sFieldName' => 'TrustPath', + 'sType' => 'VARCHAR(255)', + 'blNull' => false, + 'sDefault' => false, + 'sComment' => '', + 'sExtra' => '', + 'blMultilang' => false, + ), + 'AAGUID' => array( + 'sTableName' => 'd3PublicKeyCredential', + 'sFieldName' => 'Aaguid', + 'sType' => 'VARCHAR(255)', + 'blNull' => false, + 'sDefault' => false, + 'sComment' => '', + 'sExtra' => '', + 'blMultilang' => false, + ), + 'PUBLICKEY' => array( + 'sTableName' => 'd3PublicKeyCredential', + 'sFieldName' => 'PublicKey', + 'sType' => 'BINARY(77)', + 'blNull' => false, + 'sDefault' => false, + 'sComment' => '', + 'sExtra' => '', + 'blMultilang' => false, + ), + 'USERHANDLE' => array( + 'sTableName' => 'd3PublicKeyCredential', + 'sFieldName' => 'UserHandle', + 'sType' => 'CHAR(36)', + 'blNull' => false, + 'sDefault' => false, + 'sComment' => '', + 'sExtra' => '', + 'blMultilang' => false, + ), + 'COUNTER' => array( + 'sTableName' => 'd3PublicKeyCredential', + 'sFieldName' => 'Counter', + 'sType' => 'INT(5)', + 'blNull' => false, + 'sDefault' => false, + 'sComment' => '', + 'sExtra' => '', + 'blMultilang' => false, + ), + 'OXTIMESTAMP' => array( + 'sTableName' => 'd3PublicKeyCredential', + 'sFieldName' => 'oxtimestamp', + 'sType' => 'TIMESTAMP', + 'blNull' => false, + 'sDefault' => 'CURRENT_TIMESTAMP', + 'sComment' => 'Timestamp', + 'sExtra' => '', + 'blMultilang' => false, + ) + ); + + public $aIndizes = array( + 'OXID' => array( + 'sTableName' => 'd3PublicKeyCredential', + 'sType' => d3database::INDEX_TYPE_PRIMARY, + 'sName' => 'PRIMARY', + 'aFields' => array( + 'OXID' => 'OXID', + ), + ), + 'OXUSERID' => array( + 'sTableName' => 'd3PublicKeyCredential', + 'sType' => d3database::INDEX_TYPE_UNIQUE, + 'sName' => 'CredentialId', + 'aFields' => array( + 'CredentialId' => 'CredentialId', + ), + ) + ); + + protected $_aRefreshMetaModuleIds = array('d3webauthn'); + + /** + * @return bool + * @throws DBALException + * @throws DatabaseConnectionException + * @throws DatabaseErrorException + */ + public function doesPublicKeyCredentialTableNotExist() + { + return $this->_checkTableNotExist('d3PublicKeyCredential'); + } + + /** + * @return bool + * @throws ConnectionException + * @throws DBALException + * @throws DatabaseConnectionException + * @throws DatabaseErrorException + */ + public function addPublicKeyCredentialTable() + { + $blRet = false; + if ($this->doesPublicKeyCredentialTableNotExist()) { + $this->setInitialExecMethod(__METHOD__); + $blRet = $this->_addTable2( + 'd3PublicKeyCredential', + $this->aFields, + $this->aIndizes, + 'key credentials', + 'InnoDB' + ); + } + + return $blRet; + } + + /** + * @return bool + * @throws DatabaseConnectionException + */ + public function checkSEONotExists() + { + $query = "SELECT 1 FROM " . getViewName('oxseo') . " WHERE oxstdurl = 'index.php?cl=d3_account_webauthn'"; + + return !DatabaseProvider::getDb()->getOne($query); + } + + /** + * @return bool + * @throws DatabaseConnectionException + * @throws DatabaseErrorException + */ + public function addSEO() + { + $query = [ + "INSERT INTO `oxseo` (`OXOBJECTID`, `OXIDENT`, `OXSHOPID`, `OXLANG`, `OXSTDURL`, `OXSEOURL`, `OXTYPE`, `OXFIXED`, `OXEXPIRED`, `OXPARAMS`, `OXTIMESTAMP`) VALUES +('ff57646b47249ee33c6b672741ac371a', 'be07f06fe03a4d5d7936f2eac5e3a87b', 1, 1, 'index.php?cl=d3_account_webauthn', 'en/key-authintication/', 'static', 0, 0, '', NOW()), +('ff57646b47249ee33c6b672741ac371a', '220a1af77362196789eeed4741dda184', 1, 0, 'index.php?cl=d3_account_webauthn', 'key-authentisierung/', 'static', 0, 0, '', NOW());" + ]; + + return $this->_executeMultipleQueries($query); + } +} \ No newline at end of file diff --git a/src/menu.xml b/src/menu.xml new file mode 100755 index 0000000..e958b26 --- /dev/null +++ b/src/menu.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/metadata.php b/src/metadata.php new file mode 100755 index 0000000..4f96098 --- /dev/null +++ b/src/metadata.php @@ -0,0 +1,107 @@ + + * @link http://www.oxidmodule.com + */ + +// https://github.com/web-auth/webauthn-framework/tree/master/doc +// https://webauthn-doc.spomky-labs.com/ +// https://docs.solokeys.io/solo/ + +use D3\Webauthn\Application\Controller\Admin\d3user_webauthn; +use D3\Webauthn\Application\Controller\d3_account_webauthn; +use D3\Webauthn\Application\Controller\d3webauthnlogin; +use D3\Webauthn\Modules\Application\Component\d3_webauthn_UserComponent; +use D3\Webauthn\Modules\Application\Controller\Admin\d3_LoginController_Webauthn; +use D3\Webauthn\Modules\Application\Controller\d3_StartController_Webauthn; +use D3\Webauthn\Modules\Application\Controller\d3_webauthn_OrderController; +use D3\Webauthn\Modules\Application\Controller\d3_webauthn_PaymentController; +use D3\Webauthn\Modules\Application\Controller\d3_webauthn_UserController; +use D3\Webauthn\Modules\Application\Model\d3_User_Webauthn; +use D3\Webauthn\Modules\Core\d3_webauthn_utils; +use D3\Webauthn\Setup as ModuleSetup; +use D3\ModCfg\Application\Model\d3utils; +use OxidEsales\Eshop\Application\Component\UserComponent; +use OxidEsales\Eshop\Application\Controller\Admin\LoginController; +use OxidEsales\Eshop\Application\Controller\OrderController; +use OxidEsales\Eshop\Application\Controller\PaymentController; +use OxidEsales\Eshop\Application\Controller\StartController; +use OxidEsales\Eshop\Application\Controller\UserController; +use OxidEsales\Eshop\Core\Utils; +use OxidEsales\Eshop\Application\Model as OxidModel; + +/** + * Metadata version + */ +$sMetadataVersion = '2.1'; + +$sModuleId = 'd3webauthn'; +$logo = '(D3)'; + +/** + * Module information + */ +$aModule = array( + 'id' => $sModuleId, + 'title' => $logo.' Webauthn / FIDO2 Login', + 'description' => [ + 'de' => 'Webauthn für OXID eSales Shop', + 'en' => 'Webauthn for OXID eSales shop', + ], + 'version' => '0.0.1', + 'author' => 'D³ Data Development (Inh.: Thomas Dartsch)', + 'email' => 'support@shopmodule.com', + 'url' => 'http://www.oxidmodule.com/', + 'extend' => [ + UserController::class => d3_webauthn_UserController::class, + PaymentController::class => d3_webauthn_PaymentController::class, + OrderController::class => d3_webauthn_OrderController::class, + OxidModel\User::class => d3_User_Webauthn::class, + StartController::class => d3_StartController_Webauthn::class, + LoginController::class => d3_LoginController_Webauthn::class, + Utils::class => d3_webauthn_utils::class, + UserComponent::class => d3_webauthn_UserComponent::class, + ], + 'controllers' => [ + 'd3user_webauthn' => d3user_webauthn::class, + 'd3webauthnlogin' => d3webauthnlogin::class, + 'd3_account_webauthn' => d3_account_webauthn::class + ], + 'templates' => [ + 'd3user_webauthn.tpl' => 'd3/webauthn/Application/views/admin/tpl/d3user_webauthn.tpl', + 'd3webauthnlogin.tpl' => 'd3/webauthn/Application/views/tpl/d3webauthnlogin.tpl', + 'd3_account_webauthn.tpl' => 'd3/webauthn/Application/views/tpl/d3_account_webauthn.tpl', + ], + 'events' => [ + 'onActivate' => '\D3\Webauthn\Setup\Events::onActivate', + 'onDeactivate' => '\D3\Webauthn\Setup\Events::onDeactivate', + ], + 'blocks' => [ + [ + 'template' => 'page/account/inc/account_menu.tpl', + 'block' => 'account_menu', + 'file' => 'Application/views/blocks/page/account/inc/account_menu.tpl', + ], + [ + 'template' => 'page/shop/start.tpl', + 'block' => 'start_welcome_text', + 'file' => 'Application/views/blocks/page/shop/start_welcome_text.tpl', + ], + [ + 'template' => 'login.tpl', + 'block' => 'admin_login_form', + 'file' => 'Application/views/admin/blocks/d3webauthn_login_admin_login_form.tpl', + ] + ] +); \ No newline at end of file diff --git a/src/out/admin/src/css/d3webauthnlogin.css b/src/out/admin/src/css/d3webauthnlogin.css new file mode 100755 index 0000000..a8945ef --- /dev/null +++ b/src/out/admin/src/css/d3webauthnlogin.css @@ -0,0 +1,28 @@ +.d3webauthn_icon { + background-color: transparent; + height: 175px; + width: 278px; +} + +.d3webauthn_icon .svg-container { + position: relative; + height:0; + width: 100%; + padding: 0 0 50% 0; +} + +.d3webauthn_icon .svg-container svg { + fill: darkgrey; + position: absolute; + height: 100%; + width: 100%; + left: 0; + top: 0; +} + +.d3webauthn_icon .message { + color: darkgrey; + margin-top: 12px; + text-align: center; + font-size: 105%; +} \ No newline at end of file diff --git a/src/out/flow/src/css/d3webauthnlogin.css b/src/out/flow/src/css/d3webauthnlogin.css new file mode 100755 index 0000000..45ada23 --- /dev/null +++ b/src/out/flow/src/css/d3webauthnlogin.css @@ -0,0 +1,45 @@ +#login { + display: flex; + flex-flow: column; +} +#login .btn.btn_cancel { + background: silver; + color: black; +} + +.d3webauthn_icon { + background-color: transparent; + height: 185px; + width: 100%; +} + +.d3webauthn_icon .svg-container { + position: relative; + height:0; + width: 100%; + padding: 0 0 25% 0; +} + +.d3webauthn_icon .svg-container svg { + fill: darkgrey; + position: absolute; + height: 100%; + width: 100%; + left: 0; + top: 0; +} + +.d3webauthn_icon .message { + color: darkgrey; + margin-top: 12px; + text-align: center; + font-size: 105%; +} + +#webauthnlogout { + text-align: center; +} + +body.cl-d3webauthnlogin .webauthncol { + margin-bottom: 20px; +} \ No newline at end of file diff --git a/src/out/img/fingerprint.svg b/src/out/img/fingerprint.svg new file mode 100755 index 0000000..c7bf461 --- /dev/null +++ b/src/out/img/fingerprint.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + +