From c7f48bf960dac05547f0fbde6d2dc8d4788dd958 Mon Sep 17 00:00:00 2001 From: Daniel Seifert Date: Wed, 30 Nov 2022 01:27:05 +0100 Subject: [PATCH] extract assertAuth and login procedure to separate classes --- .../Controller/Admin/d3webauthnadminlogin.php | 123 +- .../WebauthnLoginErrorException.php | 22 + src/Application/Model/WebauthnAfterLogin.php | 64 + src/Application/Model/WebauthnLogin.php | 395 ++++++ src/Application/views/tpl/d3webauthnlogin.tpl | 2 +- .../Component/d3_webauthn_UserComponent.php | 61 +- .../Admin/d3_LoginController_Webauthn.php | 39 - .../Admin/d3webauthnadminloginTest.php | 259 ++-- .../Controller/d3_account_webauthnTest.php | 2 + .../Application/Model/WebauthnLoginTest.php | 1084 +++++++++++++++++ 10 files changed, 1692 insertions(+), 359 deletions(-) create mode 100644 src/Application/Model/Exceptions/WebauthnLoginErrorException.php create mode 100644 src/Application/Model/WebauthnAfterLogin.php create mode 100644 src/Application/Model/WebauthnLogin.php create mode 100644 src/tests/unit/Application/Model/WebauthnLoginTest.php diff --git a/src/Application/Controller/Admin/d3webauthnadminlogin.php b/src/Application/Controller/Admin/d3webauthnadminlogin.php index bfaf420..105d7e4 100755 --- a/src/Application/Controller/Admin/d3webauthnadminlogin.php +++ b/src/Application/Controller/Admin/d3webauthnadminlogin.php @@ -18,24 +18,20 @@ namespace D3\Webauthn\Application\Controller\Admin; use D3\TestingTools\Production\IsMockable; use D3\Webauthn\Application\Controller\Traits\helpersTrait; use D3\Webauthn\Application\Model\Exceptions\WebauthnGetException; -use D3\Webauthn\Application\Model\Webauthn; +use D3\Webauthn\Application\Model\WebauthnAfterLogin; use D3\Webauthn\Application\Model\WebauthnConf; use D3\Webauthn\Application\Model\Exceptions\WebauthnException; -use D3\Webauthn\Modules\Application\Controller\Admin\d3_LoginController_Webauthn; -use D3\Webauthn\Modules\Application\Model\d3_User_Webauthn; +use D3\Webauthn\Application\Model\WebauthnLogin; use Doctrine\DBAL\Driver\Exception as DoctrineDriverException; use Doctrine\DBAL\Exception as DoctrineException; use OxidEsales\Eshop\Application\Controller\Admin\AdminController; -use OxidEsales\Eshop\Application\Controller\Admin\LoginController; use OxidEsales\Eshop\Application\Controller\FrontendController; -use OxidEsales\Eshop\Core\Exception\ConnectionException; -use OxidEsales\Eshop\Core\Exception\CookieException; -use OxidEsales\Eshop\Core\Exception\UserException; use OxidEsales\Eshop\Core\Registry; use OxidEsales\Eshop\Core\Request; use OxidEsales\Eshop\Core\SystemEventHandler; use OxidEsales\Eshop\Core\Utils; use OxidEsales\Eshop\Core\UtilsServer; +use OxidEsales\Eshop\Core\UtilsView; use Psr\Container\ContainerExceptionInterface; use Psr\Container\NotFoundExceptionInterface; @@ -69,17 +65,15 @@ class d3webauthnadminlogin extends AdminController $this->getUtils()->redirect('index.php?cl=login'); } - /** @var d3_LoginController_Webauthn $loginController */ - $loginController = $this->d3WebauthnGetLoginController(); - $loginController->d3WebauthnAfterLoginChangeLanguage(); - $this->generateCredentialRequest(); $this->addTplParam('navFormParams', $this->d3GetSession()->getVariable(WebauthnConf::WEBAUTHN_SESSION_NAVFORMPARAMS)); $this->addTplParam('currentProfile', $this->d3GetSession()->getVariable(WebauthnConf::WEBAUTHN_ADMIN_PROFILE)); $this->d3GetSession()->deleteVariable(WebauthnConf::WEBAUTHN_ADMIN_PROFILE); $this->addTplParam('currentChLanguage', $this->d3GetSession()->getVariable(WebauthnConf::WEBAUTHN_ADMIN_CHLANGUAGE)); - $this->d3GetSession()->deleteVariable(WebauthnConf::WEBAUTHN_ADMIN_CHLANGUAGE); + + $afterLogin = $this->d3WebauthnGetAfterLogin(); + $afterLogin->changeLanguage(); return $this->d3CallMockableParent('render'); } @@ -95,7 +89,6 @@ class d3webauthnadminlogin extends AdminController { $userId = $this->d3GetSession()->getVariable(WebauthnConf::WEBAUTHN_ADMIN_SESSION_CURRENTUSER); try { - /** @var Webauthn $webauthn */ $webauthn = $this->d3GetWebauthnObject(); $publicKeyCredentialRequestOptions = $webauthn->getRequestOptions($userId); $this->d3GetSession()->setVariable(WebauthnConf::WEBAUTHN_ADMIN_LOGIN_OBJECT, $publicKeyCredentialRequestOptions); @@ -103,95 +96,41 @@ class d3webauthnadminlogin extends AdminController $this->addTplParam('isAdmin', isAdmin()); } catch (WebauthnException $e) { $this->d3GetSession()->setVariable(WebauthnConf::GLOBAL_SWITCH, true); - Registry::getUtilsView()->addErrorToDisplay($e); + $this->d3GetUtilsViewObject()->addErrorToDisplay($e); $this->d3GetLoggerObject()->error($e->getDetailedErrorMessage(), ['UserId' => $userId]); $this->d3GetLoggerObject()->debug($e->getTraceAsString()); $this->getUtils()->redirect('index.php?cl=login'); } } + /** + * @param string $credential + * @param string|null $error + * @throws WebauthnGetException + * @return WebauthnLogin + */ + public function getWebauthnLoginObject(string $credential, ?string $error): WebauthnLogin + { + return oxNew(WebauthnLogin::class, $credential, $error); + } + /** * @return string|null */ public function d3AssertAuthn(): ?string { - $myUtilsView = $this->d3GetUtilsViewObject(); - /** @var d3_User_Webauthn $user */ - $user = $this->d3GetUserObject(); - $userId = $this->d3GetSession()->getVariable(WebauthnConf::WEBAUTHN_ADMIN_SESSION_CURRENTUSER); - $selectedProfile = $this->d3WebAuthnGetRequest()->getRequestEscapedParameter('profile'); - try { - $error = $this->d3WebAuthnGetRequest()->getRequestEscapedParameter('error'); - if (strlen((string) $error)) { - /** @var WebauthnGetException $e */ - $e = oxNew(WebauthnGetException::class, $error); - throw $e; - } - - $credential = $this->d3WebAuthnGetRequest()->getRequestEscapedParameter('credential'); - if (!strlen((string) $credential)) { - /** @var WebauthnGetException $e */ - $e = oxNew(WebauthnGetException::class, 'missing credential data'); - throw $e; - } - - $webAuthn = $this->d3GetWebauthnObject(); - $webAuthn->assertAuthn($credential); - $user->load($userId); - $session = $this->d3GetSession(); - $adminProfiles = $session->getVariable("aAdminProfiles"); - $session->initNewSession(); - $session->setVariable("aAdminProfiles", $adminProfiles); - $session->setVariable(WebauthnConf::OXID_ADMIN_AUTH, $userId); - - $cookie = $this->d3WebauthnGetUtilsServer()->getOxCookie(); - if ($cookie === null) { - /** @var CookieException $exc */ - $exc = oxNew(CookieException::class, 'ERROR_MESSAGE_COOKIE_NOCOOKIE'); - throw $exc; - } - - if ($user->getFieldData('oxrights') === 'user') { - /** @var UserException $exc */ - $exc = oxNew(UserException::class, 'ERROR_MESSAGE_USER_NOVALIDLOGIN'); - throw $exc; - } - $iSubshop = (int) $user->getFieldData('oxrights'); - - if ($iSubshop) { - $session->setVariable("shp", $iSubshop); - $session->setVariable('currentadminshop', $iSubshop); - Registry::getConfig()->setShopId($iSubshop); - } - - //execute onAdminLogin() event - $oEvenHandler = $this->d3WebauthnGetEventHandler(); - $oEvenHandler->onAdminLogin(Registry::getConfig()->getShopId()); - - /** @var d3_LoginController_Webauthn $loginController */ - $loginController = $this->d3WebauthnGetLoginController(); - $loginController->d3webauthnAfterLogin(); - - return "admin_start"; - } catch (UserException $oEx) { - $myUtilsView->addErrorToDisplay('LOGIN_ERROR'); - $oStr = getStr(); - $this->addTplParam('user', $oStr->htmlspecialchars($userId)); - $this->addTplParam('profile', $oStr->htmlspecialchars($selectedProfile)); - } catch (CookieException $oEx) { - $myUtilsView->addErrorToDisplay('LOGIN_NO_COOKIE_SUPPORT'); - $oStr = getStr(); - $this->addTplParam('user', $oStr->htmlspecialchars($userId)); - $this->addTplParam('profile', $oStr->htmlspecialchars($selectedProfile)); - } catch (WebauthnException $e) { - $myUtilsView->addErrorToDisplay($e); - $this->d3GetLoggerObject()->error($e->getDetailedErrorMessage(), ['UserId' => $userId]); - $this->d3GetLoggerObject()->debug($e->getTraceAsString()); - $user->logout(); + $login = $this->getWebauthnLoginObject( + $this->d3WebAuthnGetRequest()->getRequestEscapedParameter('credential'), + $this->d3WebAuthnGetRequest()->getRequestEscapedParameter('error') + ); + return $login->adminLogin( + $this->d3WebAuthnGetRequest()->getRequestEscapedParameter('profile') + ); + } catch (WebauthnGetException $e) { + $this->d3GetUtilsViewObject()->addErrorToDisplay($e); + return 'login'; } - - return 'login'; } /** @@ -233,11 +172,11 @@ class d3webauthnadminlogin extends AdminController } /** - * @return mixed|LoginController + * @return WebauthnAfterLogin */ - public function d3WebauthnGetLoginController() + public function d3WebauthnGetAfterLogin(): WebauthnAfterLogin { - return oxNew(LoginController::class); + return oxNew(WebauthnAfterLogin::class); } /** diff --git a/src/Application/Model/Exceptions/WebauthnLoginErrorException.php b/src/Application/Model/Exceptions/WebauthnLoginErrorException.php new file mode 100644 index 0000000..f97a1f2 --- /dev/null +++ b/src/Application/Model/Exceptions/WebauthnLoginErrorException.php @@ -0,0 +1,22 @@ + + * @link https://www.oxidmodule.com + */ + +declare(strict_types=1); + +namespace D3\Webauthn\Application\Model\Exceptions; + +use OxidEsales\Eshop\Core\Exception\StandardException; + +class WebauthnLoginErrorException extends StandardException +{ +} \ No newline at end of file diff --git a/src/Application/Model/WebauthnAfterLogin.php b/src/Application/Model/WebauthnAfterLogin.php new file mode 100644 index 0000000..a346833 --- /dev/null +++ b/src/Application/Model/WebauthnAfterLogin.php @@ -0,0 +1,64 @@ + + * @link https://www.oxidmodule.com + */ + +declare(strict_types=1); + +namespace D3\Webauthn\Application\Model; + +use OxidEsales\Eshop\Core\Registry; + +class WebauthnAfterLogin +{ + public function setDisplayProfile() + { + $sProfile = Registry::getRequest()->getRequestEscapedParameter('profile') ?: + Registry::getSession()->getVariable(WebauthnConf::WEBAUTHN_ADMIN_PROFILE); + + Registry::getSession()->deleteVariable(WebauthnConf::WEBAUTHN_ADMIN_PROFILE); + + $myUtilsServer = Registry::getUtilsServer(); + + if (isset($sProfile)) { + $aProfiles = Registry::getSession()->getVariable("aAdminProfiles"); + if ($aProfiles && isset($aProfiles[$sProfile])) { + // setting cookie to store last locally used profile + $myUtilsServer->setOxCookie("oxidadminprofile", $sProfile . "@" . implode("@", $aProfiles[$sProfile]), time() + 31536000); + Registry::getSession()->setVariable("profile", $aProfiles[$sProfile]); + } + } else { + //deleting cookie info, as setting profile to default + $myUtilsServer->setOxCookie("oxidadminprofile", "", time() - 3600); + } + } + + /** + * @return void + */ + public function changeLanguage() + { + $myUtilsServer = Registry::getUtilsServer(); + // languages + $iLang = Registry::getRequest()->getRequestEscapedParameter('chlanguage') ?: + Registry::getSession()->getVariable(WebauthnConf::WEBAUTHN_ADMIN_CHLANGUAGE); + + Registry::getSession()->deleteVariable(WebauthnConf::WEBAUTHN_ADMIN_CHLANGUAGE); + + $aLanguages = Registry::getLang()->getAdminTplLanguageArray(); + if (!isset($aLanguages[$iLang])) { + $iLang = key($aLanguages); + } + + $myUtilsServer->setOxCookie("oxidadminlanguage", $aLanguages[$iLang]->abbr, time() + 31536000); + Registry::getLang()->setTplLanguage($iLang); + } +} \ No newline at end of file diff --git a/src/Application/Model/WebauthnLogin.php b/src/Application/Model/WebauthnLogin.php new file mode 100644 index 0000000..43cc5ae --- /dev/null +++ b/src/Application/Model/WebauthnLogin.php @@ -0,0 +1,395 @@ + + * @link https://www.oxidmodule.com + */ + +declare(strict_types=1); + +namespace D3\Webauthn\Application\Model; + +use D3\Webauthn\Application\Controller\Traits\helpersTrait; +use D3\Webauthn\Application\Model\Exceptions\WebauthnException; +use D3\Webauthn\Application\Model\Exceptions\WebauthnGetException; +use D3\Webauthn\Application\Model\Exceptions\WebauthnLoginErrorException; +use D3\Webauthn\Modules\Application\Model\d3_User_Webauthn; +use OxidEsales\Eshop\Application\Model\User; +use OxidEsales\Eshop\Core\Config; +use OxidEsales\Eshop\Core\Exception\CookieException; +use OxidEsales\Eshop\Core\Exception\UserException; +use OxidEsales\Eshop\Core\Registry; +use OxidEsales\Eshop\Core\Session; +use OxidEsales\Eshop\Core\Str; +use OxidEsales\Eshop\Core\SystemEventHandler; +use OxidEsales\Eshop\Core\UtilsServer; +use OxidEsales\EshopCommunity\Application\Component\UserComponent; + +class WebauthnLogin +{ + use helpersTrait; + + public $credential; + + public $errorMsg; + + /** + * @param string $credential + * @param string|null $error + * @throws WebauthnGetException + */ + public function __construct(string $credential, string $error = null) + { + $this->setCredential($credential); + $this->setErrorMsg($error); + } + + /** + * @return string + * @throws WebauthnGetException + */ + public function getCredential(): string + { + if (!strlen(trim((string) $this->credential))) { + /** @var WebauthnGetException $e */ + $e = oxNew(WebauthnGetException::class, 'missing credential data'); + throw $e; + } + + return trim($this->credential); + } + + /** + * @param string $credential + * @throws WebauthnGetException + */ + public function setCredential(string $credential): void + { + if (!strlen(trim($credential))) { + /** @var WebauthnGetException $e */ + $e = oxNew(WebauthnGetException::class, 'missing credential data'); + throw $e; + } + + $this->credential = trim($credential); + } + + /** + * @return ?string + */ + public function getErrorMsg(): ?string + { + return $this->errorMsg; + } + + /** + * @param string|null $errorMsg + */ + public function setErrorMsg(?string $errorMsg): void + { + $this->errorMsg = $errorMsg; + } + + /** + * @param UserComponent $usrCmp + * @param bool $setSessionCookie + * @return void + * @throws WebauthnLoginErrorException + */ + public function frontendLogin(UserComponent $usrCmp, bool $setSessionCookie = false) + { + $myUtilsView = $this->d3GetUtilsViewObject(); + /** @var d3_User_Webauthn $user */ + $user = $this->d3GetUserObject(); + $userId = $this->getUserId(); + + try { + $this->handleErrorMessage(); + + $user = $this->assertUser($userId); + $this->assertAuthn(); + + // relogin, don't extract from this try block + $usrCmp->setUser($this->d3GetUserObject()); + $this->setFrontendSession($user); + $usrCmp->setLoginStatus(USER_LOGIN_SUCCESS); + + if ($setSessionCookie) { + $this->setSessionCookie($user); + } + + $this->regenerateSessionId(); + + $usrCmp->setUser($user); + + return; + } catch (UserException $oEx) { + // for login component send exception text to a custom component (if defined) + $myUtilsView->addErrorToDisplay($oEx, false, true, '', false); + + //return 'user'; + } catch (\OxidEsales\Eshop\Core\Exception\CookieException $oEx) { + $myUtilsView->addErrorToDisplay($oEx); + + //return 'user'; + } catch (WebauthnException $e) { + $myUtilsView->addErrorToDisplay($e); + $this->d3GetLoggerObject()->error($e->getDetailedErrorMessage(), ['UserId' => $userId]); + $this->d3GetLoggerObject()->debug($e->getTraceAsString()); + } + + $user->logout(); + $exc = oxNew(WebauthnLoginErrorException::class); + throw $exc; + } + + /** + * @param string $selectedProfile + * @return string + */ + public function adminLogin(string $selectedProfile): string + { + $myUtilsView = $this->d3GetUtilsViewObject(); + /** @var d3_User_Webauthn $user */ + $user = $this->d3GetUserObject(); + $userId = $this->getUserId(); + + try { + $this->handleErrorMessage(); + $this->assertUser($userId, true); + $this->handleBlockedUser($user); + $this->assertAuthn(); + $session = $this->setAdminSession($userId); + $this->handleBackendCookie(); + $this->handleBackendSubshopRights($user, $session); + + $oEvenHandler = $this->d3WebauthnGetEventHandler(); + $oEvenHandler->onAdminLogin(); + + $afterLogin = $this->getAfterLogin(); + $afterLogin->setDisplayProfile(); + $afterLogin->changeLanguage(); + + $this->regenerateSessionId(); + $this->updateBasket(); + + return "admin_start"; + } catch (UserException $oEx) { + $myUtilsView->addErrorToDisplay('LOGIN_ERROR'); + } catch (CookieException $oEx) { + $myUtilsView->addErrorToDisplay('LOGIN_NO_COOKIE_SUPPORT'); + } catch (WebauthnException $e) { + $myUtilsView->addErrorToDisplay($e); + $this->d3GetLoggerObject()->error($e->getDetailedErrorMessage(), ['UserId' => $userId]); + $this->d3GetLoggerObject()->debug($e->getTraceAsString()); + } + + $user->logout(); + $oStr = Str::getStr(); + $this->d3GetConfig()->getActiveView()->addTplParam('user', $oStr->htmlspecialchars($userId)); + $this->d3GetConfig()->getActiveView()->addTplParam('profile', $oStr->htmlspecialchars($selectedProfile)); + + return 'login'; + } + + /** + * @throws WebauthnGetException + */ + public function handleErrorMessage() + { + $error = $this->getErrorMsg(); + + if (strlen((string)$error)) { + /** @var WebauthnGetException $e */ + $e = oxNew(WebauthnGetException::class, $error); + throw $e; + } + } + + /** + * @throws WebauthnGetException|WebauthnException + */ + public function assertAuthn(): void + { + $credential = $this->getCredential(); + $webAuthn = $this->d3GetWebauthnObject(); + $webAuthn->assertAuthn($credential); + } + + /** + * @param $userId + * @return Session + */ + public function setAdminSession($userId): Session + { + $session = $this->d3GetSession(); + $adminProfiles = $session->getVariable("aAdminProfiles"); + $session->initNewSession(); + $session->setVariable("aAdminProfiles", $adminProfiles); + $session->setVariable(WebauthnConf::OXID_ADMIN_AUTH, $userId); + return $session; + } + + /** + * @param User $user + * @return void + */ + public function setSessionCookie(User $user) + { + if ($this->d3GetConfig()->getConfigParam('blShowRememberMe')) { + $this->getUtilsServer()->setUserCookie( + $user->getFieldData('oxusername'), + $user->getFieldData('oxpassword'), + $this->d3GetConfig()->getShopId() + ); + } + } + + /** + * @param $userId + * @param bool $isBackend + * @return User + * @throws UserException + */ + public function assertUser($userId, bool $isBackend = false): User + { + $user = $this->d3GetUserObject(); + $user->load($userId); + if (!$user->isLoaded() || + ($isBackend && $user->getFieldData('oxrights') === 'user') + ) { + /** @var UserException $exc */ + $exc = oxNew(UserException::class, 'ERROR_MESSAGE_USER_NOVALIDLOGIN'); + throw $exc; + } + + return $user; + } + + /** + * @return void + * @throws CookieException + */ + public function handleBackendCookie(): void + { + $cookie = $this->getUtilsServer()->getOxCookie(); + if ($cookie === null) { + /** @var CookieException $exc */ + $exc = oxNew(CookieException::class, 'ERROR_MESSAGE_COOKIE_NOCOOKIE'); + throw $exc; + } + } + + /** + * @param User $user + * @param Session $session + * @return void + */ + public function handleBackendSubshopRights(User $user, Session $session): void + { + $iSubshop = (int)$user->getFieldData('oxrights'); + + if ($iSubshop) { + $session->setVariable("shp", $iSubshop); + $session->setVariable('currentadminshop', $iSubshop); + $this->d3GetConfig()->setShopId($iSubshop); + } + } + + /** + * @return void + */ + public function regenerateSessionId(): void + { + $oSession = $this->d3GetSession(); + if ($oSession->isSessionStarted()) { + $oSession->regenerateSessionId(); + } + } + + public function handleBlockedUser(User $user) + { + // this user is blocked, deny him + if ($user->inGroup('oxidblocked')) { + $sUrl = $this->d3GetConfig()->getShopHomeUrl() . 'cl=content&tpl=user_blocked.tpl'; + $this->d3GetUtilsObject()->redirect($sUrl, true, 302); + } + } + + /** + * @return void + */ + public function updateBasket(): void + { + if ($oBasket = $this->d3GetSession()->getBasket()) { + $oBasket->onUpdate(); + } + } + + /** + * @return SystemEventHandler + */ + public function d3WebauthnGetEventHandler(): SystemEventHandler + { + return oxNew(SystemEventHandler::class); + } + + /** + * @return WebauthnAfterLogin + */ + public function getAfterLogin(): WebauthnAfterLogin + { + return oxNew(WebauthnAfterLogin::class); + } + + /** + * @return bool + */ + public function isAdmin(): bool + { + return isAdmin(); + } + + /** + * @return string + */ + public function getUserId(): string + { + return $this->isAdmin() ? + $this->d3GetSession()->getVariable(WebauthnConf::WEBAUTHN_ADMIN_SESSION_CURRENTUSER) : + $this->d3GetSession()->getVariable(WebauthnConf::WEBAUTHN_SESSION_CURRENTUSER); + } + + /** + * @return Config + */ + public function d3GetConfig(): Config + { + return Registry::getConfig(); + } + + /** + * @return UtilsServer + */ + public function getUtilsServer(): UtilsServer + { + return Registry::getUtilsServer(); + } + + /** + * @param User $user + * @return void + * @throws WebauthnGetException + */ + public function setFrontendSession(User $user): void + { + $this->d3GetSession()->setVariable(WebauthnConf::WEBAUTHN_SESSION_AUTH, $this->getCredential()); + $this->d3GetSession()->setVariable(WebauthnConf::OXID_FRONTEND_AUTH, $user->getId()); + } +} \ No newline at end of file diff --git a/src/Application/views/tpl/d3webauthnlogin.tpl b/src/Application/views/tpl/d3webauthnlogin.tpl index 6f61453..563fecc 100755 --- a/src/Application/views/tpl/d3webauthnlogin.tpl +++ b/src/Application/views/tpl/d3webauthnlogin.tpl @@ -25,7 +25,7 @@ [{$oViewConf->getHiddenSid()}] - + [{$navFormParams}]