From 8f7b9032f7308850b6ba6a3c8a5b8bd0e6083534 Mon Sep 17 00:00:00 2001 From: Keywan Ghadami Date: Fri, 30 Apr 2021 11:23:39 +0200 Subject: [PATCH 001/199] rename test to integration tests --- tests/{unit => Integration}/Core/PasswordPolicyConfigTest.php | 2 +- .../{unit => Integration}/Core/PasswordPolicyValidatorTest.php | 2 +- .../{unit => Integration}/Core/PasswordPolicyViewConfigTest.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename tests/{unit => Integration}/Core/PasswordPolicyConfigTest.php (98%) rename tests/{unit => Integration}/Core/PasswordPolicyValidatorTest.php (98%) rename tests/{unit => Integration}/Core/PasswordPolicyViewConfigTest.php (97%) diff --git a/tests/unit/Core/PasswordPolicyConfigTest.php b/tests/Integration/Core/PasswordPolicyConfigTest.php similarity index 98% rename from tests/unit/Core/PasswordPolicyConfigTest.php rename to tests/Integration/Core/PasswordPolicyConfigTest.php index dcdd14e3..49d8e38f 100644 --- a/tests/unit/Core/PasswordPolicyConfigTest.php +++ b/tests/Integration/Core/PasswordPolicyConfigTest.php @@ -23,7 +23,7 @@ declare(strict_types=1); -namespace OxidProfessionalServices\PasswordPolicy\Tests; +namespace OxidProfessionalServices\PasswordPolicy\Tests\Integration\Core; use OxidEsales\Eshop\Core\InputValidator; use OxidEsales\Eshop\Core\Registry; diff --git a/tests/unit/Core/PasswordPolicyValidatorTest.php b/tests/Integration/Core/PasswordPolicyValidatorTest.php similarity index 98% rename from tests/unit/Core/PasswordPolicyValidatorTest.php rename to tests/Integration/Core/PasswordPolicyValidatorTest.php index 92a8f20c..f174aa1c 100644 --- a/tests/unit/Core/PasswordPolicyValidatorTest.php +++ b/tests/Integration/Core/PasswordPolicyValidatorTest.php @@ -23,7 +23,7 @@ declare(strict_types=1); -namespace OxidProfessionalServices\PasswordPolicy\Tests; +namespace OxidProfessionalServices\PasswordPolicy\Tests\Integration\Core; use OxidEsales\Eshop\Core\Exception\StandardException; use OxidEsales\Eshop\Core\Registry; diff --git a/tests/unit/Core/PasswordPolicyViewConfigTest.php b/tests/Integration/Core/PasswordPolicyViewConfigTest.php similarity index 97% rename from tests/unit/Core/PasswordPolicyViewConfigTest.php rename to tests/Integration/Core/PasswordPolicyViewConfigTest.php index 33cc9f68..b3094ad3 100644 --- a/tests/unit/Core/PasswordPolicyViewConfigTest.php +++ b/tests/Integration/Core/PasswordPolicyViewConfigTest.php @@ -23,7 +23,7 @@ declare(strict_types=1); -namespace OxidProfessionalServices\PasswordPolicy\Tests; +namespace OxidProfessionalServices\PasswordPolicy\Tests\Integration\Core; use OxidEsales\Eshop\Core\InputValidator; use OxidEsales\Eshop\Core\Registry; From 8caf47d0ec3aa83426e6378b00304dca68bba7e0 Mon Sep 17 00:00:00 2001 From: moritz_demmer Date: Wed, 14 Apr 2021 13:25:11 +0200 Subject: [PATCH 002/199] Initial Commit --- src/Core/PasswordPolicyConfig.php | 12 ++++++++++++ tests/Integration/Core/PasswordPolicyConfigTest.php | 11 +++++++++++ translations/de/passwordpolicy_lang.php | 1 + views/admin/de/passwordpolicy_lang.php | 4 ++++ 4 files changed, 28 insertions(+) diff --git a/src/Core/PasswordPolicyConfig.php b/src/Core/PasswordPolicyConfig.php index 35de77ce..e207c917 100644 --- a/src/Core/PasswordPolicyConfig.php +++ b/src/Core/PasswordPolicyConfig.php @@ -39,6 +39,8 @@ class PasswordPolicyConfig public const SettingMinPasswordLength = self::SettingsPrefix . 'MinPasswordLength'; public const SettingDigits = self::SettingsPrefix . 'Digits'; public const SettingSpecial = self::SettingsPrefix . 'Special'; + public const SettingAPIKey = self::SettingsPrefix . 'APIKey'; + public const SettingSecretKey = self::SettingsPrefix . 'SecretKey'; public const SettingUpper = self::SettingsPrefix . 'UpperCase'; public const SettingLower = self::SettingsPrefix . 'LowerCase'; @@ -85,6 +87,16 @@ public function getMaxPasswordLength(): int return 255; } + public function getAPIKey(): string + { + return (string) Registry::getConfig()->getConfigParam(self::SettingAPIKey); + } + + public function getSecretKey(): string + { + return (string) Registry::getConfig()->getConfigParam(self::SettingSecretKey); + } + private function isConfigParam(string $name): bool { return (bool) Registry::getConfig()->getConfigParam($name, true); diff --git a/tests/Integration/Core/PasswordPolicyConfigTest.php b/tests/Integration/Core/PasswordPolicyConfigTest.php index 49d8e38f..aaab322c 100644 --- a/tests/Integration/Core/PasswordPolicyConfigTest.php +++ b/tests/Integration/Core/PasswordPolicyConfigTest.php @@ -54,6 +54,11 @@ public function setUp(): void $this->subjectUnderTest = new PasswordPolicyConfig(); } + public function testGetAPIKey(): void + { + $this->saveAPIKey("2342355ss33wsada3"); + $this->assertEquals("2342355ss33wsada3", $this->subjectUnderTest->getAPIKey()); + } /** * @dataProvider lengthProvider * @param int $len @@ -110,6 +115,10 @@ public function trueFalseProvider() ]; } + public function saveAPIKey(string $key): void + { + $this->setConfig(PasswordPolicyConfig::SettingAPIKey, $key); + } public function saveGoodPasswordLength(int $len): void { $this->setConfig(PasswordPolicyConfig::SettingGoodPasswordLength, $len); @@ -139,4 +148,6 @@ public function setConfig($name, $value): void { Registry::getConfig()->setConfigParam($name, $value); } + + } diff --git a/translations/de/passwordpolicy_lang.php b/translations/de/passwordpolicy_lang.php index 3580890c..3bf74b68 100644 --- a/translations/de/passwordpolicy_lang.php +++ b/translations/de/passwordpolicy_lang.php @@ -52,4 +52,5 @@ 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESLOWERCASE' => 'Das Passwort muss mindestens einen Kleinbuchstaben enthalten.', 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESSPECIAL' => 'Das Passwort muss mindestens ein Sonderzeichen Zahl enthalten.', 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_WRONGTYPE' => 'Fehlerhafter Typ, bitte tragen Sie einen validen Wert ein. Bei weiteren Fragen wenden Sie sich an den Support.', + 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_PASSWORD_KNOWN' => 'Das Passwort befindet sich bereits in einer gehackten Datenbank und ist somit unsicher!' ); diff --git a/views/admin/de/passwordpolicy_lang.php b/views/admin/de/passwordpolicy_lang.php index c9cb67c8..525a6c0d 100644 --- a/views/admin/de/passwordpolicy_lang.php +++ b/views/admin/de/passwordpolicy_lang.php @@ -30,4 +30,8 @@ 'SHOP_MODULE_oxpspasswordpolicyUpperCase' => 'Großbuchstaben (A...Z)', 'SHOP_MODULE_oxpspasswordpolicyLowerCase' => 'Kleinbuchstaben (a...z)', 'SHOP_MODULE_oxpspasswordpolicySpecial' => 'Sonderzeichen (!,@#$%^&*?_~()-)', + 'SHOP_MODULE_GROUP_passwordpolicy_apisettings' => 'API Einstellungen', + 'SHOP_MODULE_oxpspasswordpolicyAPIKey' => 'Enzoic API Schlüssel', + 'SHOP_MODULE_oxpspasswordpolicySecretKey' => 'Enzoic Geheimschlüssel' + ); From 4fb68c342f3fd61b690f2ad2a861b60acbc99355 Mon Sep 17 00:00:00 2001 From: moritz_demmer Date: Wed, 14 Apr 2021 13:35:51 +0200 Subject: [PATCH 003/199] added hacked password check --- src/Core/PasswordPolicyValidator.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Core/PasswordPolicyValidator.php b/src/Core/PasswordPolicyValidator.php index e8c5cc1e..77884e31 100644 --- a/src/Core/PasswordPolicyValidator.php +++ b/src/Core/PasswordPolicyValidator.php @@ -29,6 +29,7 @@ use OxidEsales\Eshop\Core\Exception\InputException; use OxidEsales\Eshop\Core\Exception\StandardException; use OxidEsales\Eshop\Core\Registry; +use OxidProfessionalServices\PasswordPolicy\Api\PasswordCheck; class PasswordPolicyValidator extends PasswordPolicyValidator_parent { @@ -96,6 +97,11 @@ public function validatePassword(string $sPassword): ?StandardException $sError = 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESSPECIAL'; } + $pc = new PasswordCheck(); + if ($pc->isPasswordKnown($sPassword)) { + $sError = 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_PASSWORD_KNOWN'; + } + $res = null; if (!empty($sError)) { $translateString = Registry::getLang()->translateString($sError); From bc0f8625215abf08312636f1eb6babd944e1c58b Mon Sep 17 00:00:00 2001 From: moritz_demmer Date: Wed, 14 Apr 2021 13:36:12 +0200 Subject: [PATCH 004/199] added password check --- src/Api/PasswordCheck.php | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/Api/PasswordCheck.php diff --git a/src/Api/PasswordCheck.php b/src/Api/PasswordCheck.php new file mode 100644 index 00000000..4e76c943 --- /dev/null +++ b/src/Api/PasswordCheck.php @@ -0,0 +1,23 @@ +getAPIKey(), $config->getSecretKey()); + + $result = $apiCon->checkPassword($password); + return $result !== null; + } + +} \ No newline at end of file From f4f8ebe938e6c51468bad99a168848c9526f6d11 Mon Sep 17 00:00:00 2001 From: moritz_demmer Date: Wed, 14 Apr 2021 13:36:48 +0200 Subject: [PATCH 005/199] added API Unit test --- tests/unit/Core/PasswordPolicyAPITest.php | 49 +++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 tests/unit/Core/PasswordPolicyAPITest.php diff --git a/tests/unit/Core/PasswordPolicyAPITest.php b/tests/unit/Core/PasswordPolicyAPITest.php new file mode 100644 index 00000000..57b2fddc --- /dev/null +++ b/tests/unit/Core/PasswordPolicyAPITest.php @@ -0,0 +1,49 @@ +subjectUnderTest = new \OxidProfessionalServices\PasswordPolicy\Api\PasswordCheck(); + } + + /** + * @param $test + * @param $known + * @dataProvider passwordData + */ + public function testPasswordCheckWithKnownPassword($test, $known): void + { + $result = $this->subjectUnderTest->isPasswordKnown($test); + $this->assertEquals($known, $result); + } + + public function passwordData() + { + return [ + ['test', true], + ['test123456', true], + ['dokrtngeio39$', false], + ['test1234!', true], + ]; + + } + +} \ No newline at end of file From 69afe36bb88945803a35a92cd37929dde4d1c555 Mon Sep 17 00:00:00 2001 From: moritz_demmer Date: Thu, 15 Apr 2021 14:42:36 +0200 Subject: [PATCH 006/199] PSCORE-65 added own exception for "Passwort befindet sich bereits in gehackter Datenbank" --- metadata.php | 5 +- src/Controller/AccountPasswordController.php | 69 ++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 src/Controller/AccountPasswordController.php diff --git a/metadata.php b/metadata.php index ed70c2c5..7c8020b7 100644 --- a/metadata.php +++ b/metadata.php @@ -25,10 +25,12 @@ * Metadata version */ +use OxidEsales\Eshop\Application\Controller\AccountPasswordController; use OxidEsales\Eshop\Core\InputValidator; use OxidEsales\Eshop\Core\ViewConfig; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyValidator; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyViewConfig; +use OxidProfessionalServices\PasswordPolicy\Controller\AccountPasswordController as PasswordPolicyAccountPasswordController; $sMetadataVersion = '2.1'; @@ -52,7 +54,8 @@ 'email' => 'info@oxid-esales.com', 'extend' => [ ViewConfig::class => PasswordPolicyViewConfig::class, - InputValidator::class => PasswordPolicyValidator::class + InputValidator::class => PasswordPolicyValidator::class, + AccountPasswordController::class => PasswordPolicyAccountPasswordController::class ], 'controllers' => [], 'templates' => [ diff --git a/src/Controller/AccountPasswordController.php b/src/Controller/AccountPasswordController.php new file mode 100644 index 00000000..b2e6c1b4 --- /dev/null +++ b/src/Controller/AccountPasswordController.php @@ -0,0 +1,69 @@ +checkSessionChallenge()) { + return; + } + + $oUser = $this->getUser(); + if (!$oUser) { + return; + } + + $sOldPass = \OxidEsales\Eshop\Core\Registry::getConfig()->getRequestParameter('password_old', true); + $sNewPass = \OxidEsales\Eshop\Core\Registry::getConfig()->getRequestParameter('password_new', true); + $sConfPass = \OxidEsales\Eshop\Core\Registry::getConfig()->getRequestParameter('password_new_confirm', true); + + /** @var \OxidEsales\Eshop\Core\InputValidator $oInputValidator */ + $oInputValidator = \OxidEsales\Eshop\Core\Registry::getInputValidator(); + if (($oExcp = $oInputValidator->checkPassword($oUser, $sNewPass, $sConfPass, true))) { + switch ($oExcp->getMessage()) { + case \OxidEsales\Eshop\Core\Registry::getLang()->translateString('ERROR_MESSAGE_INPUT_EMPTYPASS'): + case \OxidEsales\Eshop\Core\Registry::getLang()->translateString('ERROR_MESSAGE_PASSWORD_TOO_SHORT'): + return \OxidEsales\Eshop\Core\Registry::getUtilsView()->addErrorToDisplay( + 'ERROR_MESSAGE_PASSWORD_TOO_SHORT', + false, + true + ); + case \OxidEsales\Eshop\Core\Registry::getLang()->translateString('OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_PASSWORD_KNOWN'): + return \OxidEsales\Eshop\Core\Registry::getUtilsView()->addErrorToDisplay( + 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_PASSWORD_KNOWN', + false, + true + ); + default: + return \OxidEsales\Eshop\Core\Registry::getUtilsView()->addErrorToDisplay( + 'ERROR_MESSAGE_PASSWORD_DO_NOT_MATCH', + false, + true + ); + } + } + + if (!$sOldPass || !$oUser->isSamePassword($sOldPass)) { + /** @var \OxidEsales\Eshop\Core\UtilsView $oUtilsView */ + $oUtilsView = \OxidEsales\Eshop\Core\Registry::getUtilsView(); + + return $oUtilsView->addErrorToDisplay('ERROR_MESSAGE_CURRENT_PASSWORD_INVALID', false, true); + } + + // testing passed - changing password + $oUser->setPassword($sNewPass); + if ($oUser->save()) { + $this->_blPasswordChanged = true; + // deleting user autologin cookies. + \OxidEsales\Eshop\Core\Registry::getUtilsServer()->deleteUserCookie($this->getConfig()->getShopId()); + } + } + +} \ No newline at end of file From 812e750f14d051873c790b2633a8fad9c44d6800 Mon Sep 17 00:00:00 2001 From: moritz_demmer Date: Fri, 16 Apr 2021 15:11:13 +0200 Subject: [PATCH 007/199] fixed some language issues --- translations/de/passwordpolicy_lang.php | 4 ++-- translations/en/passwordpolicy_lang.php | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/translations/de/passwordpolicy_lang.php b/translations/de/passwordpolicy_lang.php index 3bf74b68..7df1a972 100644 --- a/translations/de/passwordpolicy_lang.php +++ b/translations/de/passwordpolicy_lang.php @@ -50,7 +50,7 @@ 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESDIGITS' => 'Das Passwort muss mindestens eine Zahl enthalten.', 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESUPPERCASE' => 'Das Passwort muss mindestens einen Großbuchstaben enthalten.', 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESLOWERCASE' => 'Das Passwort muss mindestens einen Kleinbuchstaben enthalten.', - 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESSPECIAL' => 'Das Passwort muss mindestens ein Sonderzeichen Zahl enthalten.', + 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESSPECIAL' => 'Das Passwort muss mindestens ein Sonderzeichen enthalten.', 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_WRONGTYPE' => 'Fehlerhafter Typ, bitte tragen Sie einen validen Wert ein. Bei weiteren Fragen wenden Sie sich an den Support.', - 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_PASSWORD_KNOWN' => 'Das Passwort befindet sich bereits in einer gehackten Datenbank und ist somit unsicher!' + 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_PASSWORD_KNOWN' => 'Das Passwort befindet sich bereits in einer gehackten Datenbank und ist somit unsicher.' ); diff --git a/translations/en/passwordpolicy_lang.php b/translations/en/passwordpolicy_lang.php index e5c263d1..1b3a27cf 100644 --- a/translations/en/passwordpolicy_lang.php +++ b/translations/en/passwordpolicy_lang.php @@ -52,4 +52,5 @@ 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESLOWERCASE' => 'The password must include at least one lower case letter.', 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESSPECIAL' => 'The password must include at least one special character and one figure.', 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_WRONGTYPE' => 'Incorrect type, please enter a valid value. If you have any further questions, please contact the support.', + 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_PASSWORD_KNOWN' => 'The password already exists in a hacked database. Please choose a safer one.' ); From 58fff266a618ae972bdb608a194ee7479770fe77 Mon Sep 17 00:00:00 2001 From: moritz_demmer Date: Fri, 16 Apr 2021 15:11:43 +0200 Subject: [PATCH 008/199] cleaned up the code --- src/Controller/AccountPasswordController.php | 36 +++++++------------- 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/src/Controller/AccountPasswordController.php b/src/Controller/AccountPasswordController.php index b2e6c1b4..ff92ad7c 100644 --- a/src/Controller/AccountPasswordController.php +++ b/src/Controller/AccountPasswordController.php @@ -2,6 +2,8 @@ namespace OxidProfessionalServices\PasswordPolicy\Controller; +use OxidEsales\Eshop\Core\InputValidator; + class AccountPasswordController extends AccountPasswordController_parent { /** @@ -24,38 +26,26 @@ public function changePassword() $sNewPass = \OxidEsales\Eshop\Core\Registry::getConfig()->getRequestParameter('password_new', true); $sConfPass = \OxidEsales\Eshop\Core\Registry::getConfig()->getRequestParameter('password_new_confirm', true); + if (!$sOldPass || !$oUser->isSamePassword($sOldPass)) { + /** @var \OxidEsales\Eshop\Core\UtilsView $oUtilsView */ + $oUtilsView = \OxidEsales\Eshop\Core\Registry::getUtilsView(); + + return $oUtilsView->addErrorToDisplay('ERROR_MESSAGE_CURRENT_PASSWORD_INVALID', false, true); + } + /** @var \OxidEsales\Eshop\Core\InputValidator $oInputValidator */ $oInputValidator = \OxidEsales\Eshop\Core\Registry::getInputValidator(); + if (($oExcp = $oInputValidator->checkPassword($oUser, $sNewPass, $sConfPass, true))) { - switch ($oExcp->getMessage()) { - case \OxidEsales\Eshop\Core\Registry::getLang()->translateString('ERROR_MESSAGE_INPUT_EMPTYPASS'): - case \OxidEsales\Eshop\Core\Registry::getLang()->translateString('ERROR_MESSAGE_PASSWORD_TOO_SHORT'): + $tmpInputValidator = oxNew(InputValidator::class); + \OxidEsales\Eshop\Core\Registry::set(InputValidator::class, $tmpInputValidator); return \OxidEsales\Eshop\Core\Registry::getUtilsView()->addErrorToDisplay( - 'ERROR_MESSAGE_PASSWORD_TOO_SHORT', + $oExcp, false, true ); - case \OxidEsales\Eshop\Core\Registry::getLang()->translateString('OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_PASSWORD_KNOWN'): - return \OxidEsales\Eshop\Core\Registry::getUtilsView()->addErrorToDisplay( - 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_PASSWORD_KNOWN', - false, - true - ); - default: - return \OxidEsales\Eshop\Core\Registry::getUtilsView()->addErrorToDisplay( - 'ERROR_MESSAGE_PASSWORD_DO_NOT_MATCH', - false, - true - ); - } } - if (!$sOldPass || !$oUser->isSamePassword($sOldPass)) { - /** @var \OxidEsales\Eshop\Core\UtilsView $oUtilsView */ - $oUtilsView = \OxidEsales\Eshop\Core\Registry::getUtilsView(); - - return $oUtilsView->addErrorToDisplay('ERROR_MESSAGE_CURRENT_PASSWORD_INVALID', false, true); - } // testing passed - changing password $oUser->setPassword($sNewPass); From b24e88d853b902ebbeed064fc8abe068c9ad258e Mon Sep 17 00:00:00 2001 From: moritz_demmer Date: Mon, 19 Apr 2021 12:14:20 +0200 Subject: [PATCH 009/199] added check when logging in --- metadata.php | 8 +++++++- src/Model/User.php | 27 +++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 src/Model/User.php diff --git a/metadata.php b/metadata.php index 7c8020b7..e68e7f04 100644 --- a/metadata.php +++ b/metadata.php @@ -28,9 +28,12 @@ use OxidEsales\Eshop\Application\Controller\AccountPasswordController; use OxidEsales\Eshop\Core\InputValidator; use OxidEsales\Eshop\Core\ViewConfig; +use OxidEsales\Eshop\Application\Model\User; +use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyConfig; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyValidator; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyViewConfig; use OxidProfessionalServices\PasswordPolicy\Controller\AccountPasswordController as PasswordPolicyAccountPasswordController; +use OxidProfessionalServices\PasswordPolicy\Model\User as PasswordPolicyUser; $sMetadataVersion = '2.1'; @@ -55,7 +58,8 @@ 'extend' => [ ViewConfig::class => PasswordPolicyViewConfig::class, InputValidator::class => PasswordPolicyValidator::class, - AccountPasswordController::class => PasswordPolicyAccountPasswordController::class + AccountPasswordController::class => PasswordPolicyAccountPasswordController::class, + User::class => PasswordPolicyUser::class ], 'controllers' => [], 'templates' => [ @@ -84,6 +88,8 @@ ['group' => 'passwordpolicy_requirements', 'name' => 'oxpspasswordpolicyLowerCase', 'type' => 'bool', 'value' => true], ['group' => 'passwordpolicy_requirements', 'name' => 'oxpspasswordpolicySpecial', 'type' => 'bool', 'value' => true], ['group' => 'passwordpolicy_requirements', 'name' => 'oxpspasswordpolicyDigits', 'type' => 'bool', 'value' => true], + ['group' => 'passwordpolicy_apisettings', 'name' => PasswordPolicyConfig::SettingAPIKey, 'type' => 'str'], + ['group' => 'passwordpolicy_apisettings', 'name' => PasswordPolicyConfig::SettingSecretKey, 'type' => 'str'], ], 'events' => [], ]; diff --git a/src/Model/User.php b/src/Model/User.php new file mode 100644 index 00000000..4e6ec8ee --- /dev/null +++ b/src/Model/User.php @@ -0,0 +1,27 @@ +validatePassword($password)) { + $forgot = new ForgotPasswordController(); + $forgot->forgotPassword(); + throw oxNew(UserException::class, $err->getMessage()); + } + return parent::onLogin($userName,$password); + } +} \ No newline at end of file From a28317e394e0dd0ad0d22e45e9404b9c4bda3bf5 Mon Sep 17 00:00:00 2001 From: moritz_demmer Date: Tue, 20 Apr 2021 02:01:27 +0200 Subject: [PATCH 010/199] added possibility to deactivate api in the backend --- metadata.php | 1 + src/Api/PasswordCheck.php | 5 ++++- src/Core/PasswordPolicyConfig.php | 6 ++++++ views/admin/de/passwordpolicy_lang.php | 1 + 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/metadata.php b/metadata.php index e68e7f04..dfc6a5c3 100644 --- a/metadata.php +++ b/metadata.php @@ -88,6 +88,7 @@ ['group' => 'passwordpolicy_requirements', 'name' => 'oxpspasswordpolicyLowerCase', 'type' => 'bool', 'value' => true], ['group' => 'passwordpolicy_requirements', 'name' => 'oxpspasswordpolicySpecial', 'type' => 'bool', 'value' => true], ['group' => 'passwordpolicy_requirements', 'name' => 'oxpspasswordpolicyDigits', 'type' => 'bool', 'value' => true], + ['group' => 'passwordpolicy_apisettings', 'name' => PasswordPolicyConfig::SettingAPI, 'type' => 'bool', 'value' => false], ['group' => 'passwordpolicy_apisettings', 'name' => PasswordPolicyConfig::SettingAPIKey, 'type' => 'str'], ['group' => 'passwordpolicy_apisettings', 'name' => PasswordPolicyConfig::SettingSecretKey, 'type' => 'str'], ], diff --git a/src/Api/PasswordCheck.php b/src/Api/PasswordCheck.php index 4e76c943..aa3c9627 100644 --- a/src/Api/PasswordCheck.php +++ b/src/Api/PasswordCheck.php @@ -14,8 +14,11 @@ class PasswordCheck public function isPasswordKnown(string $password): bool { $config = new PasswordPolicyConfig(); + if(!$config->getAPINeeded()) + { + return false; + } $apiCon = new Enzoic($config->getAPIKey(), $config->getSecretKey()); - $result = $apiCon->checkPassword($password); return $result !== null; } diff --git a/src/Core/PasswordPolicyConfig.php b/src/Core/PasswordPolicyConfig.php index e207c917..9d4a2e1f 100644 --- a/src/Core/PasswordPolicyConfig.php +++ b/src/Core/PasswordPolicyConfig.php @@ -39,6 +39,7 @@ class PasswordPolicyConfig public const SettingMinPasswordLength = self::SettingsPrefix . 'MinPasswordLength'; public const SettingDigits = self::SettingsPrefix . 'Digits'; public const SettingSpecial = self::SettingsPrefix . 'Special'; + public const SettingAPI = self::SettingsPrefix . 'API'; public const SettingAPIKey = self::SettingsPrefix . 'APIKey'; public const SettingSecretKey = self::SettingsPrefix . 'SecretKey'; public const SettingUpper = self::SettingsPrefix . 'UpperCase'; @@ -97,6 +98,11 @@ public function getSecretKey(): string return (string) Registry::getConfig()->getConfigParam(self::SettingSecretKey); } + public function getAPINeeded(): bool + { + return $this->isConfigParam(self::SettingAPI); + } + private function isConfigParam(string $name): bool { return (bool) Registry::getConfig()->getConfigParam($name, true); diff --git a/views/admin/de/passwordpolicy_lang.php b/views/admin/de/passwordpolicy_lang.php index 525a6c0d..88c1ce93 100644 --- a/views/admin/de/passwordpolicy_lang.php +++ b/views/admin/de/passwordpolicy_lang.php @@ -31,6 +31,7 @@ 'SHOP_MODULE_oxpspasswordpolicyLowerCase' => 'Kleinbuchstaben (a...z)', 'SHOP_MODULE_oxpspasswordpolicySpecial' => 'Sonderzeichen (!,@#$%^&*?_~()-)', 'SHOP_MODULE_GROUP_passwordpolicy_apisettings' => 'API Einstellungen', + 'SHOP_MODULE_oxpspasswordpolicyAPI' => 'Passwort Prüfung', 'SHOP_MODULE_oxpspasswordpolicyAPIKey' => 'Enzoic API Schlüssel', 'SHOP_MODULE_oxpspasswordpolicySecretKey' => 'Enzoic Geheimschlüssel' From 7c0ea295e1d82eef0c85e86cae2dd66949d06d66 Mon Sep 17 00:00:00 2001 From: moritz_demmer Date: Tue, 20 Apr 2021 02:01:48 +0200 Subject: [PATCH 011/199] renamed variables --- src/Model/User.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Model/User.php b/src/Model/User.php index 4e6ec8ee..7ae16619 100644 --- a/src/Model/User.php +++ b/src/Model/User.php @@ -16,10 +16,10 @@ class User extends User_parent */ public function onLogin($userName, $password) { - $check = new PasswordPolicyValidator(); - if ($err = $check->validatePassword($password)) { - $forgot = new ForgotPasswordController(); - $forgot->forgotPassword(); + $passValidator = new PasswordPolicyValidator(); + if ($err = $passValidator->validatePassword($password)) { + $forgotPass = new ForgotPasswordController(); + $forgotPass->forgotPassword(); throw oxNew(UserException::class, $err->getMessage()); } return parent::onLogin($userName,$password); From bdfe730318524a9582bfbe4429eafbed419a714c Mon Sep 17 00:00:00 2001 From: moritz_demmer Date: Tue, 20 Apr 2021 17:31:23 +0200 Subject: [PATCH 012/199] removed unecessary API lib --- src/Api/PasswordCheck.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Api/PasswordCheck.php b/src/Api/PasswordCheck.php index aa3c9627..b1eb5f4a 100644 --- a/src/Api/PasswordCheck.php +++ b/src/Api/PasswordCheck.php @@ -4,7 +4,6 @@ namespace OxidProfessionalServices\PasswordPolicy\Api; use Enzoic\Enzoic; -use Enzoic\PasswordType; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyConfig; class PasswordCheck From be2e3b98c0c7b672516920e129c34c249482b67e Mon Sep 17 00:00:00 2001 From: moritz_demmer Date: Tue, 20 Apr 2021 17:32:09 +0200 Subject: [PATCH 013/199] removed unecessary use --- src/Core/PasswordPolicyConfig.php | 2 -- src/Core/PasswordPolicyViewConfig.php | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/Core/PasswordPolicyConfig.php b/src/Core/PasswordPolicyConfig.php index 9d4a2e1f..e5eb2eef 100644 --- a/src/Core/PasswordPolicyConfig.php +++ b/src/Core/PasswordPolicyConfig.php @@ -26,8 +26,6 @@ namespace OxidProfessionalServices\PasswordPolicy\Core; use OxidEsales\Eshop\Core\Registry; -use OxidEsales\Eshop\Core\Model\BaseModel; -use OxidEsales\Eshop\Core\DatabaseProvider; /** * Password policy config helpers used in controllers mostly diff --git a/src/Core/PasswordPolicyViewConfig.php b/src/Core/PasswordPolicyViewConfig.php index 0dd3e5f5..bc3b01da 100644 --- a/src/Core/PasswordPolicyViewConfig.php +++ b/src/Core/PasswordPolicyViewConfig.php @@ -26,8 +26,6 @@ namespace OxidProfessionalServices\PasswordPolicy\Core; use OxidEsales\Eshop\Core\Registry; -use OxidEsales\Eshop\Core\Model\BaseModel; -use OxidEsales\Eshop\Core\DatabaseProvider; /** * Password policy config helpers used in controllers mostly From 6a8643a970ed9634cb4661af993f2cfb15f7f04c Mon Sep 17 00:00:00 2001 From: moritz_demmer Date: Tue, 20 Apr 2021 17:33:28 +0200 Subject: [PATCH 014/199] improved code with dependency injection --- src/Core/PasswordPolicyValidator.php | 76 +++---------------- .../PasswordPolicyCheckInterface.php | 19 +++++ .../PasswordPolicyDatabaseCheck.php | 21 +++++ .../PasswordPolicyRequirementsCheck.php | 51 +++++++++++++ src/Validators/PasswordPolicyVisitor.php | 31 ++++++++ 5 files changed, 134 insertions(+), 64 deletions(-) create mode 100644 src/Validators/PasswordPolicyCheckInterface.php create mode 100644 src/Validators/PasswordPolicyDatabaseCheck.php create mode 100644 src/Validators/PasswordPolicyRequirementsCheck.php create mode 100644 src/Validators/PasswordPolicyVisitor.php diff --git a/src/Core/PasswordPolicyValidator.php b/src/Core/PasswordPolicyValidator.php index 77884e31..144661ab 100644 --- a/src/Core/PasswordPolicyValidator.php +++ b/src/Core/PasswordPolicyValidator.php @@ -29,10 +29,11 @@ use OxidEsales\Eshop\Core\Exception\InputException; use OxidEsales\Eshop\Core\Exception\StandardException; use OxidEsales\Eshop\Core\Registry; -use OxidProfessionalServices\PasswordPolicy\Api\PasswordCheck; +use OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyVisitor; class PasswordPolicyValidator extends PasswordPolicyValidator_parent { + /** * @param User $user * @param string $newPassword @@ -43,84 +44,31 @@ class PasswordPolicyValidator extends PasswordPolicyValidator_parent */ public function checkPassword($user, $newPassword, $confirmationPassword, $shouldCheckPasswordLength = false) { - $ex = $this->validatePassword($newPassword); + $username = $user->oxuser__oxusername->value ?: ""; + $ex = $this->validatePassword($username, $newPassword); if (isset($ex)) { return $ex; } return parent::checkPassword($user, $newPassword, $confirmationPassword, $shouldCheckPasswordLength); } - public function getModuleSettings(): PasswordPolicyConfig - { - return Registry::get(PasswordPolicyConfig::class); - } - /** * Validate password with password policy rules. * * @param string $sPassword * @return null|StandardException */ - public function validatePassword(string $sPassword): ?StandardException + public function validatePassword(string $sUsername, string $sPassword): ?StandardException { - $sError = ''; - $iPasswordLength = mb_strlen($sPassword, 'UTF-8'); - - // Load module settings - $settings = $this->getModuleSettings(); - - // Validate password according to settings params - if ($iPasswordLength < $settings->getMinPasswordLength()) { - $sError = 'ERROR_MESSAGE_PASSWORD_TOO_SHORT'; - } - - if ($iPasswordLength > $settings->getMaxPasswordLength()) { - $sError = 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_TOOLONG'; - } - - if ($settings->getPasswordNeedDigits() and !preg_match('(\d+)', $sPassword)) { - $sError = 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESDIGITS'; - } - - if ($settings->getPasswordNeedUpperCase() and !preg_match('(\p{Lu}+)', $sPassword)) { - $sError = 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESUPPERCASE'; - } - - if ($settings->getPasswordNeedLowerCase() and !preg_match('(\p{Ll}+)', $sPassword)) { - $sError = 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESLOWERCASE'; - } - - if ( - $settings->getPasswordNeedSpecialCharacter() and - !preg_match('([\.,_@\~\(\)\!\#\$%\^\&\*\+=\-\\\/|:;`]+)', $sPassword) - ) { - $sError = 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESSPECIAL'; - } - - $pc = new PasswordCheck(); - if ($pc->isPasswordKnown($sPassword)) { - $sError = 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_PASSWORD_KNOWN'; - } - - $res = null; - if (!empty($sError)) { + $container = $this->getContainer(); + $passwordPolicyVisitor = $container->get(PasswordPolicyVisitor::class); + $sError = $passwordPolicyVisitor->validate($sUsername, $sPassword); + if (is_string($sError)) { $translateString = Registry::getLang()->translateString($sError); - /** @var StandardException $exception (makes psalm happy) */ + /** @var StandardException $exception (makes psalm happy) */ $exception = oxNew(InputException::class, $translateString); - - $res = $this->addValidationError("oxuser__oxpassword", $exception); + return $exception; } - - return $res; - } - - /** - * Min length of password. - * - * @return int - */ - public function getPasswordLength() - { - return (int) Registry::getConfig()->getConfigParam(PasswordPolicyConfig::SettingMinPasswordLength, 8); + return null; } } diff --git a/src/Validators/PasswordPolicyCheckInterface.php b/src/Validators/PasswordPolicyCheckInterface.php new file mode 100644 index 00000000..118960fa --- /dev/null +++ b/src/Validators/PasswordPolicyCheckInterface.php @@ -0,0 +1,19 @@ +isPasswordKnown($sPassword)) { + return 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_PASSWORD_KNOWN'; + } + return true; + } + +} \ No newline at end of file diff --git a/src/Validators/PasswordPolicyRequirementsCheck.php b/src/Validators/PasswordPolicyRequirementsCheck.php new file mode 100644 index 00000000..e6745a4d --- /dev/null +++ b/src/Validators/PasswordPolicyRequirementsCheck.php @@ -0,0 +1,51 @@ +getModuleSettings(); + + if ($iPasswordLength < $settings->getMinPasswordLength()) { + return $sError = 'ERROR_MESSAGE_PASSWORD_TOO_SHORT'; + } + + if ($iPasswordLength > $settings->getMaxPasswordLength()) { + return $sError = 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_TOOLONG'; + } + + if ($settings->getPasswordNeedDigits() and !preg_match('(\d+)', $sPassword)) { + return $sError = 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESDIGITS'; + } + + if ($settings->getPasswordNeedUpperCase() and !preg_match('(\p{Lu}+)', $sPassword)) { + return $sError = 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESUPPERCASE'; + } + + if ($settings->getPasswordNeedLowerCase() and !preg_match('(\p{Ll}+)', $sPassword)) { + return $sError = 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESLOWERCASE'; + } + + if ( + $settings->getPasswordNeedSpecialCharacter() and + !preg_match('([\.,_@\~\(\)\!\#\$%\^\&\*\+=\-\\\/|:;`]+)', $sPassword) + ) { + return $sError = 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESSPECIAL'; + } + return true; + } +} \ No newline at end of file diff --git a/src/Validators/PasswordPolicyVisitor.php b/src/Validators/PasswordPolicyVisitor.php new file mode 100644 index 00000000..bc8d7d90 --- /dev/null +++ b/src/Validators/PasswordPolicyVisitor.php @@ -0,0 +1,31 @@ +validators = $validators; + } + + public function validate(string $sUsername, string $sPassword) + { + foreach ($this->validators as $validator) + { + $sError = $validator->validate($sUsername, $sPassword); + if (is_string($sError)) { + return $sError; + } + } + return true; + } +} \ No newline at end of file From de3d4d0d76d47b718af18eb65ec20e77c725d137 Mon Sep 17 00:00:00 2001 From: moritz_demmer Date: Tue, 20 Apr 2021 17:34:23 +0200 Subject: [PATCH 015/199] fixed bug that password is checked for an unauthorized user --- src/Model/User.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Model/User.php b/src/Model/User.php index 7ae16619..d568da33 100644 --- a/src/Model/User.php +++ b/src/Model/User.php @@ -4,6 +4,7 @@ use OxidEsales\Eshop\Application\Controller\ForgotPasswordController; use OxidEsales\Eshop\Core\Exception\UserException; +use OxidEsales\Eshop\Core\InputValidator; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyValidator; class User extends User_parent @@ -16,8 +17,10 @@ class User extends User_parent */ public function onLogin($userName, $password) { - $passValidator = new PasswordPolicyValidator(); - if ($err = $passValidator->validatePassword($password)) { + /** @var PasswordPolicyValidator $passValidator */ + $passValidator = oxNew(InputValidator::class); + + if ($this->isLoaded() && $err = $passValidator->validatePassword($userName, $password)) { $forgotPass = new ForgotPasswordController(); $forgotPass->forgotPassword(); throw oxNew(UserException::class, $err->getMessage()); From a45d8613b45580d2c15e1703bb5b9eceb4808b8d Mon Sep 17 00:00:00 2001 From: moritz_demmer Date: Tue, 20 Apr 2021 17:35:20 +0200 Subject: [PATCH 016/199] added dependencies in services.yaml --- services.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 services.yaml diff --git a/services.yaml b/services.yaml new file mode 100644 index 00000000..fac11f57 --- /dev/null +++ b/services.yaml @@ -0,0 +1,12 @@ +services: + OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyVisitor: + class: OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyVisitor + arguments: + $validators: + - '@OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyRequirementsCheck' + - '@OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyDatabaseCheck' + autowire: true + OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyRequirementsCheck: + class: OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyRequirementsCheck + OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyDatabaseCheck: + class: OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyDatabaseCheck \ No newline at end of file From ef9f747ef85c5ff70d58ce00baafbbd1fae0c143 Mon Sep 17 00:00:00 2001 From: moritz_demmer Date: Wed, 21 Apr 2021 12:05:38 +0200 Subject: [PATCH 017/199] removed settings check and added it in the DataBreach Validator Class --- src/Api/PasswordCheck.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Api/PasswordCheck.php b/src/Api/PasswordCheck.php index b1eb5f4a..1963dd3a 100644 --- a/src/Api/PasswordCheck.php +++ b/src/Api/PasswordCheck.php @@ -13,10 +13,6 @@ class PasswordCheck public function isPasswordKnown(string $password): bool { $config = new PasswordPolicyConfig(); - if(!$config->getAPINeeded()) - { - return false; - } $apiCon = new Enzoic($config->getAPIKey(), $config->getSecretKey()); $result = $apiCon->checkPassword($password); return $result !== null; From 858874304bf396acf647d0fcc3cc841a31f412a9 Mon Sep 17 00:00:00 2001 From: moritz_demmer Date: Wed, 21 Apr 2021 12:06:46 +0200 Subject: [PATCH 018/199] renamed file names --- src/Core/PasswordPolicyValidator.php | 7 +-- ...Check.php => PasswordPolicyDataBreach.php} | 8 +-- src/Validators/PasswordPolicyDigits.php | 19 +++++++ .../PasswordPolicyPasswordLength.php | 24 +++++++++ .../PasswordPolicyRequirementsCheck.php | 51 ------------------- .../PasswordPolicySpecialCharacter.php | 22 ++++++++ .../PasswordPolicyUpperLowerCase.php | 23 +++++++++ ... => PasswordPolicyValidationInterface.php} | 4 +- ... => PasswordPolicyValidatorsCollector.php} | 6 +-- 9 files changed, 102 insertions(+), 62 deletions(-) rename src/Validators/{PasswordPolicyDatabaseCheck.php => PasswordPolicyDataBreach.php} (50%) create mode 100644 src/Validators/PasswordPolicyDigits.php create mode 100644 src/Validators/PasswordPolicyPasswordLength.php delete mode 100644 src/Validators/PasswordPolicyRequirementsCheck.php create mode 100644 src/Validators/PasswordPolicySpecialCharacter.php create mode 100644 src/Validators/PasswordPolicyUpperLowerCase.php rename src/Validators/{PasswordPolicyCheckInterface.php => PasswordPolicyValidationInterface.php} (77%) rename src/Validators/{PasswordPolicyVisitor.php => PasswordPolicyValidatorsCollector.php} (74%) diff --git a/src/Core/PasswordPolicyValidator.php b/src/Core/PasswordPolicyValidator.php index 144661ab..07c88574 100644 --- a/src/Core/PasswordPolicyValidator.php +++ b/src/Core/PasswordPolicyValidator.php @@ -29,7 +29,7 @@ use OxidEsales\Eshop\Core\Exception\InputException; use OxidEsales\Eshop\Core\Exception\StandardException; use OxidEsales\Eshop\Core\Registry; -use OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyVisitor; +use OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyValidatorsCollector; class PasswordPolicyValidator extends PasswordPolicyValidator_parent { @@ -44,6 +44,7 @@ class PasswordPolicyValidator extends PasswordPolicyValidator_parent */ public function checkPassword($user, $newPassword, $confirmationPassword, $shouldCheckPasswordLength = false) { + /** Muss noch besser gelöst werden */ $username = $user->oxuser__oxusername->value ?: ""; $ex = $this->validatePassword($username, $newPassword); if (isset($ex)) { @@ -61,8 +62,8 @@ public function checkPassword($user, $newPassword, $confirmationPassword, $shoul public function validatePassword(string $sUsername, string $sPassword): ?StandardException { $container = $this->getContainer(); - $passwordPolicyVisitor = $container->get(PasswordPolicyVisitor::class); - $sError = $passwordPolicyVisitor->validate($sUsername, $sPassword); + $passwordPolicyValidatorsCollector = $container->get(PasswordPolicyValidatorsCollector::class); + $sError = $passwordPolicyValidatorsCollector->validate($sUsername, $sPassword); if (is_string($sError)) { $translateString = Registry::getLang()->translateString($sError); /** @var StandardException $exception (makes psalm happy) */ diff --git a/src/Validators/PasswordPolicyDatabaseCheck.php b/src/Validators/PasswordPolicyDataBreach.php similarity index 50% rename from src/Validators/PasswordPolicyDatabaseCheck.php rename to src/Validators/PasswordPolicyDataBreach.php index e79e024d..84988f03 100644 --- a/src/Validators/PasswordPolicyDatabaseCheck.php +++ b/src/Validators/PasswordPolicyDataBreach.php @@ -5,14 +5,16 @@ use OxidProfessionalServices\PasswordPolicy\Api\PasswordCheck; +use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyConfig; -class PasswordPolicyDatabaseCheck implements PasswordPolicyCheckInterface +class PasswordPolicyDataBreach implements PasswordPolicyValidationInterface { public function validate(string $sUsername, string $sPassword) { - $pc = new PasswordCheck(); - if ($pc->isPasswordKnown($sPassword)) { + $settings = new PasswordPolicyConfig(); + $passwordCheck = new PasswordCheck(); + if ($settings->getAPINeeded() && $passwordCheck->isPasswordKnown($sPassword)) { return 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_PASSWORD_KNOWN'; } return true; diff --git a/src/Validators/PasswordPolicyDigits.php b/src/Validators/PasswordPolicyDigits.php new file mode 100644 index 00000000..706ab946 --- /dev/null +++ b/src/Validators/PasswordPolicyDigits.php @@ -0,0 +1,19 @@ +getPasswordNeedDigits() and !preg_match('(\d+)', $sPassword)) { + return 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESDIGITS'; + } + return true; + } +} \ No newline at end of file diff --git a/src/Validators/PasswordPolicyPasswordLength.php b/src/Validators/PasswordPolicyPasswordLength.php new file mode 100644 index 00000000..eb6191ee --- /dev/null +++ b/src/Validators/PasswordPolicyPasswordLength.php @@ -0,0 +1,24 @@ +getMinPasswordLength()) { + return 'ERROR_MESSAGE_PASSWORD_TOO_SHORT'; + } + + if ($iPasswordLength > $settings->getMaxPasswordLength()) { + return 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_TOOLONG'; + } + return true; + } +} \ No newline at end of file diff --git a/src/Validators/PasswordPolicyRequirementsCheck.php b/src/Validators/PasswordPolicyRequirementsCheck.php deleted file mode 100644 index e6745a4d..00000000 --- a/src/Validators/PasswordPolicyRequirementsCheck.php +++ /dev/null @@ -1,51 +0,0 @@ -getModuleSettings(); - - if ($iPasswordLength < $settings->getMinPasswordLength()) { - return $sError = 'ERROR_MESSAGE_PASSWORD_TOO_SHORT'; - } - - if ($iPasswordLength > $settings->getMaxPasswordLength()) { - return $sError = 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_TOOLONG'; - } - - if ($settings->getPasswordNeedDigits() and !preg_match('(\d+)', $sPassword)) { - return $sError = 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESDIGITS'; - } - - if ($settings->getPasswordNeedUpperCase() and !preg_match('(\p{Lu}+)', $sPassword)) { - return $sError = 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESUPPERCASE'; - } - - if ($settings->getPasswordNeedLowerCase() and !preg_match('(\p{Ll}+)', $sPassword)) { - return $sError = 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESLOWERCASE'; - } - - if ( - $settings->getPasswordNeedSpecialCharacter() and - !preg_match('([\.,_@\~\(\)\!\#\$%\^\&\*\+=\-\\\/|:;`]+)', $sPassword) - ) { - return $sError = 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESSPECIAL'; - } - return true; - } -} \ No newline at end of file diff --git a/src/Validators/PasswordPolicySpecialCharacter.php b/src/Validators/PasswordPolicySpecialCharacter.php new file mode 100644 index 00000000..f6620902 --- /dev/null +++ b/src/Validators/PasswordPolicySpecialCharacter.php @@ -0,0 +1,22 @@ +getPasswordNeedSpecialCharacter() and + !preg_match('([\.,_@\~\(\)\!\#\$%\^\&\*\+=\-\\\/|:;`]+)', $sPassword) + ) { + return 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESSPECIAL'; + } + return true; + } +} \ No newline at end of file diff --git a/src/Validators/PasswordPolicyUpperLowerCase.php b/src/Validators/PasswordPolicyUpperLowerCase.php new file mode 100644 index 00000000..86d6d67d --- /dev/null +++ b/src/Validators/PasswordPolicyUpperLowerCase.php @@ -0,0 +1,23 @@ +getPasswordNeedUpperCase() and !preg_match('(\p{Lu}+)', $sPassword)) { + return 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESUPPERCASE'; + } + + if ($settings->getPasswordNeedLowerCase() and !preg_match('(\p{Ll}+)', $sPassword)) { + return 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESLOWERCASE'; + } + return true; + } +} \ No newline at end of file diff --git a/src/Validators/PasswordPolicyCheckInterface.php b/src/Validators/PasswordPolicyValidationInterface.php similarity index 77% rename from src/Validators/PasswordPolicyCheckInterface.php rename to src/Validators/PasswordPolicyValidationInterface.php index 118960fa..34f51700 100644 --- a/src/Validators/PasswordPolicyCheckInterface.php +++ b/src/Validators/PasswordPolicyValidationInterface.php @@ -5,10 +5,10 @@ /** - * Interface PasswordPolicyCheckInterface + * Interface PasswordPolicyValidationInterface * @package OxidProfessionalServices\PasswordPolicy\Validators */ -interface PasswordPolicyCheckInterface +interface PasswordPolicyValidationInterface { /** * @param string $sUsername diff --git a/src/Validators/PasswordPolicyVisitor.php b/src/Validators/PasswordPolicyValidatorsCollector.php similarity index 74% rename from src/Validators/PasswordPolicyVisitor.php rename to src/Validators/PasswordPolicyValidatorsCollector.php index bc8d7d90..30414a63 100644 --- a/src/Validators/PasswordPolicyVisitor.php +++ b/src/Validators/PasswordPolicyValidatorsCollector.php @@ -3,14 +3,14 @@ namespace OxidProfessionalServices\PasswordPolicy\Validators; -class PasswordPolicyVisitor implements PasswordPolicyCheckInterface +class PasswordPolicyValidatorsCollector implements PasswordPolicyValidationInterface { /** - * @var PasswordPolicyCheckInterface[] + * @var PasswordPolicyValidationInterface[] */ private $validators; /** - * PasswordPolicyVisitor constructor. + * PasswordPolicyValidatorsCollector constructor. */ public function __construct(array $validators) { From fa85fa12d89b188eaf9c4d78a3d0dae25a9ecf98 Mon Sep 17 00:00:00 2001 From: moritz_demmer Date: Wed, 21 Apr 2021 12:07:00 +0200 Subject: [PATCH 019/199] added new validators --- services.yaml | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/services.yaml b/services.yaml index fac11f57..68670865 100644 --- a/services.yaml +++ b/services.yaml @@ -1,12 +1,21 @@ services: - OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyVisitor: - class: OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyVisitor + OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyValidatorsCollector: + class: OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyValidatorsCollector arguments: $validators: - - '@OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyRequirementsCheck' - - '@OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyDatabaseCheck' + - '@OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyPasswordLength' + - '@OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyUpperLowerCase' + - '@OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyDigits' + - '@OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicySpecialCharacter' + - '@OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyDataBreach' autowire: true - OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyRequirementsCheck: - class: OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyRequirementsCheck - OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyDatabaseCheck: - class: OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyDatabaseCheck \ No newline at end of file + OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyPasswordLength: + class: OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyPasswordLength + OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyUpperLowerCase: + class: OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyUpperLowerCase + OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyDigits: + class: OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyDigits + OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicySpecialCharacter: + class: OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicySpecialCharacter + OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyDataBreach: + class: OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyDataBreach \ No newline at end of file From 363aadee4fade1e79f7ad1c27a8cb725a659a24f Mon Sep 17 00:00:00 2001 From: moritz_demmer Date: Wed, 21 Apr 2021 17:08:14 +0200 Subject: [PATCH 020/199] added credentials api --- src/Api/PasswordCheck.php | 35 +++++++++++++++++++-- src/Validators/PasswordPolicyDataBreach.php | 2 +- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/Api/PasswordCheck.php b/src/Api/PasswordCheck.php index 1963dd3a..5ad1af5b 100644 --- a/src/Api/PasswordCheck.php +++ b/src/Api/PasswordCheck.php @@ -3,19 +3,48 @@ declare(strict_types=1); namespace OxidProfessionalServices\PasswordPolicy\Api; + use Enzoic\Enzoic; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyConfig; +/** + * Class PasswordCheck + * @package OxidProfessionalServices\PasswordPolicy\Api + */ class PasswordCheck { + private $config; + private $apiCon; + + /** + * PasswordCheck constructor. + */ + public function __construct() + { + $this->config = new PasswordPolicyConfig(); + $this->apiCon = new Enzoic($this->config->getAPIKey(), $this->config->getSecretKey()); + } + /** + * @param string $password + * @return bool + */ public function isPasswordKnown(string $password): bool { - $config = new PasswordPolicyConfig(); - $apiCon = new Enzoic($config->getAPIKey(), $config->getSecretKey()); - $result = $apiCon->checkPassword($password); + $result = $this->apiCon->checkPassword($password); return $result !== null; } + /** + * @param string $username + * @param string $password + * @return bool + */ + public function isCredentialsKnown(string $username, string $password): bool + { + $result = $this->apiCon->checkCredentials($username, $password); + return $result; + } + } \ No newline at end of file diff --git a/src/Validators/PasswordPolicyDataBreach.php b/src/Validators/PasswordPolicyDataBreach.php index 84988f03..13d2044c 100644 --- a/src/Validators/PasswordPolicyDataBreach.php +++ b/src/Validators/PasswordPolicyDataBreach.php @@ -14,7 +14,7 @@ public function validate(string $sUsername, string $sPassword) { $settings = new PasswordPolicyConfig(); $passwordCheck = new PasswordCheck(); - if ($settings->getAPINeeded() && $passwordCheck->isPasswordKnown($sPassword)) { + if ($settings->getAPINeeded() && ($passwordCheck->isPasswordKnown($sPassword) || $passwordCheck->isCredentialsKnown($sUsername,$sPassword))) { return 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_PASSWORD_KNOWN'; } return true; From 484527305b0c2b1921437946f6730df00c706f58 Mon Sep 17 00:00:00 2001 From: moritz_demmer Date: Wed, 21 Apr 2021 17:08:48 +0200 Subject: [PATCH 021/199] fixed registration bug (no error message) --- src/Core/PasswordPolicyValidator.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Core/PasswordPolicyValidator.php b/src/Core/PasswordPolicyValidator.php index 07c88574..2205cccb 100644 --- a/src/Core/PasswordPolicyValidator.php +++ b/src/Core/PasswordPolicyValidator.php @@ -66,10 +66,19 @@ public function validatePassword(string $sUsername, string $sPassword): ?Standar $sError = $passwordPolicyValidatorsCollector->validate($sUsername, $sPassword); if (is_string($sError)) { $translateString = Registry::getLang()->translateString($sError); - /** @var StandardException $exception (makes psalm happy) */ $exception = oxNew(InputException::class, $translateString); - return $exception; + return $this->addValidationError("oxuser__oxpassword", $exception); } return null; } + + /** + * Min length of password. + * + * @return int + */ + public function getPasswordLength() + { + return (int) Registry::getConfig()->getConfigParam(PasswordPolicyConfig::SettingMinPasswordLength, 8); + } } From 87c9e167d260bcd209ab20fd4aec2062a52b2ad4 Mon Sep 17 00:00:00 2001 From: moritz_demmer Date: Thu, 22 Apr 2021 16:32:55 +0200 Subject: [PATCH 022/199] fixed unit tests --- .../Core/PasswordPolicyConfigTest.php | 12 ----- .../Core/PasswordPolicyValidatorTest.php | 6 ++- tests/unit/Core/PasswordPolicyAPITest.php | 49 ------------------- 3 files changed, 5 insertions(+), 62 deletions(-) delete mode 100644 tests/unit/Core/PasswordPolicyAPITest.php diff --git a/tests/Integration/Core/PasswordPolicyConfigTest.php b/tests/Integration/Core/PasswordPolicyConfigTest.php index aaab322c..ca83abc5 100644 --- a/tests/Integration/Core/PasswordPolicyConfigTest.php +++ b/tests/Integration/Core/PasswordPolicyConfigTest.php @@ -25,9 +25,7 @@ namespace OxidProfessionalServices\PasswordPolicy\Tests\Integration\Core; -use OxidEsales\Eshop\Core\InputValidator; use OxidEsales\Eshop\Core\Registry; -use OxidEsales\Eshop\Core\ViewConfig; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyConfig; use PHPUnit\Framework\TestCase; @@ -53,12 +51,6 @@ public function setUp(): void parent::setUp(); $this->subjectUnderTest = new PasswordPolicyConfig(); } - - public function testGetAPIKey(): void - { - $this->saveAPIKey("2342355ss33wsada3"); - $this->assertEquals("2342355ss33wsada3", $this->subjectUnderTest->getAPIKey()); - } /** * @dataProvider lengthProvider * @param int $len @@ -115,10 +107,6 @@ public function trueFalseProvider() ]; } - public function saveAPIKey(string $key): void - { - $this->setConfig(PasswordPolicyConfig::SettingAPIKey, $key); - } public function saveGoodPasswordLength(int $len): void { $this->setConfig(PasswordPolicyConfig::SettingGoodPasswordLength, $len); diff --git a/tests/Integration/Core/PasswordPolicyValidatorTest.php b/tests/Integration/Core/PasswordPolicyValidatorTest.php index f174aa1c..73b5478a 100644 --- a/tests/Integration/Core/PasswordPolicyValidatorTest.php +++ b/tests/Integration/Core/PasswordPolicyValidatorTest.php @@ -82,6 +82,10 @@ public function savePasswordLength(int $len): void public function passwordPolicyProvider(): array { return array_merge( + $this->withPolicyCombinations( + "Test1234!", + false + ), $this->withPolicyCombinations( "ThisPasswordFulfills5Requirements!", true @@ -163,7 +167,7 @@ private function policyCombinations($mainPolicyName, $mainPolicyValue): array public function testValidatePasswordPolicy(array $policy, string $password, bool $shouldPass) { $this->setPolicy($policy); - $this->subjectUnderTest->validatePassword($password); + $this->subjectUnderTest->validatePassword('', $password); $ex = $this->subjectUnderTest->getFirstValidationError(); if ($shouldPass) { $this->assertNull($ex); diff --git a/tests/unit/Core/PasswordPolicyAPITest.php b/tests/unit/Core/PasswordPolicyAPITest.php deleted file mode 100644 index 57b2fddc..00000000 --- a/tests/unit/Core/PasswordPolicyAPITest.php +++ /dev/null @@ -1,49 +0,0 @@ -subjectUnderTest = new \OxidProfessionalServices\PasswordPolicy\Api\PasswordCheck(); - } - - /** - * @param $test - * @param $known - * @dataProvider passwordData - */ - public function testPasswordCheckWithKnownPassword($test, $known): void - { - $result = $this->subjectUnderTest->isPasswordKnown($test); - $this->assertEquals($known, $result); - } - - public function passwordData() - { - return [ - ['test', true], - ['test123456', true], - ['dokrtngeio39$', false], - ['test1234!', true], - ]; - - } - -} \ No newline at end of file From f9898a6aadb66dc5adabeb3323b42ad60c70ca5e Mon Sep 17 00:00:00 2001 From: moritz_demmer Date: Thu, 22 Apr 2021 16:35:03 +0200 Subject: [PATCH 023/199] added validators unit tests --- .../PasswordPolicyDataBreachTest.php | 61 +++++++++++++++++++ .../Validators/PasswordPolicyDigitsTest.php | 56 +++++++++++++++++ .../PasswordPolicyPasswordLengthTest.php | 56 +++++++++++++++++ .../PasswordPolicySpecialCharacterTest.php | 56 +++++++++++++++++ .../PasswordPolicyUpperLowerCaseTest.php | 56 +++++++++++++++++ 5 files changed, 285 insertions(+) create mode 100644 tests/unit/Validators/PasswordPolicyDataBreachTest.php create mode 100644 tests/unit/Validators/PasswordPolicyDigitsTest.php create mode 100644 tests/unit/Validators/PasswordPolicyPasswordLengthTest.php create mode 100644 tests/unit/Validators/PasswordPolicySpecialCharacterTest.php create mode 100644 tests/unit/Validators/PasswordPolicyUpperLowerCaseTest.php diff --git a/tests/unit/Validators/PasswordPolicyDataBreachTest.php b/tests/unit/Validators/PasswordPolicyDataBreachTest.php new file mode 100644 index 00000000..d98a43e8 --- /dev/null +++ b/tests/unit/Validators/PasswordPolicyDataBreachTest.php @@ -0,0 +1,61 @@ +subjectUnderTest = new PasswordPolicyDataBreach(); + } + + /** + * @param $username + * @param $password + * @param $known + * @dataProvider credentialsData + */ + public function testCredentialsCheck($username, $password, $known): void + { + $result = $this->subjectUnderTest->validate($username, $password); + if($known) + { + $this->assertInternalType('string', $result); + } + else{ + $this->assertTrue($result); + } + + } + + public function credentialsData() + { + return [ + ['', 'test', true], + ['', 'Test1234!', true], + ['', 'Manfred', true], + ['', 'Seltsam', true], + ['','Test124020!',false], + ['','h38nn?hdos9!', false], + ['','975673fh29!', false] + ]; + + } + + +} \ No newline at end of file diff --git a/tests/unit/Validators/PasswordPolicyDigitsTest.php b/tests/unit/Validators/PasswordPolicyDigitsTest.php new file mode 100644 index 00000000..17453713 --- /dev/null +++ b/tests/unit/Validators/PasswordPolicyDigitsTest.php @@ -0,0 +1,56 @@ +subjectUnderTest = new PasswordPolicyDigits(); + } + + /** + * @param $password + * @param $shouldPass + * @dataProvider PasswordData + */ + public function testCredentialsCheck($password, $shouldPass): void + { + $result = $this->subjectUnderTest->validate('', $password); + if($shouldPass) + { + $this->assertTrue($result); + } + else{ + $this->assertInternalType('string', $result); + } + + } + + public function PasswordData() + { + return [ + ['test', false], + ['L!ama', false], + ['L1ama', true], + ['t3st', true], + ['HalloLeut3', true], + ['pf!ped', false], + + ]; + + } +} \ No newline at end of file diff --git a/tests/unit/Validators/PasswordPolicyPasswordLengthTest.php b/tests/unit/Validators/PasswordPolicyPasswordLengthTest.php new file mode 100644 index 00000000..4e7ed138 --- /dev/null +++ b/tests/unit/Validators/PasswordPolicyPasswordLengthTest.php @@ -0,0 +1,56 @@ +subjectUnderTest = new PasswordPolicyPasswordLength(); + } + + /** + * @param $password + * @param $shouldPass + * @dataProvider PasswordData + */ + public function testCredentialsCheck($password, $shouldPass): void + { + $result = $this->subjectUnderTest->validate('', $password); + if($shouldPass) + { + $this->assertTrue($result); + } + else{ + $this->assertInternalType('string', $result); + } + + } + + public function PasswordData() + { + return [ + ['sdfn', false], + ['hallote', false], + ['testtest', true], + ['8Zeichen', true], + ['255ZeichenSindDoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooof', false], + ['pf!ped', false], + + ]; + + } +} \ No newline at end of file diff --git a/tests/unit/Validators/PasswordPolicySpecialCharacterTest.php b/tests/unit/Validators/PasswordPolicySpecialCharacterTest.php new file mode 100644 index 00000000..1987378b --- /dev/null +++ b/tests/unit/Validators/PasswordPolicySpecialCharacterTest.php @@ -0,0 +1,56 @@ +subjectUnderTest = new PasswordPolicySpecialCharacter(); + } + + /** + * @param $password + * @param $shouldPass + * @dataProvider PasswordData + */ + public function testCredentialsCheck($password, $shouldPass): void + { + $result = $this->subjectUnderTest->validate('', $password); + if($shouldPass) + { + $this->assertTrue($result); + } + else{ + $this->assertInternalType('string', $result); + } + + } + + public function PasswordData() + { + return [ + ['test', false], + ['L!ama', true], + ['L1ama', false], + ['t3st', false], + ['HalloLeut3', false], + ['pf!ped', true], + + ]; + + } +} \ No newline at end of file diff --git a/tests/unit/Validators/PasswordPolicyUpperLowerCaseTest.php b/tests/unit/Validators/PasswordPolicyUpperLowerCaseTest.php new file mode 100644 index 00000000..fa936143 --- /dev/null +++ b/tests/unit/Validators/PasswordPolicyUpperLowerCaseTest.php @@ -0,0 +1,56 @@ +subjectUnderTest = new PasswordPolicyUpperLowerCase(); + } + + /** + * @param $password + * @param $shouldPass + * @dataProvider PasswordData + */ + public function testCredentialsCheck($password, $shouldPass): void + { + $result = $this->subjectUnderTest->validate('', $password); + if($shouldPass) + { + $this->assertTrue($result); + } + else{ + $this->assertInternalType('string', $result); + } + + } + + public function PasswordData() + { + return [ + ['test', false], + ['Lama', true], + ['l1ama', false], + ['t3st', false], + ['HalloLeut3', true], + ['pf!ped', false], + + ]; + + } +} \ No newline at end of file From 879202601736770bc4cd64510ea6e41aa8711a24 Mon Sep 17 00:00:00 2001 From: moritz_demmer Date: Thu, 22 Apr 2021 16:47:48 +0200 Subject: [PATCH 024/199] now loads the user so you can get the username all the time --- src/Core/PasswordPolicyValidator.php | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Core/PasswordPolicyValidator.php b/src/Core/PasswordPolicyValidator.php index 2205cccb..597e78e5 100644 --- a/src/Core/PasswordPolicyValidator.php +++ b/src/Core/PasswordPolicyValidator.php @@ -26,10 +26,9 @@ namespace OxidProfessionalServices\PasswordPolicy\Core; use OxidEsales\Eshop\Application\Model\User; -use OxidEsales\Eshop\Core\Exception\InputException; use OxidEsales\Eshop\Core\Exception\StandardException; -use OxidEsales\Eshop\Core\Registry; -use OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyValidatorsCollector; +use OxidEsales\Eshop\Core\Exception\UserException; +use OxidEsales\Eshop\Core\Registry;use OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyValidatorsCollector; class PasswordPolicyValidator extends PasswordPolicyValidator_parent { @@ -44,8 +43,8 @@ class PasswordPolicyValidator extends PasswordPolicyValidator_parent */ public function checkPassword($user, $newPassword, $confirmationPassword, $shouldCheckPasswordLength = false) { - /** Muss noch besser gelöst werden */ - $username = $user->oxuser__oxusername->value ?: ""; + $user->loadUserByUpdateId((new \OxidEsales\Eshop\Core\Request)->getRequestEscapedParameter('uid')); + $username = $user->oxuser__oxusername->value; $ex = $this->validatePassword($username, $newPassword); if (isset($ex)) { return $ex; @@ -66,7 +65,7 @@ public function validatePassword(string $sUsername, string $sPassword): ?Standar $sError = $passwordPolicyValidatorsCollector->validate($sUsername, $sPassword); if (is_string($sError)) { $translateString = Registry::getLang()->translateString($sError); - $exception = oxNew(InputException::class, $translateString); + $exception = oxNew(UserException::class, $translateString); return $this->addValidationError("oxuser__oxpassword", $exception); } return null; From 32d35f355465c77e59632c149a385a31579e5be0 Mon Sep 17 00:00:00 2001 From: moritz_demmer Date: Thu, 22 Apr 2021 17:13:16 +0200 Subject: [PATCH 025/199] now testing credentials (password AND mail) --- .../Core/PasswordPolicyValidatorTest.php | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/Integration/Core/PasswordPolicyValidatorTest.php b/tests/Integration/Core/PasswordPolicyValidatorTest.php index 73b5478a..ea42baba 100644 --- a/tests/Integration/Core/PasswordPolicyValidatorTest.php +++ b/tests/Integration/Core/PasswordPolicyValidatorTest.php @@ -82,30 +82,30 @@ public function savePasswordLength(int $len): void public function passwordPolicyProvider(): array { return array_merge( - $this->withPolicyCombinations( + $this->withPolicyCombinations("", "Test1234!", false ), - $this->withPolicyCombinations( + $this->withPolicyCombinations("test@test.de", "ThisPasswordFulfills5Requirements!", true ), - $this->withPolicyCombinations( + $this->withPolicyCombinations("", "NOLOWER1/", false, PasswordPolicyConfig::SettingLower, ), - $this->withPolicyCombinations( + $this->withPolicyCombinations("", "noupper1/", false, PasswordPolicyConfig::SettingUpper, ), - $this->withPolicyCombinations( + $this->withPolicyCombinations("", "noSpecial2Day", false, PasswordPolicyConfig::SettingSpecial ), - $this->withPolicyCombinations( + $this->withPolicyCombinations("", "2Short!", false ) @@ -113,6 +113,7 @@ public function passwordPolicyProvider(): array } private function withPolicyCombinations( + string $username, string $psw, bool $willPass, string $mainPolicyName='', @@ -122,7 +123,7 @@ private function withPolicyCombinations( $res = []; foreach ($permutations as $permutation) { $permutation[PasswordPolicyConfig::SettingMinPasswordLength] = 8; - $res[] = [$permutation, $psw, $willPass]; + $res[] = [$permutation, $username,$psw, $willPass]; } return $res; @@ -164,10 +165,10 @@ private function policyCombinations($mainPolicyName, $mainPolicyValue): array /** * @dataProvider passwordPolicyProvider */ - public function testValidatePasswordPolicy(array $policy, string $password, bool $shouldPass) + public function testValidatePasswordPolicy(array $policy, string $username, string $password, bool $shouldPass) { $this->setPolicy($policy); - $this->subjectUnderTest->validatePassword('', $password); + $this->subjectUnderTest->validatePassword($username, $password); $ex = $this->subjectUnderTest->getFirstValidationError(); if ($shouldPass) { $this->assertNull($ex); From 401ad33984702bb06346c5bdc0bb5f30b40c3242 Mon Sep 17 00:00:00 2001 From: moritz_demmer Date: Fri, 23 Apr 2021 15:09:38 +0200 Subject: [PATCH 026/199] now doesnt check when an admin is logging in --- src/Model/User.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Model/User.php b/src/Model/User.php index d568da33..ff8afea1 100644 --- a/src/Model/User.php +++ b/src/Model/User.php @@ -19,8 +19,7 @@ public function onLogin($userName, $password) { /** @var PasswordPolicyValidator $passValidator */ $passValidator = oxNew(InputValidator::class); - - if ($this->isLoaded() && $err = $passValidator->validatePassword($userName, $password)) { + if (!isAdmin() && $this->isLoaded() && $err = $passValidator->validatePassword($userName, $password)) { $forgotPass = new ForgotPasswordController(); $forgotPass->forgotPassword(); throw oxNew(UserException::class, $err->getMessage()); From a709e6bb8850d184c584cf8c50083a5a4e1a02b1 Mon Sep 17 00:00:00 2001 From: moritz_demmer Date: Fri, 23 Apr 2021 15:10:07 +0200 Subject: [PATCH 027/199] added better error message when resetting the password --- src/Model/User.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Model/User.php b/src/Model/User.php index ff8afea1..262304e1 100644 --- a/src/Model/User.php +++ b/src/Model/User.php @@ -5,6 +5,7 @@ use OxidEsales\Eshop\Application\Controller\ForgotPasswordController; use OxidEsales\Eshop\Core\Exception\UserException; use OxidEsales\Eshop\Core\InputValidator; +use OxidEsales\Eshop\Core\Registry; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyValidator; class User extends User_parent @@ -22,7 +23,8 @@ public function onLogin($userName, $password) if (!isAdmin() && $this->isLoaded() && $err = $passValidator->validatePassword($userName, $password)) { $forgotPass = new ForgotPasswordController(); $forgotPass->forgotPassword(); - throw oxNew(UserException::class, $err->getMessage()); + $errorMessage = $err->getMessage() . ' ' . Registry::getLang()->translateString('REQUEST_PASSWORD_AFTERCLICK'); + throw oxNew(UserException::class, $errorMessage); } return parent::onLogin($userName,$password); } From 22076adadb5d09ae7f945e64c387996997a31b8c Mon Sep 17 00:00:00 2001 From: moritz Date: Tue, 27 Apr 2021 10:45:06 +0200 Subject: [PATCH 028/199] in case of registration, get username via getRequestEscapedParameter('lgn_usr') --- src/Core/PasswordPolicyValidator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/PasswordPolicyValidator.php b/src/Core/PasswordPolicyValidator.php index 597e78e5..682cb652 100644 --- a/src/Core/PasswordPolicyValidator.php +++ b/src/Core/PasswordPolicyValidator.php @@ -44,7 +44,7 @@ class PasswordPolicyValidator extends PasswordPolicyValidator_parent public function checkPassword($user, $newPassword, $confirmationPassword, $shouldCheckPasswordLength = false) { $user->loadUserByUpdateId((new \OxidEsales\Eshop\Core\Request)->getRequestEscapedParameter('uid')); - $username = $user->oxuser__oxusername->value; + $username = $user->oxuser__oxusername->value?: (new \OxidEsales\Eshop\Core\Request)->getRequestEscapedParameter('lgn_usr'); $ex = $this->validatePassword($username, $newPassword); if (isset($ex)) { return $ex; From b5486bd05f32df349de9266de238a802aa460885 Mon Sep 17 00:00:00 2001 From: moritz Date: Tue, 27 Apr 2021 10:45:20 +0200 Subject: [PATCH 029/199] added enzoic lib --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 71e5f80f..dbbfc81d 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,8 @@ "license": "GPL-3.0-only", "require": { "php": ">=7.3", - "ext-json": "*" + "ext-json": "*", + "Enzoic/Enzoic": "dev-master" }, "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.2", From 61bea85414c57d344fe10caa3224eedef68a5fd1 Mon Sep 17 00:00:00 2001 From: moritz Date: Tue, 27 Apr 2021 10:48:43 +0200 Subject: [PATCH 030/199] fixed enzoic lib --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index dbbfc81d..a361fd5d 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "require": { "php": ">=7.3", "ext-json": "*", - "Enzoic/Enzoic": "dev-master" + "enzoic/enzoic": "dev-master" }, "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.2", From 7baa7ebde4ce475b50f75a1f03d7dddab7f82ff3 Mon Sep 17 00:00:00 2001 From: moritz Date: Tue, 27 Apr 2021 11:31:18 +0200 Subject: [PATCH 031/199] fixed codestyle --- src/Api/PasswordCheck.php | 4 +--- src/Controller/AccountPasswordController.php | 13 +++++----- src/Core/PasswordPolicyValidator.php | 3 ++- src/Model/User.php | 4 ++-- src/Validators/PasswordPolicyDataBreach.php | 9 +++---- src/Validators/PasswordPolicyDigits.php | 3 +-- .../PasswordPolicyPasswordLength.php | 3 +-- .../PasswordPolicySpecialCharacter.php | 3 +-- .../PasswordPolicyUpperLowerCase.php | 3 +-- .../PasswordPolicyValidationInterface.php | 3 +-- .../PasswordPolicyValidatorsCollector.php | 5 ++-- .../Core/PasswordPolicyConfigTest.php | 2 -- .../Core/PasswordPolicyValidatorTest.php | 24 ++++++++++++------- .../PasswordPolicyDataBreachTest.php | 15 +++--------- .../Validators/PasswordPolicyDigitsTest.php | 11 +++------ .../PasswordPolicyPasswordLengthTest.php | 11 +++------ .../PasswordPolicySpecialCharacterTest.php | 11 +++------ .../PasswordPolicyUpperLowerCaseTest.php | 11 +++------ 18 files changed, 51 insertions(+), 87 deletions(-) diff --git a/src/Api/PasswordCheck.php b/src/Api/PasswordCheck.php index 5ad1af5b..393eaee9 100644 --- a/src/Api/PasswordCheck.php +++ b/src/Api/PasswordCheck.php @@ -13,7 +13,6 @@ */ class PasswordCheck { - private $config; private $apiCon; @@ -46,5 +45,4 @@ public function isCredentialsKnown(string $username, string $password): bool $result = $this->apiCon->checkCredentials($username, $password); return $result; } - -} \ No newline at end of file +} diff --git a/src/Controller/AccountPasswordController.php b/src/Controller/AccountPasswordController.php index ff92ad7c..bc3abd13 100644 --- a/src/Controller/AccountPasswordController.php +++ b/src/Controller/AccountPasswordController.php @@ -39,11 +39,11 @@ public function changePassword() if (($oExcp = $oInputValidator->checkPassword($oUser, $sNewPass, $sConfPass, true))) { $tmpInputValidator = oxNew(InputValidator::class); \OxidEsales\Eshop\Core\Registry::set(InputValidator::class, $tmpInputValidator); - return \OxidEsales\Eshop\Core\Registry::getUtilsView()->addErrorToDisplay( - $oExcp, - false, - true - ); + return \OxidEsales\Eshop\Core\Registry::getUtilsView()->addErrorToDisplay( + $oExcp, + false, + true + ); } @@ -55,5 +55,4 @@ public function changePassword() \OxidEsales\Eshop\Core\Registry::getUtilsServer()->deleteUserCookie($this->getConfig()->getShopId()); } } - -} \ No newline at end of file +} diff --git a/src/Core/PasswordPolicyValidator.php b/src/Core/PasswordPolicyValidator.php index 682cb652..649e4e81 100644 --- a/src/Core/PasswordPolicyValidator.php +++ b/src/Core/PasswordPolicyValidator.php @@ -28,7 +28,8 @@ use OxidEsales\Eshop\Application\Model\User; use OxidEsales\Eshop\Core\Exception\StandardException; use OxidEsales\Eshop\Core\Exception\UserException; -use OxidEsales\Eshop\Core\Registry;use OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyValidatorsCollector; +use OxidEsales\Eshop\Core\Registry; +use OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyValidatorsCollector; class PasswordPolicyValidator extends PasswordPolicyValidator_parent { diff --git a/src/Model/User.php b/src/Model/User.php index 262304e1..4094c2b9 100644 --- a/src/Model/User.php +++ b/src/Model/User.php @@ -26,6 +26,6 @@ public function onLogin($userName, $password) $errorMessage = $err->getMessage() . ' ' . Registry::getLang()->translateString('REQUEST_PASSWORD_AFTERCLICK'); throw oxNew(UserException::class, $errorMessage); } - return parent::onLogin($userName,$password); + return parent::onLogin($userName, $password); } -} \ No newline at end of file +} diff --git a/src/Validators/PasswordPolicyDataBreach.php b/src/Validators/PasswordPolicyDataBreach.php index 13d2044c..990e3439 100644 --- a/src/Validators/PasswordPolicyDataBreach.php +++ b/src/Validators/PasswordPolicyDataBreach.php @@ -3,21 +3,18 @@ namespace OxidProfessionalServices\PasswordPolicy\Validators; - use OxidProfessionalServices\PasswordPolicy\Api\PasswordCheck; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyConfig; class PasswordPolicyDataBreach implements PasswordPolicyValidationInterface { - public function validate(string $sUsername, string $sPassword) { $settings = new PasswordPolicyConfig(); $passwordCheck = new PasswordCheck(); - if ($settings->getAPINeeded() && ($passwordCheck->isPasswordKnown($sPassword) || $passwordCheck->isCredentialsKnown($sUsername,$sPassword))) { - return 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_PASSWORD_KNOWN'; + if ($settings->getAPINeeded() && ($passwordCheck->isPasswordKnown($sPassword) || $passwordCheck->isCredentialsKnown($sUsername, $sPassword))) { + return 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_PASSWORD_KNOWN'; } return true; } - -} \ No newline at end of file +} diff --git a/src/Validators/PasswordPolicyDigits.php b/src/Validators/PasswordPolicyDigits.php index 706ab946..b52517f1 100644 --- a/src/Validators/PasswordPolicyDigits.php +++ b/src/Validators/PasswordPolicyDigits.php @@ -3,7 +3,6 @@ namespace OxidProfessionalServices\PasswordPolicy\Validators; - use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyConfig; class PasswordPolicyDigits implements PasswordPolicyValidationInterface @@ -16,4 +15,4 @@ public function validate(string $sUsername, string $sPassword) } return true; } -} \ No newline at end of file +} diff --git a/src/Validators/PasswordPolicyPasswordLength.php b/src/Validators/PasswordPolicyPasswordLength.php index eb6191ee..2e86c507 100644 --- a/src/Validators/PasswordPolicyPasswordLength.php +++ b/src/Validators/PasswordPolicyPasswordLength.php @@ -3,7 +3,6 @@ namespace OxidProfessionalServices\PasswordPolicy\Validators; - use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyConfig; class PasswordPolicyPasswordLength implements PasswordPolicyValidationInterface @@ -21,4 +20,4 @@ public function validate(string $sUsername, string $sPassword) } return true; } -} \ No newline at end of file +} diff --git a/src/Validators/PasswordPolicySpecialCharacter.php b/src/Validators/PasswordPolicySpecialCharacter.php index f6620902..5f8e8f95 100644 --- a/src/Validators/PasswordPolicySpecialCharacter.php +++ b/src/Validators/PasswordPolicySpecialCharacter.php @@ -3,7 +3,6 @@ namespace OxidProfessionalServices\PasswordPolicy\Validators; - use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyConfig; class PasswordPolicySpecialCharacter implements PasswordPolicyValidationInterface @@ -19,4 +18,4 @@ public function validate(string $sUsername, string $sPassword) } return true; } -} \ No newline at end of file +} diff --git a/src/Validators/PasswordPolicyUpperLowerCase.php b/src/Validators/PasswordPolicyUpperLowerCase.php index 86d6d67d..a5f541ff 100644 --- a/src/Validators/PasswordPolicyUpperLowerCase.php +++ b/src/Validators/PasswordPolicyUpperLowerCase.php @@ -3,7 +3,6 @@ namespace OxidProfessionalServices\PasswordPolicy\Validators; - use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyConfig; class PasswordPolicyUpperLowerCase implements PasswordPolicyValidationInterface @@ -20,4 +19,4 @@ public function validate(string $sUsername, string $sPassword) } return true; } -} \ No newline at end of file +} diff --git a/src/Validators/PasswordPolicyValidationInterface.php b/src/Validators/PasswordPolicyValidationInterface.php index 34f51700..0b0a838b 100644 --- a/src/Validators/PasswordPolicyValidationInterface.php +++ b/src/Validators/PasswordPolicyValidationInterface.php @@ -3,7 +3,6 @@ namespace OxidProfessionalServices\PasswordPolicy\Validators; - /** * Interface PasswordPolicyValidationInterface * @package OxidProfessionalServices\PasswordPolicy\Validators @@ -16,4 +15,4 @@ interface PasswordPolicyValidationInterface * return true|string */ public function validate(string $sUsername, string $sPassword); -} \ No newline at end of file +} diff --git a/src/Validators/PasswordPolicyValidatorsCollector.php b/src/Validators/PasswordPolicyValidatorsCollector.php index 30414a63..f8875f8b 100644 --- a/src/Validators/PasswordPolicyValidatorsCollector.php +++ b/src/Validators/PasswordPolicyValidatorsCollector.php @@ -19,8 +19,7 @@ public function __construct(array $validators) public function validate(string $sUsername, string $sPassword) { - foreach ($this->validators as $validator) - { + foreach ($this->validators as $validator) { $sError = $validator->validate($sUsername, $sPassword); if (is_string($sError)) { return $sError; @@ -28,4 +27,4 @@ public function validate(string $sUsername, string $sPassword) } return true; } -} \ No newline at end of file +} diff --git a/tests/Integration/Core/PasswordPolicyConfigTest.php b/tests/Integration/Core/PasswordPolicyConfigTest.php index ca83abc5..404ec764 100644 --- a/tests/Integration/Core/PasswordPolicyConfigTest.php +++ b/tests/Integration/Core/PasswordPolicyConfigTest.php @@ -136,6 +136,4 @@ public function setConfig($name, $value): void { Registry::getConfig()->setConfigParam($name, $value); } - - } diff --git a/tests/Integration/Core/PasswordPolicyValidatorTest.php b/tests/Integration/Core/PasswordPolicyValidatorTest.php index ea42baba..1965b4c9 100644 --- a/tests/Integration/Core/PasswordPolicyValidatorTest.php +++ b/tests/Integration/Core/PasswordPolicyValidatorTest.php @@ -82,30 +82,36 @@ public function savePasswordLength(int $len): void public function passwordPolicyProvider(): array { return array_merge( - $this->withPolicyCombinations("", - "Test1234!", - false - ), - $this->withPolicyCombinations("test@test.de", + $this->withPolicyCombinations( + "", + "Test1234!", + false + ), + $this->withPolicyCombinations( + "test@test.de", "ThisPasswordFulfills5Requirements!", true ), - $this->withPolicyCombinations("", + $this->withPolicyCombinations( + "", "NOLOWER1/", false, PasswordPolicyConfig::SettingLower, ), - $this->withPolicyCombinations("", + $this->withPolicyCombinations( + "", "noupper1/", false, PasswordPolicyConfig::SettingUpper, ), - $this->withPolicyCombinations("", + $this->withPolicyCombinations( + "", "noSpecial2Day", false, PasswordPolicyConfig::SettingSpecial ), - $this->withPolicyCombinations("", + $this->withPolicyCombinations( + "", "2Short!", false ) diff --git a/tests/unit/Validators/PasswordPolicyDataBreachTest.php b/tests/unit/Validators/PasswordPolicyDataBreachTest.php index d98a43e8..1c557f64 100644 --- a/tests/unit/Validators/PasswordPolicyDataBreachTest.php +++ b/tests/unit/Validators/PasswordPolicyDataBreachTest.php @@ -5,11 +5,8 @@ use OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyDataBreach; use PHPUnit\Framework\TestCase; - - class PasswordPolicyDataBreachTest extends TestCase { - protected $subjectUnderTest; @@ -33,14 +30,11 @@ public function setUp(): void public function testCredentialsCheck($username, $password, $known): void { $result = $this->subjectUnderTest->validate($username, $password); - if($known) - { + if ($known) { $this->assertInternalType('string', $result); - } - else{ + } else { $this->assertTrue($result); } - } public function credentialsData() @@ -54,8 +48,5 @@ public function credentialsData() ['','h38nn?hdos9!', false], ['','975673fh29!', false] ]; - } - - -} \ No newline at end of file +} diff --git a/tests/unit/Validators/PasswordPolicyDigitsTest.php b/tests/unit/Validators/PasswordPolicyDigitsTest.php index 17453713..66cc62d6 100644 --- a/tests/unit/Validators/PasswordPolicyDigitsTest.php +++ b/tests/unit/Validators/PasswordPolicyDigitsTest.php @@ -2,7 +2,6 @@ namespace OxidProfessionalServices\PasswordPolicy\Tests; - use OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyDigits; use PHPUnit\Framework\TestCase; @@ -30,14 +29,11 @@ public function setUp(): void public function testCredentialsCheck($password, $shouldPass): void { $result = $this->subjectUnderTest->validate('', $password); - if($shouldPass) - { + if ($shouldPass) { $this->assertTrue($result); - } - else{ + } else { $this->assertInternalType('string', $result); } - } public function PasswordData() @@ -51,6 +47,5 @@ public function PasswordData() ['pf!ped', false], ]; - } -} \ No newline at end of file +} diff --git a/tests/unit/Validators/PasswordPolicyPasswordLengthTest.php b/tests/unit/Validators/PasswordPolicyPasswordLengthTest.php index 4e7ed138..1fc8b66c 100644 --- a/tests/unit/Validators/PasswordPolicyPasswordLengthTest.php +++ b/tests/unit/Validators/PasswordPolicyPasswordLengthTest.php @@ -2,7 +2,6 @@ namespace OxidProfessionalServices\PasswordPolicy\Tests; - use PHPUnit\Framework\TestCase; use OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyPasswordLength; @@ -30,14 +29,11 @@ public function setUp(): void public function testCredentialsCheck($password, $shouldPass): void { $result = $this->subjectUnderTest->validate('', $password); - if($shouldPass) - { + if ($shouldPass) { $this->assertTrue($result); - } - else{ + } else { $this->assertInternalType('string', $result); } - } public function PasswordData() @@ -51,6 +47,5 @@ public function PasswordData() ['pf!ped', false], ]; - } -} \ No newline at end of file +} diff --git a/tests/unit/Validators/PasswordPolicySpecialCharacterTest.php b/tests/unit/Validators/PasswordPolicySpecialCharacterTest.php index 1987378b..eefea428 100644 --- a/tests/unit/Validators/PasswordPolicySpecialCharacterTest.php +++ b/tests/unit/Validators/PasswordPolicySpecialCharacterTest.php @@ -2,7 +2,6 @@ namespace OxidProfessionalServices\PasswordPolicy\Tests; - use PHPUnit\Framework\TestCase; use OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicySpecialCharacter; @@ -30,14 +29,11 @@ public function setUp(): void public function testCredentialsCheck($password, $shouldPass): void { $result = $this->subjectUnderTest->validate('', $password); - if($shouldPass) - { + if ($shouldPass) { $this->assertTrue($result); - } - else{ + } else { $this->assertInternalType('string', $result); } - } public function PasswordData() @@ -51,6 +47,5 @@ public function PasswordData() ['pf!ped', true], ]; - } -} \ No newline at end of file +} diff --git a/tests/unit/Validators/PasswordPolicyUpperLowerCaseTest.php b/tests/unit/Validators/PasswordPolicyUpperLowerCaseTest.php index fa936143..9af6be40 100644 --- a/tests/unit/Validators/PasswordPolicyUpperLowerCaseTest.php +++ b/tests/unit/Validators/PasswordPolicyUpperLowerCaseTest.php @@ -2,7 +2,6 @@ namespace OxidProfessionalServices\PasswordPolicy\Tests; - use PHPUnit\Framework\TestCase; use OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyUpperLowerCase; @@ -30,14 +29,11 @@ public function setUp(): void public function testCredentialsCheck($password, $shouldPass): void { $result = $this->subjectUnderTest->validate('', $password); - if($shouldPass) - { + if ($shouldPass) { $this->assertTrue($result); - } - else{ + } else { $this->assertInternalType('string', $result); } - } public function PasswordData() @@ -51,6 +47,5 @@ public function PasswordData() ['pf!ped', false], ]; - } -} \ No newline at end of file +} From ad6be21883e6b64b11eb801cda1828e19ae85ad7 Mon Sep 17 00:00:00 2001 From: moritz Date: Tue, 27 Apr 2021 11:44:24 +0200 Subject: [PATCH 032/199] clean up --- src/Core/PasswordPolicyValidator.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Core/PasswordPolicyValidator.php b/src/Core/PasswordPolicyValidator.php index 649e4e81..592d6647 100644 --- a/src/Core/PasswordPolicyValidator.php +++ b/src/Core/PasswordPolicyValidator.php @@ -29,6 +29,7 @@ use OxidEsales\Eshop\Core\Exception\StandardException; use OxidEsales\Eshop\Core\Exception\UserException; use OxidEsales\Eshop\Core\Registry; +use OxidEsales\Eshop\Core\Request; use OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyValidatorsCollector; class PasswordPolicyValidator extends PasswordPolicyValidator_parent @@ -44,8 +45,8 @@ class PasswordPolicyValidator extends PasswordPolicyValidator_parent */ public function checkPassword($user, $newPassword, $confirmationPassword, $shouldCheckPasswordLength = false) { - $user->loadUserByUpdateId((new \OxidEsales\Eshop\Core\Request)->getRequestEscapedParameter('uid')); - $username = $user->oxuser__oxusername->value?: (new \OxidEsales\Eshop\Core\Request)->getRequestEscapedParameter('lgn_usr'); + $user->loadUserByUpdateId((new Request)->getRequestEscapedParameter('uid')); + $username = $user->oxuser__oxusername->value?: (new Request)->getRequestEscapedParameter('lgn_usr'); $ex = $this->validatePassword($username, $newPassword); if (isset($ex)) { return $ex; From 710c11da8320db097a72ead587961db0c808e277 Mon Sep 17 00:00:00 2001 From: moritz Date: Tue, 27 Apr 2021 16:49:46 +0200 Subject: [PATCH 033/199] added haveibeenpwned password check --- composer.json | 3 ++- src/Api/PasswordCheck.php | 18 ++++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index a361fd5d..6a832f88 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,8 @@ "require": { "php": ">=7.3", "ext-json": "*", - "enzoic/enzoic": "dev-master" + "enzoic/enzoic": "dev-master", + "divineomega/password_exposed": "v3.2.0" }, "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.2", diff --git a/src/Api/PasswordCheck.php b/src/Api/PasswordCheck.php index 393eaee9..938fa8a5 100644 --- a/src/Api/PasswordCheck.php +++ b/src/Api/PasswordCheck.php @@ -4,6 +4,7 @@ namespace OxidProfessionalServices\PasswordPolicy\Api; +use DivineOmega\PasswordExposed\PasswordExposedChecker; use Enzoic\Enzoic; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyConfig; @@ -14,15 +15,16 @@ class PasswordCheck { private $config; - private $apiCon; - + private $enzoicApiCon; + private $haveIBeenPwned; /** * PasswordCheck constructor. */ public function __construct() { $this->config = new PasswordPolicyConfig(); - $this->apiCon = new Enzoic($this->config->getAPIKey(), $this->config->getSecretKey()); + $this->enzoicApiCon = new Enzoic($this->config->getAPIKey(), $this->config->getSecretKey()); + $this->haveIBeenPwned = new PasswordExposedChecker(); } /** @@ -31,8 +33,11 @@ public function __construct() */ public function isPasswordKnown(string $password): bool { - $result = $this->apiCon->checkPassword($password); + if($this->haveIBeenPwned->passwordExposed($password)=="exposed") + return true; + $result = $this->enzoicApiCon->checkPassword($password); return $result !== null; + } /** @@ -42,7 +47,8 @@ public function isPasswordKnown(string $password): bool */ public function isCredentialsKnown(string $username, string $password): bool { - $result = $this->apiCon->checkCredentials($username, $password); - return $result; + return false; +// $result = $this->apiCon->checkCredentials($username, $password); +// return $result; } } From d6d646fda8ca04c8672ba623ded8726c5b581a76 Mon Sep 17 00:00:00 2001 From: moritz Date: Tue, 27 Apr 2021 18:26:56 +0200 Subject: [PATCH 034/199] added new module setting to deacitvate/activate haveIBeenPwned and Enzoic --- metadata.php | 9 +++++---- src/Api/PasswordCheck.php | 13 +++++++------ src/Core/PasswordPolicyConfig.php | 20 ++++++++++++++++---- views/admin/de/passwordpolicy_lang.php | 9 +++++---- views/admin/en/passwordpolicy_lang.php | 6 ++++++ 5 files changed, 39 insertions(+), 18 deletions(-) diff --git a/metadata.php b/metadata.php index dfc6a5c3..42feb2a4 100644 --- a/metadata.php +++ b/metadata.php @@ -29,7 +29,6 @@ use OxidEsales\Eshop\Core\InputValidator; use OxidEsales\Eshop\Core\ViewConfig; use OxidEsales\Eshop\Application\Model\User; -use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyConfig; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyValidator; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyViewConfig; use OxidProfessionalServices\PasswordPolicy\Controller\AccountPasswordController as PasswordPolicyAccountPasswordController; @@ -88,9 +87,11 @@ ['group' => 'passwordpolicy_requirements', 'name' => 'oxpspasswordpolicyLowerCase', 'type' => 'bool', 'value' => true], ['group' => 'passwordpolicy_requirements', 'name' => 'oxpspasswordpolicySpecial', 'type' => 'bool', 'value' => true], ['group' => 'passwordpolicy_requirements', 'name' => 'oxpspasswordpolicyDigits', 'type' => 'bool', 'value' => true], - ['group' => 'passwordpolicy_apisettings', 'name' => PasswordPolicyConfig::SettingAPI, 'type' => 'bool', 'value' => false], - ['group' => 'passwordpolicy_apisettings', 'name' => PasswordPolicyConfig::SettingAPIKey, 'type' => 'str'], - ['group' => 'passwordpolicy_apisettings', 'name' => PasswordPolicyConfig::SettingSecretKey, 'type' => 'str'], + ['group' => 'passwordpolicy_apisettings', 'name' => 'oxpspasswordpolicyAPI', 'type' => 'bool', 'value' => true], + ['group' => 'passwordpolicy_apisettings', 'name' => 'oxpspasswordpolicyHaveIBeenPwned', 'type' => 'bool', 'value' => true], + ['group' => 'passwordpolicy_apisettings', 'name' => 'oxpspasswordpolicyEnzoic', 'type' => 'bool', 'value' => false], + ['group' => 'passwordpolicy_apisettings', 'name' => 'oxpspasswordpolicyEnzoicAPIKey', 'type' => 'str'], + ['group' => 'passwordpolicy_apisettings', 'name' => 'oxpspasswordpolicyEnzoicSecretKey', 'type' => 'str'], ], 'events' => [], ]; diff --git a/src/Api/PasswordCheck.php b/src/Api/PasswordCheck.php index 938fa8a5..aa2037dd 100644 --- a/src/Api/PasswordCheck.php +++ b/src/Api/PasswordCheck.php @@ -33,11 +33,12 @@ public function __construct() */ public function isPasswordKnown(string $password): bool { - if($this->haveIBeenPwned->passwordExposed($password)=="exposed") + // Überarbeiten, dass hier nicht so oft die Config geprüft werden muss + if($this->config->getHaveIBeenPwnedNeeded() && $this->haveIBeenPwned->passwordExposed($password)=="exposed") return true; - $result = $this->enzoicApiCon->checkPassword($password); - return $result !== null; - + elseif ($this->config->getEnzoicNeeded() && $result = $this->enzoicApiCon->checkPassword($password)) + return $result !== null; + return false; } /** @@ -47,8 +48,8 @@ public function isPasswordKnown(string $password): bool */ public function isCredentialsKnown(string $username, string $password): bool { + if($this->config->getEnzoicNeeded() && $this->enzoicApiCon->checkCredentials($username, $password)) + return true; return false; -// $result = $this->apiCon->checkCredentials($username, $password); -// return $result; } } diff --git a/src/Core/PasswordPolicyConfig.php b/src/Core/PasswordPolicyConfig.php index e5eb2eef..fd5df285 100644 --- a/src/Core/PasswordPolicyConfig.php +++ b/src/Core/PasswordPolicyConfig.php @@ -38,8 +38,10 @@ class PasswordPolicyConfig public const SettingDigits = self::SettingsPrefix . 'Digits'; public const SettingSpecial = self::SettingsPrefix . 'Special'; public const SettingAPI = self::SettingsPrefix . 'API'; - public const SettingAPIKey = self::SettingsPrefix . 'APIKey'; - public const SettingSecretKey = self::SettingsPrefix . 'SecretKey'; + public const SettingEnzoicAPIKey = self::SettingsPrefix . 'EnzoicAPIKey'; + public const SettingEnzoicSecretKey = self::SettingsPrefix . 'EnzoicSecretKey'; + public const SettingEnzoic = self::SettingsPrefix . 'Enzoic'; + public const SettingHaveIBeenPwned = self::SettingsPrefix . 'HaveIBeenPwned'; public const SettingUpper = self::SettingsPrefix . 'UpperCase'; public const SettingLower = self::SettingsPrefix . 'LowerCase'; @@ -88,12 +90,12 @@ public function getMaxPasswordLength(): int public function getAPIKey(): string { - return (string) Registry::getConfig()->getConfigParam(self::SettingAPIKey); + return (string) Registry::getConfig()->getConfigParam(self::SettingEnzoicAPIKey); } public function getSecretKey(): string { - return (string) Registry::getConfig()->getConfigParam(self::SettingSecretKey); + return (string) Registry::getConfig()->getConfigParam(self::SettingEnzoicSecretKey); } public function getAPINeeded(): bool @@ -101,6 +103,16 @@ public function getAPINeeded(): bool return $this->isConfigParam(self::SettingAPI); } + public function getEnzoicNeeded(): bool + { + return $this->isConfigParam(self::SettingEnzoic); + } + + public function getHaveIBeenPwnedNeeded(): bool + { + return $this->isConfigParam(self::SettingHaveIBeenPwned); + } + private function isConfigParam(string $name): bool { return (bool) Registry::getConfig()->getConfigParam($name, true); diff --git a/views/admin/de/passwordpolicy_lang.php b/views/admin/de/passwordpolicy_lang.php index 88c1ce93..b340e054 100644 --- a/views/admin/de/passwordpolicy_lang.php +++ b/views/admin/de/passwordpolicy_lang.php @@ -31,8 +31,9 @@ 'SHOP_MODULE_oxpspasswordpolicyLowerCase' => 'Kleinbuchstaben (a...z)', 'SHOP_MODULE_oxpspasswordpolicySpecial' => 'Sonderzeichen (!,@#$%^&*?_~()-)', 'SHOP_MODULE_GROUP_passwordpolicy_apisettings' => 'API Einstellungen', - 'SHOP_MODULE_oxpspasswordpolicyAPI' => 'Passwort Prüfung', - 'SHOP_MODULE_oxpspasswordpolicyAPIKey' => 'Enzoic API Schlüssel', - 'SHOP_MODULE_oxpspasswordpolicySecretKey' => 'Enzoic Geheimschlüssel' - + 'SHOP_MODULE_oxpspasswordpolicyAPI' => 'Veröffentliche Passwörter überprüfen', + 'SHOP_MODULE_oxpspasswordpolicyHaveIBeenPwned' => 'HaveIBeenPwned', + 'SHOP_MODULE_oxpspasswordpolicyEnzoic' => 'Enzoic', + 'SHOP_MODULE_oxpspasswordpolicyEnzoicAPIKey' => 'Enzoic API Schlüssel', + 'SHOP_MODULE_oxpspasswordpolicyEnzoicSecretKey' => 'Enzoic Geheimschlüssel' ); diff --git a/views/admin/en/passwordpolicy_lang.php b/views/admin/en/passwordpolicy_lang.php index 0cad5ca3..4553aab1 100644 --- a/views/admin/en/passwordpolicy_lang.php +++ b/views/admin/en/passwordpolicy_lang.php @@ -30,4 +30,10 @@ 'SHOP_MODULE_oxpspasswordpolicyUpperCase' => 'Capital (UPPERCASE) letters (A...Z)', 'SHOP_MODULE_oxpspasswordpolicyLowerCase' => 'Lowercase letters (a...z)', 'SHOP_MODULE_oxpspasswordpolicySpecial' => 'Special characters (!,@#$%^&*?_~()-)', + 'SHOP_MODULE_GROUP_passwordpolicy_apisettings' => 'API Settings', + 'SHOP_MODULE_oxpspasswordpolicyAPI' => 'Check leaked passwords', + 'SHOP_MODULE_oxpspasswordpolicyHaveIBeenPwned' => 'HaveIBeenPwned', + 'SHOP_MODULE_oxpspasswordpolicyEnzoic' => 'Enzoic', + 'SHOP_MODULE_oxpspasswordpolicyEnzoicAPIKey' => 'Enzoic API Key', + 'SHOP_MODULE_oxpspasswordpolicyEnzoicSecretKey' => 'Enzoic Secret Key' ); From fd38b051a5cdb49a83bec3cddece2ad30ea3411d Mon Sep 17 00:00:00 2001 From: moritz Date: Tue, 27 Apr 2021 18:29:04 +0200 Subject: [PATCH 035/199] fixed codestyle --- src/Api/PasswordCheck.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Api/PasswordCheck.php b/src/Api/PasswordCheck.php index aa2037dd..f30d3dc6 100644 --- a/src/Api/PasswordCheck.php +++ b/src/Api/PasswordCheck.php @@ -34,10 +34,11 @@ public function __construct() public function isPasswordKnown(string $password): bool { // Überarbeiten, dass hier nicht so oft die Config geprüft werden muss - if($this->config->getHaveIBeenPwnedNeeded() && $this->haveIBeenPwned->passwordExposed($password)=="exposed") + if ($this->config->getHaveIBeenPwnedNeeded() && $this->haveIBeenPwned->passwordExposed($password)=="exposed") { return true; - elseif ($this->config->getEnzoicNeeded() && $result = $this->enzoicApiCon->checkPassword($password)) + } elseif ($this->config->getEnzoicNeeded() && $result = $this->enzoicApiCon->checkPassword($password)) { return $result !== null; + } return false; } @@ -48,8 +49,9 @@ public function isPasswordKnown(string $password): bool */ public function isCredentialsKnown(string $username, string $password): bool { - if($this->config->getEnzoicNeeded() && $this->enzoicApiCon->checkCredentials($username, $password)) - return true; + if ($this->config->getEnzoicNeeded() && $this->enzoicApiCon->checkCredentials($username, $password)) { + return true; + } return false; } } From b48f705d724316d685400017b9e244e85edd68e9 Mon Sep 17 00:00:00 2001 From: moritz Date: Wed, 28 Apr 2021 17:36:28 +0200 Subject: [PATCH 036/199] now checking whether status of response is 200 (= password found) --- src/Api/PasswordCheck.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Api/PasswordCheck.php b/src/Api/PasswordCheck.php index f30d3dc6..d36820d1 100644 --- a/src/Api/PasswordCheck.php +++ b/src/Api/PasswordCheck.php @@ -36,8 +36,8 @@ public function isPasswordKnown(string $password): bool // Überarbeiten, dass hier nicht so oft die Config geprüft werden muss if ($this->config->getHaveIBeenPwnedNeeded() && $this->haveIBeenPwned->passwordExposed($password)=="exposed") { return true; - } elseif ($this->config->getEnzoicNeeded() && $result = $this->enzoicApiCon->checkPassword($password)) { - return $result !== null; + } elseif ($this->config->getEnzoicNeeded() && $this->enzoicApiCon->checkPassword($password)["status"] = "200") { + return true; } return false; } From b13d892f80756eb3fa718b05bf9d90ec3ddf65fd Mon Sep 17 00:00:00 2001 From: moritz Date: Wed, 28 Apr 2021 17:36:47 +0200 Subject: [PATCH 037/199] removed return parent --- src/Model/User.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Model/User.php b/src/Model/User.php index 4094c2b9..8b3da5bf 100644 --- a/src/Model/User.php +++ b/src/Model/User.php @@ -15,6 +15,7 @@ class User extends User_parent * * @param string $userName * @param string $password + * @throws UserException */ public function onLogin($userName, $password) { @@ -26,6 +27,6 @@ public function onLogin($userName, $password) $errorMessage = $err->getMessage() . ' ' . Registry::getLang()->translateString('REQUEST_PASSWORD_AFTERCLICK'); throw oxNew(UserException::class, $errorMessage); } - return parent::onLogin($userName, $password); + parent::onLogin($userName, $password); } } From f9e9fe1ab189ba408127de6bcf18e2024f85e200 Mon Sep 17 00:00:00 2001 From: moritz Date: Wed, 28 Apr 2021 17:37:35 +0200 Subject: [PATCH 038/199] added settings validation of enzoic api --- metadata.php | 5 ++- src/Controller/Admin/ModuleConfiguration.php | 42 ++++++++++++++++++++ views/admin/de/passwordpolicy_lang.php | 4 +- 3 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 src/Controller/Admin/ModuleConfiguration.php diff --git a/metadata.php b/metadata.php index 42feb2a4..a9a6fea8 100644 --- a/metadata.php +++ b/metadata.php @@ -29,10 +29,12 @@ use OxidEsales\Eshop\Core\InputValidator; use OxidEsales\Eshop\Core\ViewConfig; use OxidEsales\Eshop\Application\Model\User; +use OxidEsales\Eshop\Application\Controller\Admin\ModuleConfiguration; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyValidator; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyViewConfig; use OxidProfessionalServices\PasswordPolicy\Controller\AccountPasswordController as PasswordPolicyAccountPasswordController; use OxidProfessionalServices\PasswordPolicy\Model\User as PasswordPolicyUser; +use OxidProfessionalServices\PasswordPolicy\Controller\Admin\ModuleConfiguration as PasswordPolicyModuleConfiguration; $sMetadataVersion = '2.1'; @@ -58,7 +60,8 @@ ViewConfig::class => PasswordPolicyViewConfig::class, InputValidator::class => PasswordPolicyValidator::class, AccountPasswordController::class => PasswordPolicyAccountPasswordController::class, - User::class => PasswordPolicyUser::class + User::class => PasswordPolicyUser::class, + ModuleConfiguration::class => PasswordPolicyModuleConfiguration::class ], 'controllers' => [], 'templates' => [ diff --git a/src/Controller/Admin/ModuleConfiguration.php b/src/Controller/Admin/ModuleConfiguration.php new file mode 100644 index 00000000..0e42c27d --- /dev/null +++ b/src/Controller/Admin/ModuleConfiguration.php @@ -0,0 +1,42 @@ +getConfigVariablesFromRequest(); + $enzoicAPIKey = $variables[PasswordPolicyConfig::SettingEnzoicAPIKey]; + $enzoicSecretKey = $variables[PasswordPolicyConfig::SettingEnzoicSecretKey]; + $enzoicApiCon = new Enzoic($enzoicAPIKey,$enzoicSecretKey); + $testAPICall = $enzoicApiCon->checkPassword("Test"); + if($testAPICall["status"] != "200") + { + Registry::getUtilsView()->addErrorToDisplay(Registry::getLang()->translateString("oxpspasswordpolicy_EnzoicError" . $testAPICall["status"])); + return; + } + parent::saveConfVars(); + } + + private function getConfigVariablesFromRequest(): array + { + $settings = []; + + foreach ($this->_aConfParams as $requestParameterKey) { + $settingsFromRequest = Registry::getRequest()->getRequestEscapedParameter($requestParameterKey); + + if (\is_array($settingsFromRequest)) { + foreach ($settingsFromRequest as $name => $value) { + $settings[$name] = $value; + } + } + } + + return $settings; + } +} \ No newline at end of file diff --git a/views/admin/de/passwordpolicy_lang.php b/views/admin/de/passwordpolicy_lang.php index b340e054..40d5edbc 100644 --- a/views/admin/de/passwordpolicy_lang.php +++ b/views/admin/de/passwordpolicy_lang.php @@ -35,5 +35,7 @@ 'SHOP_MODULE_oxpspasswordpolicyHaveIBeenPwned' => 'HaveIBeenPwned', 'SHOP_MODULE_oxpspasswordpolicyEnzoic' => 'Enzoic', 'SHOP_MODULE_oxpspasswordpolicyEnzoicAPIKey' => 'Enzoic API Schlüssel', - 'SHOP_MODULE_oxpspasswordpolicyEnzoicSecretKey' => 'Enzoic Geheimschlüssel' + 'SHOP_MODULE_oxpspasswordpolicyEnzoicSecretKey' => 'Enzoic Geheimschlüssel', + 'oxpspasswordpolicy_EnzoicError401' => 'Ihr API Key oder Secret Key ist nicht gültig. Sie sind nicht autorisiert.', + 'oxpspasswordpolicy_EnzoicError0' => 'Es gibt ein Problem beim Verbinden mit der Enzoic API. Bitte versuchen Sie es erneut.' ); From 7045974ee0c80337f216f3695b94c0c5a5d57d95 Mon Sep 17 00:00:00 2001 From: moritz Date: Thu, 29 Apr 2021 13:37:03 +0200 Subject: [PATCH 039/199] fixed language --- views/admin/de/passwordpolicy_lang.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/admin/de/passwordpolicy_lang.php b/views/admin/de/passwordpolicy_lang.php index 40d5edbc..f327c063 100644 --- a/views/admin/de/passwordpolicy_lang.php +++ b/views/admin/de/passwordpolicy_lang.php @@ -36,6 +36,6 @@ 'SHOP_MODULE_oxpspasswordpolicyEnzoic' => 'Enzoic', 'SHOP_MODULE_oxpspasswordpolicyEnzoicAPIKey' => 'Enzoic API Schlüssel', 'SHOP_MODULE_oxpspasswordpolicyEnzoicSecretKey' => 'Enzoic Geheimschlüssel', - 'oxpspasswordpolicy_EnzoicError401' => 'Ihr API Key oder Secret Key ist nicht gültig. Sie sind nicht autorisiert.', + 'oxpspasswordpolicy_EnzoicError401' => 'Ihr Enzoic API Key oder Secret Key ist nicht gültig. Sie sind nicht autorisiert.', 'oxpspasswordpolicy_EnzoicError0' => 'Es gibt ein Problem beim Verbinden mit der Enzoic API. Bitte versuchen Sie es erneut.' ); From 06a561a9d30a4f47e38cdebac7e84cfbd6600313 Mon Sep 17 00:00:00 2001 From: moritz Date: Thu, 29 Apr 2021 13:37:40 +0200 Subject: [PATCH 040/199] resets enzoic fields --- src/Controller/Admin/ModuleConfiguration.php | 21 ++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/Controller/Admin/ModuleConfiguration.php b/src/Controller/Admin/ModuleConfiguration.php index 0e42c27d..617b76ee 100644 --- a/src/Controller/Admin/ModuleConfiguration.php +++ b/src/Controller/Admin/ModuleConfiguration.php @@ -11,14 +11,19 @@ class ModuleConfiguration extends ModuleConfiguration_parent public function saveConfVars() { $variables = $this->getConfigVariablesFromRequest(); - $enzoicAPIKey = $variables[PasswordPolicyConfig::SettingEnzoicAPIKey]; - $enzoicSecretKey = $variables[PasswordPolicyConfig::SettingEnzoicSecretKey]; - $enzoicApiCon = new Enzoic($enzoicAPIKey,$enzoicSecretKey); - $testAPICall = $enzoicApiCon->checkPassword("Test"); - if($testAPICall["status"] != "200") - { - Registry::getUtilsView()->addErrorToDisplay(Registry::getLang()->translateString("oxpspasswordpolicy_EnzoicError" . $testAPICall["status"])); - return; + if($variables[PasswordPolicyConfig::SettingEnzoic] == "true") { + $enzoicAPIKey = $variables[PasswordPolicyConfig::SettingEnzoicAPIKey]; + $enzoicSecretKey = $variables[PasswordPolicyConfig::SettingEnzoicSecretKey]; + $enzoicApiCon = new Enzoic($enzoicAPIKey, $enzoicSecretKey); + $testAPICall = $enzoicApiCon->checkPassword("Test"); + if ($testAPICall["status"] != "200") { + Registry::getUtilsView()->addErrorToDisplay("oxpspasswordpolicy_EnzoicError" . $testAPICall["status"]); + # reset API, Secret Key and deactivate Enzoic setting + # needs better solution + $_POST["confstrs"][PasswordPolicyConfig::SettingEnzoicAPIKey] = ""; + $_POST["confstrs"][PasswordPolicyConfig::SettingEnzoicSecretKey] = ""; + $_POST["confbools"][PasswordPolicyConfig::SettingEnzoic] = "false"; + } } parent::saveConfVars(); } From 99fcc9696a418be0c89a40d1bd9f2bdb59046782 Mon Sep 17 00:00:00 2001 From: moritz Date: Thu, 29 Apr 2021 13:59:16 +0200 Subject: [PATCH 041/199] fixed little bug --- src/Api/PasswordCheck.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Api/PasswordCheck.php b/src/Api/PasswordCheck.php index d36820d1..eb3944fb 100644 --- a/src/Api/PasswordCheck.php +++ b/src/Api/PasswordCheck.php @@ -17,6 +17,7 @@ class PasswordCheck private $config; private $enzoicApiCon; private $haveIBeenPwned; + /** * PasswordCheck constructor. */ @@ -34,9 +35,9 @@ public function __construct() public function isPasswordKnown(string $password): bool { // Überarbeiten, dass hier nicht so oft die Config geprüft werden muss - if ($this->config->getHaveIBeenPwnedNeeded() && $this->haveIBeenPwned->passwordExposed($password)=="exposed") { + if ($this->config->getHaveIBeenPwnedNeeded() && $this->haveIBeenPwned->passwordExposed($password) == "exposed") { return true; - } elseif ($this->config->getEnzoicNeeded() && $this->enzoicApiCon->checkPassword($password)["status"] = "200") { + } elseif ($this->config->getEnzoicNeeded() && $this->enzoicApiCon->checkPassword($password)["status"] == 200) { return true; } return false; @@ -54,4 +55,4 @@ public function isCredentialsKnown(string $username, string $password): bool } return false; } -} +} \ No newline at end of file From 9decde0b8e666a78d0a24116d47e4e5415a6f1a4 Mon Sep 17 00:00:00 2001 From: Keywan Ghadami Date: Fri, 30 Apr 2021 14:07:59 +0200 Subject: [PATCH 042/199] refactoring unittests --- metadata.php | 4 ++-- services.yaml | 11 ++++++++++- src/Api/PasswordCheck.php | 8 ++++---- src/Validators/PasswordPolicyDigits.php | 11 +++++++++-- src/Validators/PasswordPolicyPasswordLength.php | 10 +++++++++- src/Validators/PasswordPolicyUpperLowerCase.php | 9 ++++++++- .../Validators/PasswordPolicyDataBreachTest.php | 3 +++ tests/unit/Validators/PasswordPolicyDigitsTest.php | 3 ++- .../Validators/PasswordPolicyPasswordLengthTest.php | 1 + .../Validators/PasswordPolicySpecialCharacterTest.php | 3 ++- .../Validators/PasswordPolicyUpperLowerCaseTest.php | 1 + 11 files changed, 51 insertions(+), 13 deletions(-) rename tests/{unit => Integration}/Validators/PasswordPolicyDataBreachTest.php (86%) diff --git a/metadata.php b/metadata.php index a9a6fea8..33242ec7 100644 --- a/metadata.php +++ b/metadata.php @@ -93,8 +93,8 @@ ['group' => 'passwordpolicy_apisettings', 'name' => 'oxpspasswordpolicyAPI', 'type' => 'bool', 'value' => true], ['group' => 'passwordpolicy_apisettings', 'name' => 'oxpspasswordpolicyHaveIBeenPwned', 'type' => 'bool', 'value' => true], ['group' => 'passwordpolicy_apisettings', 'name' => 'oxpspasswordpolicyEnzoic', 'type' => 'bool', 'value' => false], - ['group' => 'passwordpolicy_apisettings', 'name' => 'oxpspasswordpolicyEnzoicAPIKey', 'type' => 'str'], - ['group' => 'passwordpolicy_apisettings', 'name' => 'oxpspasswordpolicyEnzoicSecretKey', 'type' => 'str'], + ['group' => 'passwordpolicy_apisettings', 'name' => 'oxpspasswordpolicyEnzoicAPIKey', 'type' => 'str', 'value'=>''], + ['group' => 'passwordpolicy_apisettings', 'name' => 'oxpspasswordpolicyEnzoicSecretKey', 'type' => 'str', 'value'=>''], ], 'events' => [], ]; diff --git a/services.yaml b/services.yaml index 68670865..c990a7b6 100644 --- a/services.yaml +++ b/services.yaml @@ -18,4 +18,13 @@ services: OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicySpecialCharacter: class: OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicySpecialCharacter OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyDataBreach: - class: OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyDataBreach \ No newline at end of file + class: OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyDataBreach + autowire: true; + OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyConfig: + class: OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyConfig + autowire: true; + OxidProfessionalServices\PasswordPolicy\Api\PasswordCheck: + class: OxidProfessionalServices\PasswordPolicy\Api\PasswordCheck + autowire: true; + DivineOmega\PasswordExposed\PasswordExposedChecker: + class: DivineOmega\PasswordExposed\PasswordExposedChecker \ No newline at end of file diff --git a/src/Api/PasswordCheck.php b/src/Api/PasswordCheck.php index eb3944fb..eb9e3871 100644 --- a/src/Api/PasswordCheck.php +++ b/src/Api/PasswordCheck.php @@ -14,9 +14,9 @@ */ class PasswordCheck { - private $config; - private $enzoicApiCon; - private $haveIBeenPwned; + private PasswordPolicyConfig $config; + private Enzoic $enzoicApiCon; + private PasswordExposedChecker $haveIBeenPwned; /** * PasswordCheck constructor. @@ -37,7 +37,7 @@ public function isPasswordKnown(string $password): bool // Überarbeiten, dass hier nicht so oft die Config geprüft werden muss if ($this->config->getHaveIBeenPwnedNeeded() && $this->haveIBeenPwned->passwordExposed($password) == "exposed") { return true; - } elseif ($this->config->getEnzoicNeeded() && $this->enzoicApiCon->checkPassword($password)["status"] == 200) { + } elseif ($this->config->getEnzoicNeeded() && $this->enzoicApiCon->checkPassword($password) !== null) { return true; } return false; diff --git a/src/Validators/PasswordPolicyDigits.php b/src/Validators/PasswordPolicyDigits.php index b52517f1..51299213 100644 --- a/src/Validators/PasswordPolicyDigits.php +++ b/src/Validators/PasswordPolicyDigits.php @@ -7,10 +7,17 @@ class PasswordPolicyDigits implements PasswordPolicyValidationInterface { + + private PasswordPolicyConfig $config; + + public function __construct(PasswordPolicyConfig $config) + { + $this->config = $config; + } + public function validate(string $sUsername, string $sPassword) { - $settings = new PasswordPolicyConfig(); - if ($settings->getPasswordNeedDigits() and !preg_match('(\d+)', $sPassword)) { + if ($this->config->getPasswordNeedDigits() and !preg_match('(\d+)', $sPassword)) { return 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESDIGITS'; } return true; diff --git a/src/Validators/PasswordPolicyPasswordLength.php b/src/Validators/PasswordPolicyPasswordLength.php index 2e86c507..16de4a76 100644 --- a/src/Validators/PasswordPolicyPasswordLength.php +++ b/src/Validators/PasswordPolicyPasswordLength.php @@ -7,10 +7,18 @@ class PasswordPolicyPasswordLength implements PasswordPolicyValidationInterface { + private PasswordPolicyConfig $config; + + public function __construct(PasswordPolicyConfig $config) + { + $this->config = $config; + } + public function validate(string $sUsername, string $sPassword) { + $settings = $this->config; $iPasswordLength = mb_strlen($sPassword, 'UTF-8'); - $settings = new PasswordPolicyConfig(); + if ($iPasswordLength < $settings->getMinPasswordLength()) { return 'ERROR_MESSAGE_PASSWORD_TOO_SHORT'; } diff --git a/src/Validators/PasswordPolicyUpperLowerCase.php b/src/Validators/PasswordPolicyUpperLowerCase.php index a5f541ff..7816d537 100644 --- a/src/Validators/PasswordPolicyUpperLowerCase.php +++ b/src/Validators/PasswordPolicyUpperLowerCase.php @@ -7,9 +7,16 @@ class PasswordPolicyUpperLowerCase implements PasswordPolicyValidationInterface { + private PasswordPolicyConfig $config; + + public function __construct(PasswordPolicyConfig $config) + { + $this->config = $config; + } + public function validate(string $sUsername, string $sPassword) { - $settings = new PasswordPolicyConfig(); + $settings = $this->config; if ($settings->getPasswordNeedUpperCase() and !preg_match('(\p{Lu}+)', $sPassword)) { return 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESUPPERCASE'; } diff --git a/tests/unit/Validators/PasswordPolicyDataBreachTest.php b/tests/Integration/Validators/PasswordPolicyDataBreachTest.php similarity index 86% rename from tests/unit/Validators/PasswordPolicyDataBreachTest.php rename to tests/Integration/Validators/PasswordPolicyDataBreachTest.php index 1c557f64..19d3e8b2 100644 --- a/tests/unit/Validators/PasswordPolicyDataBreachTest.php +++ b/tests/Integration/Validators/PasswordPolicyDataBreachTest.php @@ -2,6 +2,9 @@ namespace OxidProfessionalServices\PasswordPolicy\Tests; +use DivineOmega\PasswordExposed\PasswordExposedChecker; +use OxidProfessionalServices\PasswordPolicy\Api\PasswordCheck; +use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyConfig; use OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyDataBreach; use PHPUnit\Framework\TestCase; diff --git a/tests/unit/Validators/PasswordPolicyDigitsTest.php b/tests/unit/Validators/PasswordPolicyDigitsTest.php index 66cc62d6..f3f26f6b 100644 --- a/tests/unit/Validators/PasswordPolicyDigitsTest.php +++ b/tests/unit/Validators/PasswordPolicyDigitsTest.php @@ -2,6 +2,7 @@ namespace OxidProfessionalServices\PasswordPolicy\Tests; +use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyConfig; use OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyDigits; use PHPUnit\Framework\TestCase; @@ -18,7 +19,7 @@ class PasswordPolicyDigitsTest extends TestCase public function setUp(): void { parent::setUp(); - $this->subjectUnderTest = new PasswordPolicyDigits(); + $this->subjectUnderTest = new PasswordPolicyDigits(new PasswordPolicyConfig()); } /** diff --git a/tests/unit/Validators/PasswordPolicyPasswordLengthTest.php b/tests/unit/Validators/PasswordPolicyPasswordLengthTest.php index 1fc8b66c..98b57f52 100644 --- a/tests/unit/Validators/PasswordPolicyPasswordLengthTest.php +++ b/tests/unit/Validators/PasswordPolicyPasswordLengthTest.php @@ -2,6 +2,7 @@ namespace OxidProfessionalServices\PasswordPolicy\Tests; +use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyConfig; use PHPUnit\Framework\TestCase; use OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyPasswordLength; diff --git a/tests/unit/Validators/PasswordPolicySpecialCharacterTest.php b/tests/unit/Validators/PasswordPolicySpecialCharacterTest.php index eefea428..2bfacb3d 100644 --- a/tests/unit/Validators/PasswordPolicySpecialCharacterTest.php +++ b/tests/unit/Validators/PasswordPolicySpecialCharacterTest.php @@ -2,6 +2,7 @@ namespace OxidProfessionalServices\PasswordPolicy\Tests; +use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyConfig; use PHPUnit\Framework\TestCase; use OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicySpecialCharacter; @@ -18,7 +19,7 @@ class PasswordPolicySpecialCharacterTest extends TestCase public function setUp(): void { parent::setUp(); - $this->subjectUnderTest = new PasswordPolicySpecialCharacter(); + $this->subjectUnderTest = new PasswordPolicySpecialCharacter(new PasswordPolicyConfig()); } /** diff --git a/tests/unit/Validators/PasswordPolicyUpperLowerCaseTest.php b/tests/unit/Validators/PasswordPolicyUpperLowerCaseTest.php index 9af6be40..ac49c9cd 100644 --- a/tests/unit/Validators/PasswordPolicyUpperLowerCaseTest.php +++ b/tests/unit/Validators/PasswordPolicyUpperLowerCaseTest.php @@ -2,6 +2,7 @@ namespace OxidProfessionalServices\PasswordPolicy\Tests; +use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyConfig; use PHPUnit\Framework\TestCase; use OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyUpperLowerCase; From 3cdd344d74d7a4c61c85b564a6032e514f0b7a55 Mon Sep 17 00:00:00 2001 From: Keywan Ghadami Date: Fri, 30 Apr 2021 14:17:14 +0200 Subject: [PATCH 043/199] new API key check with new lib --- src/Controller/Admin/ModuleConfiguration.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Controller/Admin/ModuleConfiguration.php b/src/Controller/Admin/ModuleConfiguration.php index 617b76ee..d57483dd 100644 --- a/src/Controller/Admin/ModuleConfiguration.php +++ b/src/Controller/Admin/ModuleConfiguration.php @@ -2,6 +2,7 @@ namespace OxidProfessionalServices\PasswordPolicy\Controller\Admin; +use Enzoic\AuthenticationException; use Enzoic\Enzoic; use OxidEsales\Eshop\Core\Registry; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyConfig; @@ -15,9 +16,11 @@ public function saveConfVars() $enzoicAPIKey = $variables[PasswordPolicyConfig::SettingEnzoicAPIKey]; $enzoicSecretKey = $variables[PasswordPolicyConfig::SettingEnzoicSecretKey]; $enzoicApiCon = new Enzoic($enzoicAPIKey, $enzoicSecretKey); - $testAPICall = $enzoicApiCon->checkPassword("Test"); - if ($testAPICall["status"] != "200") { - Registry::getUtilsView()->addErrorToDisplay("oxpspasswordpolicy_EnzoicError" . $testAPICall["status"]); + + try { + $testAPICall = $enzoicApiCon->checkPassword("Test"); + } catch (AuthenticationException $e) { + Registry::getUtilsView()->addErrorToDisplay("oxpspasswordpolicy_EnzoicError401"); # reset API, Secret Key and deactivate Enzoic setting # needs better solution $_POST["confstrs"][PasswordPolicyConfig::SettingEnzoicAPIKey] = ""; From d21a2c86da2effa5e5c7d9d726a2e7811e27a527 Mon Sep 17 00:00:00 2001 From: moritz Date: Fri, 30 Apr 2021 15:49:46 +0200 Subject: [PATCH 044/199] fixed refactoring --- src/Validators/PasswordPolicyDataBreach.php | 14 +++++++++++--- src/Validators/PasswordPolicySpecialCharacter.php | 11 +++++++++-- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/Validators/PasswordPolicyDataBreach.php b/src/Validators/PasswordPolicyDataBreach.php index 990e3439..f7e915b9 100644 --- a/src/Validators/PasswordPolicyDataBreach.php +++ b/src/Validators/PasswordPolicyDataBreach.php @@ -8,11 +8,19 @@ class PasswordPolicyDataBreach implements PasswordPolicyValidationInterface { + + private PasswordPolicyConfig $config; + private PasswordCheck $passwordCheck; + + public function __construct(PasswordPolicyConfig $config, PasswordCheck $passwordCheck) + { + $this->config = $config; + $this->passwordCheck = $passwordCheck; + } + public function validate(string $sUsername, string $sPassword) { - $settings = new PasswordPolicyConfig(); - $passwordCheck = new PasswordCheck(); - if ($settings->getAPINeeded() && ($passwordCheck->isPasswordKnown($sPassword) || $passwordCheck->isCredentialsKnown($sUsername, $sPassword))) { + if ($this->config->getAPINeeded() && ($this->passwordCheck->isPasswordKnown($sPassword) || $this->passwordCheck->isCredentialsKnown($sUsername, $sPassword))) { return 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_PASSWORD_KNOWN'; } return true; diff --git a/src/Validators/PasswordPolicySpecialCharacter.php b/src/Validators/PasswordPolicySpecialCharacter.php index 5f8e8f95..58c7ebe4 100644 --- a/src/Validators/PasswordPolicySpecialCharacter.php +++ b/src/Validators/PasswordPolicySpecialCharacter.php @@ -7,11 +7,18 @@ class PasswordPolicySpecialCharacter implements PasswordPolicyValidationInterface { + private PasswordPolicyConfig $config; + + public function __construct(PasswordPolicyConfig $config) + { + $this->config = $config; + } + public function validate(string $sUsername, string $sPassword) { - $settings = new PasswordPolicyConfig(); + if ( - $settings->getPasswordNeedSpecialCharacter() and + $this->config->getPasswordNeedSpecialCharacter() and !preg_match('([\.,_@\~\(\)\!\#\$%\^\&\*\+=\-\\\/|:;`]+)', $sPassword) ) { return 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESSPECIAL'; From 834e5cc86e37487fafc753a7a3996ece1edb3e03 Mon Sep 17 00:00:00 2001 From: moritz Date: Fri, 30 Apr 2021 15:49:58 +0200 Subject: [PATCH 045/199] added new error code --- views/admin/de/passwordpolicy_lang.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/views/admin/de/passwordpolicy_lang.php b/views/admin/de/passwordpolicy_lang.php index f327c063..88bd9ce6 100644 --- a/views/admin/de/passwordpolicy_lang.php +++ b/views/admin/de/passwordpolicy_lang.php @@ -37,5 +37,7 @@ 'SHOP_MODULE_oxpspasswordpolicyEnzoicAPIKey' => 'Enzoic API Schlüssel', 'SHOP_MODULE_oxpspasswordpolicyEnzoicSecretKey' => 'Enzoic Geheimschlüssel', 'oxpspasswordpolicy_EnzoicError401' => 'Ihr Enzoic API Key oder Secret Key ist nicht gültig. Sie sind nicht autorisiert.', - 'oxpspasswordpolicy_EnzoicError0' => 'Es gibt ein Problem beim Verbinden mit der Enzoic API. Bitte versuchen Sie es erneut.' + 'oxpspasswordpolicy_EnzoicError0' => 'Es gibt ein Problem beim Verbinden mit der Enzoic API. Bitte versuchen Sie es erneut.', + 'oxpspasswordpolicy_EnzoicError500' => 'Ein unerwarteter Fehler ist aufgetreten. Btte probieren Sie es später erneut.', + ); From 3fab3b61dcbc2e1f1b0d6c8382309b3c54750f25 Mon Sep 17 00:00:00 2001 From: moritz Date: Fri, 30 Apr 2021 15:50:31 +0200 Subject: [PATCH 046/199] changed exception handling --- src/Controller/Admin/ModuleConfiguration.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Controller/Admin/ModuleConfiguration.php b/src/Controller/Admin/ModuleConfiguration.php index d57483dd..40f4a0ad 100644 --- a/src/Controller/Admin/ModuleConfiguration.php +++ b/src/Controller/Admin/ModuleConfiguration.php @@ -18,9 +18,9 @@ public function saveConfVars() $enzoicApiCon = new Enzoic($enzoicAPIKey, $enzoicSecretKey); try { - $testAPICall = $enzoicApiCon->checkPassword("Test"); - } catch (AuthenticationException $e) { - Registry::getUtilsView()->addErrorToDisplay("oxpspasswordpolicy_EnzoicError401"); + $enzoicApiCon->checkPassword("Test"); + } catch (\RuntimeException $ex) { + Registry::getUtilsView()->addErrorToDisplay("oxpspasswordpolicy_EnzoicError" . $ex->getCode()); # reset API, Secret Key and deactivate Enzoic setting # needs better solution $_POST["confstrs"][PasswordPolicyConfig::SettingEnzoicAPIKey] = ""; From 637414d897c813694c6335dcd01d5deb3f8030bd Mon Sep 17 00:00:00 2001 From: moritz Date: Mon, 3 May 2021 11:55:24 +0200 Subject: [PATCH 047/199] set autowire to true --- services.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services.yaml b/services.yaml index c990a7b6..e3805dc8 100644 --- a/services.yaml +++ b/services.yaml @@ -11,12 +11,16 @@ services: autowire: true OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyPasswordLength: class: OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyPasswordLength + autowire: true; OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyUpperLowerCase: class: OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyUpperLowerCase + autowire: true; OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyDigits: class: OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyDigits + autowire: true; OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicySpecialCharacter: class: OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicySpecialCharacter + autowire: true; OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyDataBreach: class: OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyDataBreach autowire: true; From 9a2ff8127ff06e69aecdc922a33478b0b822aec7 Mon Sep 17 00:00:00 2001 From: moritz Date: Mon, 3 May 2021 11:56:38 +0200 Subject: [PATCH 048/199] added github lib to composer.json --- composer.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 6a832f88..5ced7d72 100644 --- a/composer.json +++ b/composer.json @@ -4,11 +4,18 @@ "type": "oxideshop-module", "keywords": ["oxid", "modules", "eShop", "password", "password-strength", "password-policy"], "license": "GPL-3.0-only", + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/moritzdemmer/enzoic-php-client" + } + ], "require": { "php": ">=7.3", "ext-json": "*", "enzoic/enzoic": "dev-master", - "divineomega/password_exposed": "v3.2.0" + "divineomega/password_exposed": "v3.2.0", + "nikolaposa/rate-limit": "dev-master" }, "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.2", From 16fa3a82cbc6072198cff425b0063d7cb0dfd058 Mon Sep 17 00:00:00 2001 From: moritz Date: Mon, 3 May 2021 12:19:26 +0200 Subject: [PATCH 049/199] removed local git --- composer.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/composer.json b/composer.json index 5ced7d72..ac435c9f 100644 --- a/composer.json +++ b/composer.json @@ -4,12 +4,6 @@ "type": "oxideshop-module", "keywords": ["oxid", "modules", "eShop", "password", "password-strength", "password-policy"], "license": "GPL-3.0-only", - "repositories": [ - { - "type": "vcs", - "url": "https://github.com/moritzdemmer/enzoic-php-client" - } - ], "require": { "php": ">=7.3", "ext-json": "*", From 4f7353c100b57255b63e66fb9f315c7d7ade2d7e Mon Sep 17 00:00:00 2001 From: moritz Date: Mon, 3 May 2021 14:45:30 +0200 Subject: [PATCH 050/199] added rate limiting translations --- metadata.php | 11 ++++++----- translations/de/passwordpolicy_lang.php | 3 ++- views/admin/de/passwordpolicy_lang.php | 8 +++++++- views/admin/en/passwordpolicy_lang.php | 11 ++++++++++- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/metadata.php b/metadata.php index 33242ec7..069b4571 100644 --- a/metadata.php +++ b/metadata.php @@ -90,11 +90,12 @@ ['group' => 'passwordpolicy_requirements', 'name' => 'oxpspasswordpolicyLowerCase', 'type' => 'bool', 'value' => true], ['group' => 'passwordpolicy_requirements', 'name' => 'oxpspasswordpolicySpecial', 'type' => 'bool', 'value' => true], ['group' => 'passwordpolicy_requirements', 'name' => 'oxpspasswordpolicyDigits', 'type' => 'bool', 'value' => true], - ['group' => 'passwordpolicy_apisettings', 'name' => 'oxpspasswordpolicyAPI', 'type' => 'bool', 'value' => true], - ['group' => 'passwordpolicy_apisettings', 'name' => 'oxpspasswordpolicyHaveIBeenPwned', 'type' => 'bool', 'value' => true], - ['group' => 'passwordpolicy_apisettings', 'name' => 'oxpspasswordpolicyEnzoic', 'type' => 'bool', 'value' => false], - ['group' => 'passwordpolicy_apisettings', 'name' => 'oxpspasswordpolicyEnzoicAPIKey', 'type' => 'str', 'value'=>''], - ['group' => 'passwordpolicy_apisettings', 'name' => 'oxpspasswordpolicyEnzoicSecretKey', 'type' => 'str', 'value'=>''], + ['group' => 'passwordpolicy_api', 'name' => 'oxpspasswordpolicyAPI', 'type' => 'bool', 'value' => true], + ['group' => 'passwordpolicy_api', 'name' => 'oxpspasswordpolicyHaveIBeenPwned', 'type' => 'bool', 'value' => true], + ['group' => 'passwordpolicy_api', 'name' => 'oxpspasswordpolicyEnzoic', 'type' => 'bool', 'value' => false], + ['group' => 'passwordpolicy_api', 'name' => 'oxpspasswordpolicyEnzoicAPIKey', 'type' => 'str', 'value'=>''], + ['group' => 'passwordpolicy_api', 'name' => 'oxpspasswordpolicyEnzoicSecretKey', 'type' => 'str', 'value'=>''], + ['group' => 'passwordpolicy_ratelimiting', 'name' => 'oxpspasswordpolicyRateLimitingDrivers', 'type' => 'select', 'value' => 'APCu', 'constraints' => 'Redis|Predis|Memcached|APCu'], ], 'events' => [], ]; diff --git a/translations/de/passwordpolicy_lang.php b/translations/de/passwordpolicy_lang.php index 7df1a972..74155f67 100644 --- a/translations/de/passwordpolicy_lang.php +++ b/translations/de/passwordpolicy_lang.php @@ -52,5 +52,6 @@ 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESLOWERCASE' => 'Das Passwort muss mindestens einen Kleinbuchstaben enthalten.', 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESSPECIAL' => 'Das Passwort muss mindestens ein Sonderzeichen enthalten.', 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_WRONGTYPE' => 'Fehlerhafter Typ, bitte tragen Sie einen validen Wert ein. Bei weiteren Fragen wenden Sie sich an den Support.', - 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_PASSWORD_KNOWN' => 'Das Passwort befindet sich bereits in einer gehackten Datenbank und ist somit unsicher.' + 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_PASSWORD_KNOWN' => 'Das Passwort befindet sich bereits in einer gehackten Datenbank und ist somit unsicher.', + 'OXPS_PASSWORDPOLICY_RATELIMIT_EXCEEDED' => 'Sie haben sich zu oft versucht einzuloggen. Bitte versuchen Sie es später erneut.' ); diff --git a/views/admin/de/passwordpolicy_lang.php b/views/admin/de/passwordpolicy_lang.php index 88bd9ce6..9825c0f2 100644 --- a/views/admin/de/passwordpolicy_lang.php +++ b/views/admin/de/passwordpolicy_lang.php @@ -30,7 +30,7 @@ 'SHOP_MODULE_oxpspasswordpolicyUpperCase' => 'Großbuchstaben (A...Z)', 'SHOP_MODULE_oxpspasswordpolicyLowerCase' => 'Kleinbuchstaben (a...z)', 'SHOP_MODULE_oxpspasswordpolicySpecial' => 'Sonderzeichen (!,@#$%^&*?_~()-)', - 'SHOP_MODULE_GROUP_passwordpolicy_apisettings' => 'API Einstellungen', + 'SHOP_MODULE_GROUP_passwordpolicy_api' => 'API Einstellungen', 'SHOP_MODULE_oxpspasswordpolicyAPI' => 'Veröffentliche Passwörter überprüfen', 'SHOP_MODULE_oxpspasswordpolicyHaveIBeenPwned' => 'HaveIBeenPwned', 'SHOP_MODULE_oxpspasswordpolicyEnzoic' => 'Enzoic', @@ -39,5 +39,11 @@ 'oxpspasswordpolicy_EnzoicError401' => 'Ihr Enzoic API Key oder Secret Key ist nicht gültig. Sie sind nicht autorisiert.', 'oxpspasswordpolicy_EnzoicError0' => 'Es gibt ein Problem beim Verbinden mit der Enzoic API. Bitte versuchen Sie es erneut.', 'oxpspasswordpolicy_EnzoicError500' => 'Ein unerwarteter Fehler ist aufgetreten. Btte probieren Sie es später erneut.', + 'SHOP_MODULE_GROUP_passwordpolicy_ratelimiting' => 'Rate Limiting Einstellungen', + 'SHOP_MODULE_oxpspasswordpolicyRateLimitingDrivers' => 'Treiber', + 'SHOP_MODULE_oxpspasswordpolicyRateLimitingDrivers_Redis' => 'Redis', + 'SHOP_MODULE_oxpspasswordpolicyRateLimitingDrivers_Predis' => 'Predis', + 'SHOP_MODULE_oxpspasswordpolicyRateLimitingDrivers_Memcached' => 'Memcached', + 'SHOP_MODULE_oxpspasswordpolicyRateLimitingDrivers_APCu' => 'APCu', ); diff --git a/views/admin/en/passwordpolicy_lang.php b/views/admin/en/passwordpolicy_lang.php index 4553aab1..da061dfc 100644 --- a/views/admin/en/passwordpolicy_lang.php +++ b/views/admin/en/passwordpolicy_lang.php @@ -35,5 +35,14 @@ 'SHOP_MODULE_oxpspasswordpolicyHaveIBeenPwned' => 'HaveIBeenPwned', 'SHOP_MODULE_oxpspasswordpolicyEnzoic' => 'Enzoic', 'SHOP_MODULE_oxpspasswordpolicyEnzoicAPIKey' => 'Enzoic API Key', - 'SHOP_MODULE_oxpspasswordpolicyEnzoicSecretKey' => 'Enzoic Secret Key' + 'SHOP_MODULE_oxpspasswordpolicyEnzoicSecretKey' => 'Enzoic Secret Key', + 'oxpspasswordpolicy_EnzoicError401' => 'Your entered Enzoic api/secret key is not valid.', + 'oxpspasswordpolicy_EnzoicError0' => 'There was an error connecting to the Enzoic service. Please try again later.', + 'oxpspasswordpolicy_EnzoicError500' => 'An unexpected error ocurred. Please try again later.', + 'SHOP_MODULE_GROUP_passwordpolicy_ratelimiting' => 'Rate Limiting Settings', + 'SHOP_MODULE_oxpspasswordpolicyRateLimitingDrivers' => 'Drivers', + 'SHOP_MODULE_oxpspasswordpolicyRateLimitingDrivers_Redis' => 'Redis', + 'SHOP_MODULE_oxpspasswordpolicyRateLimitingDrivers_Predis' => 'Predis', + 'SHOP_MODULE_oxpspasswordpolicyRateLimitingDrivers_Memcached' => 'Memcached', + 'SHOP_MODULE_oxpspasswordpolicyRateLimitingDrivers_APCu' => 'APCu', ); From 154b6ac379567bd2f5023c403b32f07d464b545a Mon Sep 17 00:00:00 2001 From: moritz Date: Mon, 3 May 2021 15:46:03 +0200 Subject: [PATCH 051/199] added rate limit to settings --- metadata.php | 1 + src/Core/PasswordPolicyConfig.php | 12 ++++++++++++ views/admin/de/passwordpolicy_lang.php | 1 + 3 files changed, 14 insertions(+) diff --git a/metadata.php b/metadata.php index 069b4571..35b67ee6 100644 --- a/metadata.php +++ b/metadata.php @@ -96,6 +96,7 @@ ['group' => 'passwordpolicy_api', 'name' => 'oxpspasswordpolicyEnzoicAPIKey', 'type' => 'str', 'value'=>''], ['group' => 'passwordpolicy_api', 'name' => 'oxpspasswordpolicyEnzoicSecretKey', 'type' => 'str', 'value'=>''], ['group' => 'passwordpolicy_ratelimiting', 'name' => 'oxpspasswordpolicyRateLimitingDrivers', 'type' => 'select', 'value' => 'APCu', 'constraints' => 'Redis|Predis|Memcached|APCu'], + ['group' => 'passwordpolicy_ratelimiting', 'name' => 'oxpspasswordpolicyRateLimitingLimit', 'type' => 'num', 'value' => 60], ], 'events' => [], ]; diff --git a/src/Core/PasswordPolicyConfig.php b/src/Core/PasswordPolicyConfig.php index fd5df285..191c4781 100644 --- a/src/Core/PasswordPolicyConfig.php +++ b/src/Core/PasswordPolicyConfig.php @@ -44,6 +44,8 @@ class PasswordPolicyConfig public const SettingHaveIBeenPwned = self::SettingsPrefix . 'HaveIBeenPwned'; public const SettingUpper = self::SettingsPrefix . 'UpperCase'; public const SettingLower = self::SettingsPrefix . 'LowerCase'; + public const SettingDrivers = self::SettingsPrefix . 'RateLimitingDrivers'; + public const SettingLimit = self::SettingsPrefix . 'RateLimitingLimit'; public function getMinPasswordLength(): int { @@ -113,6 +115,16 @@ public function getHaveIBeenPwnedNeeded(): bool return $this->isConfigParam(self::SettingHaveIBeenPwned); } + public function getSelectedDriver(): string + { + return (string) Registry::getConfig()->getConfigParam(self::SettingDrivers); + } + + public function getRateLimit(): int + { + return (int) Registry::getConfig()->getConfigParam(self::SettingLimit, 60); + } + private function isConfigParam(string $name): bool { return (bool) Registry::getConfig()->getConfigParam($name, true); diff --git a/views/admin/de/passwordpolicy_lang.php b/views/admin/de/passwordpolicy_lang.php index 9825c0f2..20fb0d33 100644 --- a/views/admin/de/passwordpolicy_lang.php +++ b/views/admin/de/passwordpolicy_lang.php @@ -45,5 +45,6 @@ 'SHOP_MODULE_oxpspasswordpolicyRateLimitingDrivers_Predis' => 'Predis', 'SHOP_MODULE_oxpspasswordpolicyRateLimitingDrivers_Memcached' => 'Memcached', 'SHOP_MODULE_oxpspasswordpolicyRateLimitingDrivers_APCu' => 'APCu', + 'SHOP_MODULE_oxpspasswordpolicyRateLimitingLimit' => 'Einlogversuche pro Minute' ); From 94de57ea9d912551e9182c2af7144303ed2029eb Mon Sep 17 00:00:00 2001 From: moritz Date: Mon, 3 May 2021 15:46:38 +0200 Subject: [PATCH 052/199] added ugly rate limiting functionality --- src/Model/User.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Model/User.php b/src/Model/User.php index 8b3da5bf..9f46424d 100644 --- a/src/Model/User.php +++ b/src/Model/User.php @@ -6,7 +6,11 @@ use OxidEsales\Eshop\Core\Exception\UserException; use OxidEsales\Eshop\Core\InputValidator; use OxidEsales\Eshop\Core\Registry; +use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyConfig; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyValidator; +use RateLimit\ApcuRateLimiter; +use RateLimit\Exception\LimitExceeded; +use RateLimit\Rate; class User extends User_parent { @@ -27,6 +31,15 @@ public function onLogin($userName, $password) $errorMessage = $err->getMessage() . ' ' . Registry::getLang()->translateString('REQUEST_PASSWORD_AFTERCLICK'); throw oxNew(UserException::class, $errorMessage); } + if (!$this->isLoaded()) { + $rateLimiter = new ApcuRateLimiter(); + $config = new PasswordPolicyConfig(); + try { + $rateLimiter->limit($userName, Rate::perMinute($config->getRateLimit())); + } catch (LimitExceeded $exception) { + throw oxNew(UserException::class, 'OXPS_PASSWORDPOLICY_RATELIMIT_EXCEEDED'); + } + } parent::onLogin($userName, $password); } } From b7a85791bb5dc63ae84e0df40e4d7a135dde8a58 Mon Sep 17 00:00:00 2001 From: moritz Date: Mon, 3 May 2021 17:55:50 +0200 Subject: [PATCH 053/199] now checking the rate limit before login --- src/Model/User.php | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/Model/User.php b/src/Model/User.php index 9f46424d..57759497 100644 --- a/src/Model/User.php +++ b/src/Model/User.php @@ -28,18 +28,21 @@ public function onLogin($userName, $password) if (!isAdmin() && $this->isLoaded() && $err = $passValidator->validatePassword($userName, $password)) { $forgotPass = new ForgotPasswordController(); $forgotPass->forgotPassword(); - $errorMessage = $err->getMessage() . ' ' . Registry::getLang()->translateString('REQUEST_PASSWORD_AFTERCLICK'); + $errorMessage = $err->getMessage() . ' ' . Registry::getLang()->translateString('REQUEST_PASSWORD_AFTERCLICK'); throw oxNew(UserException::class, $errorMessage); } - if (!$this->isLoaded()) { - $rateLimiter = new ApcuRateLimiter(); - $config = new PasswordPolicyConfig(); - try { - $rateLimiter->limit($userName, Rate::perMinute($config->getRateLimit())); - } catch (LimitExceeded $exception) { - throw oxNew(UserException::class, 'OXPS_PASSWORDPOLICY_RATELIMIT_EXCEEDED'); - } - } parent::onLogin($userName, $password); } + + public function login($userName, $password, $setSessionCookie = false) + { + $rateLimiter = new ApcuRateLimiter(); + $config = new PasswordPolicyConfig(); + try { + $rateLimiter->limit($userName, Rate::perMinute($config->getRateLimit())); + } catch (LimitExceeded $exception) { + throw oxNew(UserException::class, 'OXPS_PASSWORDPOLICY_RATELIMIT_EXCEEDED'); + } + parent::login($userName, $password, $setSessionCookie); + } } From f055ddacae31e47abefddd0f127ca67137983cbf Mon Sep 17 00:00:00 2001 From: moritz Date: Wed, 5 May 2021 16:08:49 +0200 Subject: [PATCH 054/199] added credentials check in isPasswordKnown function, used dependency injection for config and haveIbeenpwned lib --- src/Api/PasswordCheck.php | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/src/Api/PasswordCheck.php b/src/Api/PasswordCheck.php index eb9e3871..97d0378b 100644 --- a/src/Api/PasswordCheck.php +++ b/src/Api/PasswordCheck.php @@ -21,38 +21,26 @@ class PasswordCheck /** * PasswordCheck constructor. */ - public function __construct() + public function __construct(PasswordPolicyConfig $config, PasswordExposedChecker $haveIBeenPwned) { - $this->config = new PasswordPolicyConfig(); + $this->config = $config; $this->enzoicApiCon = new Enzoic($this->config->getAPIKey(), $this->config->getSecretKey()); - $this->haveIBeenPwned = new PasswordExposedChecker(); + $this->haveIBeenPwned = $haveIBeenPwned; } /** + * @param string $username * @param string $password * @return bool */ - public function isPasswordKnown(string $password): bool + public function isPasswordKnown(string $username, string $password): bool { - // Überarbeiten, dass hier nicht so oft die Config geprüft werden muss if ($this->config->getHaveIBeenPwnedNeeded() && $this->haveIBeenPwned->passwordExposed($password) == "exposed") { return true; - } elseif ($this->config->getEnzoicNeeded() && $this->enzoicApiCon->checkPassword($password) !== null) { + } elseif ($this->config->getEnzoicNeeded() && ($this->enzoicApiCon->checkPassword($password) !== null || $this->enzoicApiCon->checkCredentials($username, $password))) { return true; } return false; } - /** - * @param string $username - * @param string $password - * @return bool - */ - public function isCredentialsKnown(string $username, string $password): bool - { - if ($this->config->getEnzoicNeeded() && $this->enzoicApiCon->checkCredentials($username, $password)) { - return true; - } - return false; - } } \ No newline at end of file From 26a5972c36809bb5ac814bc0de8cec0b3ce162fc Mon Sep 17 00:00:00 2001 From: moritz Date: Wed, 5 May 2021 16:09:03 +0200 Subject: [PATCH 055/199] renamed files --- ...eConfiguration.php => PasswordPolicyModuleConfiguration.php} | 2 +- ...ntroller.php => PasswordPolicyAccountPasswordController.php} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/Controller/Admin/{ModuleConfiguration.php => PasswordPolicyModuleConfiguration.php} (95%) rename src/Controller/{AccountPasswordController.php => PasswordPolicyAccountPasswordController.php} (95%) diff --git a/src/Controller/Admin/ModuleConfiguration.php b/src/Controller/Admin/PasswordPolicyModuleConfiguration.php similarity index 95% rename from src/Controller/Admin/ModuleConfiguration.php rename to src/Controller/Admin/PasswordPolicyModuleConfiguration.php index 40f4a0ad..c12588e0 100644 --- a/src/Controller/Admin/ModuleConfiguration.php +++ b/src/Controller/Admin/PasswordPolicyModuleConfiguration.php @@ -7,7 +7,7 @@ use OxidEsales\Eshop\Core\Registry; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyConfig; -class ModuleConfiguration extends ModuleConfiguration_parent +class PasswordPolicyModuleConfiguration extends PasswordPolicyModuleConfiguration_parent { public function saveConfVars() { diff --git a/src/Controller/AccountPasswordController.php b/src/Controller/PasswordPolicyAccountPasswordController.php similarity index 95% rename from src/Controller/AccountPasswordController.php rename to src/Controller/PasswordPolicyAccountPasswordController.php index bc3abd13..9be2ec00 100644 --- a/src/Controller/AccountPasswordController.php +++ b/src/Controller/PasswordPolicyAccountPasswordController.php @@ -4,7 +4,7 @@ use OxidEsales\Eshop\Core\InputValidator; -class AccountPasswordController extends AccountPasswordController_parent +class PasswordPolicyAccountPasswordController extends PasswordPolicyAccountPasswordController_parent { /** * changes current user password From 5dfa0d173d15bba9f6bfdbf50ba29927dff39615 Mon Sep 17 00:00:00 2001 From: moritz Date: Wed, 5 May 2021 16:12:06 +0200 Subject: [PATCH 056/199] added rate limiting module settings --- metadata.php | 9 ++++++--- src/Core/PasswordPolicyConfig.php | 17 +++++++++++++++++ views/admin/de/passwordpolicy_lang.php | 6 +++++- views/admin/en/passwordpolicy_lang.php | 10 ++++++++-- 4 files changed, 36 insertions(+), 6 deletions(-) diff --git a/metadata.php b/metadata.php index 35b67ee6..cba0d066 100644 --- a/metadata.php +++ b/metadata.php @@ -32,9 +32,9 @@ use OxidEsales\Eshop\Application\Controller\Admin\ModuleConfiguration; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyValidator; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyViewConfig; -use OxidProfessionalServices\PasswordPolicy\Controller\AccountPasswordController as PasswordPolicyAccountPasswordController; -use OxidProfessionalServices\PasswordPolicy\Model\User as PasswordPolicyUser; -use OxidProfessionalServices\PasswordPolicy\Controller\Admin\ModuleConfiguration as PasswordPolicyModuleConfiguration; +use OxidProfessionalServices\PasswordPolicy\Controller\PasswordPolicyAccountPasswordController; +use OxidProfessionalServices\PasswordPolicy\Model\PasswordPolicyUser; +use OxidProfessionalServices\PasswordPolicy\Controller\Admin\PasswordPolicyModuleConfiguration; $sMetadataVersion = '2.1'; @@ -95,8 +95,11 @@ ['group' => 'passwordpolicy_api', 'name' => 'oxpspasswordpolicyEnzoic', 'type' => 'bool', 'value' => false], ['group' => 'passwordpolicy_api', 'name' => 'oxpspasswordpolicyEnzoicAPIKey', 'type' => 'str', 'value'=>''], ['group' => 'passwordpolicy_api', 'name' => 'oxpspasswordpolicyEnzoicSecretKey', 'type' => 'str', 'value'=>''], + ['group' => 'passwordpolicy_ratelimiting', 'name' => 'oxpspasswordpolicyRateLimiting', 'type' => 'bool', 'value' => true], ['group' => 'passwordpolicy_ratelimiting', 'name' => 'oxpspasswordpolicyRateLimitingDrivers', 'type' => 'select', 'value' => 'APCu', 'constraints' => 'Redis|Predis|Memcached|APCu'], ['group' => 'passwordpolicy_ratelimiting', 'name' => 'oxpspasswordpolicyRateLimitingLimit', 'type' => 'num', 'value' => 60], + ['group' => 'passwordpolicy_memcached', 'name' => 'oxpspasswordpolicyMemcachedHost', 'type' => 'str', 'value' => 'memcached'], + ['group' => 'passwordpolicy_memcached', 'name' => 'oxpspasswordpolicyMemcachedPort', 'type' => 'num', 'value' => 11211], ], 'events' => [], ]; diff --git a/src/Core/PasswordPolicyConfig.php b/src/Core/PasswordPolicyConfig.php index 191c4781..a11ad11d 100644 --- a/src/Core/PasswordPolicyConfig.php +++ b/src/Core/PasswordPolicyConfig.php @@ -46,6 +46,9 @@ class PasswordPolicyConfig public const SettingLower = self::SettingsPrefix . 'LowerCase'; public const SettingDrivers = self::SettingsPrefix . 'RateLimitingDrivers'; public const SettingLimit = self::SettingsPrefix . 'RateLimitingLimit'; + public const SettingRateLimiting = self::SettingsPrefix . 'RateLimiting'; + public const SettingMemcachedHost = self::SettingsPrefix . 'MemcachedHost'; + public const SettingMemcachedPort = self::SettingsPrefix . 'MemcachedPort'; public function getMinPasswordLength(): int { @@ -115,6 +118,11 @@ public function getHaveIBeenPwnedNeeded(): bool return $this->isConfigParam(self::SettingHaveIBeenPwned); } + public function getRateLimitingNeeded(): bool + { + return $this->isConfigParam(self::SettingRateLimiting); + } + public function getSelectedDriver(): string { return (string) Registry::getConfig()->getConfigParam(self::SettingDrivers); @@ -125,6 +133,15 @@ public function getRateLimit(): int return (int) Registry::getConfig()->getConfigParam(self::SettingLimit, 60); } + public function getMemcachedHost(): string + { + return (string) Registry::getConfig()->getConfigParam(self::SettingMemcachedHost); + } + + public function getMemcachedPort(): int + { + return (int) Registry::getConfig()->getConfigParam(self::SettingMemcachedPort, 11211); + } private function isConfigParam(string $name): bool { return (bool) Registry::getConfig()->getConfigParam($name, true); diff --git a/views/admin/de/passwordpolicy_lang.php b/views/admin/de/passwordpolicy_lang.php index 20fb0d33..9678fa09 100644 --- a/views/admin/de/passwordpolicy_lang.php +++ b/views/admin/de/passwordpolicy_lang.php @@ -40,11 +40,15 @@ 'oxpspasswordpolicy_EnzoicError0' => 'Es gibt ein Problem beim Verbinden mit der Enzoic API. Bitte versuchen Sie es erneut.', 'oxpspasswordpolicy_EnzoicError500' => 'Ein unerwarteter Fehler ist aufgetreten. Btte probieren Sie es später erneut.', 'SHOP_MODULE_GROUP_passwordpolicy_ratelimiting' => 'Rate Limiting Einstellungen', + 'SHOP_MODULE_oxpspasswordpolicyRateLimiting' => 'Aktiv', 'SHOP_MODULE_oxpspasswordpolicyRateLimitingDrivers' => 'Treiber', 'SHOP_MODULE_oxpspasswordpolicyRateLimitingDrivers_Redis' => 'Redis', 'SHOP_MODULE_oxpspasswordpolicyRateLimitingDrivers_Predis' => 'Predis', 'SHOP_MODULE_oxpspasswordpolicyRateLimitingDrivers_Memcached' => 'Memcached', 'SHOP_MODULE_oxpspasswordpolicyRateLimitingDrivers_APCu' => 'APCu', - 'SHOP_MODULE_oxpspasswordpolicyRateLimitingLimit' => 'Einlogversuche pro Minute' + 'SHOP_MODULE_oxpspasswordpolicyRateLimitingLimit' => 'Einlogversuche pro Minute', + 'SHOP_MODULE_GROUP_passwordpolicy_memcached' => 'Memcached Einstellungen', + 'SHOP_MODULE_oxpspasswordpolicyMemcachedHost' => 'Host', + 'SHOP_MODULE_oxpspasswordpolicyMemcachedPort' => 'Port', ); diff --git a/views/admin/en/passwordpolicy_lang.php b/views/admin/en/passwordpolicy_lang.php index da061dfc..0f8c8169 100644 --- a/views/admin/en/passwordpolicy_lang.php +++ b/views/admin/en/passwordpolicy_lang.php @@ -34,15 +34,21 @@ 'SHOP_MODULE_oxpspasswordpolicyAPI' => 'Check leaked passwords', 'SHOP_MODULE_oxpspasswordpolicyHaveIBeenPwned' => 'HaveIBeenPwned', 'SHOP_MODULE_oxpspasswordpolicyEnzoic' => 'Enzoic', - 'SHOP_MODULE_oxpspasswordpolicyEnzoicAPIKey' => 'Enzoic API Key', - 'SHOP_MODULE_oxpspasswordpolicyEnzoicSecretKey' => 'Enzoic Secret Key', + 'SHOP_MODULE_oxpspasswordpolicyEnzoicAPIKey' => 'Enzoic API key', + 'SHOP_MODULE_oxpspasswordpolicyEnzoicSecretKey' => 'Enzoic secret key', 'oxpspasswordpolicy_EnzoicError401' => 'Your entered Enzoic api/secret key is not valid.', 'oxpspasswordpolicy_EnzoicError0' => 'There was an error connecting to the Enzoic service. Please try again later.', 'oxpspasswordpolicy_EnzoicError500' => 'An unexpected error ocurred. Please try again later.', 'SHOP_MODULE_GROUP_passwordpolicy_ratelimiting' => 'Rate Limiting Settings', + 'SHOP_MODULE_oxpspasswordpolicyRateLimiting' => 'Active', 'SHOP_MODULE_oxpspasswordpolicyRateLimitingDrivers' => 'Drivers', 'SHOP_MODULE_oxpspasswordpolicyRateLimitingDrivers_Redis' => 'Redis', 'SHOP_MODULE_oxpspasswordpolicyRateLimitingDrivers_Predis' => 'Predis', 'SHOP_MODULE_oxpspasswordpolicyRateLimitingDrivers_Memcached' => 'Memcached', 'SHOP_MODULE_oxpspasswordpolicyRateLimitingDrivers_APCu' => 'APCu', + 'SHOP_MODULE_oxpspasswordpolicyRateLimitingLimit' => 'Login attemps per minute', + 'SHOP_MODULE_GROUP_passwordpolicy_memcached' => 'Memcached Settings', + 'SHOP_MODULE_oxpspasswordpolicyMemcachedHost' => 'Host', + 'SHOP_MODULE_oxpspasswordpolicyMemcachedPort' => 'Port', + ); From d506555a185bb0d76abbd3ec56357fd1f496307f Mon Sep 17 00:00:00 2001 From: moritz Date: Wed, 5 May 2021 16:12:24 +0200 Subject: [PATCH 057/199] fixed spelling mistake --- src/Core/PasswordPolicyViewConfig.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Core/PasswordPolicyViewConfig.php b/src/Core/PasswordPolicyViewConfig.php index bc3b01da..ddbbcab4 100644 --- a/src/Core/PasswordPolicyViewConfig.php +++ b/src/Core/PasswordPolicyViewConfig.php @@ -32,6 +32,9 @@ */ class PasswordPolicyViewConfig extends PasswordPolicyViewConfig_parent { + /** + * @throws \Exception + */ public function getJsonPasswordPolicySettings(): string { $config = Registry::get(PasswordPolicyConfig::class); @@ -45,7 +48,7 @@ public function getJsonPasswordPolicySettings(): string $res = json_encode($array, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE); if ($res === false) { $error = json_last_error_msg(); - throw new \Exception("Password policy configuration broken? - Cloud not convert to JSON: $error"); + throw new \Exception("Password policy configuration broken? - Could not convert to JSON: $error"); } return $res; } From e4db46edb118dda9a24f086d1e3292137eff03f4 Mon Sep 17 00:00:00 2001 From: moritz Date: Wed, 5 May 2021 16:14:47 +0200 Subject: [PATCH 058/199] added rate limiting --- src/Exception/LimiterNotFound.php | 18 +++++++++++ .../PasswordPolicyRateLimiterFactory.php | 30 +++++++++++++++++ .../RateLimiter/PasswordPolicyAPCu.php | 16 ++++++++++ .../RateLimiter/PasswordPolicyMemcached.php | 32 +++++++++++++++++++ .../PasswordPolicyRateLimiterInterface.php | 10 ++++++ .../{User.php => PasswordPolicyUser.php} | 27 ++++++++++++---- 6 files changed, 126 insertions(+), 7 deletions(-) create mode 100644 src/Exception/LimiterNotFound.php create mode 100644 src/Factory/PasswordPolicyRateLimiterFactory.php create mode 100644 src/Factory/RateLimiter/PasswordPolicyAPCu.php create mode 100644 src/Factory/RateLimiter/PasswordPolicyMemcached.php create mode 100644 src/Factory/RateLimiter/PasswordPolicyRateLimiterInterface.php rename src/Model/{User.php => PasswordPolicyUser.php} (62%) diff --git a/src/Exception/LimiterNotFound.php b/src/Exception/LimiterNotFound.php new file mode 100644 index 00000000..64c55c81 --- /dev/null +++ b/src/Exception/LimiterNotFound.php @@ -0,0 +1,18 @@ + PasswordPolicyMemcached::class, + "APCu" => PasswordPolicyAPCu::class + ]; + + /** + * @param string $version + * @return PasswordPolicyRateLimiterInterface + * @throws LimiterNotFound + */ + public function getRateLimiter(string $version): PasswordPolicyRateLimiterInterface + { + if (!isset($this->versionMap[$version])) { + throw new LimiterNotFound(); + } + $driver = new $this->versionMap[$version](); + return $driver; + } +} \ No newline at end of file diff --git a/src/Factory/RateLimiter/PasswordPolicyAPCu.php b/src/Factory/RateLimiter/PasswordPolicyAPCu.php new file mode 100644 index 00000000..d1ccfd91 --- /dev/null +++ b/src/Factory/RateLimiter/PasswordPolicyAPCu.php @@ -0,0 +1,16 @@ +host = $config->getMemcachedHost(); + $this->port = $config->getMemcachedPort(); + } + + public function getLimiter(): MemcachedRateLimiter + { + $memcached = new \Memcached(); + $memcached->addServer($this->host,$this->port); + $memcached->setOption(\Memcached::OPT_BINARY_PROTOCOL, TRUE); + return new MemcachedRateLimiter($memcached); + } + +} \ No newline at end of file diff --git a/src/Factory/RateLimiter/PasswordPolicyRateLimiterInterface.php b/src/Factory/RateLimiter/PasswordPolicyRateLimiterInterface.php new file mode 100644 index 00000000..5466a396 --- /dev/null +++ b/src/Factory/RateLimiter/PasswordPolicyRateLimiterInterface.php @@ -0,0 +1,10 @@ +limit($userName, Rate::perMinute($config->getRateLimit())); - } catch (LimitExceeded $exception) { - throw oxNew(UserException::class, 'OXPS_PASSWORDPOLICY_RATELIMIT_EXCEEDED'); + if($config->getRateLimitingNeeded()) { + $driverName = $config->getSelectedDriver(); + $rateLimiter = (new PasswordPolicyRateLimiterFactory())->getRateLimiter($driverName)->getLimiter(); + + try { + $rateLimiter->limit($userName, Rate::perMinute($config->getRateLimit())); + } catch (LimitExceeded $exception) { + throw oxNew(UserException::class, 'OXPS_PASSWORDPOLICY_RATELIMIT_EXCEEDED'); + } } parent::login($userName, $password, $setSessionCookie); } From e881415e5d693b155b232eab7377bae30ce651ad Mon Sep 17 00:00:00 2001 From: moritz Date: Wed, 5 May 2021 16:15:45 +0200 Subject: [PATCH 059/199] added missing var type --- src/Validators/PasswordPolicyValidatorsCollector.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Validators/PasswordPolicyValidatorsCollector.php b/src/Validators/PasswordPolicyValidatorsCollector.php index f8875f8b..7a9243f5 100644 --- a/src/Validators/PasswordPolicyValidatorsCollector.php +++ b/src/Validators/PasswordPolicyValidatorsCollector.php @@ -8,7 +8,7 @@ class PasswordPolicyValidatorsCollector implements PasswordPolicyValidationInter /** * @var PasswordPolicyValidationInterface[] */ - private $validators; + private array $validators; /** * PasswordPolicyValidatorsCollector constructor. */ From c0801ec98100b578a0a09bcd728b23e75b76b249 Mon Sep 17 00:00:00 2001 From: moritz Date: Wed, 5 May 2021 16:16:09 +0200 Subject: [PATCH 060/199] fixed phpdocs --- src/Validators/PasswordPolicyValidationInterface.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Validators/PasswordPolicyValidationInterface.php b/src/Validators/PasswordPolicyValidationInterface.php index 0b0a838b..1b212103 100644 --- a/src/Validators/PasswordPolicyValidationInterface.php +++ b/src/Validators/PasswordPolicyValidationInterface.php @@ -12,7 +12,7 @@ interface PasswordPolicyValidationInterface /** * @param string $sUsername * @param string $sPassword - * return true|string + * @return true|string true if validated successfully, on error a string will be returned */ public function validate(string $sUsername, string $sPassword); } From 97ec7719f66b84ea0376fc4349ecd63a18ae404f Mon Sep 17 00:00:00 2001 From: moritz Date: Wed, 5 May 2021 16:16:54 +0200 Subject: [PATCH 061/199] now logs when enzoic throws an error (not visible for user) --- src/Validators/PasswordPolicyDataBreach.php | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Validators/PasswordPolicyDataBreach.php b/src/Validators/PasswordPolicyDataBreach.php index f7e915b9..80849410 100644 --- a/src/Validators/PasswordPolicyDataBreach.php +++ b/src/Validators/PasswordPolicyDataBreach.php @@ -5,23 +5,32 @@ use OxidProfessionalServices\PasswordPolicy\Api\PasswordCheck; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyConfig; +use Psr\Log\LoggerInterface; class PasswordPolicyDataBreach implements PasswordPolicyValidationInterface { private PasswordPolicyConfig $config; private PasswordCheck $passwordCheck; + private LoggerInterface $logger; - public function __construct(PasswordPolicyConfig $config, PasswordCheck $passwordCheck) + public function __construct(PasswordPolicyConfig $config, PasswordCheck $passwordCheck, LoggerInterface $logger) { + $this->logger = $logger; $this->config = $config; $this->passwordCheck = $passwordCheck; } public function validate(string $sUsername, string $sPassword) { - if ($this->config->getAPINeeded() && ($this->passwordCheck->isPasswordKnown($sPassword) || $this->passwordCheck->isCredentialsKnown($sUsername, $sPassword))) { - return 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_PASSWORD_KNOWN'; + try { + if ($this->config->getAPINeeded() && $this->passwordCheck->isPasswordKnown($sUsername, $sPassword)) { + return 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_PASSWORD_KNOWN'; + } + } catch (\RuntimeException $exception) { + $errorClass = get_class($exception); + $code = $exception->getCode(); + $this->logger->warning("Enzoic API Error: $errorClass Code: $code"); } return true; } From 91b009da0419a6fa83c064533b286ef0d71ba049 Mon Sep 17 00:00:00 2001 From: moritz Date: Thu, 6 May 2021 09:44:04 +0200 Subject: [PATCH 062/199] changed return type to RateLimiter Interface --- src/Factory/RateLimiter/PasswordPolicyAPCu.php | 3 ++- src/Factory/RateLimiter/PasswordPolicyMemcached.php | 8 +++++--- .../RateLimiter/PasswordPolicyRateLimiterInterface.php | 4 +++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Factory/RateLimiter/PasswordPolicyAPCu.php b/src/Factory/RateLimiter/PasswordPolicyAPCu.php index d1ccfd91..ef79dffa 100644 --- a/src/Factory/RateLimiter/PasswordPolicyAPCu.php +++ b/src/Factory/RateLimiter/PasswordPolicyAPCu.php @@ -5,11 +5,12 @@ use RateLimit\ApcuRateLimiter; +use RateLimit\RateLimiter; class PasswordPolicyAPCu implements PasswordPolicyRateLimiterInterface { - public function getLimiter(): ApcuRateLimiter + public function getLimiter(): RateLimiter { return new ApcuRateLimiter(); } diff --git a/src/Factory/RateLimiter/PasswordPolicyMemcached.php b/src/Factory/RateLimiter/PasswordPolicyMemcached.php index b1460b8c..050e48a6 100644 --- a/src/Factory/RateLimiter/PasswordPolicyMemcached.php +++ b/src/Factory/RateLimiter/PasswordPolicyMemcached.php @@ -6,22 +6,24 @@ use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyConfig; use RateLimit\MemcachedRateLimiter; +use RateLimit\RateLimiter; class PasswordPolicyMemcached implements PasswordPolicyRateLimiterInterface { + private PasswordPolicyConfig $config; private string $host; private int $port; /** * PasswordPolicyMemcached constructor. */ - public function __construct() + public function __construct(PasswordPolicyConfig $config) { - $config = new PasswordPolicyConfig(); + $this->config = $config; $this->host = $config->getMemcachedHost(); $this->port = $config->getMemcachedPort(); } - public function getLimiter(): MemcachedRateLimiter + public function getLimiter(): RateLimiter { $memcached = new \Memcached(); $memcached->addServer($this->host,$this->port); diff --git a/src/Factory/RateLimiter/PasswordPolicyRateLimiterInterface.php b/src/Factory/RateLimiter/PasswordPolicyRateLimiterInterface.php index 5466a396..2bf4770b 100644 --- a/src/Factory/RateLimiter/PasswordPolicyRateLimiterInterface.php +++ b/src/Factory/RateLimiter/PasswordPolicyRateLimiterInterface.php @@ -4,7 +4,9 @@ namespace OxidProfessionalServices\PasswordPolicy\Factory\RateLimiter; +use RateLimit\RateLimiter; + interface PasswordPolicyRateLimiterInterface { - public function getLimiter(); + public function getLimiter() : RateLimiter; } \ No newline at end of file From 7dd97f4ffcb76f8c668a51b4b57c963a2ff34c6f Mon Sep 17 00:00:00 2001 From: moritz Date: Thu, 6 May 2021 09:44:35 +0200 Subject: [PATCH 063/199] now gets the selected ratelimiter from container --- .../PasswordPolicyRateLimiterFactory.php | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Factory/PasswordPolicyRateLimiterFactory.php b/src/Factory/PasswordPolicyRateLimiterFactory.php index f6525d37..2636626e 100644 --- a/src/Factory/PasswordPolicyRateLimiterFactory.php +++ b/src/Factory/PasswordPolicyRateLimiterFactory.php @@ -2,29 +2,29 @@ namespace OxidProfessionalServices\PasswordPolicy\Factory; +use OxidEsales\EshopCommunity\Internal\Container\ContainerFactory; use OxidProfessionalServices\PasswordPolicy\Exception\LimiterNotFound; -use OxidProfessionalServices\PasswordPolicy\Factory\RateLimiter\PasswordPolicyMemcached; -use OxidProfessionalServices\PasswordPolicy\Factory\RateLimiter\PasswordPolicyAPCu; use OxidProfessionalServices\PasswordPolicy\Factory\RateLimiter\PasswordPolicyRateLimiterInterface; +use Psr\Container\NotFoundExceptionInterface; class PasswordPolicyRateLimiterFactory { - private array $versionMap = [ - "Memcached" => PasswordPolicyMemcached::class, - "APCu" => PasswordPolicyAPCu::class - ]; - /** - * @param string $version + * @param string $limiter * @return PasswordPolicyRateLimiterInterface * @throws LimiterNotFound */ - public function getRateLimiter(string $version): PasswordPolicyRateLimiterInterface + public function getRateLimiter(string $limiter): PasswordPolicyRateLimiterInterface { - if (!isset($this->versionMap[$version])) { + $class = 'OxidProfessionalServices\PasswordPolicy\Factory\RateLimiter\\PasswordPolicy' . $limiter; + $container = ContainerFactory::getInstance()->getContainer(); + try { + $driver = $container->get($class); + } + catch (NotFoundExceptionInterface $ex) + { throw new LimiterNotFound(); } - $driver = new $this->versionMap[$version](); return $driver; } } \ No newline at end of file From 69d67bae08dd1b687e5dd400acf3b24300ac794e Mon Sep 17 00:00:00 2001 From: moritz Date: Thu, 6 May 2021 12:55:49 +0200 Subject: [PATCH 064/199] added RateLimiter to services.yaml --- services.yaml | 5 +++++ src/Factory/PasswordPolicyRateLimiterFactory.php | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/services.yaml b/services.yaml index e3805dc8..edb93a35 100644 --- a/services.yaml +++ b/services.yaml @@ -30,5 +30,10 @@ services: OxidProfessionalServices\PasswordPolicy\Api\PasswordCheck: class: OxidProfessionalServices\PasswordPolicy\Api\PasswordCheck autowire: true; + PasswordPolicyAPCu: + class: OxidProfessionalServices\PasswordPolicy\Factory\RateLimiter\PasswordPolicyAPCu + PasswordPolicyMemcached: + class: OxidProfessionalServices\PasswordPolicy\Factory\RateLimiter\PasswordPolicyMemcached + autowire: true; DivineOmega\PasswordExposed\PasswordExposedChecker: class: DivineOmega\PasswordExposed\PasswordExposedChecker \ No newline at end of file diff --git a/src/Factory/PasswordPolicyRateLimiterFactory.php b/src/Factory/PasswordPolicyRateLimiterFactory.php index 2636626e..9501fcd7 100644 --- a/src/Factory/PasswordPolicyRateLimiterFactory.php +++ b/src/Factory/PasswordPolicyRateLimiterFactory.php @@ -16,7 +16,7 @@ class PasswordPolicyRateLimiterFactory */ public function getRateLimiter(string $limiter): PasswordPolicyRateLimiterInterface { - $class = 'OxidProfessionalServices\PasswordPolicy\Factory\RateLimiter\\PasswordPolicy' . $limiter; + $class = 'PasswordPolicy' . $limiter; $container = ContainerFactory::getInstance()->getContainer(); try { $driver = $container->get($class); From c5173a0ad7018fd9c502d3a5a9b275d082a863a0 Mon Sep 17 00:00:00 2001 From: moritz Date: Fri, 7 May 2021 09:20:44 +0200 Subject: [PATCH 065/199] removed redis and predis from drivers --- metadata.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metadata.php b/metadata.php index cba0d066..185a5a15 100644 --- a/metadata.php +++ b/metadata.php @@ -96,7 +96,7 @@ ['group' => 'passwordpolicy_api', 'name' => 'oxpspasswordpolicyEnzoicAPIKey', 'type' => 'str', 'value'=>''], ['group' => 'passwordpolicy_api', 'name' => 'oxpspasswordpolicyEnzoicSecretKey', 'type' => 'str', 'value'=>''], ['group' => 'passwordpolicy_ratelimiting', 'name' => 'oxpspasswordpolicyRateLimiting', 'type' => 'bool', 'value' => true], - ['group' => 'passwordpolicy_ratelimiting', 'name' => 'oxpspasswordpolicyRateLimitingDrivers', 'type' => 'select', 'value' => 'APCu', 'constraints' => 'Redis|Predis|Memcached|APCu'], + ['group' => 'passwordpolicy_ratelimiting', 'name' => 'oxpspasswordpolicyRateLimitingDrivers', 'type' => 'select', 'value' => 'APCu', 'constraints' => 'Memcached|APCu'], ['group' => 'passwordpolicy_ratelimiting', 'name' => 'oxpspasswordpolicyRateLimitingLimit', 'type' => 'num', 'value' => 60], ['group' => 'passwordpolicy_memcached', 'name' => 'oxpspasswordpolicyMemcachedHost', 'type' => 'str', 'value' => 'memcached'], ['group' => 'passwordpolicy_memcached', 'name' => 'oxpspasswordpolicyMemcachedPort', 'type' => 'num', 'value' => 11211], From b82783e846ed44080cc03268bb855797501c3659 Mon Sep 17 00:00:00 2001 From: moritz Date: Fri, 7 May 2021 21:46:25 +0200 Subject: [PATCH 066/199] now only deactivates enzoic --- src/Controller/Admin/PasswordPolicyModuleConfiguration.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Controller/Admin/PasswordPolicyModuleConfiguration.php b/src/Controller/Admin/PasswordPolicyModuleConfiguration.php index c12588e0..59d87e47 100644 --- a/src/Controller/Admin/PasswordPolicyModuleConfiguration.php +++ b/src/Controller/Admin/PasswordPolicyModuleConfiguration.php @@ -20,11 +20,8 @@ public function saveConfVars() try { $enzoicApiCon->checkPassword("Test"); } catch (\RuntimeException $ex) { - Registry::getUtilsView()->addErrorToDisplay("oxpspasswordpolicy_EnzoicError" . $ex->getCode()); - # reset API, Secret Key and deactivate Enzoic setting + Registry::getUtilsView()->addErrorToDisplay("OXPS_PASSWORDPOLICY_ENZOICERROR" . $ex->getCode()); # needs better solution - $_POST["confstrs"][PasswordPolicyConfig::SettingEnzoicAPIKey] = ""; - $_POST["confstrs"][PasswordPolicyConfig::SettingEnzoicSecretKey] = ""; $_POST["confbools"][PasswordPolicyConfig::SettingEnzoic] = "false"; } } From 7072b50ba36adeddf97748a71a78cd4fd88626a6 Mon Sep 17 00:00:00 2001 From: moritz Date: Fri, 7 May 2021 21:47:04 +0200 Subject: [PATCH 067/199] temporary solution for admin users added --- src/Model/PasswordPolicyUser.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Model/PasswordPolicyUser.php b/src/Model/PasswordPolicyUser.php index ab0d7a0e..cc0822ed 100644 --- a/src/Model/PasswordPolicyUser.php +++ b/src/Model/PasswordPolicyUser.php @@ -3,6 +3,7 @@ namespace OxidProfessionalServices\PasswordPolicy\Model; use OxidEsales\Eshop\Application\Controller\ForgotPasswordController; +use OxidEsales\Eshop\Core\Exception\ConnectionException; use OxidEsales\Eshop\Core\Exception\UserException; use OxidEsales\Eshop\Core\InputValidator; use OxidEsales\Eshop\Core\Registry; @@ -26,10 +27,14 @@ public function onLogin($userName, $password) { /** @var PasswordPolicyValidator $passValidator */ $passValidator = oxNew(InputValidator::class); - if (!isAdmin() && $this->isLoaded() && $err = $passValidator->validatePassword($userName, $password)) { + if ($this->isLoaded() && $err = $passValidator->validatePassword($userName, $password)) { $forgotPass = new ForgotPasswordController(); $forgotPass->forgotPassword(); $errorMessage = $err->getMessage() . ' ' . Registry::getLang()->translateString('REQUEST_PASSWORD_AFTERCLICK'); + if(isAdmin()) + { + throw oxNew(ConnectionException::class, $errorMessage); + } throw oxNew(UserException::class, $errorMessage); } parent::onLogin($userName, $password); @@ -51,8 +56,13 @@ public function login($userName, $password, $setSessionCookie = false) $rateLimiter = (new PasswordPolicyRateLimiterFactory())->getRateLimiter($driverName)->getLimiter(); try { - $rateLimiter->limit($userName, Rate::perMinute($config->getRateLimit())); + $rateLimiter->limit($userName, Rate::perDay($config->getRateLimit())); } catch (LimitExceeded $exception) { + if(isAdmin()) + { + throw oxNew(ConnectionException::class, 'OXPS_PASSWORDPOLICY_RATELIMIT_EXCEEDED'); + + } throw oxNew(UserException::class, 'OXPS_PASSWORDPOLICY_RATELIMIT_EXCEEDED'); } } From 6bc29fb3d2072f530e622c1c66264c3965fc9d57 Mon Sep 17 00:00:00 2001 From: moritz Date: Fri, 7 May 2021 21:47:38 +0200 Subject: [PATCH 068/199] added password check translations for admin --- views/admin/de/passwordpolicy_lang.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/views/admin/de/passwordpolicy_lang.php b/views/admin/de/passwordpolicy_lang.php index 9678fa09..853949f5 100644 --- a/views/admin/de/passwordpolicy_lang.php +++ b/views/admin/de/passwordpolicy_lang.php @@ -36,9 +36,11 @@ 'SHOP_MODULE_oxpspasswordpolicyEnzoic' => 'Enzoic', 'SHOP_MODULE_oxpspasswordpolicyEnzoicAPIKey' => 'Enzoic API Schlüssel', 'SHOP_MODULE_oxpspasswordpolicyEnzoicSecretKey' => 'Enzoic Geheimschlüssel', - 'oxpspasswordpolicy_EnzoicError401' => 'Ihr Enzoic API Key oder Secret Key ist nicht gültig. Sie sind nicht autorisiert.', - 'oxpspasswordpolicy_EnzoicError0' => 'Es gibt ein Problem beim Verbinden mit der Enzoic API. Bitte versuchen Sie es erneut.', - 'oxpspasswordpolicy_EnzoicError500' => 'Ein unerwarteter Fehler ist aufgetreten. Btte probieren Sie es später erneut.', + 'OXPS_PASSWORDPOLICY_ENZOICERROR401' => 'Ihr Enzoic API Key oder Secret Key ist nicht gültig. Sie sind nicht autorisiert.', + 'OXPS_PASSWORDPOLICY_ENZOICERRORError0' => 'Es gibt ein Problem beim Verbinden mit der Enzoic API. Bitte versuchen Sie es erneut.', + 'OXPS_PASSWORDPOLICY_ENZOICERROR500' => 'Ein unerwarteter Fehler ist aufgetreten. Btte probieren Sie es später erneut.', + 'OXPS_PASSWORDPOLICY_RATELIMIT_EXCEEDED' => 'Sie haben sich zu oft versucht einzuloggen. Bitte versuchen Sie es später erneut.', + 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_PASSWORD_KNOWN' => 'Das Passwort befindet sich bereits in einer gehackten Datenbank und ist somit unsicher.', 'SHOP_MODULE_GROUP_passwordpolicy_ratelimiting' => 'Rate Limiting Einstellungen', 'SHOP_MODULE_oxpspasswordpolicyRateLimiting' => 'Aktiv', 'SHOP_MODULE_oxpspasswordpolicyRateLimitingDrivers' => 'Treiber', From 5df469935a82ab5289c369aac01bf761fb969fe8 Mon Sep 17 00:00:00 2001 From: moritz Date: Fri, 7 May 2021 22:05:53 +0200 Subject: [PATCH 069/199] removed checks for admin and used config from container --- src/Model/PasswordPolicyUser.php | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/Model/PasswordPolicyUser.php b/src/Model/PasswordPolicyUser.php index cc0822ed..983a6fc1 100644 --- a/src/Model/PasswordPolicyUser.php +++ b/src/Model/PasswordPolicyUser.php @@ -3,10 +3,10 @@ namespace OxidProfessionalServices\PasswordPolicy\Model; use OxidEsales\Eshop\Application\Controller\ForgotPasswordController; -use OxidEsales\Eshop\Core\Exception\ConnectionException; use OxidEsales\Eshop\Core\Exception\UserException; use OxidEsales\Eshop\Core\InputValidator; use OxidEsales\Eshop\Core\Registry; +use OxidEsales\EshopCommunity\Internal\Container\ContainerFactory; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyConfig; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyValidator; use OxidProfessionalServices\PasswordPolicy\Exception\LimiterNotFound; @@ -31,10 +31,6 @@ public function onLogin($userName, $password) $forgotPass = new ForgotPasswordController(); $forgotPass->forgotPassword(); $errorMessage = $err->getMessage() . ' ' . Registry::getLang()->translateString('REQUEST_PASSWORD_AFTERCLICK'); - if(isAdmin()) - { - throw oxNew(ConnectionException::class, $errorMessage); - } throw oxNew(UserException::class, $errorMessage); } parent::onLogin($userName, $password); @@ -50,19 +46,15 @@ public function onLogin($userName, $password) */ public function login($userName, $password, $setSessionCookie = false) { - $config = new PasswordPolicyConfig(); - if($config->getRateLimitingNeeded()) { + $container = ContainerFactory::getInstance()->getContainer(); + $config = $container->get(PasswordPolicyConfig::class); + if ($config->getRateLimitingNeeded()) { $driverName = $config->getSelectedDriver(); $rateLimiter = (new PasswordPolicyRateLimiterFactory())->getRateLimiter($driverName)->getLimiter(); try { $rateLimiter->limit($userName, Rate::perDay($config->getRateLimit())); } catch (LimitExceeded $exception) { - if(isAdmin()) - { - throw oxNew(ConnectionException::class, 'OXPS_PASSWORDPOLICY_RATELIMIT_EXCEEDED'); - - } throw oxNew(UserException::class, 'OXPS_PASSWORDPOLICY_RATELIMIT_EXCEEDED'); } } From 4cc689aad3298e86adfe94b3312d3e758151011c Mon Sep 17 00:00:00 2001 From: moritz Date: Mon, 10 May 2021 19:17:05 +0200 Subject: [PATCH 070/199] added requirements for 2FA --- composer.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index ac435c9f..9d23b419 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,9 @@ "ext-json": "*", "enzoic/enzoic": "dev-master", "divineomega/password_exposed": "v3.2.0", - "nikolaposa/rate-limit": "dev-master" + "nikolaposa/rate-limit": "dev-master", + "spomky-labs/otphp": "v10.0.1", + "bacon/bacon-qr-code": "v2.0.3" }, "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.2", From 730c140b8cc9513a606f524695e9ed61900aeca2 Mon Sep 17 00:00:00 2001 From: moritz Date: Tue, 11 May 2021 15:22:53 +0200 Subject: [PATCH 071/199] login checks not available for admins --- src/Model/PasswordPolicyUser.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Model/PasswordPolicyUser.php b/src/Model/PasswordPolicyUser.php index 983a6fc1..7f7066c1 100644 --- a/src/Model/PasswordPolicyUser.php +++ b/src/Model/PasswordPolicyUser.php @@ -27,7 +27,7 @@ public function onLogin($userName, $password) { /** @var PasswordPolicyValidator $passValidator */ $passValidator = oxNew(InputValidator::class); - if ($this->isLoaded() && $err = $passValidator->validatePassword($userName, $password)) { + if (!isAdmin() && $this->isLoaded() && $err = $passValidator->validatePassword($userName, $password)) { $forgotPass = new ForgotPasswordController(); $forgotPass->forgotPassword(); $errorMessage = $err->getMessage() . ' ' . Registry::getLang()->translateString('REQUEST_PASSWORD_AFTERCLICK'); @@ -53,7 +53,7 @@ public function login($userName, $password, $setSessionCookie = false) $rateLimiter = (new PasswordPolicyRateLimiterFactory())->getRateLimiter($driverName)->getLimiter(); try { - $rateLimiter->limit($userName, Rate::perDay($config->getRateLimit())); + $rateLimiter->limit($userName, Rate::perMinute($config->getRateLimit())); } catch (LimitExceeded $exception) { throw oxNew(UserException::class, 'OXPS_PASSWORDPOLICY_RATELIMIT_EXCEEDED'); } From ab377b3881218e01f1fa939b18296e52f8d5ee29 Mon Sep 17 00:00:00 2001 From: moritz Date: Tue, 11 May 2021 15:23:28 +0200 Subject: [PATCH 072/199] added translation for frontend --- translations/de/passwordpolicy_lang.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/translations/de/passwordpolicy_lang.php b/translations/de/passwordpolicy_lang.php index 74155f67..d9c75ebc 100644 --- a/translations/de/passwordpolicy_lang.php +++ b/translations/de/passwordpolicy_lang.php @@ -53,5 +53,6 @@ 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESSPECIAL' => 'Das Passwort muss mindestens ein Sonderzeichen enthalten.', 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_WRONGTYPE' => 'Fehlerhafter Typ, bitte tragen Sie einen validen Wert ein. Bei weiteren Fragen wenden Sie sich an den Support.', 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_PASSWORD_KNOWN' => 'Das Passwort befindet sich bereits in einer gehackten Datenbank und ist somit unsicher.', - 'OXPS_PASSWORDPOLICY_RATELIMIT_EXCEEDED' => 'Sie haben sich zu oft versucht einzuloggen. Bitte versuchen Sie es später erneut.' + 'OXPS_PASSWORDPOLICY_RATELIMIT_EXCEEDED' => 'Sie haben sich zu oft versucht einzuloggen. Bitte versuchen Sie es später erneut.', + 'TWOFACTORAUTH' => '2FA Code' ); From 88f12b065cb8ff8ca549e79ae14985b5d4130457 Mon Sep 17 00:00:00 2001 From: moritz Date: Tue, 11 May 2021 15:24:01 +0200 Subject: [PATCH 073/199] added event --- metadata.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/metadata.php b/metadata.php index 185a5a15..f379eee8 100644 --- a/metadata.php +++ b/metadata.php @@ -101,5 +101,7 @@ ['group' => 'passwordpolicy_memcached', 'name' => 'oxpspasswordpolicyMemcachedHost', 'type' => 'str', 'value' => 'memcached'], ['group' => 'passwordpolicy_memcached', 'name' => 'oxpspasswordpolicyMemcachedPort', 'type' => 'num', 'value' => 11211], ], - 'events' => [], + 'events' => array( + 'onActivate' => 'OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyEvents::onActivate', + ), ]; From 5aa61f580f24be34749382d68ea24c9e3cf8a73d Mon Sep 17 00:00:00 2001 From: moritz Date: Tue, 11 May 2021 15:24:42 +0200 Subject: [PATCH 074/199] now adds new columns on activation for 2FA --- src/Core/PasswordPolicyEvents.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/Core/PasswordPolicyEvents.php diff --git a/src/Core/PasswordPolicyEvents.php b/src/Core/PasswordPolicyEvents.php new file mode 100644 index 00000000..a4503d48 --- /dev/null +++ b/src/Core/PasswordPolicyEvents.php @@ -0,0 +1,19 @@ +execute($query); + }catch (\Exception $exception) + { + } + + } +} \ No newline at end of file From fdf3590187841ba1b7fe6334085f3609c7ebf8a7 Mon Sep 17 00:00:00 2001 From: moritz Date: Tue, 11 May 2021 15:24:59 +0200 Subject: [PATCH 075/199] added 2fa code --- .../PasswordPolicyQrCodeRenderer.php | 21 ++++++++++ src/TwoFactorAuth/PasswordPolicyTOTP.php | 38 +++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 src/TwoFactorAuth/PasswordPolicyQrCodeRenderer.php create mode 100644 src/TwoFactorAuth/PasswordPolicyTOTP.php diff --git a/src/TwoFactorAuth/PasswordPolicyQrCodeRenderer.php b/src/TwoFactorAuth/PasswordPolicyQrCodeRenderer.php new file mode 100644 index 00000000..1b673901 --- /dev/null +++ b/src/TwoFactorAuth/PasswordPolicyQrCodeRenderer.php @@ -0,0 +1,21 @@ +writeString($dataUrl); + } + +} \ No newline at end of file diff --git a/src/TwoFactorAuth/PasswordPolicyTOTP.php b/src/TwoFactorAuth/PasswordPolicyTOTP.php new file mode 100644 index 00000000..7880c1ce --- /dev/null +++ b/src/TwoFactorAuth/PasswordPolicyTOTP.php @@ -0,0 +1,38 @@ +getSecret(); + Registry::getSession()->setVariable('otp_secret', $secret); + $label = Registry::getConfig()->getShopUrl(); + $otp->setLabel(str_replace('/','',preg_replace('#http://#','',$label))); + $dataUrl = $otp->getProvisioningUri(); + return $dataUrl; + } + + /** + * @param string $secret + * @param string $auth + * @return bool returns true if entered auth code is correct, false if not + */ + public function checkOtp(string $secret, string $auth): bool + { + $otp = TOTP::create($secret); + if($otp->now() == $auth) + { + return true; + } + return false; + } +} \ No newline at end of file From f5ae68ccd14ebccc6c47e2ea1204a81f6edb8dc8 Mon Sep 17 00:00:00 2001 From: moritz Date: Tue, 11 May 2021 18:59:32 +0200 Subject: [PATCH 076/199] added temporary code for 2FA --- services.yaml | 4 +++ src/Core/PasswordPolicyViewConfig.php | 11 ++++++++ src/Model/PasswordPolicyUser.php | 9 +++++++ .../PasswordPolicyQrCodeRenderer.php | 4 +++ src/TwoFactorAuth/PasswordPolicyTOTP.php | 7 ++--- src/Validators/PasswordPolicyOTP.php | 27 +++++++++++++++++++ translations/de/passwordpolicy_lang.php | 1 + 7 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 src/Validators/PasswordPolicyOTP.php diff --git a/services.yaml b/services.yaml index edb93a35..4d2eea2a 100644 --- a/services.yaml +++ b/services.yaml @@ -3,6 +3,7 @@ services: class: OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyValidatorsCollector arguments: $validators: + - '@OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyOTP' - '@OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyPasswordLength' - '@OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyUpperLowerCase' - '@OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyDigits' @@ -30,6 +31,9 @@ services: OxidProfessionalServices\PasswordPolicy\Api\PasswordCheck: class: OxidProfessionalServices\PasswordPolicy\Api\PasswordCheck autowire: true; + OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyOTP: + class: OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyOTP + autowire: true; PasswordPolicyAPCu: class: OxidProfessionalServices\PasswordPolicy\Factory\RateLimiter\PasswordPolicyAPCu PasswordPolicyMemcached: diff --git a/src/Core/PasswordPolicyViewConfig.php b/src/Core/PasswordPolicyViewConfig.php index ddbbcab4..9a60d8b6 100644 --- a/src/Core/PasswordPolicyViewConfig.php +++ b/src/Core/PasswordPolicyViewConfig.php @@ -26,6 +26,8 @@ namespace OxidProfessionalServices\PasswordPolicy\Core; use OxidEsales\Eshop\Core\Registry; +use OxidProfessionalServices\PasswordPolicy\TwoFactorAuth\PasswordPolicyQrCodeRenderer; +use OxidProfessionalServices\PasswordPolicy\TwoFactorAuth\PasswordPolicyTOTP; /** * Password policy config helpers used in controllers mostly @@ -52,4 +54,13 @@ public function getJsonPasswordPolicySettings(): string } return $res; } + + public function getTOTPQrCode() + { + $TOTP = new PasswordPolicyTOTP(); + $TOTPurl = $TOTP->getTotpQrUrl(); + $qrrenderer = new PasswordPolicyQrCodeRenderer(); + $qrcode = $qrrenderer->generateQrCode($TOTPurl); + return $qrcode; + } } diff --git a/src/Model/PasswordPolicyUser.php b/src/Model/PasswordPolicyUser.php index 7f7066c1..4f059409 100644 --- a/src/Model/PasswordPolicyUser.php +++ b/src/Model/PasswordPolicyUser.php @@ -60,4 +60,13 @@ public function login($userName, $password, $setSessionCookie = false) } parent::login($userName, $password, $setSessionCookie); } + + public function loadUserByUsername($userName) + { + $oDb = \OxidEsales\Eshop\Core\DatabaseProvider::getDb(); + $sQ = "select oxid from " . $this->getViewName() . " + where oxusername = '$userName'"; + $oxid = $oDb->getOne($sQ); + return $this->load($oxid); + } } diff --git a/src/TwoFactorAuth/PasswordPolicyQrCodeRenderer.php b/src/TwoFactorAuth/PasswordPolicyQrCodeRenderer.php index 1b673901..ca03490a 100644 --- a/src/TwoFactorAuth/PasswordPolicyQrCodeRenderer.php +++ b/src/TwoFactorAuth/PasswordPolicyQrCodeRenderer.php @@ -11,6 +11,10 @@ class PasswordPolicyQrCodeRenderer { + /** + * @param string $dataUrl + * @return string writes QR Code + */ public function generateQrCode(string $dataUrl) { $renderer = new ImageRenderer(new RendererStyle(200), new SvgImageBackEnd()); diff --git a/src/TwoFactorAuth/PasswordPolicyTOTP.php b/src/TwoFactorAuth/PasswordPolicyTOTP.php index 7880c1ce..5b45a05e 100644 --- a/src/TwoFactorAuth/PasswordPolicyTOTP.php +++ b/src/TwoFactorAuth/PasswordPolicyTOTP.php @@ -10,7 +10,7 @@ class PasswordPolicyTOTP /** * @return string */ - public function getTotpQrUrl(): string + public function getTOTPQrUrl(): string { $otp = TOTP::create(); $secret = $otp->getSecret(); @@ -22,11 +22,12 @@ public function getTotpQrUrl(): string } /** + * reads current OTP of the user from the DB and compares it with the entered OTP * @param string $secret * @param string $auth - * @return bool returns true if entered auth code is correct, false if not + * @return bool returns true if entered OTP is correct, false if not */ - public function checkOtp(string $secret, string $auth): bool + public function checkOTP(string $secret, string $auth): bool { $otp = TOTP::create($secret); if($otp->now() == $auth) diff --git a/src/Validators/PasswordPolicyOTP.php b/src/Validators/PasswordPolicyOTP.php new file mode 100644 index 00000000..86eaaf8b --- /dev/null +++ b/src/Validators/PasswordPolicyOTP.php @@ -0,0 +1,27 @@ +loadUserByUsername($sUsername); + $TOTP= new PasswordPolicyTOTP(); + $userOTP = $user->oxuser__oxotp->value?: Registry::getSession()->getVariable('otp_secret'); + $auth = (new Request)->getRequestEscapedParameter('lgn_auth'); + if(!$TOTP->checkOtp($userOTP,$auth)) + { + return 'OXPS_PASSWORDPOLICY_TOTP_ERROR_WRONGOTP'; + } + return true; + + } +} \ No newline at end of file diff --git a/translations/de/passwordpolicy_lang.php b/translations/de/passwordpolicy_lang.php index d9c75ebc..58377ac1 100644 --- a/translations/de/passwordpolicy_lang.php +++ b/translations/de/passwordpolicy_lang.php @@ -53,6 +53,7 @@ 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESSPECIAL' => 'Das Passwort muss mindestens ein Sonderzeichen enthalten.', 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_WRONGTYPE' => 'Fehlerhafter Typ, bitte tragen Sie einen validen Wert ein. Bei weiteren Fragen wenden Sie sich an den Support.', 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_PASSWORD_KNOWN' => 'Das Passwort befindet sich bereits in einer gehackten Datenbank und ist somit unsicher.', + 'OXPS_PASSWORDPOLICY_TOTP_ERROR_WRONGOTP' => 'Der eingebene Code für die Zwei-Faktor-Authentifizierung war falsch.', 'OXPS_PASSWORDPOLICY_RATELIMIT_EXCEEDED' => 'Sie haben sich zu oft versucht einzuloggen. Bitte versuchen Sie es später erneut.', 'TWOFACTORAUTH' => '2FA Code' ); From 29895e56ff8d311009a511560278801e5252b7e7 Mon Sep 17 00:00:00 2001 From: moritz Date: Mon, 17 May 2021 16:06:46 +0200 Subject: [PATCH 077/199] added 2FA registration --- metadata.php | 11 +++- services.yaml | 1 - src/Component/PasswordPolicyUserComponent.php | 30 +++++++++ src/Controller/PasswordPolicyTwoFactor.php | 66 +++++++++++++++++++ src/Core/PasswordPolicyViewConfig.php | 11 ---- src/TwoFactorAuth/PasswordPolicyTOTP.php | 4 +- src/js/otpField.js | 46 +++++++++++++ translations/de/passwordpolicy_lang.php | 5 +- views/tpl/twofactor.tpl | 29 ++++++++ 9 files changed, 185 insertions(+), 18 deletions(-) create mode 100644 src/Component/PasswordPolicyUserComponent.php create mode 100644 src/Controller/PasswordPolicyTwoFactor.php create mode 100644 src/js/otpField.js create mode 100644 views/tpl/twofactor.tpl diff --git a/metadata.php b/metadata.php index f379eee8..2b9305d5 100644 --- a/metadata.php +++ b/metadata.php @@ -25,11 +25,14 @@ * Metadata version */ +use OxidEsales\Eshop\Application\Component\UserComponent; use OxidEsales\Eshop\Application\Controller\AccountPasswordController; use OxidEsales\Eshop\Core\InputValidator; use OxidEsales\Eshop\Core\ViewConfig; use OxidEsales\Eshop\Application\Model\User; use OxidEsales\Eshop\Application\Controller\Admin\ModuleConfiguration; +use OxidProfessionalServices\PasswordPolicy\Component\PasswordPolicyUserComponent; +use OxidProfessionalServices\PasswordPolicy\Controller\PasswordPolicyTwoFactor; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyValidator; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyViewConfig; use OxidProfessionalServices\PasswordPolicy\Controller\PasswordPolicyAccountPasswordController; @@ -61,10 +64,14 @@ InputValidator::class => PasswordPolicyValidator::class, AccountPasswordController::class => PasswordPolicyAccountPasswordController::class, User::class => PasswordPolicyUser::class, - ModuleConfiguration::class => PasswordPolicyModuleConfiguration::class + ModuleConfiguration::class => PasswordPolicyModuleConfiguration::class, + UserComponent::class => PasswordPolicyUserComponent::class ], - 'controllers' => [], + 'controllers' => [ + 'twofactor' => PasswordPolicyTwoFactor::class + ], 'templates' => [ + 'twofactor.tpl' => 'oxps/passwordpolicy/views/tpl/twofactor.tpl', ], 'blocks' => [ [ diff --git a/services.yaml b/services.yaml index 4d2eea2a..550a6a1c 100644 --- a/services.yaml +++ b/services.yaml @@ -3,7 +3,6 @@ services: class: OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyValidatorsCollector arguments: $validators: - - '@OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyOTP' - '@OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyPasswordLength' - '@OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyUpperLowerCase' - '@OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyDigits' diff --git a/src/Component/PasswordPolicyUserComponent.php b/src/Component/PasswordPolicyUserComponent.php new file mode 100644 index 00000000..73015115 --- /dev/null +++ b/src/Component/PasswordPolicyUserComponent.php @@ -0,0 +1,30 @@ +getRequestEscapedParameter('2FA'); + $success = parent::createUser(); + if($twoFactor && $success) + { + Registry::getUtils()->redirect(Registry::getConfig()->getShopHomeUrl() . 'cl=twofactor&mode='. $this->mode . '&success='.urlencode($success)); + } + return $success; + + + } + + public function registerUser() + { + $this->mode = 'registration'; + return parent::registerUser(); + } + +} \ No newline at end of file diff --git a/src/Controller/PasswordPolicyTwoFactor.php b/src/Controller/PasswordPolicyTwoFactor.php new file mode 100644 index 00000000..be6079c6 --- /dev/null +++ b/src/Controller/PasswordPolicyTwoFactor.php @@ -0,0 +1,66 @@ +getRequestEscapedParameter('mode'); + $success = (new Request())->getRequestEscapedParameter('success'); + $this->addTplParam('mode',$mode); + $this->addTplParam('success', $success); + parent::render(); + return 'twofactor.tpl'; + } + + public function finalizeRegistration() + { + $OTP = (new Request())->getRequestEscapedParameter('otp'); + $mode = (new Request())->getRequestEscapedParameter('mode'); + $success = (new Request())->getRequestEscapedParameter('success'); + $secret = Registry::getSession()->getVariable('otp_secret'); + if($mode == 'registration') + { + $redirect = 'register?success=1'; + } + else + { + $redirect = urldecode($success); + } + $TOTP = new PasswordPolicyTOTP(); + $checkOTP = $TOTP->checkOTP($secret, $OTP); + if($checkOTP) + { + //finalize + $user = $this->getUser(); + $user->oxuser__oxotps = new Field($secret, Field::T_TEXT); + $user->save(); + return $redirect; + } + \OxidEsales\Eshop\Core\Registry::getUtilsView()->addErrorToDisplay( + 'OXPS_PASSWORDPOLICY_TOTP_ERROR_WRONGOTP', + false, + true + ); + + } + public function getTOTPQrCode() + { + $TOTP = new PasswordPolicyTOTP(); + $TOTPurl = $TOTP->getTotpQrUrl(); + $qrrenderer = new PasswordPolicyQrCodeRenderer(); + $qrcode = $qrrenderer->generateQrCode($TOTPurl); + return $qrcode; + } +} \ No newline at end of file diff --git a/src/Core/PasswordPolicyViewConfig.php b/src/Core/PasswordPolicyViewConfig.php index 9a60d8b6..ddbbcab4 100644 --- a/src/Core/PasswordPolicyViewConfig.php +++ b/src/Core/PasswordPolicyViewConfig.php @@ -26,8 +26,6 @@ namespace OxidProfessionalServices\PasswordPolicy\Core; use OxidEsales\Eshop\Core\Registry; -use OxidProfessionalServices\PasswordPolicy\TwoFactorAuth\PasswordPolicyQrCodeRenderer; -use OxidProfessionalServices\PasswordPolicy\TwoFactorAuth\PasswordPolicyTOTP; /** * Password policy config helpers used in controllers mostly @@ -54,13 +52,4 @@ public function getJsonPasswordPolicySettings(): string } return $res; } - - public function getTOTPQrCode() - { - $TOTP = new PasswordPolicyTOTP(); - $TOTPurl = $TOTP->getTotpQrUrl(); - $qrrenderer = new PasswordPolicyQrCodeRenderer(); - $qrcode = $qrrenderer->generateQrCode($TOTPurl); - return $qrcode; - } } diff --git a/src/TwoFactorAuth/PasswordPolicyTOTP.php b/src/TwoFactorAuth/PasswordPolicyTOTP.php index 5b45a05e..f3a68a5d 100644 --- a/src/TwoFactorAuth/PasswordPolicyTOTP.php +++ b/src/TwoFactorAuth/PasswordPolicyTOTP.php @@ -23,8 +23,8 @@ public function getTOTPQrUrl(): string /** * reads current OTP of the user from the DB and compares it with the entered OTP - * @param string $secret - * @param string $auth + * @param string $secret secret key of entered user + * @param string $auth entered OTP * @return bool returns true if entered OTP is correct, false if not */ public function checkOTP(string $secret, string $auth): bool diff --git a/src/js/otpField.js b/src/js/otpField.js new file mode 100644 index 00000000..e1b47541 --- /dev/null +++ b/src/js/otpField.js @@ -0,0 +1,46 @@ +const $otp_length = 6; + +const element = document.getElementById('OTPInput'); +for (let i = 0; i < $otp_length; i++) { + let inputField = document.createElement('input'); // Creates a new input element + inputField.className = "w-12 h-12 bg-gray-100 border-gray-200 outline-none focus:bg-gray-200 m-2 text-center rounded focus:border-blue-400 focus:shadow-outline"; + inputField.style.cssText = "color: transparent; text-shadow: 0 0 0 gray;"; + inputField.id = 'otp-field' + i; + inputField.maxLength = 1; + element.appendChild(inputField); +} + +const inputs = document.querySelectorAll('#OTPInput > *[id]'); +for (let i = 0; i < inputs.length; i++) { + inputs[i].addEventListener('keydown', function (event) { + if (event.key === "Backspace") { + inputs[i].value = ''; + if (i !== 0) { + inputs[i - 1].focus(); + } + } else if (event.key === "ArrowLeft" && i !== 0) { + inputs[i - 1].focus(); + } else if (event.key === "ArrowRight" && i !== inputs.length - 1) { + inputs[i + 1].focus(); + } + }); + inputs[i].addEventListener('input', function () { + this.value = this.value.replace(/[^0-9.]/g, '').replace(/(\..*?)\..*/g, '$1'); + if (i === inputs.length - 1 && inputs[i].value !== '') { + return true; + } else if (inputs[i].value !== '') { + inputs[i + 1].focus(); + } + }); + +} + +document.getElementById('accUserSaveTop').addEventListener("click", function () { + const inputs = document.querySelectorAll('#OTPInput > *[id]'); + let compiledOtp = ''; + for (let i = 0; i < inputs.length; i++) { + compiledOtp += inputs[i].value; + } + document.getElementById('otp').value = compiledOtp; + return true; +}); \ No newline at end of file diff --git a/translations/de/passwordpolicy_lang.php b/translations/de/passwordpolicy_lang.php index 58377ac1..9207d3b9 100644 --- a/translations/de/passwordpolicy_lang.php +++ b/translations/de/passwordpolicy_lang.php @@ -55,5 +55,6 @@ 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_PASSWORD_KNOWN' => 'Das Passwort befindet sich bereits in einer gehackten Datenbank und ist somit unsicher.', 'OXPS_PASSWORDPOLICY_TOTP_ERROR_WRONGOTP' => 'Der eingebene Code für die Zwei-Faktor-Authentifizierung war falsch.', 'OXPS_PASSWORDPOLICY_RATELIMIT_EXCEEDED' => 'Sie haben sich zu oft versucht einzuloggen. Bitte versuchen Sie es später erneut.', - 'TWOFACTORAUTH' => '2FA Code' -); + 'TWOFACTORAUTHCODE' => '2FA Code', + 'TWOFACTORAUTH' => '2FA aktivieren' + ); diff --git a/views/tpl/twofactor.tpl b/views/tpl/twofactor.tpl new file mode 100644 index 00000000..08725595 --- /dev/null +++ b/views/tpl/twofactor.tpl @@ -0,0 +1,29 @@ + + + +[{capture append="oxidBlock_content"}] +
+ +
+ [{$oView->getTOTPQrCode()}] +
+
+
+ +
+ +
+
+ [{oxscript include=$oViewConf->getModuleUrl('oxpspasswordpolicy','src/js/otpField.js')}] +[{/capture}] + + +[{include file="layout/page.tpl"}] + From 303bfd59a794ff35c26bf3649b62a8d548733627 Mon Sep 17 00:00:00 2001 From: moritz Date: Mon, 17 May 2021 19:58:16 +0200 Subject: [PATCH 078/199] renamed variables --- src/Component/PasswordPolicyUserComponent.php | 12 +++++----- src/Controller/PasswordPolicyTwoFactor.php | 24 ++++++++++++------- views/tpl/twofactor.tpl | 5 ++-- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/Component/PasswordPolicyUserComponent.php b/src/Component/PasswordPolicyUserComponent.php index 73015115..6960af88 100644 --- a/src/Component/PasswordPolicyUserComponent.php +++ b/src/Component/PasswordPolicyUserComponent.php @@ -7,23 +7,23 @@ class PasswordPolicyUserComponent extends PasswordPolicyUserComponent_parent { - private string $mode = 'checkout'; + private string $step = 'checkout'; public function createUser() { $twoFactor = (new Request)->getRequestEscapedParameter('2FA'); - $success = parent::createUser(); - if($twoFactor && $success) + $paymentActionLink = parent::createUser(); + if($twoFactor && $paymentActionLink) { - Registry::getUtils()->redirect(Registry::getConfig()->getShopHomeUrl() . 'cl=twofactor&mode='. $this->mode . '&success='.urlencode($success)); + Registry::getUtils()->redirect(Registry::getConfig()->getShopHomeUrl() . 'cl=twofactor&step='. $this->step . '&paymentActionLink='. urlencode($paymentActionLink)); } - return $success; + return $paymentActionLink; } public function registerUser() { - $this->mode = 'registration'; + $this->step = 'registration'; return parent::registerUser(); } diff --git a/src/Controller/PasswordPolicyTwoFactor.php b/src/Controller/PasswordPolicyTwoFactor.php index be6079c6..d6f83507 100644 --- a/src/Controller/PasswordPolicyTwoFactor.php +++ b/src/Controller/PasswordPolicyTwoFactor.php @@ -16,10 +16,10 @@ class PasswordPolicyTwoFactor extends FrontendController public function render() { - $mode = (new Request())->getRequestEscapedParameter('mode'); - $success = (new Request())->getRequestEscapedParameter('success'); - $this->addTplParam('mode',$mode); - $this->addTplParam('success', $success); + $step = (new Request())->getRequestEscapedParameter('step'); + $paymentActionLink = (new Request())->getRequestEscapedParameter('paymentActionLink'); + $this->addTplParam('step', $step); + $this->addTplParam('paymentActionLink', $paymentActionLink); parent::render(); return 'twofactor.tpl'; } @@ -27,16 +27,16 @@ public function render() public function finalizeRegistration() { $OTP = (new Request())->getRequestEscapedParameter('otp'); - $mode = (new Request())->getRequestEscapedParameter('mode'); - $success = (new Request())->getRequestEscapedParameter('success'); + $step = (new Request())->getRequestEscapedParameter('step'); + $paymentActionLink = (new Request())->getRequestEscapedParameter('paymentActionLink'); $secret = Registry::getSession()->getVariable('otp_secret'); - if($mode == 'registration') + if($step == 'registration') { $redirect = 'register?success=1'; } else { - $redirect = urldecode($success); + $redirect = urldecode($paymentActionLink); } $TOTP = new PasswordPolicyTOTP(); $checkOTP = $TOTP->checkOTP($secret, $OTP); @@ -63,4 +63,12 @@ public function getTOTPQrCode() $qrcode = $qrrenderer->generateQrCode($TOTPurl); return $qrcode; } + + public function getBreadCrumb() + { + $aPath['title'] = Registry::getLang()->translateString('TWOFACTORAUTH'); + $aPath['link'] = $this->getLink(); + $aPaths[] = $aPath; + return $aPaths; + } } \ No newline at end of file diff --git a/views/tpl/twofactor.tpl b/views/tpl/twofactor.tpl index 08725595..9e3cc8b2 100644 --- a/views/tpl/twofactor.tpl +++ b/views/tpl/twofactor.tpl @@ -2,14 +2,15 @@ [{capture append="oxidBlock_content"}] +

[{oxmultilang ident="TWOFACTORAUTH"}]

[{$oView->getTOTPQrCode()}] From 95843dd1b607e1635c1fb380ed7a37d7ed7003cd Mon Sep 17 00:00:00 2001 From: moritz Date: Mon, 17 May 2021 19:58:33 +0200 Subject: [PATCH 079/199] added black border --- src/js/otpField.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/otpField.js b/src/js/otpField.js index e1b47541..b33e33bf 100644 --- a/src/js/otpField.js +++ b/src/js/otpField.js @@ -3,7 +3,7 @@ const $otp_length = 6; const element = document.getElementById('OTPInput'); for (let i = 0; i < $otp_length; i++) { let inputField = document.createElement('input'); // Creates a new input element - inputField.className = "w-12 h-12 bg-gray-100 border-gray-200 outline-none focus:bg-gray-200 m-2 text-center rounded focus:border-blue-400 focus:shadow-outline"; + inputField.className = "border border-dark w-12 h-12 bg-gray-100 border-gray-50 outline-none focus:bg-gray-200 m-2 text-center rounded focus:border-blue-400 focus:shadow-outline"; inputField.style.cssText = "color: transparent; text-shadow: 0 0 0 gray;"; inputField.id = 'otp-field' + i; inputField.maxLength = 1; From 7a780e9d2053e99e7daa9bc6ef7788a0758a9af0 Mon Sep 17 00:00:00 2001 From: moritz Date: Mon, 17 May 2021 19:58:44 +0200 Subject: [PATCH 080/199] cleaned up code --- src/Model/PasswordPolicyUser.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/Model/PasswordPolicyUser.php b/src/Model/PasswordPolicyUser.php index 4f059409..7f7066c1 100644 --- a/src/Model/PasswordPolicyUser.php +++ b/src/Model/PasswordPolicyUser.php @@ -60,13 +60,4 @@ public function login($userName, $password, $setSessionCookie = false) } parent::login($userName, $password, $setSessionCookie); } - - public function loadUserByUsername($userName) - { - $oDb = \OxidEsales\Eshop\Core\DatabaseProvider::getDb(); - $sQ = "select oxid from " . $this->getViewName() . " - where oxusername = '$userName'"; - $oxid = $oDb->getOne($sQ); - return $this->load($oxid); - } } From ca82a05d4284c5b030c01185f3cb6e3500246359 Mon Sep 17 00:00:00 2001 From: moritz Date: Mon, 17 May 2021 19:59:10 +0200 Subject: [PATCH 081/199] used user as label and active shop name as issuer --- src/TwoFactorAuth/PasswordPolicyTOTP.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/TwoFactorAuth/PasswordPolicyTOTP.php b/src/TwoFactorAuth/PasswordPolicyTOTP.php index f3a68a5d..93553705 100644 --- a/src/TwoFactorAuth/PasswordPolicyTOTP.php +++ b/src/TwoFactorAuth/PasswordPolicyTOTP.php @@ -3,9 +3,10 @@ namespace OxidProfessionalServices\PasswordPolicy\TwoFactorAuth; use OTPHP\TOTP; +use OxidEsales\Eshop\Core\Base; use OxidEsales\Eshop\Core\Registry; -class PasswordPolicyTOTP +class PasswordPolicyTOTP extends Base { /** * @return string @@ -13,10 +14,11 @@ class PasswordPolicyTOTP public function getTOTPQrUrl(): string { $otp = TOTP::create(); + $user = $this->getUser(); $secret = $otp->getSecret(); Registry::getSession()->setVariable('otp_secret', $secret); - $label = Registry::getConfig()->getShopUrl(); - $otp->setLabel(str_replace('/','',preg_replace('#http://#','',$label))); + $otp->setLabel($user->oxuser__oxusername->value); + $otp->setIssuer(Registry::getConfig()->getActiveShop()->getFieldData('oxname')); $dataUrl = $otp->getProvisioningUri(); return $dataUrl; } From 376970785f672eea8292e9588f3b25ce19dfcccb Mon Sep 17 00:00:00 2001 From: moritz Date: Mon, 17 May 2021 19:59:25 +0200 Subject: [PATCH 082/199] added new translations --- translations/de/passwordpolicy_lang.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/translations/de/passwordpolicy_lang.php b/translations/de/passwordpolicy_lang.php index 9207d3b9..2fbb9e90 100644 --- a/translations/de/passwordpolicy_lang.php +++ b/translations/de/passwordpolicy_lang.php @@ -55,6 +55,8 @@ 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_PASSWORD_KNOWN' => 'Das Passwort befindet sich bereits in einer gehackten Datenbank und ist somit unsicher.', 'OXPS_PASSWORDPOLICY_TOTP_ERROR_WRONGOTP' => 'Der eingebene Code für die Zwei-Faktor-Authentifizierung war falsch.', 'OXPS_PASSWORDPOLICY_RATELIMIT_EXCEEDED' => 'Sie haben sich zu oft versucht einzuloggen. Bitte versuchen Sie es später erneut.', + 'TWOFACTORAUTH' => '2-Faktor-Authentifizierung Einrichtung', 'TWOFACTORAUTHCODE' => '2FA Code', - 'TWOFACTORAUTH' => '2FA aktivieren' + 'TWOFACTORAUTHCHECKBOX' => '2FA aktivieren', + 'MESSAGE_TWOFACTOR_HELP' => 'Die Zwei-Faktor-Authentifizierung bietet Ihrem Account eine zusätzliche Sicherheit. Durch aktivieren dieser Funktion, wird bei jedem Login nach einem zeitlich begrenzten einmaligen Passwort gefragt, welches Sie auf Ihrem Handy von einer App ablesen können.' ); From 1b84830d5a4cd71107b9b5f5e8d83f3a4c226026 Mon Sep 17 00:00:00 2001 From: moritz Date: Mon, 17 May 2021 19:59:57 +0200 Subject: [PATCH 083/199] added new template block for 2FA checkbox --- metadata.php | 6 ++++++ views/blocks/user_account.tpl | 11 +++++++++++ 2 files changed, 17 insertions(+) create mode 100644 views/blocks/user_account.tpl diff --git a/metadata.php b/metadata.php index 2b9305d5..43760ba5 100644 --- a/metadata.php +++ b/metadata.php @@ -88,7 +88,13 @@ 'template' => 'form/user_password.tpl', 'block' => 'user_account_password', 'file' => 'views/blocks/passwordpolicystrengthindicator.tpl', + ], + [ + 'template' => 'form/fieldset/user_account.tpl', + 'block' => 'user_account_newsletter', + 'file' => 'views/blocks/user_account.tpl', ] + ], 'settings' => [ ['group' => 'passwordpolicy', 'name' => 'oxpspasswordpolicyGoodPasswordLength', 'type' => 'num', 'value' => 12], diff --git a/views/blocks/user_account.tpl b/views/blocks/user_account.tpl new file mode 100644 index 00000000..0e376380 --- /dev/null +++ b/views/blocks/user_account.tpl @@ -0,0 +1,11 @@ +[{$smarty.block.parent}] +
+
+
+ +
+ [{oxmultilang ident="MESSAGE_TWOFACTOR_HELP"}] +
+
\ No newline at end of file From d618e86434eaf81c9ba71610d58f57e7300a7f8f Mon Sep 17 00:00:00 2001 From: moritz Date: Tue, 18 May 2021 14:18:15 +0200 Subject: [PATCH 084/199] changed name of register controller --- src/Component/PasswordPolicyUserComponent.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Component/PasswordPolicyUserComponent.php b/src/Component/PasswordPolicyUserComponent.php index 6960af88..6e55da9d 100644 --- a/src/Component/PasswordPolicyUserComponent.php +++ b/src/Component/PasswordPolicyUserComponent.php @@ -14,7 +14,7 @@ public function createUser() $paymentActionLink = parent::createUser(); if($twoFactor && $paymentActionLink) { - Registry::getUtils()->redirect(Registry::getConfig()->getShopHomeUrl() . 'cl=twofactor&step='. $this->step . '&paymentActionLink='. urlencode($paymentActionLink)); + Registry::getUtils()->redirect(Registry::getConfig()->getShopHomeUrl() . 'cl=twofactorregister&step='. $this->step . '&paymentActionLink='. urlencode($paymentActionLink)); } return $paymentActionLink; From 81c6bd60b2aca5d9f70062a61d8236a9443e573f Mon Sep 17 00:00:00 2001 From: moritz Date: Tue, 18 May 2021 14:18:47 +0200 Subject: [PATCH 085/199] now using DI and renamed --- ...hp => PasswordPolicyTwoFactorRegister.php} | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) rename src/Controller/{PasswordPolicyTwoFactor.php => PasswordPolicyTwoFactorRegister.php} (68%) diff --git a/src/Controller/PasswordPolicyTwoFactor.php b/src/Controller/PasswordPolicyTwoFactorRegister.php similarity index 68% rename from src/Controller/PasswordPolicyTwoFactor.php rename to src/Controller/PasswordPolicyTwoFactorRegister.php index d6f83507..84441c9d 100644 --- a/src/Controller/PasswordPolicyTwoFactor.php +++ b/src/Controller/PasswordPolicyTwoFactorRegister.php @@ -8,12 +8,22 @@ use OxidEsales\Eshop\Core\Registry; use OxidEsales\Eshop\Core\Request; use OxidEsales\EshopCommunity\Core\Field; +use OxidEsales\EshopCommunity\Internal\Container\ContainerFactory; use OxidProfessionalServices\PasswordPolicy\TwoFactorAuth\PasswordPolicyQrCodeRenderer; use OxidProfessionalServices\PasswordPolicy\TwoFactorAuth\PasswordPolicyTOTP; -class PasswordPolicyTwoFactor extends FrontendController +class PasswordPolicyTwoFactorRegister extends FrontendController { + private PasswordPolicyTOTP $TOTP; + private PasswordPolicyQrCodeRenderer $qrCodeRenderer; + public function __construct() + { + $container = ContainerFactory::getInstance()->getContainer(); + $this->TOTP = $container->get(PasswordPolicyTOTP::class); + $this->qrCodeRenderer = $container->get(PasswordPolicyQrCodeRenderer::class); + } + public function render() { $step = (new Request())->getRequestEscapedParameter('step'); @@ -21,7 +31,7 @@ public function render() $this->addTplParam('step', $step); $this->addTplParam('paymentActionLink', $paymentActionLink); parent::render(); - return 'twofactor.tpl'; + return 'twofactorregister.tpl'; } public function finalizeRegistration() @@ -38,17 +48,16 @@ public function finalizeRegistration() { $redirect = urldecode($paymentActionLink); } - $TOTP = new PasswordPolicyTOTP(); - $checkOTP = $TOTP->checkOTP($secret, $OTP); + $checkOTP = $this->TOTP->checkOTP($secret, $OTP); if($checkOTP) { //finalize $user = $this->getUser(); - $user->oxuser__oxotps = new Field($secret, Field::T_TEXT); + $user->oxuser__oxtotpsecret = new Field($secret, Field::T_TEXT); $user->save(); return $redirect; } - \OxidEsales\Eshop\Core\Registry::getUtilsView()->addErrorToDisplay( + Registry::getUtilsView()->addErrorToDisplay( 'OXPS_PASSWORDPOLICY_TOTP_ERROR_WRONGOTP', false, true @@ -57,10 +66,8 @@ public function finalizeRegistration() } public function getTOTPQrCode() { - $TOTP = new PasswordPolicyTOTP(); - $TOTPurl = $TOTP->getTotpQrUrl(); - $qrrenderer = new PasswordPolicyQrCodeRenderer(); - $qrcode = $qrrenderer->generateQrCode($TOTPurl); + $TOTPurl = $this->TOTP->getTotpQrUrl(); + $qrcode = $this->qrCodeRenderer->generateQrCode($TOTPurl); return $qrcode; } From 9780288cd5a9b084dec233f9b7812224abee198f Mon Sep 17 00:00:00 2001 From: moritz Date: Tue, 18 May 2021 14:19:21 +0200 Subject: [PATCH 086/199] removed validator --- src/Validators/PasswordPolicyOTP.php | 27 --------------------------- 1 file changed, 27 deletions(-) delete mode 100644 src/Validators/PasswordPolicyOTP.php diff --git a/src/Validators/PasswordPolicyOTP.php b/src/Validators/PasswordPolicyOTP.php deleted file mode 100644 index 86eaaf8b..00000000 --- a/src/Validators/PasswordPolicyOTP.php +++ /dev/null @@ -1,27 +0,0 @@ -loadUserByUsername($sUsername); - $TOTP= new PasswordPolicyTOTP(); - $userOTP = $user->oxuser__oxotp->value?: Registry::getSession()->getVariable('otp_secret'); - $auth = (new Request)->getRequestEscapedParameter('lgn_auth'); - if(!$TOTP->checkOtp($userOTP,$auth)) - { - return 'OXPS_PASSWORDPOLICY_TOTP_ERROR_WRONGOTP'; - } - return true; - - } -} \ No newline at end of file From af7c7ce66a3f96fcc6f5664cb456c19505864e34 Mon Sep 17 00:00:00 2001 From: moritz Date: Tue, 18 May 2021 14:19:39 +0200 Subject: [PATCH 087/199] renamed database entry --- src/Core/PasswordPolicyEvents.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/PasswordPolicyEvents.php b/src/Core/PasswordPolicyEvents.php index a4503d48..c63786d6 100644 --- a/src/Core/PasswordPolicyEvents.php +++ b/src/Core/PasswordPolicyEvents.php @@ -8,7 +8,7 @@ class PasswordPolicyEvents { public static function onActivate() { - $query = "ALTER TABLE oxuser ADD OXOTP varchar(255) NOT NULL;"; + $query = "ALTER TABLE oxuser ADD OXTOTPSECRET varchar(255) NOT NULL;"; try { \OxidEsales\Eshop\Core\DatabaseProvider::getDb()->execute($query); }catch (\Exception $exception) From fff5308454db9efc022d2c8a39cc586b01ab4498 Mon Sep 17 00:00:00 2001 From: moritz Date: Tue, 18 May 2021 14:20:37 +0200 Subject: [PATCH 088/199] added POC 2FA Login --- metadata.php | 9 +++-- services.yaml | 9 ++--- .../PasswordPolicyTwoFactorLogin.php | 35 +++++++++++++++++++ src/Model/PasswordPolicyUser.php | 9 +++++ views/tpl/twofactorlogin.tpl | 21 +++++++++++ 5 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 src/Controller/PasswordPolicyTwoFactorLogin.php create mode 100644 views/tpl/twofactorlogin.tpl diff --git a/metadata.php b/metadata.php index 43760ba5..ded466a3 100644 --- a/metadata.php +++ b/metadata.php @@ -32,7 +32,8 @@ use OxidEsales\Eshop\Application\Model\User; use OxidEsales\Eshop\Application\Controller\Admin\ModuleConfiguration; use OxidProfessionalServices\PasswordPolicy\Component\PasswordPolicyUserComponent; -use OxidProfessionalServices\PasswordPolicy\Controller\PasswordPolicyTwoFactor; +use OxidProfessionalServices\PasswordPolicy\Controller\PasswordPolicyTwoFactorRegister; +use OxidProfessionalServices\PasswordPolicy\Controller\PasswordPolicyTwoFactorLogin; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyValidator; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyViewConfig; use OxidProfessionalServices\PasswordPolicy\Controller\PasswordPolicyAccountPasswordController; @@ -68,10 +69,12 @@ UserComponent::class => PasswordPolicyUserComponent::class ], 'controllers' => [ - 'twofactor' => PasswordPolicyTwoFactor::class + 'twofactorregister' => PasswordPolicyTwoFactorRegister::class, + 'twofactorlogin' => PasswordPolicyTwoFactorLogin::class ], 'templates' => [ - 'twofactor.tpl' => 'oxps/passwordpolicy/views/tpl/twofactor.tpl', + 'twofactorregister.tpl' => 'oxps/passwordpolicy/views/tpl/twofactorregister.tpl', + 'twofactorlogin.tpl' => 'oxps/passwordpolicy/views/tpl/twofactorlogin.tpl' ], 'blocks' => [ [ diff --git a/services.yaml b/services.yaml index 550a6a1c..21548bcb 100644 --- a/services.yaml +++ b/services.yaml @@ -30,13 +30,14 @@ services: OxidProfessionalServices\PasswordPolicy\Api\PasswordCheck: class: OxidProfessionalServices\PasswordPolicy\Api\PasswordCheck autowire: true; - OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyOTP: - class: OxidProfessionalServices\PasswordPolicy\Validators\PasswordPolicyOTP - autowire: true; PasswordPolicyAPCu: class: OxidProfessionalServices\PasswordPolicy\Factory\RateLimiter\PasswordPolicyAPCu PasswordPolicyMemcached: class: OxidProfessionalServices\PasswordPolicy\Factory\RateLimiter\PasswordPolicyMemcached autowire: true; DivineOmega\PasswordExposed\PasswordExposedChecker: - class: DivineOmega\PasswordExposed\PasswordExposedChecker \ No newline at end of file + class: DivineOmega\PasswordExposed\PasswordExposedChecker + OxidProfessionalServices\PasswordPolicy\TwoFactorAuth\PasswordPolicyQrCodeRenderer: + class: OxidProfessionalServices\PasswordPolicy\TwoFactorAuth\PasswordPolicyQrCodeRenderer + OxidProfessionalServices\PasswordPolicy\TwoFactorAuth\PasswordPolicyTOTP: + class: OxidProfessionalServices\PasswordPolicy\TwoFactorAuth\PasswordPolicyTOTP \ No newline at end of file diff --git a/src/Controller/PasswordPolicyTwoFactorLogin.php b/src/Controller/PasswordPolicyTwoFactorLogin.php new file mode 100644 index 00000000..d97e23c4 --- /dev/null +++ b/src/Controller/PasswordPolicyTwoFactorLogin.php @@ -0,0 +1,35 @@ +getRequestEscapedParameter('otp'); + $usr = Registry::getSession()->getVariable('usr2'); + $user = oxNew(User::class); + $user->load($usr); + $secret = $user->oxuser__oxtotpsecret->value; + $TOTP = new PasswordPolicyTOTP(); + $checkOTP = $TOTP->checkOTP($secret, $otp); + if($checkOTP) + { + Registry::getSession()->setVariable('usr', $usr); + } + } +} \ No newline at end of file diff --git a/src/Model/PasswordPolicyUser.php b/src/Model/PasswordPolicyUser.php index 7f7066c1..f99d993d 100644 --- a/src/Model/PasswordPolicyUser.php +++ b/src/Model/PasswordPolicyUser.php @@ -59,5 +59,14 @@ public function login($userName, $password, $setSessionCookie = false) } } parent::login($userName, $password, $setSessionCookie); + if(!isAdmin()) + { + $sessionuser = Registry::getSession()->getVariable('usr'); + Registry::getSession()->setVariable('usr', ""); + Registry::getUtilsServer()->deleteUserCookie(); + Registry::getSession()->setVariable('usr2', $sessionuser); + Registry::getUtils()->redirect(Registry::getConfig()->getShopHomeUrl() . 'cl=twofactorlogin'); + } + } } diff --git a/views/tpl/twofactorlogin.tpl b/views/tpl/twofactorlogin.tpl new file mode 100644 index 00000000..db3b9b34 --- /dev/null +++ b/views/tpl/twofactorlogin.tpl @@ -0,0 +1,21 @@ + +[{capture append="oxidBlock_content"}] + + +
+
+ +
+ +
+ + [{oxscript include=$oViewConf->getModuleUrl('oxpspasswordpolicy','src/js/otpField.js')}] +[{/capture}] + + +[{include file="layout/page.tpl"}] \ No newline at end of file From c0e997fe0674833e2d43d46ab7f325f10b8eb762 Mon Sep 17 00:00:00 2001 From: moritz Date: Tue, 18 May 2021 14:20:49 +0200 Subject: [PATCH 089/199] renamed register template --- .../{twofactor.tpl => twofactorregister.tpl} | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) rename views/tpl/{twofactor.tpl => twofactorregister.tpl} (66%) diff --git a/views/tpl/twofactor.tpl b/views/tpl/twofactorregister.tpl similarity index 66% rename from views/tpl/twofactor.tpl rename to views/tpl/twofactorregister.tpl index 9e3cc8b2..e55b655b 100644 --- a/views/tpl/twofactor.tpl +++ b/views/tpl/twofactorregister.tpl @@ -1,14 +1,15 @@ -[{capture append="oxidBlock_content"}] -

[{oxmultilang ident="TWOFACTORAUTH"}]

+[{capture append="oxidBlock_pageBody"}] +
+
@@ -19,12 +20,19 @@
- + +
+
+ [{oxscript include=$oViewConf->getModuleUrl('oxpspasswordpolicy','src/js/otpField.js')}] [{/capture}] +[{include file="layout/base.tpl"}] -[{include file="layout/page.tpl"}] - +[{include file="layout/footer.tpl"}] From b54fdf0bcf393be44e88e816c76bd0feb31ac976 Mon Sep 17 00:00:00 2001 From: moritz Date: Tue, 18 May 2021 19:02:30 +0200 Subject: [PATCH 090/199] cleaned up login --- src/Controller/PasswordPolicyTwoFactorLogin.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Controller/PasswordPolicyTwoFactorLogin.php b/src/Controller/PasswordPolicyTwoFactorLogin.php index d97e23c4..634776d0 100644 --- a/src/Controller/PasswordPolicyTwoFactorLogin.php +++ b/src/Controller/PasswordPolicyTwoFactorLogin.php @@ -21,7 +21,7 @@ public function render() public function finalizeLogin() { $otp = (new Request())->getRequestEscapedParameter('otp'); - $usr = Registry::getSession()->getVariable('usr2'); + $usr = Registry::getSession()->getVariable('tmpusr'); $user = oxNew(User::class); $user->load($usr); $secret = $user->oxuser__oxtotpsecret->value; @@ -29,7 +29,16 @@ public function finalizeLogin() $checkOTP = $TOTP->checkOTP($secret, $otp); if($checkOTP) { + Registry::getSession()->deleteVariable('tmpusr'); Registry::getSession()->setVariable('usr', $usr); } } + + public function getBreadCrumb() + { + $aPath['title'] = Registry::getLang()->translateString('TWOFACTORAUTHLOGIN'); + $aPath['link'] = $this->getLink(); + $aPaths[] = $aPath; + return $aPaths; + } } \ No newline at end of file From 82fd468213a795409edae6a9195acb8b224159ae Mon Sep 17 00:00:00 2001 From: moritz Date: Tue, 18 May 2021 19:03:06 +0200 Subject: [PATCH 091/199] added session clean up (delete otp_secret after successfull registration) --- src/Controller/PasswordPolicyTwoFactorRegister.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Controller/PasswordPolicyTwoFactorRegister.php b/src/Controller/PasswordPolicyTwoFactorRegister.php index 84441c9d..71692a02 100644 --- a/src/Controller/PasswordPolicyTwoFactorRegister.php +++ b/src/Controller/PasswordPolicyTwoFactorRegister.php @@ -55,6 +55,8 @@ public function finalizeRegistration() $user = $this->getUser(); $user->oxuser__oxtotpsecret = new Field($secret, Field::T_TEXT); $user->save(); + //cleans up session for next registration + Registry::getSession()->deleteVariable('otp_secret'); return $redirect; } Registry::getUtilsView()->addErrorToDisplay( From 5a32bfbdce9ea9d2e6dbb3633e0e2d73fbd977bf Mon Sep 17 00:00:00 2001 From: moritz Date: Tue, 18 May 2021 19:03:34 +0200 Subject: [PATCH 092/199] now deletes usr session --- src/Model/PasswordPolicyUser.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Model/PasswordPolicyUser.php b/src/Model/PasswordPolicyUser.php index f99d993d..4dbf49a4 100644 --- a/src/Model/PasswordPolicyUser.php +++ b/src/Model/PasswordPolicyUser.php @@ -62,9 +62,8 @@ public function login($userName, $password, $setSessionCookie = false) if(!isAdmin()) { $sessionuser = Registry::getSession()->getVariable('usr'); - Registry::getSession()->setVariable('usr', ""); - Registry::getUtilsServer()->deleteUserCookie(); - Registry::getSession()->setVariable('usr2', $sessionuser); + Registry::getSession()->deleteVariable('usr'); + Registry::getSession()->setVariable('tmpusr', $sessionuser); Registry::getUtils()->redirect(Registry::getConfig()->getShopHomeUrl() . 'cl=twofactorlogin'); } From 1ee684d8296949bccda647f95671d194c4ed60f9 Mon Sep 17 00:00:00 2001 From: moritz Date: Tue, 18 May 2021 19:04:11 +0200 Subject: [PATCH 093/199] added functionality so that qr doesnt get created again when user enters wrong otp --- src/TwoFactorAuth/PasswordPolicyTOTP.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/TwoFactorAuth/PasswordPolicyTOTP.php b/src/TwoFactorAuth/PasswordPolicyTOTP.php index 93553705..b5ac2997 100644 --- a/src/TwoFactorAuth/PasswordPolicyTOTP.php +++ b/src/TwoFactorAuth/PasswordPolicyTOTP.php @@ -13,9 +13,10 @@ class PasswordPolicyTOTP extends Base */ public function getTOTPQrUrl(): string { - $otp = TOTP::create(); - $user = $this->getUser(); + $sessionsecret = Registry::getSession()->getVariable('otp_secret'); + $otp = TOTP::create($sessionsecret); $secret = $otp->getSecret(); + $user = $this->getUser(); Registry::getSession()->setVariable('otp_secret', $secret); $otp->setLabel($user->oxuser__oxusername->value); $otp->setIssuer(Registry::getConfig()->getActiveShop()->getFieldData('oxname')); From 5cf0d619533f7b5f5f95476d61727b96be7b476e Mon Sep 17 00:00:00 2001 From: moritz Date: Tue, 18 May 2021 19:04:52 +0200 Subject: [PATCH 094/199] new translations --- translations/de/passwordpolicy_lang.php | 1 + views/tpl/twofactorlogin.tpl | 4 +++- views/tpl/twofactorregister.tpl | 18 +++++------------- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/translations/de/passwordpolicy_lang.php b/translations/de/passwordpolicy_lang.php index 2fbb9e90..574ec662 100644 --- a/translations/de/passwordpolicy_lang.php +++ b/translations/de/passwordpolicy_lang.php @@ -56,6 +56,7 @@ 'OXPS_PASSWORDPOLICY_TOTP_ERROR_WRONGOTP' => 'Der eingebene Code für die Zwei-Faktor-Authentifizierung war falsch.', 'OXPS_PASSWORDPOLICY_RATELIMIT_EXCEEDED' => 'Sie haben sich zu oft versucht einzuloggen. Bitte versuchen Sie es später erneut.', 'TWOFACTORAUTH' => '2-Faktor-Authentifizierung Einrichtung', + 'TWOFACTORAUTHLOGIN' => '2-Faktor-Authentifizierung', 'TWOFACTORAUTHCODE' => '2FA Code', 'TWOFACTORAUTHCHECKBOX' => '2FA aktivieren', 'MESSAGE_TWOFACTOR_HELP' => 'Die Zwei-Faktor-Authentifizierung bietet Ihrem Account eine zusätzliche Sicherheit. Durch aktivieren dieser Funktion, wird bei jedem Login nach einem zeitlich begrenzten einmaligen Passwort gefragt, welches Sie auf Ihrem Handy von einer App ablesen können.' diff --git a/views/tpl/twofactorlogin.tpl b/views/tpl/twofactorlogin.tpl index db3b9b34..980e091f 100644 --- a/views/tpl/twofactorlogin.tpl +++ b/views/tpl/twofactorlogin.tpl @@ -1,5 +1,7 @@ [{capture append="oxidBlock_content"}] +

[{oxmultilang ident="TWOFACTORAUTHLOGIN"}]

+
- +
[{oxscript include=$oViewConf->getModuleUrl('oxpspasswordpolicy','src/js/otpField.js')}] diff --git a/views/tpl/twofactorregister.tpl b/views/tpl/twofactorregister.tpl index e55b655b..690afdb0 100644 --- a/views/tpl/twofactorregister.tpl +++ b/views/tpl/twofactorregister.tpl @@ -1,9 +1,8 @@ -[{capture append="oxidBlock_pageBody"}] -
- +[{capture append="oxidBlock_content"}] +

[{oxmultilang ident="TWOFACTORAUTH"}]

- - +
-
- [{oxscript include=$oViewConf->getModuleUrl('oxpspasswordpolicy','src/js/otpField.js')}] [{/capture}] -[{include file="layout/base.tpl"}] -[{include file="layout/footer.tpl"}] +[{include file="layout/page.tpl"}] + From acb204f981bd1b4bcb9ed21ac04e61d130f8454f Mon Sep 17 00:00:00 2001 From: moritz Date: Tue, 18 May 2021 19:05:14 +0200 Subject: [PATCH 095/199] now autofocus the first text field --- src/js/otpField.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/js/otpField.js b/src/js/otpField.js index b33e33bf..0c9962ed 100644 --- a/src/js/otpField.js +++ b/src/js/otpField.js @@ -3,7 +3,7 @@ const $otp_length = 6; const element = document.getElementById('OTPInput'); for (let i = 0; i < $otp_length; i++) { let inputField = document.createElement('input'); // Creates a new input element - inputField.className = "border border-dark w-12 h-12 bg-gray-100 border-gray-50 outline-none focus:bg-gray-200 m-2 text-center rounded focus:border-blue-400 focus:shadow-outline"; + inputField.className = "border border-dark w-12 h-12 bg-gray-100 border-gray-50 outline-none focus:bg-gray-200 m-2 text-center rounded focus:border-blue-200 focus:shadow-outline"; inputField.style.cssText = "color: transparent; text-shadow: 0 0 0 gray;"; inputField.id = 'otp-field' + i; inputField.maxLength = 1; @@ -11,6 +11,7 @@ for (let i = 0; i < $otp_length; i++) { } const inputs = document.querySelectorAll('#OTPInput > *[id]'); +inputs[0].focus(); for (let i = 0; i < inputs.length; i++) { inputs[i].addEventListener('keydown', function (event) { if (event.key === "Backspace") { From 7862b3b3506e57e4ad8488b81fa4c9cfce80cab5 Mon Sep 17 00:00:00 2001 From: moritz Date: Thu, 20 May 2021 12:43:22 +0200 Subject: [PATCH 096/199] changed get function name --- src/Api/PasswordCheck.php | 4 ++-- src/Core/PasswordPolicyConfig.php | 14 ++++++++---- src/Core/PasswordPolicyViewConfig.php | 24 +++++++++++++++------ src/Model/PasswordPolicyUser.php | 5 +++-- src/Validators/PasswordPolicyDataBreach.php | 2 +- 5 files changed, 34 insertions(+), 15 deletions(-) diff --git a/src/Api/PasswordCheck.php b/src/Api/PasswordCheck.php index 97d0378b..ab816ac1 100644 --- a/src/Api/PasswordCheck.php +++ b/src/Api/PasswordCheck.php @@ -35,9 +35,9 @@ public function __construct(PasswordPolicyConfig $config, PasswordExposedChecker */ public function isPasswordKnown(string $username, string $password): bool { - if ($this->config->getHaveIBeenPwnedNeeded() && $this->haveIBeenPwned->passwordExposed($password) == "exposed") { + if ($this->config->isHaveIBeenPwnedNeeded() && $this->haveIBeenPwned->passwordExposed($password) == "exposed") { return true; - } elseif ($this->config->getEnzoicNeeded() && ($this->enzoicApiCon->checkPassword($password) !== null || $this->enzoicApiCon->checkCredentials($username, $password))) { + } elseif ($this->config->isEnzoicNeeded() && ($this->enzoicApiCon->checkPassword($password) !== null || $this->enzoicApiCon->checkCredentials($username, $password))) { return true; } return false; diff --git a/src/Core/PasswordPolicyConfig.php b/src/Core/PasswordPolicyConfig.php index a11ad11d..d32b68f9 100644 --- a/src/Core/PasswordPolicyConfig.php +++ b/src/Core/PasswordPolicyConfig.php @@ -49,6 +49,7 @@ class PasswordPolicyConfig public const SettingRateLimiting = self::SettingsPrefix . 'RateLimiting'; public const SettingMemcachedHost = self::SettingsPrefix . 'MemcachedHost'; public const SettingMemcachedPort = self::SettingsPrefix . 'MemcachedPort'; + public const SettingTOTP = self::SettingsPrefix . 'TOTP'; public function getMinPasswordLength(): int { @@ -103,22 +104,22 @@ public function getSecretKey(): string return (string) Registry::getConfig()->getConfigParam(self::SettingEnzoicSecretKey); } - public function getAPINeeded(): bool + public function isAPINeeded(): bool { return $this->isConfigParam(self::SettingAPI); } - public function getEnzoicNeeded(): bool + public function isEnzoicNeeded(): bool { return $this->isConfigParam(self::SettingEnzoic); } - public function getHaveIBeenPwnedNeeded(): bool + public function isHaveIBeenPwnedNeeded(): bool { return $this->isConfigParam(self::SettingHaveIBeenPwned); } - public function getRateLimitingNeeded(): bool + public function isRateLimitingNeeded(): bool { return $this->isConfigParam(self::SettingRateLimiting); } @@ -142,6 +143,11 @@ public function getMemcachedPort(): int { return (int) Registry::getConfig()->getConfigParam(self::SettingMemcachedPort, 11211); } + + public function isTOTPNeeded(): bool + { + return $this->isConfigParam(self::SettingTOTP); + } private function isConfigParam(string $name): bool { return (bool) Registry::getConfig()->getConfigParam($name, true); diff --git a/src/Core/PasswordPolicyViewConfig.php b/src/Core/PasswordPolicyViewConfig.php index ddbbcab4..2d19f54f 100644 --- a/src/Core/PasswordPolicyViewConfig.php +++ b/src/Core/PasswordPolicyViewConfig.php @@ -32,19 +32,26 @@ */ class PasswordPolicyViewConfig extends PasswordPolicyViewConfig_parent { + private PasswordPolicyConfig $config; + + + public function __construct() + { + $this->config = Registry::get(PasswordPolicyConfig::class); + } + /** * @throws \Exception */ public function getJsonPasswordPolicySettings(): string { - $config = Registry::get(PasswordPolicyConfig::class); $array = []; - $array['goodPasswordLength'] = $config->getGoodPasswordLength(); + $array['goodPasswordLength'] = $this->config->getGoodPasswordLength(); $array['minPasswordLength'] = $this->getPasswordLength(); - $array['digits'] = $config->getPasswordNeedDigits(); - $array['special'] = $config->getPasswordNeedSpecialCharacter(); - $array['lowercase'] = $config->getPasswordNeedLowerCase(); - $array['uppercase'] = $config->getPasswordNeedUpperCase(); + $array['digits'] = $this->config->getPasswordNeedDigits(); + $array['special'] = $this->config->getPasswordNeedSpecialCharacter(); + $array['lowercase'] = $this->config->getPasswordNeedLowerCase(); + $array['uppercase'] = $this->config->getPasswordNeedUpperCase(); $res = json_encode($array, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE); if ($res === false) { $error = json_last_error_msg(); @@ -52,4 +59,9 @@ public function getJsonPasswordPolicySettings(): string } return $res; } + + public function isTOTPNeeded(): bool + { + return $this->config->isTOTPNeeded(); + } } diff --git a/src/Model/PasswordPolicyUser.php b/src/Model/PasswordPolicyUser.php index 4dbf49a4..e1d6bc03 100644 --- a/src/Model/PasswordPolicyUser.php +++ b/src/Model/PasswordPolicyUser.php @@ -48,7 +48,7 @@ public function login($userName, $password, $setSessionCookie = false) { $container = ContainerFactory::getInstance()->getContainer(); $config = $container->get(PasswordPolicyConfig::class); - if ($config->getRateLimitingNeeded()) { + if ($config->isRateLimitingNeeded()) { $driverName = $config->getSelectedDriver(); $rateLimiter = (new PasswordPolicyRateLimiterFactory())->getRateLimiter($driverName)->getLimiter(); @@ -63,8 +63,9 @@ public function login($userName, $password, $setSessionCookie = false) { $sessionuser = Registry::getSession()->getVariable('usr'); Registry::getSession()->deleteVariable('usr'); + Registry::getUtilsServer()->deleteUserCookie(); Registry::getSession()->setVariable('tmpusr', $sessionuser); - Registry::getUtils()->redirect(Registry::getConfig()->getShopHomeUrl() . 'cl=twofactorlogin'); + Registry::getUtils()->redirect(Registry::getConfig()->getShopHomeUrl() . 'cl=twofactorlogin&setsessioncookie=' . $setSessionCookie); } } diff --git a/src/Validators/PasswordPolicyDataBreach.php b/src/Validators/PasswordPolicyDataBreach.php index 80849410..8653b375 100644 --- a/src/Validators/PasswordPolicyDataBreach.php +++ b/src/Validators/PasswordPolicyDataBreach.php @@ -24,7 +24,7 @@ public function __construct(PasswordPolicyConfig $config, PasswordCheck $passwor public function validate(string $sUsername, string $sPassword) { try { - if ($this->config->getAPINeeded() && $this->passwordCheck->isPasswordKnown($sUsername, $sPassword)) { + if ($this->config->isAPINeeded() && $this->passwordCheck->isPasswordKnown($sUsername, $sPassword)) { return 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_PASSWORD_KNOWN'; } } catch (\RuntimeException $exception) { From ea8847b4b412b26fd60cc58050cc5fdf0e26d62d Mon Sep 17 00:00:00 2001 From: moritz Date: Thu, 20 May 2021 12:44:02 +0200 Subject: [PATCH 097/199] moved finalizeLogin/Registration from login controller to UserComponent --- src/Component/PasswordPolicyUserComponent.php | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/src/Component/PasswordPolicyUserComponent.php b/src/Component/PasswordPolicyUserComponent.php index 6e55da9d..38c3345e 100644 --- a/src/Component/PasswordPolicyUserComponent.php +++ b/src/Component/PasswordPolicyUserComponent.php @@ -2,12 +2,17 @@ namespace OxidProfessionalServices\PasswordPolicy\Component; +use OxidEsales\Eshop\Application\Model\User; use OxidEsales\Eshop\Core\Registry; use OxidEsales\Eshop\Core\Request; +use OxidEsales\EshopCommunity\Core\Field; +use OxidProfessionalServices\PasswordPolicy\TwoFactorAuth\PasswordPolicyTOTP; class PasswordPolicyUserComponent extends PasswordPolicyUserComponent_parent { + const USER_COOKIE_SALT = 'user_cookie_salt'; private string $step = 'checkout'; + public function createUser() { $twoFactor = (new Request)->getRequestEscapedParameter('2FA'); @@ -26,5 +31,69 @@ public function registerUser() $this->step = 'registration'; return parent::registerUser(); } + public function finalizeRegistration() + { + $OTP = (new Request())->getRequestEscapedParameter('otp'); + $step = (new Request())->getRequestEscapedParameter('step'); + $paymentActionLink = (new Request())->getRequestEscapedParameter('paymentActionLink'); + $secret = Registry::getSession()->getVariable('otp_secret'); + $redirect = urldecode($paymentActionLink); + if($step == 'registration') + { + $redirect = 'register?success=1'; + } + + $checkOTP = $this->TOTP->checkOTP($secret, $OTP); + if($checkOTP) + { + //finalize + $user = $this->getUser(); + $user->oxuser__oxtotpsecret = new Field($secret, Field::T_TEXT); + $user->save(); + //cleans up session for next registration + Registry::getSession()->deleteVariable('otp_secret'); + return $redirect; + } + Registry::getUtilsView()->addErrorToDisplay( + 'OXPS_PASSWORDPOLICY_TOTP_ERROR_WRONGOTP', + false, + true + ); + } + public function finalizeLogin() + { + $config = Registry::getConfig(); + $otp = (new Request())->getRequestEscapedParameter('otp'); + $sessioncookie = (new Request())->getRequestEscapedParameter('setsessioncookie'); + $usr = Registry::getSession()->getVariable('tmpusr'); + $user = oxNew(User::class); + $user->load($usr); + $secret = $user->oxuser__oxtotpsecret->value; + $TOTP = new PasswordPolicyTOTP(); + $checkOTP = $TOTP->checkOTP($secret, $otp); + if($checkOTP) + { + Registry::getSession()->deleteVariable('tmpusr'); + Registry::getSession()->setVariable('usr', $usr); + $this->setLoginStatus(USER_LOGIN_SUCCESS); + // in case user wants to stay logged in, setsessioncookie again + if ($sessioncookie && $config->getConfigParam('blShowRememberMe')) { + Registry::getUtilsServer()->setUserCookie( + $user->oxuser__oxusername->value, + $user->oxuser__oxpassword->value, + $config->getShopId(), + 31536000, + static::USER_COOKIE_SALT + ); + } + $user->set(null); + $this->_afterLogin($user); + } + Registry::getUtilsView()->addErrorToDisplay( + 'OXPS_PASSWORDPOLICY_TOTP_ERROR_WRONGOTP', + false, + true + ); + } } \ No newline at end of file From 1a0bf2717ba6ff8e3eb0ee1d2db1c87273eac664 Mon Sep 17 00:00:00 2001 From: moritz Date: Thu, 20 May 2021 12:45:46 +0200 Subject: [PATCH 098/199] cleaned up code --- .../PasswordPolicyTwoFactorLogin.php | 23 ++++--------------- .../PasswordPolicyTwoFactorRegister.php | 6 +++-- 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/src/Controller/PasswordPolicyTwoFactorLogin.php b/src/Controller/PasswordPolicyTwoFactorLogin.php index 634776d0..b4fcdbe6 100644 --- a/src/Controller/PasswordPolicyTwoFactorLogin.php +++ b/src/Controller/PasswordPolicyTwoFactorLogin.php @@ -5,10 +5,8 @@ use OxidEsales\Eshop\Application\Controller\FrontendController; -use OxidEsales\Eshop\Application\Model\User; use OxidEsales\Eshop\Core\Registry; use OxidEsales\Eshop\Core\Request; -use OxidProfessionalServices\PasswordPolicy\TwoFactorAuth\PasswordPolicyTOTP; class PasswordPolicyTwoFactorLogin extends FrontendController { @@ -18,25 +16,12 @@ public function render() return 'twofactorlogin.tpl'; } - public function finalizeLogin() - { - $otp = (new Request())->getRequestEscapedParameter('otp'); - $usr = Registry::getSession()->getVariable('tmpusr'); - $user = oxNew(User::class); - $user->load($usr); - $secret = $user->oxuser__oxtotpsecret->value; - $TOTP = new PasswordPolicyTOTP(); - $checkOTP = $TOTP->checkOTP($secret, $otp); - if($checkOTP) - { - Registry::getSession()->deleteVariable('tmpusr'); - Registry::getSession()->setVariable('usr', $usr); - } - } - public function getBreadCrumb() { - $aPath['title'] = Registry::getLang()->translateString('TWOFACTORAUTHLOGIN'); + $aPaths = []; + $aPath = []; + $iBaseLanguage = Registry::getLang()->getBaseLanguage(); + $aPath['title'] = Registry::getLang()->translateString('TWOFACTORLOGIN', $iBaseLanguage, false); $aPath['link'] = $this->getLink(); $aPaths[] = $aPath; return $aPaths; diff --git a/src/Controller/PasswordPolicyTwoFactorRegister.php b/src/Controller/PasswordPolicyTwoFactorRegister.php index 71692a02..f2650513 100644 --- a/src/Controller/PasswordPolicyTwoFactorRegister.php +++ b/src/Controller/PasswordPolicyTwoFactorRegister.php @@ -7,7 +7,6 @@ use OxidEsales\Eshop\Application\Controller\FrontendController; use OxidEsales\Eshop\Core\Registry; use OxidEsales\Eshop\Core\Request; -use OxidEsales\EshopCommunity\Core\Field; use OxidEsales\EshopCommunity\Internal\Container\ContainerFactory; use OxidProfessionalServices\PasswordPolicy\TwoFactorAuth\PasswordPolicyQrCodeRenderer; use OxidProfessionalServices\PasswordPolicy\TwoFactorAuth\PasswordPolicyTOTP; @@ -75,7 +74,10 @@ public function getTOTPQrCode() public function getBreadCrumb() { - $aPath['title'] = Registry::getLang()->translateString('TWOFACTORAUTH'); + $aPaths = []; + $aPath = []; + $iBaseLanguage = Registry::getLang()->getBaseLanguage(); + $aPath['title'] = Registry::getLang()->translateString('TWOFACTORAUTH', $iBaseLanguage, false); $aPath['link'] = $this->getLink(); $aPaths[] = $aPath; return $aPaths; From 2307d520bef8e352a02322e53968b364c73e07a3 Mon Sep 17 00:00:00 2001 From: moritz Date: Thu, 20 May 2021 12:46:13 +0200 Subject: [PATCH 099/199] added save setting cookie --- .../PasswordPolicyTwoFactorLogin.php | 2 ++ .../PasswordPolicyTwoFactorRegister.php | 32 ------------------- views/tpl/twofactorlogin.tpl | 1 + 3 files changed, 3 insertions(+), 32 deletions(-) diff --git a/src/Controller/PasswordPolicyTwoFactorLogin.php b/src/Controller/PasswordPolicyTwoFactorLogin.php index b4fcdbe6..a804001e 100644 --- a/src/Controller/PasswordPolicyTwoFactorLogin.php +++ b/src/Controller/PasswordPolicyTwoFactorLogin.php @@ -13,6 +13,8 @@ class PasswordPolicyTwoFactorLogin extends FrontendController public function render() { parent::render(); + $setsessioncookie = (new Request())->getRequestEscapedParameter('setsessioncookie'); + $this->addTplParam('setsessioncookie', $setsessioncookie); return 'twofactorlogin.tpl'; } diff --git a/src/Controller/PasswordPolicyTwoFactorRegister.php b/src/Controller/PasswordPolicyTwoFactorRegister.php index f2650513..51f37067 100644 --- a/src/Controller/PasswordPolicyTwoFactorRegister.php +++ b/src/Controller/PasswordPolicyTwoFactorRegister.php @@ -33,38 +33,6 @@ public function render() return 'twofactorregister.tpl'; } - public function finalizeRegistration() - { - $OTP = (new Request())->getRequestEscapedParameter('otp'); - $step = (new Request())->getRequestEscapedParameter('step'); - $paymentActionLink = (new Request())->getRequestEscapedParameter('paymentActionLink'); - $secret = Registry::getSession()->getVariable('otp_secret'); - if($step == 'registration') - { - $redirect = 'register?success=1'; - } - else - { - $redirect = urldecode($paymentActionLink); - } - $checkOTP = $this->TOTP->checkOTP($secret, $OTP); - if($checkOTP) - { - //finalize - $user = $this->getUser(); - $user->oxuser__oxtotpsecret = new Field($secret, Field::T_TEXT); - $user->save(); - //cleans up session for next registration - Registry::getSession()->deleteVariable('otp_secret'); - return $redirect; - } - Registry::getUtilsView()->addErrorToDisplay( - 'OXPS_PASSWORDPOLICY_TOTP_ERROR_WRONGOTP', - false, - true - ); - - } public function getTOTPQrCode() { $TOTPurl = $this->TOTP->getTotpQrUrl(); diff --git a/views/tpl/twofactorlogin.tpl b/views/tpl/twofactorlogin.tpl index 980e091f..eb170d38 100644 --- a/views/tpl/twofactorlogin.tpl +++ b/views/tpl/twofactorlogin.tpl @@ -8,6 +8,7 @@ [{$oViewConf->getNavFormParams()}] +
From 69b4f8c8d017e62d58d1935a99bb8b980e623c45 Mon Sep 17 00:00:00 2001 From: moritz Date: Thu, 20 May 2021 12:47:11 +0200 Subject: [PATCH 100/199] added 2FA account setting --- metadata.php | 13 ++++++- src/Controller/PasswordPolicyAccountTOTP.php | 41 ++++++++++++++++++++ views/admin/de/passwordpolicy_lang.php | 2 + views/blocks/account_menu.tpl | 6 +++ views/tpl/twofactoraccount.tpl | 8 ++++ 5 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 src/Controller/PasswordPolicyAccountTOTP.php create mode 100644 views/blocks/account_menu.tpl create mode 100644 views/tpl/twofactoraccount.tpl diff --git a/metadata.php b/metadata.php index ded466a3..8289c72f 100644 --- a/metadata.php +++ b/metadata.php @@ -32,6 +32,7 @@ use OxidEsales\Eshop\Application\Model\User; use OxidEsales\Eshop\Application\Controller\Admin\ModuleConfiguration; use OxidProfessionalServices\PasswordPolicy\Component\PasswordPolicyUserComponent; +use OxidProfessionalServices\PasswordPolicy\Controller\PasswordPolicyAccountTOTP; use OxidProfessionalServices\PasswordPolicy\Controller\PasswordPolicyTwoFactorRegister; use OxidProfessionalServices\PasswordPolicy\Controller\PasswordPolicyTwoFactorLogin; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyValidator; @@ -70,11 +71,13 @@ ], 'controllers' => [ 'twofactorregister' => PasswordPolicyTwoFactorRegister::class, - 'twofactorlogin' => PasswordPolicyTwoFactorLogin::class + 'twofactorlogin' => PasswordPolicyTwoFactorLogin::class, + 'twofactoraccount' => PasswordPolicyAccountTOTP::class ], 'templates' => [ 'twofactorregister.tpl' => 'oxps/passwordpolicy/views/tpl/twofactorregister.tpl', - 'twofactorlogin.tpl' => 'oxps/passwordpolicy/views/tpl/twofactorlogin.tpl' + 'twofactorlogin.tpl' => 'oxps/passwordpolicy/views/tpl/twofactorlogin.tpl', + 'twofactoraccount.tpl' => 'oxps/passwordpolicy/views/tpl/twofactoraccount.tpl' ], 'blocks' => [ [ @@ -96,6 +99,11 @@ 'template' => 'form/fieldset/user_account.tpl', 'block' => 'user_account_newsletter', 'file' => 'views/blocks/user_account.tpl', + ], + [ + 'template' => 'page/account/inc/account_menu.tpl', + 'block' => 'account_menu', + 'file' => 'views/blocks/account_menu.tpl', ] ], @@ -116,6 +124,7 @@ ['group' => 'passwordpolicy_ratelimiting', 'name' => 'oxpspasswordpolicyRateLimitingLimit', 'type' => 'num', 'value' => 60], ['group' => 'passwordpolicy_memcached', 'name' => 'oxpspasswordpolicyMemcachedHost', 'type' => 'str', 'value' => 'memcached'], ['group' => 'passwordpolicy_memcached', 'name' => 'oxpspasswordpolicyMemcachedPort', 'type' => 'num', 'value' => 11211], + ['group' => 'passwordpolicy_twofactor', 'name' => 'oxpspasswordpolicyTOTP', 'type' => 'bool', 'value' => false], ], 'events' => array( 'onActivate' => 'OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyEvents::onActivate', diff --git a/src/Controller/PasswordPolicyAccountTOTP.php b/src/Controller/PasswordPolicyAccountTOTP.php new file mode 100644 index 00000000..70021ee7 --- /dev/null +++ b/src/Controller/PasswordPolicyAccountTOTP.php @@ -0,0 +1,41 @@ +getUser(); + if (!$oUser) { + return $this->_sThisLoginTemplate; + } + + return 'twofactoraccount.tpl'; +} + public function getBreadCrumb() + { + $aPaths = []; + $aPath = []; + $oUtils = Registry::getUtilsUrl(); + $iBaseLanguage = Registry::getLang()->getBaseLanguage(); + $sSelfLink = $this->getViewConfig()->getSelfLink(); + + $aPath['title'] = Registry::getLang()->translateString('MY_ACCOUNT', $iBaseLanguage, false); + $aPath['link'] = Registry::getSeoEncoder()->getStaticUrl($sSelfLink . 'cl=account'); + $aPaths[] = $aPath; + + $aPath['title'] = Registry::getLang()->translateString('TWOFACTORAUTHLOGIN', $iBaseLanguage, false); + $aPath['link'] = $oUtils->cleanUrl($this->getLink(), ['fnc']); + $aPaths[] = $aPath; + + return $aPaths; + } + +} \ No newline at end of file diff --git a/views/admin/de/passwordpolicy_lang.php b/views/admin/de/passwordpolicy_lang.php index 853949f5..0273026e 100644 --- a/views/admin/de/passwordpolicy_lang.php +++ b/views/admin/de/passwordpolicy_lang.php @@ -52,5 +52,7 @@ 'SHOP_MODULE_GROUP_passwordpolicy_memcached' => 'Memcached Einstellungen', 'SHOP_MODULE_oxpspasswordpolicyMemcachedHost' => 'Host', 'SHOP_MODULE_oxpspasswordpolicyMemcachedPort' => 'Port', + 'SHOP_MODULE_GROUP_passwordpolicy_twofactor' => '2FA Authentifizierung', + 'SHOP_MODULE_oxpspasswordpolicyTOTP' => '2FA aktivieren' ); diff --git a/views/blocks/account_menu.tpl b/views/blocks/account_menu.tpl new file mode 100644 index 00000000..64928215 --- /dev/null +++ b/views/blocks/account_menu.tpl @@ -0,0 +1,6 @@ +[{$smarty.block.parent}] +[{if $oViewConf->isTOTPNeeded()}] + +[{/if}] \ No newline at end of file diff --git a/views/tpl/twofactoraccount.tpl b/views/tpl/twofactoraccount.tpl new file mode 100644 index 00000000..3f52c26b --- /dev/null +++ b/views/tpl/twofactoraccount.tpl @@ -0,0 +1,8 @@ +[{capture append="oxidBlock_content"}] + [{assign var="template_title" value="TWOFACTORLOGIN"|oxmultilangassign}] + + [{/capture}] +[{capture append="oxidBlock_sidebar"}] + [{include file="page/account/inc/account_menu.tpl" active_link="twofactor"}] + [{/capture}] +[{include file="layout/page.tpl" sidebar="Left"}] \ No newline at end of file From f680e8e63072eccebe71720778adf76ba87234c8 Mon Sep 17 00:00:00 2001 From: moritz Date: Thu, 20 May 2021 15:36:27 +0200 Subject: [PATCH 101/199] renamed settings getter --- src/Api/PasswordCheck.php | 4 ++-- src/Core/PasswordPolicyConfig.php | 8 ++++---- src/Model/PasswordPolicyUser.php | 2 +- src/Validators/PasswordPolicyDataBreach.php | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Api/PasswordCheck.php b/src/Api/PasswordCheck.php index ab816ac1..80d80b21 100644 --- a/src/Api/PasswordCheck.php +++ b/src/Api/PasswordCheck.php @@ -35,9 +35,9 @@ public function __construct(PasswordPolicyConfig $config, PasswordExposedChecker */ public function isPasswordKnown(string $username, string $password): bool { - if ($this->config->isHaveIBeenPwnedNeeded() && $this->haveIBeenPwned->passwordExposed($password) == "exposed") { + if ($this->config->isHaveIBeenPwned() && $this->haveIBeenPwned->passwordExposed($password) == "exposed") { return true; - } elseif ($this->config->isEnzoicNeeded() && ($this->enzoicApiCon->checkPassword($password) !== null || $this->enzoicApiCon->checkCredentials($username, $password))) { + } elseif ($this->config->isEnzoic() && ($this->enzoicApiCon->checkPassword($password) !== null || $this->enzoicApiCon->checkCredentials($username, $password))) { return true; } return false; diff --git a/src/Core/PasswordPolicyConfig.php b/src/Core/PasswordPolicyConfig.php index d32b68f9..e0d96aaf 100644 --- a/src/Core/PasswordPolicyConfig.php +++ b/src/Core/PasswordPolicyConfig.php @@ -104,22 +104,22 @@ public function getSecretKey(): string return (string) Registry::getConfig()->getConfigParam(self::SettingEnzoicSecretKey); } - public function isAPINeeded(): bool + public function isAPI(): bool { return $this->isConfigParam(self::SettingAPI); } - public function isEnzoicNeeded(): bool + public function isEnzoic(): bool { return $this->isConfigParam(self::SettingEnzoic); } - public function isHaveIBeenPwnedNeeded(): bool + public function isHaveIBeenPwned(): bool { return $this->isConfigParam(self::SettingHaveIBeenPwned); } - public function isRateLimitingNeeded(): bool + public function isRateLimiting(): bool { return $this->isConfigParam(self::SettingRateLimiting); } diff --git a/src/Model/PasswordPolicyUser.php b/src/Model/PasswordPolicyUser.php index e1d6bc03..9c37d1b3 100644 --- a/src/Model/PasswordPolicyUser.php +++ b/src/Model/PasswordPolicyUser.php @@ -48,7 +48,7 @@ public function login($userName, $password, $setSessionCookie = false) { $container = ContainerFactory::getInstance()->getContainer(); $config = $container->get(PasswordPolicyConfig::class); - if ($config->isRateLimitingNeeded()) { + if ($config->isRateLimiting()) { $driverName = $config->getSelectedDriver(); $rateLimiter = (new PasswordPolicyRateLimiterFactory())->getRateLimiter($driverName)->getLimiter(); diff --git a/src/Validators/PasswordPolicyDataBreach.php b/src/Validators/PasswordPolicyDataBreach.php index 8653b375..80f2df76 100644 --- a/src/Validators/PasswordPolicyDataBreach.php +++ b/src/Validators/PasswordPolicyDataBreach.php @@ -24,7 +24,7 @@ public function __construct(PasswordPolicyConfig $config, PasswordCheck $passwor public function validate(string $sUsername, string $sPassword) { try { - if ($this->config->isAPINeeded() && $this->passwordCheck->isPasswordKnown($sUsername, $sPassword)) { + if ($this->config->isAPI() && $this->passwordCheck->isPasswordKnown($sUsername, $sPassword)) { return 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_PASSWORD_KNOWN'; } } catch (\RuntimeException $exception) { From 002430c6bca176104dea40831dff20c90a07895b Mon Sep 17 00:00:00 2001 From: moritz Date: Fri, 21 May 2021 11:40:55 +0200 Subject: [PATCH 102/199] added option to deactivate/activate 2FA in my account section --- metadata.php | 7 ++- src/Component/PasswordPolicyUserComponent.php | 4 ++ src/Controller/PasswordPolicyAccountTOTP.php | 42 +++++++++++--- .../PasswordPolicyTwoFactorConfirmation.php | 58 +++++++++++++++++++ src/Core/PasswordPolicyConfig.php | 10 ++-- src/Core/PasswordPolicyViewConfig.php | 4 +- translations/de/passwordpolicy_lang.php | 7 ++- views/blocks/account_menu.tpl | 4 +- views/blocks/user_account.tpl | 4 +- views/tpl/twofactoraccount.tpl | 31 +++++++++- views/tpl/twofactorconfirmation.tpl | 23 ++++++++ views/tpl/twofactorregister.tpl | 2 +- 12 files changed, 172 insertions(+), 24 deletions(-) create mode 100644 src/Controller/PasswordPolicyTwoFactorConfirmation.php create mode 100644 views/tpl/twofactorconfirmation.tpl diff --git a/metadata.php b/metadata.php index 8289c72f..feb16fed 100644 --- a/metadata.php +++ b/metadata.php @@ -33,6 +33,7 @@ use OxidEsales\Eshop\Application\Controller\Admin\ModuleConfiguration; use OxidProfessionalServices\PasswordPolicy\Component\PasswordPolicyUserComponent; use OxidProfessionalServices\PasswordPolicy\Controller\PasswordPolicyAccountTOTP; +use OxidProfessionalServices\PasswordPolicy\Controller\PasswordPolicyTwoFactorConfirmation; use OxidProfessionalServices\PasswordPolicy\Controller\PasswordPolicyTwoFactorRegister; use OxidProfessionalServices\PasswordPolicy\Controller\PasswordPolicyTwoFactorLogin; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyValidator; @@ -72,12 +73,14 @@ 'controllers' => [ 'twofactorregister' => PasswordPolicyTwoFactorRegister::class, 'twofactorlogin' => PasswordPolicyTwoFactorLogin::class, - 'twofactoraccount' => PasswordPolicyAccountTOTP::class + 'twofactoraccount' => PasswordPolicyAccountTOTP::class, + 'twofactorconfirmation' => PasswordPolicyTwoFactorConfirmation::class ], 'templates' => [ 'twofactorregister.tpl' => 'oxps/passwordpolicy/views/tpl/twofactorregister.tpl', 'twofactorlogin.tpl' => 'oxps/passwordpolicy/views/tpl/twofactorlogin.tpl', - 'twofactoraccount.tpl' => 'oxps/passwordpolicy/views/tpl/twofactoraccount.tpl' + 'twofactoraccount.tpl' => 'oxps/passwordpolicy/views/tpl/twofactoraccount.tpl', + 'twofactorconfirmation.tpl' => 'oxps/passwordpolicy/views/tpl/twofactorconfirmation.tpl' ], 'blocks' => [ [ diff --git a/src/Component/PasswordPolicyUserComponent.php b/src/Component/PasswordPolicyUserComponent.php index 38c3345e..649bd411 100644 --- a/src/Component/PasswordPolicyUserComponent.php +++ b/src/Component/PasswordPolicyUserComponent.php @@ -42,6 +42,10 @@ public function finalizeRegistration() { $redirect = 'register?success=1'; } + elseif($step == 'settings') + { + $redirect = 'twofactoraccount?success=1'; + } $checkOTP = $this->TOTP->checkOTP($secret, $OTP); if($checkOTP) diff --git a/src/Controller/PasswordPolicyAccountTOTP.php b/src/Controller/PasswordPolicyAccountTOTP.php index 70021ee7..22b788ca 100644 --- a/src/Controller/PasswordPolicyAccountTOTP.php +++ b/src/Controller/PasswordPolicyAccountTOTP.php @@ -6,19 +6,21 @@ use OxidEsales\Eshop\Application\Controller\AccountController; use OxidEsales\Eshop\Core\Registry; +use OxidEsales\Eshop\Core\Request; class PasswordPolicyAccountTOTP extends AccountController { -public function render() -{ - parent::render(); - $oUser = $this->getUser(); - if (!$oUser) { - return $this->_sThisLoginTemplate; + public function render() + { + parent::render(); + $oUser = $this->getUser(); + if (!$oUser) { + return $this->_sThisLoginTemplate; + } + + return 'twofactoraccount.tpl'; } - return 'twofactoraccount.tpl'; -} public function getBreadCrumb() { $aPaths = []; @@ -38,4 +40,28 @@ public function getBreadCrumb() return $aPaths; } + public function isTOTP(): bool + { + $user = $this->getUser(); + $secret = $user->oxuser__oxtotpsecret->value; + return $secret != null; + } + + public function redirect() + { + $totpenabled = (new Request())->getRequestEscapedParameter('status'); + if ($totpenabled && !$this->isTOTP()) { + return 'twofactorregister?step=settings'; + } + elseif (!$totpenabled && $this->isTOTP()) + { + return 'twofactorconfirmation'; + } + } + + public function getStatus() + { + $success = (new Request())->getRequestEscapedParameter('success'); + return $success; + } } \ No newline at end of file diff --git a/src/Controller/PasswordPolicyTwoFactorConfirmation.php b/src/Controller/PasswordPolicyTwoFactorConfirmation.php new file mode 100644 index 00000000..378e872c --- /dev/null +++ b/src/Controller/PasswordPolicyTwoFactorConfirmation.php @@ -0,0 +1,58 @@ +getUser(); + if (!$oUser) { + return 'page/account/login.tpl'; + } + return 'twofactorconfirmation.tpl'; + } + + public function confirm() + { + $container = ContainerFactory::getInstance()->getContainer(); + $TOTP = $container->get(PasswordPolicyTOTP::class); + $otp = (new Request())->getRequestEscapedParameter('otp'); + $user = $this->getUser(); + $secret = $user->oxuser__oxtotpsecret->value; + $checkOTP = $TOTP->checkOTP($secret, $otp); + if($checkOTP) + { + // resets 2FA secret code for user + $user->oxuser__oxtotpsecret = new Field("", Field::T_TEXT); + $user->save(); + return 'twofactoraccount?success=2'; + } + Registry::getUtilsView()->addErrorToDisplay( + 'OXPS_PASSWORDPOLICY_TOTP_ERROR_WRONGOTP', + false, + true + ); + } + + public function getBreadCrumb() + { + $aPaths = []; + $aPath = []; + $iBaseLanguage = Registry::getLang()->getBaseLanguage(); + $aPath['title'] = Registry::getLang()->translateString('TWOFACTORAUTHLOGIN', $iBaseLanguage, false); + $aPath['link'] = $this->getLink(); + $aPaths[] = $aPath; + return $aPaths; + } +} \ No newline at end of file diff --git a/src/Core/PasswordPolicyConfig.php b/src/Core/PasswordPolicyConfig.php index e0d96aaf..5d5f1c2d 100644 --- a/src/Core/PasswordPolicyConfig.php +++ b/src/Core/PasswordPolicyConfig.php @@ -124,6 +124,11 @@ public function isRateLimiting(): bool return $this->isConfigParam(self::SettingRateLimiting); } + public function isTOTP(): bool + { + return $this->isConfigParam(self::SettingTOTP); + } + public function getSelectedDriver(): string { return (string) Registry::getConfig()->getConfigParam(self::SettingDrivers); @@ -143,11 +148,6 @@ public function getMemcachedPort(): int { return (int) Registry::getConfig()->getConfigParam(self::SettingMemcachedPort, 11211); } - - public function isTOTPNeeded(): bool - { - return $this->isConfigParam(self::SettingTOTP); - } private function isConfigParam(string $name): bool { return (bool) Registry::getConfig()->getConfigParam($name, true); diff --git a/src/Core/PasswordPolicyViewConfig.php b/src/Core/PasswordPolicyViewConfig.php index 2d19f54f..095667f5 100644 --- a/src/Core/PasswordPolicyViewConfig.php +++ b/src/Core/PasswordPolicyViewConfig.php @@ -60,8 +60,8 @@ public function getJsonPasswordPolicySettings(): string return $res; } - public function isTOTPNeeded(): bool + public function isTOTP(): bool { - return $this->config->isTOTPNeeded(); + return $this->config->isTOTP(); } } diff --git a/translations/de/passwordpolicy_lang.php b/translations/de/passwordpolicy_lang.php index 574ec662..1168b40f 100644 --- a/translations/de/passwordpolicy_lang.php +++ b/translations/de/passwordpolicy_lang.php @@ -55,9 +55,12 @@ 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_PASSWORD_KNOWN' => 'Das Passwort befindet sich bereits in einer gehackten Datenbank und ist somit unsicher.', 'OXPS_PASSWORDPOLICY_TOTP_ERROR_WRONGOTP' => 'Der eingebene Code für die Zwei-Faktor-Authentifizierung war falsch.', 'OXPS_PASSWORDPOLICY_RATELIMIT_EXCEEDED' => 'Sie haben sich zu oft versucht einzuloggen. Bitte versuchen Sie es später erneut.', - 'TWOFACTORAUTH' => '2-Faktor-Authentifizierung Einrichtung', + 'TWOFACTORAUTHREGISTER' => '2-Faktor-Authentifizierung Einrichtung', 'TWOFACTORAUTHLOGIN' => '2-Faktor-Authentifizierung', 'TWOFACTORAUTHCODE' => '2FA Code', 'TWOFACTORAUTHCHECKBOX' => '2FA aktivieren', - 'MESSAGE_TWOFACTOR_HELP' => 'Die Zwei-Faktor-Authentifizierung bietet Ihrem Account eine zusätzliche Sicherheit. Durch aktivieren dieser Funktion, wird bei jedem Login nach einem zeitlich begrenzten einmaligen Passwort gefragt, welches Sie auf Ihrem Handy von einer App ablesen können.' + 'TWOFACTORDEACTIVATE' => '2FA deaktivieren', + 'MESSAGE_TWOFACTOR_HELP' => 'Die Zwei-Faktor-Authentifizierung bietet Ihrem Account eine zusätzliche Sicherheit. Durch aktivieren dieser Funktion, wird bei jedem Login nach einem zeitlich begrenzten einmaligen Passwort gefragt, welches Sie auf Ihrem Handy von einer App ablesen können.', + 'MESSAGE_TWOFACTOR_SUCCESS' => '2FA wurde erfolgreich eingerichtet.', + 'MESSAGE_TWOFACTOR_DEACTIVATED' => '2FA wurde deaktiviert.' ); diff --git a/views/blocks/account_menu.tpl b/views/blocks/account_menu.tpl index 64928215..375448cb 100644 --- a/views/blocks/account_menu.tpl +++ b/views/blocks/account_menu.tpl @@ -1,6 +1,6 @@ [{$smarty.block.parent}] -[{if $oViewConf->isTOTPNeeded()}] +[{if $oViewConf->isTOTP()}] [{/if}] \ No newline at end of file diff --git a/views/blocks/user_account.tpl b/views/blocks/user_account.tpl index 0e376380..e6168171 100644 --- a/views/blocks/user_account.tpl +++ b/views/blocks/user_account.tpl @@ -1,4 +1,5 @@ [{$smarty.block.parent}] +[{if $oViewConf->isTOTP()}]
@@ -8,4 +9,5 @@
[{oxmultilang ident="MESSAGE_TWOFACTOR_HELP"}]
-
\ No newline at end of file + +[{/if}] diff --git a/views/tpl/twofactoraccount.tpl b/views/tpl/twofactoraccount.tpl index 3f52c26b..d6777c16 100644 --- a/views/tpl/twofactoraccount.tpl +++ b/views/tpl/twofactoraccount.tpl @@ -1,6 +1,35 @@ [{capture append="oxidBlock_content"}] - [{assign var="template_title" value="TWOFACTORLOGIN"|oxmultilangassign}] + [{assign var="template_title" value="TWOFACTORAUTHLOGIN"|oxmultilangassign}] + [{if $oView->getStatus() == '1'}] +
[{oxmultilang ident="MESSAGE_TWOFACTOR_SUCCESS"}]
+ [{elseif $oView->getStatus() == '2'}] +
[{oxmultilang ident="MESSAGE_TWOFACTOR_DEACTIVATED"}]
+ [{/if}] +

[{oxmultilang ident="TWOFACTORAUTHLOGIN"}]

+
+ +
+ +
+ +
+
+ +
+
+ +
+
+
[{/capture}] [{capture append="oxidBlock_sidebar"}] [{include file="page/account/inc/account_menu.tpl" active_link="twofactor"}] diff --git a/views/tpl/twofactorconfirmation.tpl b/views/tpl/twofactorconfirmation.tpl new file mode 100644 index 00000000..050fa366 --- /dev/null +++ b/views/tpl/twofactorconfirmation.tpl @@ -0,0 +1,23 @@ + +[{capture append="oxidBlock_content"}] +

[{oxmultilang ident="TWOFACTORAUTHLOGIN"}]

+ +
+ +
+
+ +
+ +
+
+ [{oxscript include=$oViewConf->getModuleUrl('oxpspasswordpolicy','src/js/otpField.js')}] +[{/capture}] + + +[{include file="layout/page.tpl"}] \ No newline at end of file diff --git a/views/tpl/twofactorregister.tpl b/views/tpl/twofactorregister.tpl index 690afdb0..13744b37 100644 --- a/views/tpl/twofactorregister.tpl +++ b/views/tpl/twofactorregister.tpl @@ -2,7 +2,7 @@ [{capture append="oxidBlock_content"}] -

[{oxmultilang ident="TWOFACTORAUTH"}]

+

[{oxmultilang ident="TWOFACTORAUTHREGISTER"}]

+ [{oxscript include=$oViewConf->getModuleUrl('oxpspasswordpolicy','out/src/js/otpField.js')}] [{/capture}] diff --git a/views/tpl/twofactorlogin.tpl b/views/tpl/twofactorlogin.tpl index eb170d38..78d0d854 100644 --- a/views/tpl/twofactorlogin.tpl +++ b/views/tpl/twofactorlogin.tpl @@ -1,7 +1,14 @@ -[{capture append="oxidBlock_content"}] +[{capture append="oxidBlock_pageBody"}] +[{$oViewConf->setFullWidth()}] +
+
+
+
+ [{include file="message/errors.tpl"}] +

[{oxmultilang ident="TWOFACTORAUTHLOGIN"}]

- + [{oxmultilang ident="MESSAGE_TWOFACTOR_LOGIN"}]
- [{oxscript include=$oViewConf->getModuleUrl('oxpspasswordpolicy','src/js/otpField.js')}] +
+
+ [{oxscript include=$oViewConf->getModuleUrl('oxpspasswordpolicy','out/src/js/otpField.js')}] [{/capture}] +[{include file="layout/base.tpl"}] +[{include file="layout/footer.tpl"}] -[{include file="layout/page.tpl"}] \ No newline at end of file + \ No newline at end of file diff --git a/views/tpl/twofactorregister.tpl b/views/tpl/twofactorregister.tpl index 13744b37..ca6b229e 100644 --- a/views/tpl/twofactorregister.tpl +++ b/views/tpl/twofactorregister.tpl @@ -1,7 +1,14 @@ -[{capture append="oxidBlock_content"}] +[{capture append="oxidBlock_pageBody"}] +[{$oViewConf->setFullWidth()}] +
+
+
+
+ [{include file="message/errors.tpl"}] +

[{oxmultilang ident="TWOFACTORAUTHREGISTER"}]

-
+
[{$oView->getTOTPQrCode()}]
@@ -22,9 +29,17 @@
- [{oxscript include=$oViewConf->getModuleUrl('oxpspasswordpolicy','src/js/otpField.js')}] +
+
+ [{oxscript include=$oViewConf->getModuleUrl('oxpspasswordpolicy','out/src/js/otpField.js')}] [{/capture}] +[{include file="layout/base.tpl"}] +[{include file="layout/footer.tpl"}] -[{include file="layout/page.tpl"}] - + From a95fec363c4f20067d8f82cb565bca6cfbbf2537 Mon Sep 17 00:00:00 2001 From: moritz Date: Tue, 25 May 2021 15:07:27 +0200 Subject: [PATCH 112/199] cleaned up code --- src/Component/PasswordPolicyUserComponent.php | 70 +++++++------------ src/Model/PasswordPolicyUser.php | 31 ++++++++ 2 files changed, 58 insertions(+), 43 deletions(-) diff --git a/src/Component/PasswordPolicyUserComponent.php b/src/Component/PasswordPolicyUserComponent.php index 12f0a51e..8670a804 100644 --- a/src/Component/PasswordPolicyUserComponent.php +++ b/src/Component/PasswordPolicyUserComponent.php @@ -3,7 +3,7 @@ namespace OxidProfessionalServices\PasswordPolicy\Component; use OxidEsales\Eshop\Application\Model\User; -use OxidEsales\Eshop\Core\Config; +use OxidEsales\Eshop\Core\Exception\UserException; use OxidEsales\Eshop\Core\Registry; use OxidEsales\Eshop\Core\Request; use OxidEsales\EshopCommunity\Core\Field; @@ -42,19 +42,7 @@ public function finalizeRegistration() $container = ContainerFactory::getInstance()->getContainer(); $TOTP = $container->get(PasswordPolicyTOTP::class); $OTP = (new Request())->getRequestEscapedParameter('otp'); - $step = (new Request())->getRequestEscapedParameter('step'); - $paymentActionLink = (new Request())->getRequestEscapedParameter('paymentActionLink'); $secret = Registry::getSession()->getVariable('otp_secret'); - $redirect = urldecode($paymentActionLink); - if($step == 'registration') - { - $redirect = 'register?success=1'; - } - elseif($step == 'settings') - { - $redirect = 'twofactoraccount?success=1'; - } - $checkOTP = $TOTP->checkOTP($secret, $OTP); if($checkOTP) { @@ -64,6 +52,7 @@ public function finalizeRegistration() $user->save(); //cleans up session for next registration Registry::getSession()->deleteVariable('otp_secret'); + $redirect = $this->getRedirectLink(); return $redirect; } Registry::getUtilsView()->addErrorToDisplay( @@ -73,40 +62,35 @@ public function finalizeRegistration() ); } + public function getRedirectLink() + { + $step = (new Request())->getRequestEscapedParameter('step'); + $paymentActionLink = (new Request())->getRequestEscapedParameter('paymentActionLink'); + $redirect = urldecode($paymentActionLink); + if($step == 'registration') + { + $redirect = 'register?success=1'; + } + elseif($step == 'settings') + { + $redirect = 'twofactoraccount?success=1'; + } + return $redirect; + } + public function finalizeLogin() { - $container = ContainerFactory::getInstance()->getContainer(); - $TOTP = $container->get(PasswordPolicyTOTP::class); - $config = $container->get(Config::class); $otp = (new Request())->getRequestEscapedParameter('otp'); - $sessioncookie = (new Request())->getRequestEscapedParameter('setsessioncookie'); - $usr = Registry::getSession()->getVariable('tmpusr'); - $user = oxNew(User::class); - $user->load($usr); - $secret = $user->oxuser__oxpstotpsecret->value; - $checkOTP = $TOTP->checkOTP($secret, $otp); - if($checkOTP) - { - Registry::getSession()->deleteVariable('tmpusr'); - Registry::getSession()->setVariable('usr', $usr); + $setsessioncookie = (new Request())->getRequestEscapedParameter('setsessioncookie'); + $this->setLoginStatus(USER_LOGIN_FAIL); + try { + $user = oxNew(User::class); + $user->finalizeLogin($otp, $setsessioncookie); $this->setLoginStatus(USER_LOGIN_SUCCESS); - // in case user wants to stay logged in, set user cookie again - if ($sessioncookie && $config->getConfigParam('blShowRememberMe')) { - Registry::getUtilsServer()->setUserCookie( - $user->oxuser__oxusername->value, - $user->oxuser__oxpassword->value, - $config->getShopId(), - 31536000, - static::USER_COOKIE_SALT - ); - } - $user->set(null); - $this->_afterLogin($user); + }catch(UserException $ex) + { + Registry::getUtilsView()->addErrorToDisplay($ex); } - Registry::getUtilsView()->addErrorToDisplay( - 'OXPS_PASSWORDPOLICY_TOTP_ERROR_WRONGOTP', - false, - true - ); + return $this->_afterLogin($user); } } \ No newline at end of file diff --git a/src/Model/PasswordPolicyUser.php b/src/Model/PasswordPolicyUser.php index 0df70fdf..129a72ad 100644 --- a/src/Model/PasswordPolicyUser.php +++ b/src/Model/PasswordPolicyUser.php @@ -4,6 +4,7 @@ use OxidEsales\Eshop\Application\Controller\ForgotPasswordController; use OxidEsales\Eshop\Application\Model\User; +use OxidEsales\Eshop\Core\Config; use OxidEsales\Eshop\Core\Exception\UserException; use OxidEsales\Eshop\Core\InputValidator; use OxidEsales\Eshop\Core\Registry; @@ -13,6 +14,7 @@ use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyValidator; use OxidProfessionalServices\PasswordPolicy\Exception\LimiterNotFound; use OxidProfessionalServices\PasswordPolicy\Factory\PasswordPolicyRateLimiterFactory; +use OxidProfessionalServices\PasswordPolicy\TwoFactorAuth\PasswordPolicyTOTP; use RateLimit\Exception\LimitExceeded; use RateLimit\Rate; @@ -75,4 +77,33 @@ public function login($userName, $password, $setSessionCookie = false) } } + + public function finalizeLogin($otp, $setsessioncookie = false) + { + $container = ContainerFactory::getInstance()->getContainer(); + $TOTP = $container->get(PasswordPolicyTOTP::class); + $config = $container->get(Config::class); + $session = Registry::getSession(); + $usr = $session->getVariable('tmpusr'); + $this->load($usr); + $secret = $this->oxuser__oxpstotpsecret->value; + $checkOTP = $TOTP->checkOTP($secret, $otp); + if($checkOTP) + { + $session->deleteVariable('tmpusr'); + $session->setVariable('usr', $usr); + // in case user wants to stay logged in, set user cookie again + if ($setsessioncookie && $config->getConfigParam('blShowRememberMe')) { + Registry::getUtilsServer()->setUserCookie( + $this->oxuser__oxusername->value, + $this->oxuser__oxpassword->value, + $config->getShopId(), + 31536000, + static::USER_COOKIE_SALT + ); + } + return $this; + } + throw oxNew(UserException::class, 'OXPS_PASSWORDPOLICY_TOTP_ERROR_WRONGOTP'); + } } From 562cc3dfc6bff9527b663f5369eba05568fd18d4 Mon Sep 17 00:00:00 2001 From: moritz Date: Wed, 26 May 2021 01:42:13 +0200 Subject: [PATCH 113/199] added redirection to backup code page --- src/Component/PasswordPolicyUserComponent.php | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Component/PasswordPolicyUserComponent.php b/src/Component/PasswordPolicyUserComponent.php index 8670a804..42770308 100644 --- a/src/Component/PasswordPolicyUserComponent.php +++ b/src/Component/PasswordPolicyUserComponent.php @@ -25,7 +25,7 @@ public function createUser() $paymentActionLink = parent::createUser(); if($twofactorconf && $twoFactor && $paymentActionLink) { - Registry::getUtils()->redirect(Registry::getConfig()->getShopHomeUrl() . 'cl=twofactorregister&step='. $this->step . '&paymentActionLink='. urlencode($paymentActionLink)); + Registry::getUtils()->redirect(Registry::getConfig()->getShopHomeUrl() . 'cl=twofactorregister&step='. $this->step . '&paymentActionLink='. urlencode($paymentActionLink) . '&success=1'); } return $paymentActionLink; @@ -52,14 +52,11 @@ public function finalizeRegistration() $user->save(); //cleans up session for next registration Registry::getSession()->deleteVariable('otp_secret'); - $redirect = $this->getRedirectLink(); - return $redirect; + $step = (new Request())->getRequestEscapedParameter('step'); + $paymentActionLink = (new Request())->getRequestEscapedParameter('paymentActionLink'); + return 'twofactorrecovery?step=' . $step . '&paymentActionLink=' . $paymentActionLink; } - Registry::getUtilsView()->addErrorToDisplay( - 'OXPS_PASSWORDPOLICY_TOTP_ERROR_WRONGOTP', - false, - true - ); + Registry::getUtilsView()->addErrorToDisplay('OXPS_PASSWORDPOLICY_TOTP_ERROR_WRONGOTP'); } public function getRedirectLink() @@ -89,7 +86,7 @@ public function finalizeLogin() $this->setLoginStatus(USER_LOGIN_SUCCESS); }catch(UserException $ex) { - Registry::getUtilsView()->addErrorToDisplay($ex); + return Registry::getUtilsView()->addErrorToDisplay($ex); } return $this->_afterLogin($user); } From e65b6792831ba33c71e56c0f9fe4e893574f8a0e Mon Sep 17 00:00:00 2001 From: moritz Date: Wed, 26 May 2021 01:42:50 +0200 Subject: [PATCH 114/199] removed function and replaced it with success variable --- src/Controller/PasswordPolicyAccountTOTP.php | 9 ++------- views/tpl/twofactoraccount.tpl | 6 +++--- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/Controller/PasswordPolicyAccountTOTP.php b/src/Controller/PasswordPolicyAccountTOTP.php index f2e90047..b650ba95 100644 --- a/src/Controller/PasswordPolicyAccountTOTP.php +++ b/src/Controller/PasswordPolicyAccountTOTP.php @@ -17,7 +17,8 @@ public function render() if (!$oUser) { return $this->_sThisLoginTemplate; } - + $success = (new Request())->getRequestEscapedParameter('success'); + $this->addTplParam('success', $success); return 'twofactoraccount.tpl'; } @@ -58,10 +59,4 @@ public function redirect() return 'twofactorconfirmation'; } } - - public function getStatus() - { - $success = (new Request())->getRequestEscapedParameter('success'); - return $success; - } } \ No newline at end of file diff --git a/views/tpl/twofactoraccount.tpl b/views/tpl/twofactoraccount.tpl index d6777c16..c74fdc18 100644 --- a/views/tpl/twofactoraccount.tpl +++ b/views/tpl/twofactoraccount.tpl @@ -1,11 +1,11 @@ [{capture append="oxidBlock_content"}] [{assign var="template_title" value="TWOFACTORAUTHLOGIN"|oxmultilangassign}] - [{if $oView->getStatus() == '1'}] + [{if $success == '1'}]
[{oxmultilang ident="MESSAGE_TWOFACTOR_SUCCESS"}]
- [{elseif $oView->getStatus() == '2'}] + [{elseif $success == '2'}]
[{oxmultilang ident="MESSAGE_TWOFACTOR_DEACTIVATED"}]
[{/if}] -

[{oxmultilang ident="TWOFACTORAUTHLOGIN"}]

+

[{oxmultilang ident="TWOFACTORAUTHLOGIN"}]

- [{oxstyle include=$oViewConf->getModuleUrl('oxpspasswordpolicy','out/src/css/tailwind.css')}] + [{oxstyle include=$oViewConf->getModuleUrl('oxpspasswordpolicy','out/src/css/style.css')}] [{/capture}] diff --git a/views/tpl/twofactorregister.tpl b/views/tpl/twofactorregister.tpl index da490e34..abf3b0b8 100644 --- a/views/tpl/twofactorregister.tpl +++ b/views/tpl/twofactorregister.tpl @@ -26,12 +26,12 @@
- +
- [{oxstyle include=$oViewConf->getModuleUrl('oxpspasswordpolicy','out/src/css/tailwind.css')}] + [{oxstyle include=$oViewConf->getModuleUrl('oxpspasswordpolicy','out/src/css/style.css')}] [{oxscript include=$oViewConf->getModuleUrl('oxpspasswordpolicy','out/src/js/otpField.js')}] [{/capture}] From 0aa91e466e0faa8fed164d4c8755b9c6e870f784 Mon Sep 17 00:00:00 2001 From: moritz Date: Wed, 29 Sep 2021 13:06:11 +0200 Subject: [PATCH 141/199] now redirects admin to password reset page in case his password is leaked and insecure --- src/Model/PasswordPolicyUser.php | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Model/PasswordPolicyUser.php b/src/Model/PasswordPolicyUser.php index 65666e86..b8432e67 100644 --- a/src/Model/PasswordPolicyUser.php +++ b/src/Model/PasswordPolicyUser.php @@ -9,6 +9,7 @@ use OxidEsales\Eshop\Core\Field; use OxidEsales\Eshop\Core\InputValidator; use OxidEsales\Eshop\Core\Registry; +use OxidEsales\Eshop\Core\ViewConfig; use OxidEsales\EshopCommunity\Internal\Container\ContainerFactory; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyConfig; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyValidator; @@ -31,11 +32,19 @@ public function onLogin($userName, $password) { /** @var PasswordPolicyValidator $passValidator */ $passValidator = oxNew(InputValidator::class); - if (!isAdmin() && $this->isLoaded() && $err = $passValidator->validatePassword($userName, $password)) { - $forgotPass = new ForgotPasswordController(); - $forgotPass->forgotPassword(); - $errorMessage = $err->getMessage() . ' ' . Registry::getLang()->translateString('REQUEST_PASSWORD_AFTERCLICK'); - throw oxNew(UserException::class, $errorMessage); + if ($this->isLoaded() && $err = $passValidator->validatePassword($userName, $password)) { + if(!$this->isAdmin()) { + $forgotPass = new ForgotPasswordController(); + $forgotPass->forgotPassword(); + $errorMessage = $err->getMessage() . ' ' . Registry::getLang()->translateString('REQUEST_PASSWORD_AFTERCLICK'); + throw oxNew(UserException::class, $errorMessage); + } + // redirects admin to password reset page *temporary solution + $oViewConf = oxNew(ViewConfig::class); + $passwordResetLink = $oViewConf->getBaseDir() . 'index.php?cl=forgotpwd&uid=' . $this->getUpdateId() . '&lang=' . $oViewConf->getActLanguageId() . '&shp=' . $this->getShopId(); + \OxidEsales\EshopCommunity\Core\Registry::getUtilsView()->addErrorToDisplay("Test"); + Registry::getUtils()->redirect($passwordResetLink, true, 302); + } parent::onLogin($userName, $password); } From f2de3f71c6dbc11ac92cb13656d2dbf49753d528 Mon Sep 17 00:00:00 2001 From: moritz Date: Thu, 30 Sep 2021 14:05:12 +0200 Subject: [PATCH 142/199] changed style for OTP text fields --- out/src/css/style.css | 8 +++++++- out/src/js/otpField.js | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/out/src/css/style.css b/out/src/css/style.css index 772438f5..2dde1f3c 100644 --- a/out/src/css/style.css +++ b/out/src/css/style.css @@ -84,4 +84,10 @@ .xl\:min-h-screen { min-height: 100vh; -} \ No newline at end of file +} + + +.form-control:focus { + border-color: #74abb9 !important; + box-shadow: 0 0 5px rgb(102, 173, 217) !important; +} diff --git a/out/src/js/otpField.js b/out/src/js/otpField.js index 0c9962ed..688920a5 100644 --- a/out/src/js/otpField.js +++ b/out/src/js/otpField.js @@ -3,7 +3,7 @@ const $otp_length = 6; const element = document.getElementById('OTPInput'); for (let i = 0; i < $otp_length; i++) { let inputField = document.createElement('input'); // Creates a new input element - inputField.className = "border border-dark w-12 h-12 bg-gray-100 border-gray-50 outline-none focus:bg-gray-200 m-2 text-center rounded focus:border-blue-200 focus:shadow-outline"; + inputField.className = " w-12 h-12 bg-light border-gray-50 outline-none form-control m-2 text-center rounded"; inputField.style.cssText = "color: transparent; text-shadow: 0 0 0 gray;"; inputField.id = 'otp-field' + i; inputField.maxLength = 1; From 7fb8a11b02c270815c5fb8f779eedbfef8b9fe5a Mon Sep 17 00:00:00 2001 From: moritz Date: Thu, 30 Sep 2021 14:05:34 +0200 Subject: [PATCH 143/199] disabled admin password reset temporary --- src/Model/PasswordPolicyUser.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Model/PasswordPolicyUser.php b/src/Model/PasswordPolicyUser.php index b8432e67..6ad4a3dd 100644 --- a/src/Model/PasswordPolicyUser.php +++ b/src/Model/PasswordPolicyUser.php @@ -40,10 +40,9 @@ public function onLogin($userName, $password) throw oxNew(UserException::class, $errorMessage); } // redirects admin to password reset page *temporary solution - $oViewConf = oxNew(ViewConfig::class); + /* $oViewConf = oxNew(ViewConfig::class); $passwordResetLink = $oViewConf->getBaseDir() . 'index.php?cl=forgotpwd&uid=' . $this->getUpdateId() . '&lang=' . $oViewConf->getActLanguageId() . '&shp=' . $this->getShopId(); - \OxidEsales\EshopCommunity\Core\Registry::getUtilsView()->addErrorToDisplay("Test"); - Registry::getUtils()->redirect($passwordResetLink, true, 302); + Registry::getUtils()->redirect($passwordResetLink, true);*/ } parent::onLogin($userName, $password); From 65a25987d6ca6da9f7d539a3afebc1d1562d643d Mon Sep 17 00:00:00 2001 From: moritz Date: Thu, 30 Sep 2021 14:06:19 +0200 Subject: [PATCH 144/199] moved memcached to rate limiting settings --- metadata.php | 4 ++-- views/admin/de/passwordpolicy_lang.php | 9 ++++----- views/admin/en/passwordpolicy_lang.php | 9 ++++----- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/metadata.php b/metadata.php index a222324d..e8d392f3 100644 --- a/metadata.php +++ b/metadata.php @@ -137,8 +137,8 @@ ['group' => 'passwordpolicy_ratelimiting', 'name' => 'oxpspasswordpolicyRateLimiting', 'type' => 'bool', 'value' => true], ['group' => 'passwordpolicy_ratelimiting', 'name' => 'oxpspasswordpolicyRateLimitingDrivers', 'type' => 'select', 'value' => 'APCu', 'constraints' => 'Memcached|APCu'], ['group' => 'passwordpolicy_ratelimiting', 'name' => 'oxpspasswordpolicyRateLimitingLimit', 'type' => 'num', 'value' => 60], - ['group' => 'passwordpolicy_memcached', 'name' => 'oxpspasswordpolicyMemcachedHost', 'type' => 'str', 'value' => 'memcached'], - ['group' => 'passwordpolicy_memcached', 'name' => 'oxpspasswordpolicyMemcachedPort', 'type' => 'num', 'value' => 11211], + ['group' => 'passwordpolicy_ratelimiting', 'name' => 'oxpspasswordpolicyMemcachedHost', 'type' => 'str', 'value' => 'memcached'], + ['group' => 'passwordpolicy_ratelimiting', 'name' => 'oxpspasswordpolicyMemcachedPort', 'type' => 'num', 'value' => 11211], ['group' => 'passwordpolicy_twofactor', 'name' => 'oxpspasswordpolicyTOTP', 'type' => 'bool', 'value' => false], ], 'events' => array( diff --git a/views/admin/de/passwordpolicy_lang.php b/views/admin/de/passwordpolicy_lang.php index 0273026e..83ebb6a6 100644 --- a/views/admin/de/passwordpolicy_lang.php +++ b/views/admin/de/passwordpolicy_lang.php @@ -30,8 +30,8 @@ 'SHOP_MODULE_oxpspasswordpolicyUpperCase' => 'Großbuchstaben (A...Z)', 'SHOP_MODULE_oxpspasswordpolicyLowerCase' => 'Kleinbuchstaben (a...z)', 'SHOP_MODULE_oxpspasswordpolicySpecial' => 'Sonderzeichen (!,@#$%^&*?_~()-)', - 'SHOP_MODULE_GROUP_passwordpolicy_api' => 'API Einstellungen', - 'SHOP_MODULE_oxpspasswordpolicyAPI' => 'Veröffentliche Passwörter überprüfen', + 'SHOP_MODULE_GROUP_passwordpolicy_api' => 'Verbrannte Passwörter', + 'SHOP_MODULE_oxpspasswordpolicyAPI' => 'Verbrannte Passwörter überprüfen', 'SHOP_MODULE_oxpspasswordpolicyHaveIBeenPwned' => 'HaveIBeenPwned', 'SHOP_MODULE_oxpspasswordpolicyEnzoic' => 'Enzoic', 'SHOP_MODULE_oxpspasswordpolicyEnzoicAPIKey' => 'Enzoic API Schlüssel', @@ -49,9 +49,8 @@ 'SHOP_MODULE_oxpspasswordpolicyRateLimitingDrivers_Memcached' => 'Memcached', 'SHOP_MODULE_oxpspasswordpolicyRateLimitingDrivers_APCu' => 'APCu', 'SHOP_MODULE_oxpspasswordpolicyRateLimitingLimit' => 'Einlogversuche pro Minute', - 'SHOP_MODULE_GROUP_passwordpolicy_memcached' => 'Memcached Einstellungen', - 'SHOP_MODULE_oxpspasswordpolicyMemcachedHost' => 'Host', - 'SHOP_MODULE_oxpspasswordpolicyMemcachedPort' => 'Port', + 'SHOP_MODULE_oxpspasswordpolicyMemcachedHost' => 'Memcached Host', + 'SHOP_MODULE_oxpspasswordpolicyMemcachedPort' => 'Memcached Port', 'SHOP_MODULE_GROUP_passwordpolicy_twofactor' => '2FA Authentifizierung', 'SHOP_MODULE_oxpspasswordpolicyTOTP' => '2FA aktivieren' diff --git a/views/admin/en/passwordpolicy_lang.php b/views/admin/en/passwordpolicy_lang.php index d9687e8b..2a82a767 100644 --- a/views/admin/en/passwordpolicy_lang.php +++ b/views/admin/en/passwordpolicy_lang.php @@ -30,7 +30,7 @@ 'SHOP_MODULE_oxpspasswordpolicyUpperCase' => 'Capital (UPPERCASE) letters (A...Z)', 'SHOP_MODULE_oxpspasswordpolicyLowerCase' => 'Lowercase letters (a...z)', 'SHOP_MODULE_oxpspasswordpolicySpecial' => 'Special characters (!,@#$%^&*?_~()-)', - 'SHOP_MODULE_GROUP_passwordpolicy_api' => 'API Settings', + 'SHOP_MODULE_GROUP_passwordpolicy_api' => 'Leaked passwords', 'SHOP_MODULE_oxpspasswordpolicyAPI' => 'Check leaked passwords', 'SHOP_MODULE_oxpspasswordpolicyHaveIBeenPwned' => 'HaveIBeenPwned', 'SHOP_MODULE_oxpspasswordpolicyEnzoic' => 'Enzoic', @@ -39,7 +39,7 @@ 'oxpspasswordpolicy_EnzoicError401' => 'Your entered Enzoic api/secret key is not valid.', 'oxpspasswordpolicy_EnzoicError0' => 'There was an error connecting to the Enzoic service. Please try again later.', 'oxpspasswordpolicy_EnzoicError500' => 'An unexpected error ocurred. Please try again later.', - 'SHOP_MODULE_GROUP_passwordpolicy_ratelimiting' => 'Rate Limiting Settings', + 'SHOP_MODULE_GROUP_passwordpolicy_ratelimiting' => 'Rate-limiting settings', 'SHOP_MODULE_oxpspasswordpolicyRateLimiting' => 'Active', 'SHOP_MODULE_oxpspasswordpolicyRateLimitingDrivers' => 'Drivers', 'SHOP_MODULE_oxpspasswordpolicyRateLimitingDrivers_Redis' => 'Redis', @@ -47,9 +47,8 @@ 'SHOP_MODULE_oxpspasswordpolicyRateLimitingDrivers_Memcached' => 'Memcached', 'SHOP_MODULE_oxpspasswordpolicyRateLimitingDrivers_APCu' => 'APCu', 'SHOP_MODULE_oxpspasswordpolicyRateLimitingLimit' => 'Login attemps per minute', - 'SHOP_MODULE_GROUP_passwordpolicy_memcached' => 'Memcached Settings', - 'SHOP_MODULE_oxpspasswordpolicyMemcachedHost' => 'Host', - 'SHOP_MODULE_oxpspasswordpolicyMemcachedPort' => 'Port', + 'SHOP_MODULE_oxpspasswordpolicyMemcachedHost' => 'Memcached Host', + 'SHOP_MODULE_oxpspasswordpolicyMemcachedPort' => 'Memcached Port', 'SHOP_MODULE_GROUP_passwordpolicy_twofactor' => '2FA Authentification', 'SHOP_MODULE_oxpspasswordpolicyTOTP' => 'Activate 2FA' ); From 392399505179d34ee714db1cf872c360468b8df9 Mon Sep 17 00:00:00 2001 From: moritz Date: Thu, 30 Sep 2021 17:01:43 +0200 Subject: [PATCH 145/199] changed language keys --- src/Controller/PasswordPolicyAccountTOTP.php | 2 +- .../PasswordPolicyTwoFactorConfirmation.php | 2 +- .../PasswordPolicyTwoFactorLogin.php | 2 +- .../PasswordPolicyTwoFactorRecovery.php | 2 +- .../PasswordPolicyTwoFactorRegister.php | 2 +- src/TwoFactorAuth/PasswordPolicyTOTP.php | 4 +- translations/de/passwordpolicy_lang.php | 49 ++++++++++--------- views/blocks/account_menu.tpl | 2 +- views/blocks/servicebox.tpl | 2 +- views/blocks/user_account.tpl | 4 +- views/tpl/twofactoraccount.tpl | 10 ++-- views/tpl/twofactorbackupcode.tpl | 8 +-- views/tpl/twofactorconfirmation.tpl | 6 +-- views/tpl/twofactorlogin.tpl | 8 +-- views/tpl/twofactorrecovery.tpl | 6 +-- 15 files changed, 55 insertions(+), 54 deletions(-) diff --git a/src/Controller/PasswordPolicyAccountTOTP.php b/src/Controller/PasswordPolicyAccountTOTP.php index b650ba95..f2e142e2 100644 --- a/src/Controller/PasswordPolicyAccountTOTP.php +++ b/src/Controller/PasswordPolicyAccountTOTP.php @@ -34,7 +34,7 @@ public function getBreadCrumb() $aPath['link'] = Registry::getSeoEncoder()->getStaticUrl($sSelfLink . 'cl=account'); $aPaths[] = $aPath; - $aPath['title'] = Registry::getLang()->translateString('TWOFACTORAUTHLOGIN', $iBaseLanguage, false); + $aPath['title'] = Registry::getLang()->translateString('OXPS_PASSWORDPOLICY_TWOFACTORAUTH_LOGIN', $iBaseLanguage, false); $aPath['link'] = $oUtils->cleanUrl($this->getLink(), ['fnc']); $aPaths[] = $aPath; diff --git a/src/Controller/PasswordPolicyTwoFactorConfirmation.php b/src/Controller/PasswordPolicyTwoFactorConfirmation.php index 04fca3f0..a34cdf74 100644 --- a/src/Controller/PasswordPolicyTwoFactorConfirmation.php +++ b/src/Controller/PasswordPolicyTwoFactorConfirmation.php @@ -51,7 +51,7 @@ public function getBreadCrumb() $aPaths = []; $aPath = []; $iBaseLanguage = Registry::getLang()->getBaseLanguage(); - $aPath['title'] = Registry::getLang()->translateString('TWOFACTORAUTHLOGIN', $iBaseLanguage, false); + $aPath['title'] = Registry::getLang()->translateString('OXPS_PASSWORDPOLICY_TWOFACTORAUTH_LOGIN', $iBaseLanguage, false); $aPath['link'] = $this->getLink(); $aPaths[] = $aPath; return $aPaths; diff --git a/src/Controller/PasswordPolicyTwoFactorLogin.php b/src/Controller/PasswordPolicyTwoFactorLogin.php index d41e108f..b5cef996 100644 --- a/src/Controller/PasswordPolicyTwoFactorLogin.php +++ b/src/Controller/PasswordPolicyTwoFactorLogin.php @@ -23,7 +23,7 @@ public function getBreadCrumb() $aPaths = []; $aPath = []; $iBaseLanguage = Registry::getLang()->getBaseLanguage(); - $aPath['title'] = Registry::getLang()->translateString('TWOFACTORAUTHLOGIN', $iBaseLanguage, false); + $aPath['title'] = Registry::getLang()->translateString('OXPS_PASSWORDPOLICY_TWOFACTORAUTH_LOGIN', $iBaseLanguage, false); $aPath['link'] = $this->getLink(); $aPaths[] = $aPath; return $aPaths; diff --git a/src/Controller/PasswordPolicyTwoFactorRecovery.php b/src/Controller/PasswordPolicyTwoFactorRecovery.php index 0a905734..b5daf597 100644 --- a/src/Controller/PasswordPolicyTwoFactorRecovery.php +++ b/src/Controller/PasswordPolicyTwoFactorRecovery.php @@ -30,7 +30,7 @@ public function redirect() $session->setVariable('usr', $usr); return 'start'; } - Registry::getUtilsView()->addErrorToDisplay('OXPS_PASSWORDPOLICY_TOTP_ERROR_WRONGBACKUPCODE'); + Registry::getUtilsView()->addErrorToDisplay('OXPS_PASSWORDPOLICY_TWOFACTORAUTH_ERROR_WRONGBACKUPCODE'); } public function resetCode($user) diff --git a/src/Controller/PasswordPolicyTwoFactorRegister.php b/src/Controller/PasswordPolicyTwoFactorRegister.php index 0ab5b25c..22811142 100644 --- a/src/Controller/PasswordPolicyTwoFactorRegister.php +++ b/src/Controller/PasswordPolicyTwoFactorRegister.php @@ -47,7 +47,7 @@ public function getBreadCrumb() $aPaths = []; $aPath = []; $iBaseLanguage = Registry::getLang()->getBaseLanguage(); - $aPath['title'] = Registry::getLang()->translateString('TWOFACTORAUTHREGISTER', $iBaseLanguage, false); + $aPath['title'] = Registry::getLang()->translateString('OXPS_PASSWORDPOLICY_TWOFACTORAUTH_REGISTER', $iBaseLanguage, false); $aPath['link'] = $this->getLink(); $aPaths[] = $aPath; return $aPaths; diff --git a/src/TwoFactorAuth/PasswordPolicyTOTP.php b/src/TwoFactorAuth/PasswordPolicyTOTP.php index 8de8c493..13134a28 100644 --- a/src/TwoFactorAuth/PasswordPolicyTOTP.php +++ b/src/TwoFactorAuth/PasswordPolicyTOTP.php @@ -36,7 +36,7 @@ public function verifyOTP(string $secret, string $auth, $user = null) { $totp = TOTP::create($secret); if (!$totp->verify($auth, null, 1) || $this->isOTPUsed($user, $auth)) { - throw oxNew(UserException::class, 'OXPS_PASSWORDPOLICY_TOTP_ERROR_WRONGOTP'); + throw oxNew(UserException::class, 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_ERROR_WRONGOTP'); } } @@ -45,7 +45,7 @@ public function isOTPUsed($user, string $auth) $otp = $user->oxuser__oxpsotp->value; if($otp == $auth) { - throw oxNew(UserException::class, 'OXPS_PASSWORDPOLICY_TOTP_ERROR_USEDOTP'); + throw oxNew(UserException::class, 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_ERROR_USEDOTP'); } return false; } diff --git a/translations/de/passwordpolicy_lang.php b/translations/de/passwordpolicy_lang.php index 961157aa..9ba0534c 100644 --- a/translations/de/passwordpolicy_lang.php +++ b/translations/de/passwordpolicy_lang.php @@ -53,29 +53,30 @@ 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESSPECIAL' => 'Das Passwort muss mindestens ein Sonderzeichen enthalten.', 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_WRONGTYPE' => 'Fehlerhafter Typ, bitte tragen Sie einen validen Wert ein. Bei weiteren Fragen wenden Sie sich an den Support.', 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_PASSWORD_KNOWN' => 'Das Passwort befindet sich bereits in einer gehackten Datenbank und ist somit unsicher.', - 'OXPS_PASSWORDPOLICY_TOTP_ERROR_WRONGOTP' => 'Der eingebene Code für die Zwei-Faktor-Authentifizierung war falsch.', - 'OXPS_PASSWORDPOLICY_TOTP_ERROR_USEDOTP' => 'Der eingegebene Code wurde bereits zum Login verwendet.', - 'OXPS_PASSWORDPOLICY_TOTP_ERROR_WRONGBACKUPCODE' => 'Der eingebene Backup Code war falsch.', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_ERROR_WRONGOTP' => 'Der eingebene Code für die Zwei-Faktor-Authentifizierung war falsch.', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_ERROR_USEDOTP' => 'Der eingegebene Code wurde bereits zum Login verwendet.', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_ERROR_WRONGBACKUPCODE' => 'Der eingebene Backup Code war falsch.', 'OXPS_PASSWORDPOLICY_RATELIMIT_EXCEEDED' => 'Sie haben sich zu oft versucht einzuloggen. Bitte versuchen Sie es später erneut.', - 'TWOFACTORAUTHREGISTER' => '2-Faktor-Authentifizierung Einrichtung', - 'TWOFACTORAUTHBACKUPCODE' => 'Backup-Code', - 'TWOFACTORAUTHRECOVERY' => '2-Faktor-Authentifizierung Reset', - 'TWOFACTORAUTHLOGIN' => '2-Faktor-Authentifizierung', - 'TWOFACTORAUTHCODE' => '2FA Code', - 'TWOFACTORAUTHCHECKBOX' => '2FA aktivieren', - 'TWOFACTORDEACTIVATE' => '2FA deaktivieren', - 'TWOFACTORCONTINUE' => 'Weiter', - 'TWOFACTORCONFIRM' => 'Verstanden', - 'TWOFACTORRESET' => 'Zurücksetzen', - 'MESSAGE_TWOFACTOR_HELP' => 'Die Zwei-Faktor-Authentifizierung bietet Ihrem Account eine zusätzliche Sicherheit. Durch aktivieren dieser Funktion, wird bei jedem Login nach einem zeitlich begrenzten einmaligen Passwort gefragt, welches Sie auf Ihrem Handy von einer App ablesen können.', - 'MESSAGE_TWOFACTOR_LOGIN' => 'Öffnen Sie jetzt Ihre Authentifizierungsapp und geben Sie den 6-stelligen Code ein.', - 'MESSAGE_TWOFACTOR_REGISTER' => 'Scannen Sie den abgebildeten QR Code mit ihrem Smartphone ein. Öffnen Sie dazu Ihre Kamera und richten Sie diese auf den Code.
Hinweis: Zum Verwenden der 2FA ist eine App erforderlich, welche Sie sich kostenlos herunterladen müssen. ', - 'MESSAGE_TWOFACTOR_SUCCESS' => '2FA wurde erfolgreich eingerichtet.', - 'MESSAGE_TWOFACTOR_REGISTER_SUCCESS' => 'Sie haben sich erfolgreich registriert und richten nun die 2-Faktor-Authentifizierung ein.', - 'MESSAGE_TWOFACTOR_DEACTIVATED' => '2FA wurde deaktiviert.', - 'MESSAGE_TWOFACTOR_BACKUPCODE' => 'Für Sie wurde ein Wiederherstellungscode generiert. Falls Sie mal Ihr Gerät für die 2-Faktor-Authentifizierung verlieren sollten, ist dieser die einzige Möglichkeit um wieder Zugang zu Ihrem Account zu erlangen.
Bewahren Sie den Wiederherstellungscode an einem sicheren Ort auf!', - 'MESSAGE_TWOFACTOR_BACKUPCODE_SUCCESS' => 'Die 2FA wurde erfolgreich resettet. ', - 'MESSAGE_TWOFACTOR_RECOVERY' => 'Falls Sie Ihr Gerät für die 2FA verloren haben oder es sonstige Probleme damit gibt, geben Sie den einmaligen Wiederherstellungscode ein, der Ihnen beim Einrichten der 2FA angezeigt wurde. Anschließend wird 2FA bei Ihrem Account deaktiviert.', - 'MESSAGE_TWOFACTOR_LOST' => 'Sie brauchen Hilfe beim Login/haben Ihr Gerät für die 2FA verloren? Klicken Sie hier.', - 'OXPS_CANNOTSTOREUSERSECRET' => 'Die 2-Faktor Einrichtung kann derzeit nicht durchgeführt werden, bitten versuchen Sie es später nochmal oder kontaktieren Sie uns.' + 'OXPS_PASSWORDPOLICY_RATELIMIT_TWOFACTOR_EXCEEDED' => 'Sie haben zu oft den falschen Code für die Zwei-Faktor-Authentifizierung eingegeben. Bitte versuchen Sie es später erneut.', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_REGISTER' => '2-Faktor-Authentifizierung Einrichtung', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_BACKUPCODE' => 'Backup-Code', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_RECOVERY' => '2-Faktor-Authentifizierung Reset', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_LOGIN' => '2-Faktor-Authentifizierung', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_ACTIVATE' => '2FA aktivieren', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_DEACTIVATE' => '2FA deaktivieren', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_CONTINUE' => 'Weiter', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_CONFIRM' => 'Verstanden', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_RESET' => 'Zurücksetzen', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_HELP_INFO' => 'Die Zwei-Faktor-Authentifizierung bietet Ihrem Account eine zusätzliche Sicherheit. Durch aktivieren dieser Funktion, wird bei jedem Login nach einem zeitlich begrenzten einmaligen Passwort gefragt, welches Sie auf Ihrem Handy von einer App ablesen können.', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_HELP_LOGIN' => 'Öffnen Sie jetzt Ihre Authentifizierungsapp und geben Sie den 6-stelligen Code ein.', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_HELP_REGISTER' => 'Scannen Sie den abgebildeten QR Code mit ihrem Smartphone ein. Öffnen Sie dazu Ihre Kamera und richten Sie diese auf den Code.
Hinweis: Zum Verwenden der 2FA ist eine App erforderlich, welche Sie sich kostenlos herunterladen müssen. ', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_HELP_RECOVERY' => 'Falls Sie Ihr Gerät für die 2FA verloren haben oder es sonstige Probleme damit gibt, geben Sie den einmaligen Wiederherstellungscode ein, der Ihnen beim Einrichten der 2FA angezeigt wurde. Anschließend wird 2FA bei Ihrem Account deaktiviert.', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_HELP_LOST' => 'Sie brauchen Hilfe beim Login/haben Ihr Gerät für die 2FA verloren? Klicken Sie hier.', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_HELP_BACKUPCODE' => 'Für Sie wurde ein Wiederherstellungscode generiert. Falls Sie mal Ihr Gerät für die 2-Faktor-Authentifizierung verlieren sollten, ist dieser die einzige Möglichkeit um wieder Zugang zu Ihrem Account zu erlangen.
Bewahren Sie den Wiederherstellungscode an einem sicheren Ort auf!', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_ACTIVATED' => '2FA wurde erfolgreich eingerichtet.', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_DEACTIVATED' => '2FA wurde deaktiviert.', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_REGISTER_SUCCESS' => 'Sie haben sich erfolgreich registriert und richten nun die 2-Faktor-Authentifizierung ein.', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_REGISTER_ERROR' => 'Die 2-Faktor Einrichtung kann derzeit nicht durchgeführt werden, bitten versuchen Sie es später nochmal oder kontaktieren Sie uns.', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_RESET_SUCCESS' => 'Die 2FA wurde erfolgreich resettet. ' + ); diff --git a/views/blocks/account_menu.tpl b/views/blocks/account_menu.tpl index 375448cb..89a9473e 100644 --- a/views/blocks/account_menu.tpl +++ b/views/blocks/account_menu.tpl @@ -1,6 +1,6 @@ [{$smarty.block.parent}] [{if $oViewConf->isTOTP()}] [{/if}] \ No newline at end of file diff --git a/views/blocks/servicebox.tpl b/views/blocks/servicebox.tpl index fba636aa..6bc6d1fc 100644 --- a/views/blocks/servicebox.tpl +++ b/views/blocks/servicebox.tpl @@ -1,6 +1,6 @@ [{$smarty.block.parent}] [{if $oViewConf->isTOTP() && $oxcmp_user}]
  • - [{oxmultilang ident="TWOFACTORAUTHLOGIN"}] + [{oxmultilang ident="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_LOGIN"}]
  • [{/if}] \ No newline at end of file diff --git a/views/blocks/user_account.tpl b/views/blocks/user_account.tpl index e6168171..b7b21bf2 100644 --- a/views/blocks/user_account.tpl +++ b/views/blocks/user_account.tpl @@ -4,10 +4,10 @@
    - [{oxmultilang ident="MESSAGE_TWOFACTOR_HELP"}] + [{oxmultilang ident="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_HELP_INFO"}]
    [{/if}] diff --git a/views/tpl/twofactoraccount.tpl b/views/tpl/twofactoraccount.tpl index c74fdc18..7c05b890 100644 --- a/views/tpl/twofactoraccount.tpl +++ b/views/tpl/twofactoraccount.tpl @@ -1,11 +1,11 @@ [{capture append="oxidBlock_content"}] - [{assign var="template_title" value="TWOFACTORAUTHLOGIN"|oxmultilangassign}] + [{assign var="template_title" value="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_LOGIN"|oxmultilangassign}] [{if $success == '1'}] -
    [{oxmultilang ident="MESSAGE_TWOFACTOR_SUCCESS"}]
    +
    [{oxmultilang ident="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_ACTIVATED"}]
    [{elseif $success == '2'}] -
    [{oxmultilang ident="MESSAGE_TWOFACTOR_DEACTIVATED"}]
    +
    [{oxmultilang ident="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_DEACTIVATED"}]
    [{/if}] -

    [{oxmultilang ident="TWOFACTORAUTHLOGIN"}]

    +

    [{oxmultilang ident="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_LOGIN"}]

    - +
    - +
    diff --git a/views/tpl/twofactorconfirmation.tpl b/views/tpl/twofactorconfirmation.tpl index 0fbd5c9b..38bf891d 100644 --- a/views/tpl/twofactorconfirmation.tpl +++ b/views/tpl/twofactorconfirmation.tpl @@ -5,8 +5,8 @@
    [{include file="message/errors.tpl"}]
    -

    [{oxmultilang ident="TWOFACTORAUTHLOGIN"}]

    - [{oxmultilang ident="MESSAGE_TWOFACTOR_LOGIN"}] +

    [{oxmultilang ident="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_LOGIN"}]

    + [{oxmultilang ident="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_HELP_LOGIN"}]
    - +
    diff --git a/views/tpl/twofactorlogin.tpl b/views/tpl/twofactorlogin.tpl index e1cfffc8..a5cb158f 100644 --- a/views/tpl/twofactorlogin.tpl +++ b/views/tpl/twofactorlogin.tpl @@ -5,8 +5,8 @@
    [{include file="message/errors.tpl"}]
    -

    [{oxmultilang ident="TWOFACTORAUTHLOGIN"}]

    - [{oxmultilang ident="MESSAGE_TWOFACTOR_LOGIN"}] +

    [{oxmultilang ident="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_LOGIN"}]

    + [{oxmultilang ident="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_HELP_LOGIN"}]
    - [{oxmultilang ident="MESSAGE_TWOFACTOR_LOST"}] + [{oxmultilang ident="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_HELP_LOST"}]
    - +
    diff --git a/views/tpl/twofactorrecovery.tpl b/views/tpl/twofactorrecovery.tpl index bfda109a..26e5af0d 100644 --- a/views/tpl/twofactorrecovery.tpl +++ b/views/tpl/twofactorrecovery.tpl @@ -5,8 +5,8 @@
    [{include file="message/errors.tpl"}]
    -

    [{oxmultilang ident="TWOFACTORAUTHRECOVERY"}]

    - [{oxmultilang ident="MESSAGE_TWOFACTOR_RECOVERY"}] +

    [{oxmultilang ident="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_RECOVERY"}]

    + [{oxmultilang ident="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_HELP_RECOVERY"}]
    - +
    From 0c5aa81e9177fd4621a73db2a710c9b0cb043466 Mon Sep 17 00:00:00 2001 From: moritz Date: Thu, 30 Sep 2021 17:09:27 +0200 Subject: [PATCH 146/199] added rate limiting to 2FA --- src/Model/PasswordPolicyUser.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Model/PasswordPolicyUser.php b/src/Model/PasswordPolicyUser.php index 6ad4a3dd..2fdd8a32 100644 --- a/src/Model/PasswordPolicyUser.php +++ b/src/Model/PasswordPolicyUser.php @@ -9,7 +9,6 @@ use OxidEsales\Eshop\Core\Field; use OxidEsales\Eshop\Core\InputValidator; use OxidEsales\Eshop\Core\Registry; -use OxidEsales\Eshop\Core\ViewConfig; use OxidEsales\EshopCommunity\Internal\Container\ContainerFactory; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyConfig; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyValidator; @@ -90,11 +89,22 @@ public function finalizeLogin($otp, $setsessioncookie = false) { $container = ContainerFactory::getInstance()->getContainer(); $totp = $container->get(PasswordPolicyTOTP::class); - $config = $container->get(Config::class); + $passwordpolicyConfig = $container->get(PasswordPolicyConfig::class); + $config = oxNew(Config::class); $session = Registry::getSession(); $usr = $session->getVariable('tmpusr'); $this->load($usr); $secret = $this->oxuser__oxpstotpsecret->value; + if ($passwordpolicyConfig->isRateLimiting()) { + $driverName = $passwordpolicyConfig->getSelectedDriver(); + $rateLimiter = (new PasswordPolicyRateLimiterFactory())->getRateLimiter($driverName)->getLimiter(); + // checks whether rate limit is exceeded + try { + $rateLimiter->limit($secret, Rate::perMinute($passwordpolicyConfig->getRateLimit())); + } catch (LimitExceeded $exception) { + throw oxNew(UserException::class, 'OXPS_PASSWORDPOLICY_RATELIMIT_TWOFACTOR_EXCEEDED'); + } + } $decryptedSecret = $totp->decryptSecret($secret); $totp->verifyOTP($decryptedSecret, $otp, $this); $this->oxuser__oxpsotp = new Field($otp, Field::T_TEXT); From b4c328fa55ba93081de5ae7e317d609dea74aef1 Mon Sep 17 00:00:00 2001 From: moritz Date: Thu, 30 Sep 2021 17:09:38 +0200 Subject: [PATCH 147/199] translation keys changed --- views/tpl/twofactorregister.tpl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/views/tpl/twofactorregister.tpl b/views/tpl/twofactorregister.tpl index abf3b0b8..57fb1414 100644 --- a/views/tpl/twofactorregister.tpl +++ b/views/tpl/twofactorregister.tpl @@ -8,8 +8,8 @@
    [{include file="message/errors.tpl"}]
    -

    [{oxmultilang ident="TWOFACTORAUTHREGISTER"}]

    - [{oxmultilang ident="MESSAGE_TWOFACTOR_REGISTER"}] +

    [{oxmultilang ident="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_REGISTER"}]

    + [{oxmultilang ident="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_HELP_REGISTER"}]
    - +
    From 9ca108e40a492a3e9b666129a44dcc7a94d4fd19 Mon Sep 17 00:00:00 2001 From: moritz Date: Thu, 30 Sep 2021 18:16:44 +0200 Subject: [PATCH 148/199] now checks whether the columns are already there --- src/Core/PasswordPolicyEvents.php | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/Core/PasswordPolicyEvents.php b/src/Core/PasswordPolicyEvents.php index a0af2743..394261d0 100644 --- a/src/Core/PasswordPolicyEvents.php +++ b/src/Core/PasswordPolicyEvents.php @@ -4,20 +4,31 @@ namespace OxidProfessionalServices\PasswordPolicy\Core; +use OxidEsales\Eshop\Application\Model\User; +use OxidEsales\Eshop\Core\DatabaseProvider; use OxidEsales\Eshop\Core\DbMetaDataHandler; +use OxidEsales\Eshop\Core\Exception\UserException; use OxidEsales\Eshop\Core\ViewConfig; +use OxidEsales\EshopCommunity\Core\Exception\FileException; class PasswordPolicyEvents { + /** + * @throws UserException + * @throws FileException + */ public static function onActivate() { - $query = "ALTER TABLE oxuser ADD OXPSTOTPSECRET varchar(255) NOT NULL, + $user = oxNew(User::class); + if(!(in_array('oxpstotpsecret',$user->getFieldNames()) && in_array('oxpsbackupcode',$user->getFieldNames()))) { + $query = "ALTER TABLE oxuser ADD OXPSTOTPSECRET varchar(255) NOT NULL, ADD OXPSBACKUPCODE varchar(255) NOT NULL;"; - try { - \OxidEsales\Eshop\Core\DatabaseProvider::getDb()->execute($query); - self::regenerateViews(); - }catch (\Exception $exception) - { + try { + DatabaseProvider::getDb()->execute($query); + self::regenerateViews(); + } catch (\Exception $exception) { + throw new UserException("Ein Fehler ist bei der Erzeugung der neuen Datenbankspalten aufgetreten: \n" . $exception); + } } $viewConf = oxNew(ViewConfig::class); $modulePath = $viewConf->getModulePath('oxpspasswordpolicy'); From 9fcb58f61b21b996df67fef6ad8a48f81bc3c279 Mon Sep 17 00:00:00 2001 From: moritz Date: Thu, 30 Sep 2021 19:48:24 +0200 Subject: [PATCH 149/199] now checks when activating the feature for admins whether they have a valid mail --- .../PasswordPolicyModuleConfiguration.php | 24 ++++++++++++++++++- src/Core/PasswordPolicyConfig.php | 6 +++++ views/admin/de/passwordpolicy_lang.php | 3 +++ views/admin/en/passwordpolicy_lang.php | 3 +++ 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/Controller/Admin/PasswordPolicyModuleConfiguration.php b/src/Controller/Admin/PasswordPolicyModuleConfiguration.php index 59d87e47..c9838371 100644 --- a/src/Controller/Admin/PasswordPolicyModuleConfiguration.php +++ b/src/Controller/Admin/PasswordPolicyModuleConfiguration.php @@ -2,8 +2,8 @@ namespace OxidProfessionalServices\PasswordPolicy\Controller\Admin; -use Enzoic\AuthenticationException; use Enzoic\Enzoic; +use OxidEsales\Eshop\Core\DatabaseProvider; use OxidEsales\Eshop\Core\Registry; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyConfig; @@ -25,6 +25,28 @@ public function saveConfVars() $_POST["confbools"][PasswordPolicyConfig::SettingEnzoic] = "false"; } } + if($variables[PasswordPolicyConfig::SettingAdminUser] == "true") + { + $query = "Select oxusername from oxuser where oxrights != 'user'"; + $result = DatabaseProvider::getDb()->getCol($query); + $invalidMails = []; + foreach ($result as $user) + { + if(!filter_var($user, FILTER_VALIDATE_EMAIL)) + { + $invalidMails[] = $user; + } + } + if(!empty($invalidMails)) + { + $_POST["confbools"][PasswordPolicyConfig::SettingAdminUser] = "false"; + Registry::getUtilsView()->addErrorToDisplay("OXPS_PASSWORDPOLICY_INVALIDADMINUSERS"); + foreach ($invalidMails as $invalidUser) + { + Registry::getUtilsView()->addErrorToDisplay($invalidUser); + } + } + } parent::saveConfVars(); } diff --git a/src/Core/PasswordPolicyConfig.php b/src/Core/PasswordPolicyConfig.php index 5d5f1c2d..84602ca1 100644 --- a/src/Core/PasswordPolicyConfig.php +++ b/src/Core/PasswordPolicyConfig.php @@ -50,6 +50,7 @@ class PasswordPolicyConfig public const SettingMemcachedHost = self::SettingsPrefix . 'MemcachedHost'; public const SettingMemcachedPort = self::SettingsPrefix . 'MemcachedPort'; public const SettingTOTP = self::SettingsPrefix . 'TOTP'; + public const SettingAdminUser = self::SettingsPrefix . 'admin'; public function getMinPasswordLength(): int { @@ -129,6 +130,11 @@ public function isTOTP(): bool return $this->isConfigParam(self::SettingTOTP); } + public function isAdminUsers(): bool + { + return $this->isConfigParam(self::SettingAdminUser); + } + public function getSelectedDriver(): string { return (string) Registry::getConfig()->getConfigParam(self::SettingDrivers); diff --git a/views/admin/de/passwordpolicy_lang.php b/views/admin/de/passwordpolicy_lang.php index 83ebb6a6..bfcd1b24 100644 --- a/views/admin/de/passwordpolicy_lang.php +++ b/views/admin/de/passwordpolicy_lang.php @@ -36,9 +36,12 @@ 'SHOP_MODULE_oxpspasswordpolicyEnzoic' => 'Enzoic', 'SHOP_MODULE_oxpspasswordpolicyEnzoicAPIKey' => 'Enzoic API Schlüssel', 'SHOP_MODULE_oxpspasswordpolicyEnzoicSecretKey' => 'Enzoic Geheimschlüssel', + 'SHOP_MODULE_GROUP_passwordpolicy_admin' => "Admin Einstellungen", + 'SHOP_MODULE_oxpspasswordpolicyadmin' => "Sicherheitsfeatures für Admins aktivieren", 'OXPS_PASSWORDPOLICY_ENZOICERROR401' => 'Ihr Enzoic API Key oder Secret Key ist nicht gültig. Sie sind nicht autorisiert.', 'OXPS_PASSWORDPOLICY_ENZOICERRORError0' => 'Es gibt ein Problem beim Verbinden mit der Enzoic API. Bitte versuchen Sie es erneut.', 'OXPS_PASSWORDPOLICY_ENZOICERROR500' => 'Ein unerwarteter Fehler ist aufgetreten. Btte probieren Sie es später erneut.', + 'OXPS_PASSWORDPOLICY_INVALIDADMINUSERS' => "Um diese Funktion nutzen zu können, müssen alle Administratoren eine gültige Mail besitzen. \n Folgende Administratoren haben eine ungültige Mail:", 'OXPS_PASSWORDPOLICY_RATELIMIT_EXCEEDED' => 'Sie haben sich zu oft versucht einzuloggen. Bitte versuchen Sie es später erneut.', 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_PASSWORD_KNOWN' => 'Das Passwort befindet sich bereits in einer gehackten Datenbank und ist somit unsicher.', 'SHOP_MODULE_GROUP_passwordpolicy_ratelimiting' => 'Rate Limiting Einstellungen', diff --git a/views/admin/en/passwordpolicy_lang.php b/views/admin/en/passwordpolicy_lang.php index 2a82a767..c01af719 100644 --- a/views/admin/en/passwordpolicy_lang.php +++ b/views/admin/en/passwordpolicy_lang.php @@ -36,9 +36,12 @@ 'SHOP_MODULE_oxpspasswordpolicyEnzoic' => 'Enzoic', 'SHOP_MODULE_oxpspasswordpolicyEnzoicAPIKey' => 'Enzoic API key', 'SHOP_MODULE_oxpspasswordpolicyEnzoicSecretKey' => 'Enzoic secret key', + 'SHOP_MODULE_GROUP_passwordpolicy_admin' => "Admin settings", + 'SHOP_MODULE_oxpspasswordpolicyadmin' => "Activate security features for admin users", 'oxpspasswordpolicy_EnzoicError401' => 'Your entered Enzoic api/secret key is not valid.', 'oxpspasswordpolicy_EnzoicError0' => 'There was an error connecting to the Enzoic service. Please try again later.', 'oxpspasswordpolicy_EnzoicError500' => 'An unexpected error ocurred. Please try again later.', + 'OXPS_PASSWORDPOLICY_INVALIDADMINUSERS' => 'To use this feature, all admins must have a valid mail address. The following admins have an invalid mail address:', 'SHOP_MODULE_GROUP_passwordpolicy_ratelimiting' => 'Rate-limiting settings', 'SHOP_MODULE_oxpspasswordpolicyRateLimiting' => 'Active', 'SHOP_MODULE_oxpspasswordpolicyRateLimitingDrivers' => 'Drivers', From cad84fc06754e46ee24036112c2f4d203219d4f9 Mon Sep 17 00:00:00 2001 From: moritz Date: Thu, 30 Sep 2021 19:56:57 +0200 Subject: [PATCH 150/199] cleaning tmp folder when activating the module --- src/Core/PasswordPolicyEvents.php | 72 ++++++++++++++++++++++++++----- 1 file changed, 61 insertions(+), 11 deletions(-) diff --git a/src/Core/PasswordPolicyEvents.php b/src/Core/PasswordPolicyEvents.php index 394261d0..7591b607 100644 --- a/src/Core/PasswordPolicyEvents.php +++ b/src/Core/PasswordPolicyEvents.php @@ -20,25 +20,26 @@ class PasswordPolicyEvents public static function onActivate() { $user = oxNew(User::class); - if(!(in_array('oxpstotpsecret',$user->getFieldNames()) && in_array('oxpsbackupcode',$user->getFieldNames()))) { + if (!(in_array('oxpstotpsecret', $user->getFieldNames()) && in_array('oxpsbackupcode', $user->getFieldNames()))) { $query = "ALTER TABLE oxuser ADD OXPSTOTPSECRET varchar(255) NOT NULL, ADD OXPSBACKUPCODE varchar(255) NOT NULL;"; try { DatabaseProvider::getDb()->execute($query); self::regenerateViews(); } catch (\Exception $exception) { - throw new UserException("Ein Fehler ist bei der Erzeugung der neuen Datenbankspalten aufgetreten: \n" . $exception); + throw oxNew(UserException::class, "Ein Fehler ist bei der Erzeugung der neuen Datenbankspalten aufgetreten: \n" . $exception); } + self::clearTmp(); } - $viewConf = oxNew(ViewConfig::class); - $modulePath = $viewConf->getModulePath('oxpspasswordpolicy'); - $filePath = $modulePath . 'twofactor.config.inc.php'; - if(!file_exists($filePath)) - { - $key = self::generateKey(); - file_put_contents($filePath, 'getModulePath('oxpspasswordpolicy'); + $filePath = $modulePath . 'twofactor.config.inc.php'; + if (!file_exists($filePath)) { + $key = self::generateKey(); + file_put_contents($filePath, 'key=' . "\"$key\";"); - } + } } @@ -48,9 +49,58 @@ protected static function generateKey(): string $hashedKey = base64_encode($key); return $hashedKey; } + protected static function regenerateViews() { - $oDbMetaDataHandler = oxNew(DbMetaDataHandler::class ); + $oDbMetaDataHandler = oxNew(DbMetaDataHandler::class); $oDbMetaDataHandler->updateViews(); } + + public static function clearTmp($clearFolderPath = '') + { + $folderPath = self::_getFolderToClear($clearFolderPath); + $directoryHandler = opendir($folderPath); + + if (!empty($directoryHandler)) { + while (false !== ($fileName = readdir($directoryHandler))) { + $filePath = $folderPath . DIRECTORY_SEPARATOR . $fileName; + self::_clear($fileName, $filePath); + } + + closedir($directoryHandler); + } + + return true; + } + + protected static function _getFolderToClear($clearFolderPath = '') + { + $templateFolderPath = (string)\OxidEsales\Eshop\Core\Registry::getConfig()->getConfigParam('sCompileDir'); + + if (!empty($clearFolderPath) and (strpos($clearFolderPath, $templateFolderPath) !== false)) { + $folderPath = $clearFolderPath; + } else { + $folderPath = $templateFolderPath; + } + + return $folderPath; + } + + /** + * Check if resource could be deleted, then delete it's a file or + * call recursive folder deletion if it's a directory. + * + * @param string $fileName + * @param string $filePath + */ + protected static function _clear($fileName, $filePath) + { + if (!in_array($fileName, ['.', '..', '.gitkeep', '.htaccess'])) { + if (is_file($filePath)) { + @unlink($filePath); + } else { + self::clearTmp($filePath); + } + } + } } \ No newline at end of file From cf53d92f9e3a9a3cb95031432042e6fb81af1afc Mon Sep 17 00:00:00 2001 From: moritz Date: Thu, 30 Sep 2021 20:07:17 +0200 Subject: [PATCH 151/199] added functionality for admins --- metadata.php | 1 + 1 file changed, 1 insertion(+) diff --git a/metadata.php b/metadata.php index e8d392f3..1e2a5a98 100644 --- a/metadata.php +++ b/metadata.php @@ -140,6 +140,7 @@ ['group' => 'passwordpolicy_ratelimiting', 'name' => 'oxpspasswordpolicyMemcachedHost', 'type' => 'str', 'value' => 'memcached'], ['group' => 'passwordpolicy_ratelimiting', 'name' => 'oxpspasswordpolicyMemcachedPort', 'type' => 'num', 'value' => 11211], ['group' => 'passwordpolicy_twofactor', 'name' => 'oxpspasswordpolicyTOTP', 'type' => 'bool', 'value' => false], + ['group' => 'passwordpolicy_admin', 'name' => 'oxpspasswordpolicyadmin', 'type' => 'bool', 'value' => false] ], 'events' => array( 'onActivate' => 'OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyEvents::onActivate', From 958d26b60b4d394df1316d7d20f1a08110c34c02 Mon Sep 17 00:00:00 2001 From: moritz Date: Fri, 1 Oct 2021 16:52:01 +0200 Subject: [PATCH 152/199] requires the modified enzoic lib --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 9d23b419..64624dba 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "require": { "php": ">=7.3", "ext-json": "*", - "enzoic/enzoic": "dev-master", + "moritzdemmer/enzoic": "dev-master", "divineomega/password_exposed": "v3.2.0", "nikolaposa/rate-limit": "dev-master", "spomky-labs/otphp": "v10.0.1", From 2dd81c4a5da28587fcb889bb2c32f5d6334c11e1 Mon Sep 17 00:00:00 2001 From: moritz Date: Wed, 6 Oct 2021 13:33:07 +0200 Subject: [PATCH 153/199] trying to add 2fA for backend --- menu.xml | 7 ++- metadata.php | 10 +++- .../Admin/PasswordPolicyAccountTOTPAdmin.php | 39 ++++++++++++++ .../PasswordPolicyTwoFactorRegisterAdmin.php | 46 ++++++++++++++++ src/Core/PasswordPolicyEvents.php | 1 + src/Model/PasswordPolicyUser.php | 5 +- views/admin/de/passwordpolicy_lang.php | 54 ++++++++++++++++++- views/admin/tpl/admin_twofactoraccount.tpl | 33 ++++++++++++ views/admin/tpl/admin_twofactorregister.tpl | 28 ++++++++++ views/admin/tpl/layout/page.tpl | 3 ++ 10 files changed, 217 insertions(+), 9 deletions(-) create mode 100644 src/Controller/Admin/PasswordPolicyAccountTOTPAdmin.php create mode 100644 src/Controller/Admin/PasswordPolicyTwoFactorRegisterAdmin.php create mode 100644 views/admin/tpl/admin_twofactoraccount.tpl create mode 100644 views/admin/tpl/admin_twofactorregister.tpl create mode 100644 views/admin/tpl/layout/page.tpl diff --git a/menu.xml b/menu.xml index 1e2a518e..ee01e950 100644 --- a/menu.xml +++ b/menu.xml @@ -1,10 +1,9 @@ - - - - + + + diff --git a/metadata.php b/metadata.php index 1e2a5a98..2135228d 100644 --- a/metadata.php +++ b/metadata.php @@ -32,6 +32,8 @@ use OxidEsales\Eshop\Application\Model\User; use OxidEsales\Eshop\Application\Controller\Admin\ModuleConfiguration; use OxidProfessionalServices\PasswordPolicy\Component\PasswordPolicyUserComponent; +use OxidProfessionalServices\PasswordPolicy\Controller\Admin\PasswordPolicyAccountTOTPAdmin; +use OxidProfessionalServices\PasswordPolicy\Controller\Admin\PasswordPolicyTwoFactorRegisterAdmin; use OxidProfessionalServices\PasswordPolicy\Controller\PasswordPolicyAccountTOTP; use OxidProfessionalServices\PasswordPolicy\Controller\PasswordPolicyTwoFactorConfirmation; use OxidProfessionalServices\PasswordPolicy\Controller\PasswordPolicyTwoFactorBackupCode; @@ -78,7 +80,10 @@ 'twofactoraccount' => PasswordPolicyAccountTOTP::class, 'twofactorconfirmation' => PasswordPolicyTwoFactorConfirmation::class, 'twofactorbackup' => PasswordPolicyTwoFactorBackupCode::class, - 'twofactorrecovery' => PasswordPolicyTwoFactorRecovery::class + 'twofactorrecovery' => PasswordPolicyTwoFactorRecovery::class, + 'admin_twofactoraccount' => PasswordPolicyAccountTOTPAdmin::class, + 'admin_twofactorregister' => PasswordPolicyTwoFactorRegisterAdmin::class, + ], 'templates' => [ 'twofactorregister.tpl' => 'oxps/passwordpolicy/views/tpl/twofactorregister.tpl', @@ -87,6 +92,9 @@ 'twofactorconfirmation.tpl' => 'oxps/passwordpolicy/views/tpl/twofactorconfirmation.tpl', 'twofactorbackupcode.tpl' => 'oxps/passwordpolicy/views/tpl/twofactorbackupcode.tpl', 'twofactorrecovery.tpl' => 'oxps/passwordpolicy/views/tpl/twofactorrecovery.tpl', + 'admin_twofactoraccount.tpl' => 'oxps/passwordpolicy/views/admin/tpl/admin_twofactoraccount.tpl', + 'admin_twofactorregister.tpl' => 'oxps/passwordpolicy/views/admin/tpl/admin_twofactorregister.tpl', + 'layout/page.tpl' => 'oxps/passwordpolicy/views/admin/tpl/layout/page.tpl' ], 'blocks' => [ diff --git a/src/Controller/Admin/PasswordPolicyAccountTOTPAdmin.php b/src/Controller/Admin/PasswordPolicyAccountTOTPAdmin.php new file mode 100644 index 00000000..7c73576a --- /dev/null +++ b/src/Controller/Admin/PasswordPolicyAccountTOTPAdmin.php @@ -0,0 +1,39 @@ +getUser(); + $secret = $user->oxuser__oxpstotpsecret->value; + return $secret != null; + } + + public function redirect() + { + $totpenabled = (new Request())->getRequestEscapedParameter('status'); + if ($totpenabled && !$this->isTOTP()) { + \OxidEsales\Eshop\Core\Registry::getUtils()->redirect($this->getViewConfig()->getSelfLink() . 'cl=admin_twofactorregister&step=settings'); + } + elseif (!$totpenabled && $this->isTOTP()) + { + return 'twofactorconfirmation'; + } + } +} \ No newline at end of file diff --git a/src/Controller/Admin/PasswordPolicyTwoFactorRegisterAdmin.php b/src/Controller/Admin/PasswordPolicyTwoFactorRegisterAdmin.php new file mode 100644 index 00000000..115c3558 --- /dev/null +++ b/src/Controller/Admin/PasswordPolicyTwoFactorRegisterAdmin.php @@ -0,0 +1,46 @@ +getContainer(); + $this->TOTP = $container->get(PasswordPolicyTOTP::class); + $this->qrCodeRenderer = $container->get(PasswordPolicyQrCodeRenderer::class); + } + + public function render() + { + $step = (new Request())->getRequestEscapedParameter('step'); + $paymentActionLink = (new Request())->getRequestEscapedParameter('paymentActionLink'); + $success = (new Request())->getRequestEscapedParameter('success'); + $this->addTplParam('step', $step); + $this->addTplParam('paymentActionLink', $paymentActionLink); + $this->addTplParam('success', $success); + parent::render(); + return 'admin_twofactorregister.tpl'; + } + + public function getTOTPQrCode() + { + $TOTPurl = $this->TOTP->getTotpQrUrl(); + $qrcode = $this->qrCodeRenderer->generateQrCode($TOTPurl); + return $qrcode; + } + + +} \ No newline at end of file diff --git a/src/Core/PasswordPolicyEvents.php b/src/Core/PasswordPolicyEvents.php index 7591b607..470239a0 100644 --- a/src/Core/PasswordPolicyEvents.php +++ b/src/Core/PasswordPolicyEvents.php @@ -19,6 +19,7 @@ class PasswordPolicyEvents */ public static function onActivate() { + $user = oxNew(User::class); if (!(in_array('oxpstotpsecret', $user->getFieldNames()) && in_array('oxpsbackupcode', $user->getFieldNames()))) { $query = "ALTER TABLE oxuser ADD OXPSTOTPSECRET varchar(255) NOT NULL, diff --git a/src/Model/PasswordPolicyUser.php b/src/Model/PasswordPolicyUser.php index 2fdd8a32..49cb1b10 100644 --- a/src/Model/PasswordPolicyUser.php +++ b/src/Model/PasswordPolicyUser.php @@ -31,13 +31,12 @@ public function onLogin($userName, $password) { /** @var PasswordPolicyValidator $passValidator */ $passValidator = oxNew(InputValidator::class); - if ($this->isLoaded() && $err = $passValidator->validatePassword($userName, $password)) { - if(!$this->isAdmin()) { + if (!$this->isAdmin() && $this->isLoaded() && $err = $passValidator->validatePassword($userName, $password)) { $forgotPass = new ForgotPasswordController(); $forgotPass->forgotPassword(); $errorMessage = $err->getMessage() . ' ' . Registry::getLang()->translateString('REQUEST_PASSWORD_AFTERCLICK'); throw oxNew(UserException::class, $errorMessage); - } + // redirects admin to password reset page *temporary solution /* $oViewConf = oxNew(ViewConfig::class); $passwordResetLink = $oViewConf->getBaseDir() . 'index.php?cl=forgotpwd&uid=' . $this->getUpdateId() . '&lang=' . $oViewConf->getActLanguageId() . '&shp=' . $this->getShopId(); diff --git a/views/admin/de/passwordpolicy_lang.php b/views/admin/de/passwordpolicy_lang.php index bfcd1b24..e3dc77a0 100644 --- a/views/admin/de/passwordpolicy_lang.php +++ b/views/admin/de/passwordpolicy_lang.php @@ -55,6 +55,58 @@ 'SHOP_MODULE_oxpspasswordpolicyMemcachedHost' => 'Memcached Host', 'SHOP_MODULE_oxpspasswordpolicyMemcachedPort' => 'Memcached Port', 'SHOP_MODULE_GROUP_passwordpolicy_twofactor' => '2FA Authentifizierung', - 'SHOP_MODULE_oxpspasswordpolicyTOTP' => '2FA aktivieren' + 'SHOP_MODULE_oxpspasswordpolicyTOTP' => '2FA aktivieren', + 'OXPS_PASSWORDPOLICY_ACCOUNTBLOCKED_TITLE' => 'Ihr Account wurde blockiert.', + 'OXPS_PASSWORDPOLICY_ACCOUNTBLOCKED_INFO' => 'Ihr Account wurde blockiert. Sie haben das Passwort zu oft hintereinander falsch eingegeben.', + 'OXPS_PASSWORDPOLICY_ACCOUNTBLOCKED_HINT' => 'Um dies in Zukunft zu vermeiden, benutzen Sie bitte die Funktion „Passwort Zurücksetzen”, wenn Sie das Passwort mehrmals falsch eingegeben haben.', + 'OXPS_PASSWORDPOLICY_ACCOUNTBLOCKED_ACTION' => 'Sie können Ihren Account wieder freischalten', + 'OXPS_PASSWORDPOLICY_ACCOUNTBLOCKED_RESETPASS' => 'Passwort zurücksetzen', + 'OXPS_PASSWORDPOLICY_ACCOUNTBLOCKED_CONTACT' => 'Für weitere Hilfe zum zurücksetzen des Passwortes wenden Sie sich bitte an den Support.', + 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_TITLE' => 'Stellen Sie sicher, dass ihr Passwort sicher ist.', + 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_MEMO' => 'Starke Passwörter enthalten Buchstaben, Großbuchstaben, Zahlen und Sonderzeichen (z.B. Punkte, Bindestriche, Unterstriche). Je länger das Passwort, desto stärker ist es.', + 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_NOPASSWORD' => 'Kein Passwort eingetragen', + 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_STRENGTH0' => 'Sehr schwach', + 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_STRENGTH1' => 'Schwach', + 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_STRENGTH2' => 'Besser', + 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_STRENGTH3' => 'Durchschnittlich', + 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_STRENGTH4' => 'Stark', + 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_STRENGTH5' => 'Sehr Stark', + 'OXPS_PASSWORDPOLICY_PASSWORDVALIDATION_MINLENGTH' => 'Das Passwort hat nicht genügend Zeichen.', + 'OXPS_PASSWORDPOLICY_PASSWORDVALIDATION_UPPERCASE' => 'Das Passwort enthält keine Großbuchstaben.', + 'OXPS_PASSWORDPOLICY_PASSWORDVALIDATION_LOWERCASE' => 'Das Passwort enthält keine Kleinbuchstaben.', + 'OXPS_PASSWORDPOLICY_PASSWORDVALIDATION_DIGITS' => 'Das Passwort enthält keine Ziffer.', + 'OXPS_PASSWORDPOLICY_PASSWORDVALIDATION_SPECIAL' => 'Das Passwort muss mindestens eines der folgenden Zeichen enthalten: ! @ # $ % ^ & * ? _ ~ - ( ) ', + 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_TOOLONG' => 'Das Passwort ist zu lang, bitte benutzen Sie ein kürzeres.', + 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESDIGITS' => 'Das Passwort muss mindestens eine Zahl enthalten.', + 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESUPPERCASE' => 'Das Passwort muss mindestens einen Großbuchstaben enthalten.', + 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESLOWERCASE' => 'Das Passwort muss mindestens einen Kleinbuchstaben enthalten.', + 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESSPECIAL' => 'Das Passwort muss mindestens ein Sonderzeichen enthalten.', + 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_WRONGTYPE' => 'Fehlerhafter Typ, bitte tragen Sie einen validen Wert ein. Bei weiteren Fragen wenden Sie sich an den Support.', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_ERROR_WRONGOTP' => 'Der eingebene Code für die Zwei-Faktor-Authentifizierung war falsch.', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_ERROR_USEDOTP' => 'Der eingegebene Code wurde bereits zum Login verwendet.', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_ERROR_WRONGBACKUPCODE' => 'Der eingebene Backup Code war falsch.', + 'OXPS_PASSWORDPOLICY_RATELIMIT_TWOFACTOR_EXCEEDED' => 'Sie haben zu oft den falschen Code für die Zwei-Faktor-Authentifizierung eingegeben. Bitte versuchen Sie es später erneut.', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_REGISTER' => '2-Faktor-Authentifizierung Einrichtung', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_BACKUPCODE' => 'Backup-Code', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_RECOVERY' => '2-Faktor-Authentifizierung Reset', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_LOGIN' => '2-Faktor-Authentifizierung', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_ACTIVATE' => '2FA aktivieren', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_DEACTIVATE' => '2FA deaktivieren', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_CONTINUE' => 'Weiter', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_CONFIRM' => 'Verstanden', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_RESET' => 'Zurücksetzen', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_HELP_INFO' => 'Die Zwei-Faktor-Authentifizierung bietet Ihrem Account eine zusätzliche Sicherheit. Durch aktivieren dieser Funktion, wird bei jedem Login nach einem zeitlich begrenzten einmaligen Passwort gefragt, welches Sie auf Ihrem Handy von einer App ablesen können.', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_HELP_LOGIN' => 'Öffnen Sie jetzt Ihre Authentifizierungsapp und geben Sie den 6-stelligen Code ein.', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_HELP_REGISTER' => 'Scannen Sie den abgebildeten QR Code mit ihrem Smartphone ein. Öffnen Sie dazu Ihre Kamera und richten Sie diese auf den Code.
    Hinweis: Zum Verwenden der 2FA ist eine App erforderlich, welche Sie sich kostenlos herunterladen müssen. ', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_HELP_RECOVERY' => 'Falls Sie Ihr Gerät für die 2FA verloren haben oder es sonstige Probleme damit gibt, geben Sie den einmaligen Wiederherstellungscode ein, der Ihnen beim Einrichten der 2FA angezeigt wurde. Anschließend wird 2FA bei Ihrem Account deaktiviert.', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_HELP_LOST' => 'Sie brauchen Hilfe beim Login/haben Ihr Gerät für die 2FA verloren? Klicken Sie hier.', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_HELP_BACKUPCODE' => 'Für Sie wurde ein Wiederherstellungscode generiert. Falls Sie mal Ihr Gerät für die 2-Faktor-Authentifizierung verlieren sollten, ist dieser die einzige Möglichkeit um wieder Zugang zu Ihrem Account zu erlangen.
    Bewahren Sie den Wiederherstellungscode an einem sicheren Ort auf!', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_ACTIVATED' => '2FA wurde erfolgreich eingerichtet.', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_DEACTIVATED' => '2FA wurde deaktiviert.', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_REGISTER_SUCCESS' => 'Sie haben sich erfolgreich registriert und richten nun die 2-Faktor-Authentifizierung ein.', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_REGISTER_ERROR' => 'Die 2-Faktor Einrichtung kann derzeit nicht durchgeführt werden, bitten versuchen Sie es später nochmal oder kontaktieren Sie uns.', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_RESET_SUCCESS' => 'Die 2FA wurde erfolgreich resettet. ', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_YES' => "AKtivieren", + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_NO' => 'Deaktivieren' ); diff --git a/views/admin/tpl/admin_twofactoraccount.tpl b/views/admin/tpl/admin_twofactoraccount.tpl new file mode 100644 index 00000000..a496d46d --- /dev/null +++ b/views/admin/tpl/admin_twofactoraccount.tpl @@ -0,0 +1,33 @@ +[{include file="headitem.tpl" title="$TITLE"|oxmultilangassign}] +[{oxscript include="js/libs/jquery.min.js"}] +[{assign var="template_title" value="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_LOGIN"|oxmultilangassign}] +[{if $success == '1'}] +
    [{oxmultilang ident="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_ACTIVATED"}]
    + [{elseif $success == '2'}] +
    [{oxmultilang ident="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_DEACTIVATED"}]
    + [{/if}] +

    [{oxmultilang ident="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_LOGIN"}]

    +
    + + +
    + +
    + +
    +
    + +
    +
    + +
    +
    +
    +[{include file="bottomitem.tpl"}] \ No newline at end of file diff --git a/views/admin/tpl/admin_twofactorregister.tpl b/views/admin/tpl/admin_twofactorregister.tpl new file mode 100644 index 00000000..ed3f0710 --- /dev/null +++ b/views/admin/tpl/admin_twofactorregister.tpl @@ -0,0 +1,28 @@ +
    +
    +

    [{oxmultilang ident="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_REGISTER"}]

    + [{oxmultilang ident="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_HELP_REGISTER"}] +
    + +
    + [{$oView->getTOTPQrCode()}] +
    +
    +
    + +
    + +
    +
    +
    +
    + [{oxstyle include=$oViewConf->getModuleUrl('oxpspasswordpolicy','out/src/css/style.css')}] + [{oxscript include=$oViewConf->getModuleUrl('oxpspasswordpolicy','out/src/js/otpField.js')}] + + diff --git a/views/admin/tpl/layout/page.tpl b/views/admin/tpl/layout/page.tpl new file mode 100644 index 00000000..984111a2 --- /dev/null +++ b/views/admin/tpl/layout/page.tpl @@ -0,0 +1,3 @@ +[{foreach from=$oxidBlock_content item="_block"}] + [{$_block}] + [{/foreach}] \ No newline at end of file From f0dfbb14bbb48bc0b5095cb06f44c875f0e7bc6c Mon Sep 17 00:00:00 2001 From: Keywan Ghadami Date: Wed, 6 Oct 2021 14:00:02 +0200 Subject: [PATCH 154/199] totp im admin --- .../Admin/PasswordPolicyAccountTOTPAdmin.php | 2 ++ .../PasswordPolicyTwoFactorRegisterAdmin.php | 32 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/Controller/Admin/PasswordPolicyAccountTOTPAdmin.php b/src/Controller/Admin/PasswordPolicyAccountTOTPAdmin.php index 7c73576a..aa2920f8 100644 --- a/src/Controller/Admin/PasswordPolicyAccountTOTPAdmin.php +++ b/src/Controller/Admin/PasswordPolicyAccountTOTPAdmin.php @@ -29,6 +29,8 @@ public function redirect() { $totpenabled = (new Request())->getRequestEscapedParameter('status'); if ($totpenabled && !$this->isTOTP()) { + //avoid predefined otp_secret (by attacker with temp access) + Registry::getSession()->deleteVariable('otp_secret'); \OxidEsales\Eshop\Core\Registry::getUtils()->redirect($this->getViewConfig()->getSelfLink() . 'cl=admin_twofactorregister&step=settings'); } elseif (!$totpenabled && $this->isTOTP()) diff --git a/src/Controller/Admin/PasswordPolicyTwoFactorRegisterAdmin.php b/src/Controller/Admin/PasswordPolicyTwoFactorRegisterAdmin.php index 115c3558..4c927539 100644 --- a/src/Controller/Admin/PasswordPolicyTwoFactorRegisterAdmin.php +++ b/src/Controller/Admin/PasswordPolicyTwoFactorRegisterAdmin.php @@ -5,8 +5,10 @@ use OxidEsales\B2BModule\Budget\Controller\Admin\AdminController; +use OxidEsales\Eshop\Core\Exception\UserException; use OxidEsales\Eshop\Core\Registry; use OxidEsales\Eshop\Core\Request; +use OxidEsales\EshopCommunity\Core\Field; use OxidEsales\EshopCommunity\Internal\Container\ContainerFactory; use OxidProfessionalServices\PasswordPolicy\TwoFactorAuth\PasswordPolicyQrCodeRenderer; use OxidProfessionalServices\PasswordPolicy\TwoFactorAuth\PasswordPolicyTOTP; @@ -35,6 +37,36 @@ public function render() return 'admin_twofactorregister.tpl'; } + public function finalizeRegistration() + { + $container = ContainerFactory::getInstance()->getContainer(); + $totp = $container->get(PasswordPolicyTOTP::class); + $otp = (new Request())->getRequestEscapedParameter('otp'); + $secret = Registry::getSession()->getVariable('otp_secret'); + $decryptedsecret = $totp->decryptSecret($secret); + try { + $totp->verifyOTP($decryptedsecret, $otp); + $user = $this->getUser(); + $user->oxuser__oxpstotpsecret = new Field($secret, Field::T_TEXT); + $user->save(); + //reload user to check if save is successful + //because even if save returns true the fields may be not stored by oxid + $user->load($user->getId()); + if ($user->oxuser__oxpstotpsecret->value != $secret) { + throw new UserException("OXPS_CANNOTSTOREUSERSECRET"); + } + + //cleans up session for next registration + Registry::getSession()->deleteVariable('otp_secret'); + $step = (new Request())->getRequestEscapedParameter('step'); + $paymentActionLink = (new Request())->getRequestEscapedParameter('paymentActionLink'); + return 'twofactorbackup?step=' . $step . '&paymentActionLink=' . $paymentActionLink; + }catch (UserException $ex) + { + Registry::getUtilsView()->addErrorToDisplay($ex); + } + } + public function getTOTPQrCode() { $TOTPurl = $this->TOTP->getTotpQrUrl(); From 256c22494401eb9e55bebb0495f49ed18e977c4c Mon Sep 17 00:00:00 2001 From: moritz Date: Wed, 6 Oct 2021 19:11:50 +0200 Subject: [PATCH 155/199] little fix --- src/Component/PasswordPolicyUserComponent.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Component/PasswordPolicyUserComponent.php b/src/Component/PasswordPolicyUserComponent.php index e613a7d9..4f38cc97 100644 --- a/src/Component/PasswordPolicyUserComponent.php +++ b/src/Component/PasswordPolicyUserComponent.php @@ -53,7 +53,7 @@ public function finalizeRegistration() //because even if save returns true the fields may be not stored by oxid $user->load($user->getId()); if ($user->oxuser__oxpstotpsecret->value != $secret) { - throw new UserException("OXPS_CANNOTSTOREUSERSECRET"); + throw oxNew(UserException::class, "OXPS_CANNOTSTOREUSERSECRET"); } //cleans up session for next registration From bb94e35a0e0bb40cb76ecb1f86d6254f6b3a0967 Mon Sep 17 00:00:00 2001 From: moritz Date: Wed, 6 Oct 2021 19:13:13 +0200 Subject: [PATCH 156/199] added 2FA for admin backend (only registration) --- metadata.php | 11 ++- out/src/css/styles.css | 88 +++++++++++++++++++ .../Admin/PasswordPolicyAccountTOTPAdmin.php | 6 +- ...PasswordPolicyTwoFactorBackupCodeAdmin.php | 37 ++++++++ ...sswordPolicyTwoFactorConfirmationAdmin.php | 49 +++++++++++ .../PasswordPolicyTwoFactorRegisterAdmin.php | 14 +-- views/admin/de/passwordpolicy_lang.php | 2 +- views/admin/tpl/admin_twofactoraccount.tpl | 29 ++++-- views/admin/tpl/admin_twofactorbackupcode.tpl | 32 +++++++ .../admin/tpl/admin_twofactorconfirmation.tpl | 35 ++++++++ views/admin/tpl/admin_twofactorregister.tpl | 22 +++-- views/admin/tpl/message/errors.tpl | 8 ++ views/admin/tpl/message/errors_modal.tpl | 21 +++++ 13 files changed, 325 insertions(+), 29 deletions(-) create mode 100644 out/src/css/styles.css create mode 100644 src/Controller/Admin/PasswordPolicyTwoFactorBackupCodeAdmin.php create mode 100644 src/Controller/Admin/PasswordPolicyTwoFactorConfirmationAdmin.php create mode 100644 views/admin/tpl/admin_twofactorbackupcode.tpl create mode 100644 views/admin/tpl/admin_twofactorconfirmation.tpl create mode 100644 views/admin/tpl/message/errors.tpl create mode 100644 views/admin/tpl/message/errors_modal.tpl diff --git a/metadata.php b/metadata.php index 2135228d..a86fc0de 100644 --- a/metadata.php +++ b/metadata.php @@ -33,10 +33,12 @@ use OxidEsales\Eshop\Application\Controller\Admin\ModuleConfiguration; use OxidProfessionalServices\PasswordPolicy\Component\PasswordPolicyUserComponent; use OxidProfessionalServices\PasswordPolicy\Controller\Admin\PasswordPolicyAccountTOTPAdmin; +use OxidProfessionalServices\PasswordPolicy\Controller\Admin\PasswordPolicyTwoFactorBackupCodeAdmin; use OxidProfessionalServices\PasswordPolicy\Controller\Admin\PasswordPolicyTwoFactorRegisterAdmin; use OxidProfessionalServices\PasswordPolicy\Controller\PasswordPolicyAccountTOTP; use OxidProfessionalServices\PasswordPolicy\Controller\PasswordPolicyTwoFactorConfirmation; use OxidProfessionalServices\PasswordPolicy\Controller\PasswordPolicyTwoFactorBackupCode; +use OxidProfessionalServices\PasswordPolicy\Controller\Admin\PasswordPolicyTwoFactorConfirmationAdmin; use OxidProfessionalServices\PasswordPolicy\Controller\PasswordPolicyTwoFactorRecovery; use OxidProfessionalServices\PasswordPolicy\Controller\PasswordPolicyTwoFactorRegister; use OxidProfessionalServices\PasswordPolicy\Controller\PasswordPolicyTwoFactorLogin; @@ -83,6 +85,9 @@ 'twofactorrecovery' => PasswordPolicyTwoFactorRecovery::class, 'admin_twofactoraccount' => PasswordPolicyAccountTOTPAdmin::class, 'admin_twofactorregister' => PasswordPolicyTwoFactorRegisterAdmin::class, + 'admin_twofactorconfirmation' => PasswordPolicyTwoFactorConfirmationAdmin::class, + 'admin_twofactorbackup' => PasswordPolicyTwoFactorBackupCodeAdmin::class, + ], 'templates' => [ @@ -94,7 +99,11 @@ 'twofactorrecovery.tpl' => 'oxps/passwordpolicy/views/tpl/twofactorrecovery.tpl', 'admin_twofactoraccount.tpl' => 'oxps/passwordpolicy/views/admin/tpl/admin_twofactoraccount.tpl', 'admin_twofactorregister.tpl' => 'oxps/passwordpolicy/views/admin/tpl/admin_twofactorregister.tpl', - 'layout/page.tpl' => 'oxps/passwordpolicy/views/admin/tpl/layout/page.tpl' + 'admin_twofactorconfirmation.tpl' => 'oxps/passwordpolicy/views/admin/tpl/admin_twofactorconfirmation.tpl', + 'admin_twofactorbackupcode.tpl' => 'oxps/passwordpolicy/views/admin/tpl/admin_twofactorbackupcode.tpl', + 'layout/page.tpl' => 'oxps/passwordpolicy/views/admin/tpl/layout/page.tpl', + 'message/errors.tpl' => 'oxps/passwordpolicy/views/admin/tpl/message/errors.tpl', + 'message/error.tpl' => 'oxps/passwordpolicy/views/admin/tpl/message/error.tpl', ], 'blocks' => [ diff --git a/out/src/css/styles.css b/out/src/css/styles.css new file mode 100644 index 00000000..433663af --- /dev/null +++ b/out/src/css/styles.css @@ -0,0 +1,88 @@ +@import url("https://fonts.googleapis.com/css?family=Raleway:200,400,600,700");/** + * This file is part of OXID eSales Wave theme. + * + * OXID eSales Wave theme is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * OXID eSales Wave theme is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with OXID eSales Wave theme. If not, see . + * + * @link http://www.oxid-esales.com + * @copyright (C) OXID eSales AG 2003-2016 + *//*! + * Bootstrap v4.3.1 (https://getbootstrap.com/) + * Copyright 2011-2019 The Bootstrap Authors + * Copyright 2011-2019 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */:root {--blue: #007bff;--indigo: #6610f2;--purple: #6f42c1;--pink: #e83e8c;--red: #dc3545;--orange: #fd7e14;--yellow: #ffc107;--green: #28a745;--teal: #20c997;--cyan: #17a2b8;--white: #fff;--gray: #6c757d;--gray-dark: #333333;--primary: #009EC0;--secondary: #FC6621;--success: #28a745;--info: #17a2b8;--warning: #ffc107;--danger: #dc3545;--light: #f8f9fa;--dark: #333333;--breakpoint-xs: 0;--breakpoint-sm: 576px;--breakpoint-md: 768px;--breakpoint-lg: 992px;--breakpoint-xl: 1200px;--font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;}*,*::before,*::after {-webkit-box-sizing: border-box;box-sizing: border-box;}html {font-family: sans-serif;line-height: 1.15;-webkit-text-size-adjust: 100%;-webkit-tap-highlight-color: rgba(0, 0, 0, 0);}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section {display: block;}body {margin: 0;font-family: Raleway, "Helvetica Neue", Helvetica, Arial, sans-serif;font-size: 1rem;font-weight: 400;line-height: 1.5;color: #212529;text-align: left;background-color: #fff;}[tabindex="-1"]:focus {outline: 0 !important;}hr {-webkit-box-sizing: content-box;box-sizing: content-box;height: 0;overflow: visible;}h1,h2,h3,h4,h5,h6 {margin-top: 0;margin-bottom: 0.5rem;}p {margin-top: 0;margin-bottom: 1rem;}abbr[title],abbr[data-original-title] {text-decoration: underline;-webkit-text-decoration: underline dotted;text-decoration: underline dotted;cursor: help;border-bottom: 0;text-decoration-skip-ink: none;}address {margin-bottom: 1rem;font-style: normal;line-height: inherit;}ol,ul,dl {margin-top: 0;margin-bottom: 1rem;}ol ol,ul ul,ol ul,ul ol {margin-bottom: 0;}dt {font-weight: 700;}dd {margin-bottom: .5rem;margin-left: 0;}blockquote {margin: 0 0 1rem;}b,strong {font-weight: bolder;}small {font-size: 80%;}sub,sup {position: relative;font-size: 75%;line-height: 0;vertical-align: baseline;}sub {bottom: -.25em;}sup {top: -.5em;}a {color: #333333;text-decoration: none;background-color: transparent;}a:hover {color: #009EC0;text-decoration: underline;}a:not([href]):not([tabindex]) {color: inherit;text-decoration: none;}a:not([href]):not([tabindex]):hover,a:not([href]):not([tabindex]):focus {color: inherit;text-decoration: none;}a:not([href]):not([tabindex]):focus {outline: 0;}pre,code,kbd,samp {font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;font-size: 1em;}pre {margin-top: 0;margin-bottom: 1rem;overflow: auto;}figure {margin: 0 0 1rem;}img {vertical-align: middle;border-style: none;}svg {overflow: hidden;vertical-align: middle;}table {border-collapse: collapse;}caption {padding-top: 0.75rem;padding-bottom: 0.75rem;color: #6c757d;text-align: left;caption-side: bottom;}th {text-align: inherit;}label {display: inline-block;margin-bottom: 0.5rem;}button {border-radius: 0;}button:focus {outline: 1px dotted;outline: 5px auto -webkit-focus-ring-color;}input,button,select,optgroup,textarea {margin: 0;font-family: inherit;font-size: inherit;line-height: inherit;}button,input {overflow: visible;}button,select {text-transform: none;}select {word-wrap: normal;}button,[type="button"],[type="reset"],[type="submit"] {-webkit-appearance: button;}button:not(:disabled),[type="button"]:not(:disabled),[type="reset"]:not(:disabled),[type="submit"]:not(:disabled) {cursor: pointer;}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner {padding: 0;border-style: none;}input[type="radio"],input[type="checkbox"] {-webkit-box-sizing: border-box;box-sizing: border-box;padding: 0;}input[type="date"],input[type="time"],input[type="datetime-local"],input[type="month"] {-webkit-appearance: listbox;}textarea {overflow: auto;resize: vertical;}fieldset {min-width: 0;padding: 0;margin: 0;border: 0;}legend {display: block;width: 100%;max-width: 100%;padding: 0;margin-bottom: .5rem;font-size: 1.5rem;line-height: inherit;color: inherit;white-space: normal;}progress {vertical-align: baseline;}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button {height: auto;}[type="search"] {outline-offset: -2px;-webkit-appearance: none;}[type="search"]::-webkit-search-decoration {-webkit-appearance: none;}::-webkit-file-upload-button {font: inherit;-webkit-appearance: button;}output {display: inline-block;}summary {display: list-item;cursor: pointer;}template {display: none;}[hidden] {display: none !important;}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6 {margin-bottom: 0.5rem;font-weight: 500;line-height: 1.2;}h1,.h1 {font-size: 2.25rem;}h2,.h2 {font-size: 1.875rem;}h3,.h3 {font-size: 1.5rem;}h4,.h4 {font-size: 1.125rem;}h5,.h5 {font-size: 0.875rem;}h6,.h6 {font-size: 0.75rem;}.lead {font-size: 1.25rem;font-weight: 300;}.display-1 {font-size: 6rem;font-weight: 300;line-height: 1.2;}.display-2 {font-size: 5.5rem;font-weight: 300;line-height: 1.2;}.display-3 {font-size: 4.5rem;font-weight: 300;line-height: 1.2;}.display-4 {font-size: 3.5rem;font-weight: 300;line-height: 1.2;}hr {margin-top: 1rem;margin-bottom: 1rem;border: 0;border-top: 1px solid rgba(0, 0, 0, 0.1);}small,.small {font-size: 80%;font-weight: 400;}mark,.mark {padding: 0.2em;background-color: #fcf8e3;}.list-unstyled {padding-left: 0;list-style: none;}.list-inline {padding-left: 0;list-style: none;}.list-inline-item {display: inline-block;}.list-inline-item:not(:last-child) {margin-right: 0.5rem;}.initialism {font-size: 90%;text-transform: uppercase;}.blockquote {margin-bottom: 1rem;font-size: 1.25rem;}.blockquote-footer {display: block;font-size: 80%;color: #6c757d;}.blockquote-footer::before {content: "\2014\00A0";}.img-fluid {max-width: 100%;height: auto;}.img-thumbnail {padding: 0.25rem;background-color: #fff;border: 1px solid #dee2e6;border-radius: 0.25rem;max-width: 100%;height: auto;}.figure {display: inline-block;}.figure-img {margin-bottom: 0.5rem;line-height: 1;}.figure-caption {font-size: 90%;color: #6c757d;}code {font-size: 87.5%;color: #e83e8c;word-break: break-word;}a > code {color: inherit;}kbd {padding: 0.2rem 0.4rem;font-size: 87.5%;color: #fff;background-color: #212529;border-radius: 0.2rem;}kbd kbd {padding: 0;font-size: 100%;font-weight: 700;}pre {display: block;font-size: 87.5%;color: #212529;}pre code {font-size: inherit;color: inherit;word-break: normal;}.pre-scrollable {max-height: 340px;overflow-y: scroll;}.container {width: 100%;padding-right: 15px;padding-left: 15px;margin-right: auto;margin-left: auto;}.container-fluid {width: 100%;padding-right: 15px;padding-left: 15px;margin-right: auto;margin-left: auto;}.row {display: -webkit-box;display: -ms-flexbox;display: flex;-ms-flex-wrap: wrap;flex-wrap: wrap;margin-right: -15px;margin-left: -15px;}.no-gutters {margin-right: 0;margin-left: 0;}.no-gutters > .col,.no-gutters > [class*="col-"] {padding-right: 0;padding-left: 0;}.col-1,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-10,.col-11,.col-12,.col,.col-auto,.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm,.col-sm-auto,.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12,.col-md,.col-md-auto,.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg,.col-lg-auto,.col-xl-1,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl,.col-xl-auto {position: relative;width: 100%;padding-right: 15px;padding-left: 15px;}.col {-ms-flex-preferred-size: 0;flex-basis: 0;-webkit-box-flex: 1;-ms-flex-positive: 1;flex-grow: 1;max-width: 100%;}.col-auto {-webkit-box-flex: 0;-ms-flex: 0 0 auto;flex: 0 0 auto;width: auto;max-width: 100%;}.col-1 {-webkit-box-flex: 0;-ms-flex: 0 0 8.3333333333%;flex: 0 0 8.3333333333%;max-width: 8.3333333333%;}.col-2 {-webkit-box-flex: 0;-ms-flex: 0 0 16.6666666667%;flex: 0 0 16.6666666667%;max-width: 16.6666666667%;}.col-3 {-webkit-box-flex: 0;-ms-flex: 0 0 25%;flex: 0 0 25%;max-width: 25%;}.col-4 {-webkit-box-flex: 0;-ms-flex: 0 0 33.3333333333%;flex: 0 0 33.3333333333%;max-width: 33.3333333333%;}.col-5 {-webkit-box-flex: 0;-ms-flex: 0 0 41.6666666667%;flex: 0 0 41.6666666667%;max-width: 41.6666666667%;}.col-6 {-webkit-box-flex: 0;-ms-flex: 0 0 50%;flex: 0 0 50%;max-width: 50%;}.col-7 {-webkit-box-flex: 0;-ms-flex: 0 0 58.3333333333%;flex: 0 0 58.3333333333%;max-width: 58.3333333333%;}.col-8 {-webkit-box-flex: 0;-ms-flex: 0 0 66.6666666667%;flex: 0 0 66.6666666667%;max-width: 66.6666666667%;}.col-9 {-webkit-box-flex: 0;-ms-flex: 0 0 75%;flex: 0 0 75%;max-width: 75%;}.col-10 {-webkit-box-flex: 0;-ms-flex: 0 0 83.3333333333%;flex: 0 0 83.3333333333%;max-width: 83.3333333333%;}.col-11 {-webkit-box-flex: 0;-ms-flex: 0 0 91.6666666667%;flex: 0 0 91.6666666667%;max-width: 91.6666666667%;}.col-12 {-webkit-box-flex: 0;-ms-flex: 0 0 100%;flex: 0 0 100%;max-width: 100%;}.order-first {-webkit-box-ordinal-group: 0;-ms-flex-order: -1;order: -1;}.order-last {-webkit-box-ordinal-group: 14;-ms-flex-order: 13;order: 13;}.order-0 {-webkit-box-ordinal-group: 1;-ms-flex-order: 0;order: 0;}.order-1 {-webkit-box-ordinal-group: 2;-ms-flex-order: 1;order: 1;}.order-2 {-webkit-box-ordinal-group: 3;-ms-flex-order: 2;order: 2;}.order-3 {-webkit-box-ordinal-group: 4;-ms-flex-order: 3;order: 3;}.order-4 {-webkit-box-ordinal-group: 5;-ms-flex-order: 4;order: 4;}.order-5 {-webkit-box-ordinal-group: 6;-ms-flex-order: 5;order: 5;}.order-6 {-webkit-box-ordinal-group: 7;-ms-flex-order: 6;order: 6;}.order-7 {-webkit-box-ordinal-group: 8;-ms-flex-order: 7;order: 7;}.order-8 {-webkit-box-ordinal-group: 9;-ms-flex-order: 8;order: 8;}.order-9 {-webkit-box-ordinal-group: 10;-ms-flex-order: 9;order: 9;}.order-10 {-webkit-box-ordinal-group: 11;-ms-flex-order: 10;order: 10;}.order-11 {-webkit-box-ordinal-group: 12;-ms-flex-order: 11;order: 11;}.order-12 {-webkit-box-ordinal-group: 13;-ms-flex-order: 12;order: 12;}.offset-1 {margin-left: 8.3333333333%;}.offset-2 {margin-left: 16.6666666667%;}.offset-3 {margin-left: 25%;}.offset-4 {margin-left: 33.3333333333%;}.offset-5 {margin-left: 41.6666666667%;}.offset-6 {margin-left: 50%;}.offset-7 {margin-left: 58.3333333333%;}.offset-8 {margin-left: 66.6666666667%;}.offset-9 {margin-left: 75%;}.offset-10 {margin-left: 83.3333333333%;}.offset-11 {margin-left: 91.6666666667%;}.table {width: 100%;margin-bottom: 1rem;color: #212529;}.table th,.table td {padding: 0.75rem;vertical-align: top;border-top: 1px solid #dee2e6;}.table thead th {vertical-align: bottom;border-bottom: 2px solid #dee2e6;}.table tbody + tbody {border-top: 2px solid #dee2e6;}.table-sm th,.table-sm td {padding: 0.3rem;}.table-bordered {border: 1px solid #dee2e6;}.table-bordered th,.table-bordered td {border: 1px solid #dee2e6;}.table-bordered thead th,.table-bordered thead td {border-bottom-width: 2px;}.table-borderless th,.table-borderless td,.table-borderless thead th,.table-borderless tbody + tbody {border: 0;}.table-striped tbody tr:nth-of-type(odd) {background-color: rgba(0, 0, 0, 0.05);}.table-hover tbody tr:hover {color: #212529;background-color: rgba(0, 0, 0, 0.075);}.table-primary,.table-primary > th,.table-primary > td {background-color: #b8e4ed;}.table-primary th,.table-primary td,.table-primary thead th,.table-primary tbody + tbody {border-color: #7acdde;}.table-hover .table-primary:hover {background-color: #a4dce8;}.table-hover .table-primary:hover > td,.table-hover .table-primary:hover > th {background-color: #a4dce8;}.table-secondary,.table-secondary > th,.table-secondary > td {background-color: #fed4c1;}.table-secondary th,.table-secondary td,.table-secondary thead th,.table-secondary tbody + tbody {border-color: #fdaf8c;}.table-hover .table-secondary:hover {background-color: #fec3a8;}.table-hover .table-secondary:hover > td,.table-hover .table-secondary:hover > th {background-color: #fec3a8;}.table-success,.table-success > th,.table-success > td {background-color: #c3e6cb;}.table-success th,.table-success td,.table-success thead th,.table-success tbody + tbody {border-color: #8fd19e;}.table-hover .table-success:hover {background-color: #b1dfbb;}.table-hover .table-success:hover > td,.table-hover .table-success:hover > th {background-color: #b1dfbb;}.table-info,.table-info > th,.table-info > td {background-color: #bee5eb;}.table-info th,.table-info td,.table-info thead th,.table-info tbody + tbody {border-color: #86cfda;}.table-hover .table-info:hover {background-color: #abdde5;}.table-hover .table-info:hover > td,.table-hover .table-info:hover > th {background-color: #abdde5;}.table-warning,.table-warning > th,.table-warning > td {background-color: #ffeeba;}.table-warning th,.table-warning td,.table-warning thead th,.table-warning tbody + tbody {border-color: #ffdf7e;}.table-hover .table-warning:hover {background-color: #ffe8a1;}.table-hover .table-warning:hover > td,.table-hover .table-warning:hover > th {background-color: #ffe8a1;}.table-danger,.table-danger > th,.table-danger > td {background-color: #f5c6cb;}.table-danger th,.table-danger td,.table-danger thead th,.table-danger tbody + tbody {border-color: #ed969e;}.table-hover .table-danger:hover {background-color: #f1b0b7;}.table-hover .table-danger:hover > td,.table-hover .table-danger:hover > th {background-color: #f1b0b7;}.table-light,.table-light > th,.table-light > td {background-color: #fdfdfe;}.table-light th,.table-light td,.table-light thead th,.table-light tbody + tbody {border-color: #fbfcfc;}.table-hover .table-light:hover {background-color: #ececf6;}.table-hover .table-light:hover > td,.table-hover .table-light:hover > th {background-color: #ececf6;}.table-dark,.table-dark > th,.table-dark > td {background-color: #c6c6c6;}.table-dark th,.table-dark td,.table-dark thead th,.table-dark tbody + tbody {border-color: #959595;}.table-hover .table-dark:hover {background-color: #b9b9b9;}.table-hover .table-dark:hover > td,.table-hover .table-dark:hover > th {background-color: #b9b9b9;}.table-active,.table-active > th,.table-active > td {background-color: rgba(0, 0, 0, 0.075);}.table-hover .table-active:hover {background-color: rgba(0, 0, 0, 0.075);}.table-hover .table-active:hover > td,.table-hover .table-active:hover > th {background-color: rgba(0, 0, 0, 0.075);}.table .thead-dark th {color: #fff;background-color: #333333;border-color: #464646;}.table .thead-light th {color: #495057;background-color: #e9ecef;border-color: #dee2e6;}.table-dark {color: #fff;background-color: #333333;}.table-dark th,.table-dark td,.table-dark thead th {border-color: #464646;}.table-dark.table-bordered {border: 0;}.table-dark.table-striped tbody tr:nth-of-type(odd) {background-color: rgba(255, 255, 255, 0.05);}.table-dark.table-hover tbody tr:hover {color: #fff;background-color: rgba(255, 255, 255, 0.075);}.table-responsive {display: block;width: 100%;overflow-x: auto;-webkit-overflow-scrolling: touch;}.table-responsive > .table-bordered {border: 0;}.form-control {display: block;width: 100%;height: calc(1.5em + 0.75rem + 2px);padding: 0.375rem 0.75rem;font-size: 1rem;font-weight: 400;line-height: 1.5;color: #495057;background-color: #fff;background-clip: padding-box;border: 1px solid #ced4da;border-radius: 0.25rem;-webkit-transition: border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition: border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;}.form-control::-ms-expand {background-color: transparent;border: 0;}.form-control:focus {color: #495057;background-color: #fff;border-color: #41ddff;outline: 0;-webkit-box-shadow: 0 0 0 0.2rem rgba(0, 158, 192, 0.25);box-shadow: 0 0 0 0.2rem rgba(0, 158, 192, 0.25);}.form-control::-webkit-input-placeholder {color: #6c757d;opacity: 1;}.form-control::-moz-placeholder {color: #6c757d;opacity: 1;}.form-control::-ms-input-placeholder {color: #6c757d;opacity: 1;}.form-control::placeholder {color: #6c757d;opacity: 1;}.form-control:disabled,.form-control[readonly] {background-color: #e9ecef;opacity: 1;}select.form-control:focus::-ms-value {color: #495057;background-color: #fff;}.form-control-file,.form-control-range {display: block;width: 100%;}.col-form-label {padding-top: calc(0.375rem + 1px);padding-bottom: calc(0.375rem + 1px);margin-bottom: 0;font-size: inherit;line-height: 1.5;}.col-form-label-lg {padding-top: calc(0.5rem + 1px);padding-bottom: calc(0.5rem + 1px);font-size: 1.25rem;line-height: 1.5;}.col-form-label-sm {padding-top: calc(0.25rem + 1px);padding-bottom: calc(0.25rem + 1px);font-size: 0.875rem;line-height: 1.5;}.form-control-plaintext {display: block;width: 100%;padding-top: 0.375rem;padding-bottom: 0.375rem;margin-bottom: 0;line-height: 1.5;color: #212529;background-color: transparent;border: solid transparent;border-width: 1px 0;}.form-control-plaintext.form-control-sm,.form-control-plaintext.form-control-lg {padding-right: 0;padding-left: 0;}.form-control-sm {height: calc(1.5em + 0.5rem + 2px);padding: 0.25rem 0.5rem;font-size: 0.875rem;line-height: 1.5;border-radius: 0.2rem;}.form-control-lg {height: calc(1.5em + 1rem + 2px);padding: 0.5rem 1rem;font-size: 1.25rem;line-height: 1.5;border-radius: 0.3rem;}select.form-control[size],select.form-control[multiple] {height: auto;}textarea.form-control {height: auto;}.form-group {margin-bottom: 1rem;}.form-text {display: block;margin-top: 0.25rem;}.form-row {display: -webkit-box;display: -ms-flexbox;display: flex;-ms-flex-wrap: wrap;flex-wrap: wrap;margin-right: -5px;margin-left: -5px;}.form-row > .col,.form-row > [class*="col-"] {padding-right: 5px;padding-left: 5px;}.form-check {position: relative;display: block;padding-left: 1.25rem;}.form-check-input {position: absolute;margin-top: 0.3rem;margin-left: -1.25rem;}.form-check-input:disabled ~ .form-check-label {color: #6c757d;}.form-check-label {margin-bottom: 0;}.form-check-inline {display: -webkit-inline-box;display: -ms-inline-flexbox;display: inline-flex;-webkit-box-align: center;-ms-flex-align: center;align-items: center;padding-left: 0;margin-right: 0.75rem;}.form-check-inline .form-check-input {position: static;margin-top: 0;margin-right: 0.3125rem;margin-left: 0;}.valid-feedback {display: none;width: 100%;margin-top: 0.25rem;font-size: 80%;color: #28a745;}.valid-tooltip {position: absolute;top: 100%;z-index: 5;display: none;max-width: 100%;padding: 0.25rem 0.5rem;margin-top: .1rem;font-size: 0.875rem;line-height: 1.5;color: #fff;background-color: rgba(40, 167, 69, 0.9);border-radius: 0.25rem;}.was-validated .form-control:valid,.form-control.is-valid {border-color: #28a745;padding-right: calc(1.5em + 0.75rem);background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat: no-repeat;background-position: center right calc(0.375em + 0.1875rem);background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);}.was-validated .form-control:valid:focus,.form-control.is-valid:focus {border-color: #28a745;-webkit-box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);}.was-validated .form-control:valid ~ .valid-feedback,.was-validated .form-control:valid ~ .valid-tooltip,.form-control.is-valid ~ .valid-feedback,.form-control.is-valid ~ .valid-tooltip {display: block;}.was-validated textarea.form-control:valid,textarea.form-control.is-valid {padding-right: calc(1.5em + 0.75rem);background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);}.was-validated .custom-select:valid,.custom-select.is-valid {border-color: #28a745;padding-right: calc((1em + 0.75rem) * 3 / 4 + 1.75rem);background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23333333' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px, url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);}.was-validated .custom-select:valid:focus,.custom-select.is-valid:focus {border-color: #28a745;-webkit-box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);}.was-validated .custom-select:valid ~ .valid-feedback,.was-validated .custom-select:valid ~ .valid-tooltip,.custom-select.is-valid ~ .valid-feedback,.custom-select.is-valid ~ .valid-tooltip {display: block;}.was-validated .form-control-file:valid ~ .valid-feedback,.was-validated .form-control-file:valid ~ .valid-tooltip,.form-control-file.is-valid ~ .valid-feedback,.form-control-file.is-valid ~ .valid-tooltip {display: block;}.was-validated .form-check-input:valid ~ .form-check-label,.form-check-input.is-valid ~ .form-check-label {color: #28a745;}.was-validated .form-check-input:valid ~ .valid-feedback,.was-validated .form-check-input:valid ~ .valid-tooltip,.form-check-input.is-valid ~ .valid-feedback,.form-check-input.is-valid ~ .valid-tooltip {display: block;}.was-validated .custom-control-input:valid ~ .custom-control-label,.custom-control-input.is-valid ~ .custom-control-label {color: #28a745;}.was-validated .custom-control-input:valid ~ .custom-control-label::before,.custom-control-input.is-valid ~ .custom-control-label::before {border-color: #28a745;}.was-validated .custom-control-input:valid ~ .valid-feedback,.was-validated .custom-control-input:valid ~ .valid-tooltip,.custom-control-input.is-valid ~ .valid-feedback,.custom-control-input.is-valid ~ .valid-tooltip {display: block;}.was-validated .custom-control-input:valid:checked ~ .custom-control-label::before,.custom-control-input.is-valid:checked ~ .custom-control-label::before {border-color: #34ce57;background-color: #34ce57;}.was-validated .custom-control-input:valid:focus ~ .custom-control-label::before,.custom-control-input.is-valid:focus ~ .custom-control-label::before {-webkit-box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);}.was-validated .custom-control-input:valid:focus:not(:checked) ~ .custom-control-label::before,.custom-control-input.is-valid:focus:not(:checked) ~ .custom-control-label::before {border-color: #28a745;}.was-validated .custom-file-input:valid ~ .custom-file-label,.custom-file-input.is-valid ~ .custom-file-label {border-color: #28a745;}.was-validated .custom-file-input:valid ~ .valid-feedback,.was-validated .custom-file-input:valid ~ .valid-tooltip,.custom-file-input.is-valid ~ .valid-feedback,.custom-file-input.is-valid ~ .valid-tooltip {display: block;}.was-validated .custom-file-input:valid:focus ~ .custom-file-label,.custom-file-input.is-valid:focus ~ .custom-file-label {border-color: #28a745;-webkit-box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);}.invalid-feedback {display: none;width: 100%;margin-top: 0.25rem;font-size: 80%;color: #dc3545;}.invalid-tooltip {position: absolute;top: 100%;z-index: 5;display: none;max-width: 100%;padding: 0.25rem 0.5rem;margin-top: .1rem;font-size: 0.875rem;line-height: 1.5;color: #fff;background-color: rgba(220, 53, 69, 0.9);border-radius: 0.25rem;}.was-validated .form-control:invalid,.form-control.is-invalid {border-color: #dc3545;padding-right: calc(1.5em + 0.75rem);background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23dc3545' viewBox='-2 -2 7 7'%3e%3cpath stroke='%23dc3545' d='M0 0l3 3m0-3L0 3'/%3e%3ccircle r='.5'/%3e%3ccircle cx='3' r='.5'/%3e%3ccircle cy='3' r='.5'/%3e%3ccircle cx='3' cy='3' r='.5'/%3e%3c/svg%3E");background-repeat: no-repeat;background-position: center right calc(0.375em + 0.1875rem);background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);}.was-validated .form-control:invalid:focus,.form-control.is-invalid:focus {border-color: #dc3545;-webkit-box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);}.was-validated .form-control:invalid ~ .invalid-feedback,.was-validated .form-control:invalid ~ .invalid-tooltip,.form-control.is-invalid ~ .invalid-feedback,.form-control.is-invalid ~ .invalid-tooltip {display: block;}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid {padding-right: calc(1.5em + 0.75rem);background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);}.was-validated .custom-select:invalid,.custom-select.is-invalid {border-color: #dc3545;padding-right: calc((1em + 0.75rem) * 3 / 4 + 1.75rem);background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23333333' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px, url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23dc3545' viewBox='-2 -2 7 7'%3e%3cpath stroke='%23dc3545' d='M0 0l3 3m0-3L0 3'/%3e%3ccircle r='.5'/%3e%3ccircle cx='3' r='.5'/%3e%3ccircle cy='3' r='.5'/%3e%3ccircle cx='3' cy='3' r='.5'/%3e%3c/svg%3E") #fff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);}.was-validated .custom-select:invalid:focus,.custom-select.is-invalid:focus {border-color: #dc3545;-webkit-box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);}.was-validated .custom-select:invalid ~ .invalid-feedback,.was-validated .custom-select:invalid ~ .invalid-tooltip,.custom-select.is-invalid ~ .invalid-feedback,.custom-select.is-invalid ~ .invalid-tooltip {display: block;}.was-validated .form-control-file:invalid ~ .invalid-feedback,.was-validated .form-control-file:invalid ~ .invalid-tooltip,.form-control-file.is-invalid ~ .invalid-feedback,.form-control-file.is-invalid ~ .invalid-tooltip {display: block;}.was-validated .form-check-input:invalid ~ .form-check-label,.form-check-input.is-invalid ~ .form-check-label {color: #dc3545;}.was-validated .form-check-input:invalid ~ .invalid-feedback,.was-validated .form-check-input:invalid ~ .invalid-tooltip,.form-check-input.is-invalid ~ .invalid-feedback,.form-check-input.is-invalid ~ .invalid-tooltip {display: block;}.was-validated .custom-control-input:invalid ~ .custom-control-label,.custom-control-input.is-invalid ~ .custom-control-label {color: #dc3545;}.was-validated .custom-control-input:invalid ~ .custom-control-label::before,.custom-control-input.is-invalid ~ .custom-control-label::before {border-color: #dc3545;}.was-validated .custom-control-input:invalid ~ .invalid-feedback,.was-validated .custom-control-input:invalid ~ .invalid-tooltip,.custom-control-input.is-invalid ~ .invalid-feedback,.custom-control-input.is-invalid ~ .invalid-tooltip {display: block;}.was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before,.custom-control-input.is-invalid:checked ~ .custom-control-label::before {border-color: #e4606d;background-color: #e4606d;}.was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before,.custom-control-input.is-invalid:focus ~ .custom-control-label::before {-webkit-box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);}.was-validated .custom-control-input:invalid:focus:not(:checked) ~ .custom-control-label::before,.custom-control-input.is-invalid:focus:not(:checked) ~ .custom-control-label::before {border-color: #dc3545;}.was-validated .custom-file-input:invalid ~ .custom-file-label,.custom-file-input.is-invalid ~ .custom-file-label {border-color: #dc3545;}.was-validated .custom-file-input:invalid ~ .invalid-feedback,.was-validated .custom-file-input:invalid ~ .invalid-tooltip,.custom-file-input.is-invalid ~ .invalid-feedback,.custom-file-input.is-invalid ~ .invalid-tooltip {display: block;}.was-validated .custom-file-input:invalid:focus ~ .custom-file-label,.custom-file-input.is-invalid:focus ~ .custom-file-label {border-color: #dc3545;-webkit-box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);}.form-inline {display: -webkit-box;display: -ms-flexbox;display: flex;-webkit-box-orient: horizontal;-webkit-box-direction: normal;-ms-flex-flow: row wrap;flex-flow: row wrap;-webkit-box-align: center;-ms-flex-align: center;align-items: center;}.form-inline .form-check {width: 100%;}.btn {display: inline-block;font-weight: 400;color: #212529;text-align: center;vertical-align: middle;-webkit-user-select: none;-moz-user-select: none;-ms-user-select: none;user-select: none;background-color: transparent;border: 1px solid transparent;padding: 0.375rem 0.75rem;font-size: 1rem;line-height: 1.5;border-radius: 0.25rem;-webkit-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;}.btn:hover {color: #212529;text-decoration: none;}.btn:focus,.btn.focus {outline: 0;-webkit-box-shadow: 0 0 0 0.2rem rgba(0, 158, 192, 0.25);box-shadow: 0 0 0 0.2rem rgba(0, 158, 192, 0.25);}.btn.disabled,.btn:disabled {opacity: 0.65;}a.btn.disabled,fieldset:disabled a.btn {pointer-events: none;}.btn-primary {color: #fff;background-color: #009EC0;border-color: #009EC0;}.btn-primary:hover {color: #fff;background-color: #007f9a;border-color: #00748d;}.btn-primary:focus,.btn-primary.focus {-webkit-box-shadow: 0 0 0 0.2rem rgba(38, 173, 201, 0.5);box-shadow: 0 0 0 0.2rem rgba(38, 173, 201, 0.5);}.btn-primary.disabled,.btn-primary:disabled {color: #fff;background-color: #009EC0;border-color: #009EC0;}.btn-primary:not(:disabled):not(.disabled):active,.btn-primary:not(:disabled):not(.disabled).active,.show > .btn-primary.dropdown-toggle {color: #fff;background-color: #00748d;border-color: #006a80;}.btn-primary:not(:disabled):not(.disabled):active:focus,.btn-primary:not(:disabled):not(.disabled).active:focus,.show > .btn-primary.dropdown-toggle:focus {-webkit-box-shadow: 0 0 0 0.2rem rgba(38, 173, 201, 0.5);box-shadow: 0 0 0 0.2rem rgba(38, 173, 201, 0.5);}.btn-secondary {color: #fff;background-color: #FC6621;border-color: #FC6621;}.btn-secondary:hover {color: #fff;background-color: #f34f03;border-color: #e74b03;}.btn-secondary:focus,.btn-secondary.focus {-webkit-box-shadow: 0 0 0 0.2rem rgba(252, 125, 66, 0.5);box-shadow: 0 0 0 0.2rem rgba(252, 125, 66, 0.5);}.btn-secondary.disabled,.btn-secondary:disabled {color: #fff;background-color: #FC6621;border-color: #FC6621;}.btn-secondary:not(:disabled):not(.disabled):active,.btn-secondary:not(:disabled):not(.disabled).active,.show > .btn-secondary.dropdown-toggle {color: #fff;background-color: #e74b03;border-color: #da4703;}.btn-secondary:not(:disabled):not(.disabled):active:focus,.btn-secondary:not(:disabled):not(.disabled).active:focus,.show > .btn-secondary.dropdown-toggle:focus {-webkit-box-shadow: 0 0 0 0.2rem rgba(252, 125, 66, 0.5);box-shadow: 0 0 0 0.2rem rgba(252, 125, 66, 0.5);}.btn-success {color: #fff;background-color: #28a745;border-color: #28a745;}.btn-success:hover {color: #fff;background-color: #218838;border-color: #1e7e34;}.btn-success:focus,.btn-success.focus {-webkit-box-shadow: 0 0 0 0.2rem rgba(72, 180, 97, 0.5);box-shadow: 0 0 0 0.2rem rgba(72, 180, 97, 0.5);}.btn-success.disabled,.btn-success:disabled {color: #fff;background-color: #28a745;border-color: #28a745;}.btn-success:not(:disabled):not(.disabled):active,.btn-success:not(:disabled):not(.disabled).active,.show > .btn-success.dropdown-toggle {color: #fff;background-color: #1e7e34;border-color: #1c7430;}.btn-success:not(:disabled):not(.disabled):active:focus,.btn-success:not(:disabled):not(.disabled).active:focus,.show > .btn-success.dropdown-toggle:focus {-webkit-box-shadow: 0 0 0 0.2rem rgba(72, 180, 97, 0.5);box-shadow: 0 0 0 0.2rem rgba(72, 180, 97, 0.5);}.btn-info {color: #fff;background-color: #17a2b8;border-color: #17a2b8;}.btn-info:hover {color: #fff;background-color: #138496;border-color: #117a8b;}.btn-info:focus,.btn-info.focus {-webkit-box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5);box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5);}.btn-info.disabled,.btn-info:disabled {color: #fff;background-color: #17a2b8;border-color: #17a2b8;}.btn-info:not(:disabled):not(.disabled):active,.btn-info:not(:disabled):not(.disabled).active,.show > .btn-info.dropdown-toggle {color: #fff;background-color: #117a8b;border-color: #10707f;}.btn-info:not(:disabled):not(.disabled):active:focus,.btn-info:not(:disabled):not(.disabled).active:focus,.show > .btn-info.dropdown-toggle:focus {-webkit-box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5);box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5);}.btn-warning {color: #212529;background-color: #ffc107;border-color: #ffc107;}.btn-warning:hover {color: #212529;background-color: #e0a800;border-color: #d39e00;}.btn-warning:focus,.btn-warning.focus {-webkit-box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5);box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5);}.btn-warning.disabled,.btn-warning:disabled {color: #212529;background-color: #ffc107;border-color: #ffc107;}.btn-warning:not(:disabled):not(.disabled):active,.btn-warning:not(:disabled):not(.disabled).active,.show > .btn-warning.dropdown-toggle {color: #212529;background-color: #d39e00;border-color: #c69500;}.btn-warning:not(:disabled):not(.disabled):active:focus,.btn-warning:not(:disabled):not(.disabled).active:focus,.show > .btn-warning.dropdown-toggle:focus {-webkit-box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5);box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5);}.btn-danger {color: #fff;background-color: #dc3545;border-color: #dc3545;}.btn-danger:hover {color: #fff;background-color: #c82333;border-color: #bd2130;}.btn-danger:focus,.btn-danger.focus {-webkit-box-shadow: 0 0 0 0.2rem rgba(225, 83, 97, 0.5);box-shadow: 0 0 0 0.2rem rgba(225, 83, 97, 0.5);}.btn-danger.disabled,.btn-danger:disabled {color: #fff;background-color: #dc3545;border-color: #dc3545;}.btn-danger:not(:disabled):not(.disabled):active,.btn-danger:not(:disabled):not(.disabled).active,.show > .btn-danger.dropdown-toggle {color: #fff;background-color: #bd2130;border-color: #b21f2d;}.btn-danger:not(:disabled):not(.disabled):active:focus,.btn-danger:not(:disabled):not(.disabled).active:focus,.show > .btn-danger.dropdown-toggle:focus {-webkit-box-shadow: 0 0 0 0.2rem rgba(225, 83, 97, 0.5);box-shadow: 0 0 0 0.2rem rgba(225, 83, 97, 0.5);}.btn-light {color: #212529;background-color: #f8f9fa;border-color: #f8f9fa;}.btn-light:hover {color: #212529;background-color: #e2e6ea;border-color: #dae0e5;}.btn-light:focus,.btn-light.focus {-webkit-box-shadow: 0 0 0 0.2rem rgba(216, 217, 219, 0.5);box-shadow: 0 0 0 0.2rem rgba(216, 217, 219, 0.5);}.btn-light.disabled,.btn-light:disabled {color: #212529;background-color: #f8f9fa;border-color: #f8f9fa;}.btn-light:not(:disabled):not(.disabled):active,.btn-light:not(:disabled):not(.disabled).active,.show > .btn-light.dropdown-toggle {color: #212529;background-color: #dae0e5;border-color: #d3d9df;}.btn-light:not(:disabled):not(.disabled):active:focus,.btn-light:not(:disabled):not(.disabled).active:focus,.show > .btn-light.dropdown-toggle:focus {-webkit-box-shadow: 0 0 0 0.2rem rgba(216, 217, 219, 0.5);box-shadow: 0 0 0 0.2rem rgba(216, 217, 219, 0.5);}.btn-dark {color: #fff;background-color: #333333;border-color: #333333;}.btn-dark:hover {color: #fff;background-color: #202020;border-color: #1a1919;}.btn-dark:focus,.btn-dark.focus {-webkit-box-shadow: 0 0 0 0.2rem rgba(82, 82, 82, 0.5);box-shadow: 0 0 0 0.2rem rgba(82, 82, 82, 0.5);}.btn-dark.disabled,.btn-dark:disabled {color: #fff;background-color: #333333;border-color: #333333;}.btn-dark:not(:disabled):not(.disabled):active,.btn-dark:not(:disabled):not(.disabled).active,.show > .btn-dark.dropdown-toggle {color: #fff;background-color: #1a1919;border-color: #131313;}.btn-dark:not(:disabled):not(.disabled):active:focus,.btn-dark:not(:disabled):not(.disabled).active:focus,.show > .btn-dark.dropdown-toggle:focus {-webkit-box-shadow: 0 0 0 0.2rem rgba(82, 82, 82, 0.5);box-shadow: 0 0 0 0.2rem rgba(82, 82, 82, 0.5);}.btn-outline-primary {color: #009EC0;border-color: #009EC0;}.btn-outline-primary:hover {color: #fff;background-color: #009EC0;border-color: #009EC0;}.btn-outline-primary:focus,.btn-outline-primary.focus {-webkit-box-shadow: 0 0 0 0.2rem rgba(0, 158, 192, 0.5);box-shadow: 0 0 0 0.2rem rgba(0, 158, 192, 0.5);}.btn-outline-primary.disabled,.btn-outline-primary:disabled {color: #009EC0;background-color: transparent;}.btn-outline-primary:not(:disabled):not(.disabled):active,.btn-outline-primary:not(:disabled):not(.disabled).active,.show > .btn-outline-primary.dropdown-toggle {color: #fff;background-color: #009EC0;border-color: #009EC0;}.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.show > .btn-outline-primary.dropdown-toggle:focus {-webkit-box-shadow: 0 0 0 0.2rem rgba(0, 158, 192, 0.5);box-shadow: 0 0 0 0.2rem rgba(0, 158, 192, 0.5);}.btn-outline-secondary {color: #FC6621;border-color: #FC6621;}.btn-outline-secondary:hover {color: #fff;background-color: #FC6621;border-color: #FC6621;}.btn-outline-secondary:focus,.btn-outline-secondary.focus {-webkit-box-shadow: 0 0 0 0.2rem rgba(252, 102, 33, 0.5);box-shadow: 0 0 0 0.2rem rgba(252, 102, 33, 0.5);}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled {color: #FC6621;background-color: transparent;}.btn-outline-secondary:not(:disabled):not(.disabled):active,.btn-outline-secondary:not(:disabled):not(.disabled).active,.show > .btn-outline-secondary.dropdown-toggle {color: #fff;background-color: #FC6621;border-color: #FC6621;}.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.show > .btn-outline-secondary.dropdown-toggle:focus {-webkit-box-shadow: 0 0 0 0.2rem rgba(252, 102, 33, 0.5);box-shadow: 0 0 0 0.2rem rgba(252, 102, 33, 0.5);}.btn-outline-success {color: #28a745;border-color: #28a745;}.btn-outline-success:hover {color: #fff;background-color: #28a745;border-color: #28a745;}.btn-outline-success:focus,.btn-outline-success.focus {-webkit-box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);}.btn-outline-success.disabled,.btn-outline-success:disabled {color: #28a745;background-color: transparent;}.btn-outline-success:not(:disabled):not(.disabled):active,.btn-outline-success:not(:disabled):not(.disabled).active,.show > .btn-outline-success.dropdown-toggle {color: #fff;background-color: #28a745;border-color: #28a745;}.btn-outline-success:not(:disabled):not(.disabled):active:focus,.btn-outline-success:not(:disabled):not(.disabled).active:focus,.show > .btn-outline-success.dropdown-toggle:focus {-webkit-box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);}.btn-outline-info {color: #17a2b8;border-color: #17a2b8;}.btn-outline-info:hover {color: #fff;background-color: #17a2b8;border-color: #17a2b8;}.btn-outline-info:focus,.btn-outline-info.focus {-webkit-box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);}.btn-outline-info.disabled,.btn-outline-info:disabled {color: #17a2b8;background-color: transparent;}.btn-outline-info:not(:disabled):not(.disabled):active,.btn-outline-info:not(:disabled):not(.disabled).active,.show > .btn-outline-info.dropdown-toggle {color: #fff;background-color: #17a2b8;border-color: #17a2b8;}.btn-outline-info:not(:disabled):not(.disabled):active:focus,.btn-outline-info:not(:disabled):not(.disabled).active:focus,.show > .btn-outline-info.dropdown-toggle:focus {-webkit-box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);}.btn-outline-warning {color: #ffc107;border-color: #ffc107;}.btn-outline-warning:hover {color: #212529;background-color: #ffc107;border-color: #ffc107;}.btn-outline-warning:focus,.btn-outline-warning.focus {-webkit-box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);}.btn-outline-warning.disabled,.btn-outline-warning:disabled {color: #ffc107;background-color: transparent;}.btn-outline-warning:not(:disabled):not(.disabled):active,.btn-outline-warning:not(:disabled):not(.disabled).active,.show > .btn-outline-warning.dropdown-toggle {color: #212529;background-color: #ffc107;border-color: #ffc107;}.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.show > .btn-outline-warning.dropdown-toggle:focus {-webkit-box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);}.btn-outline-danger {color: #dc3545;border-color: #dc3545;}.btn-outline-danger:hover {color: #fff;background-color: #dc3545;border-color: #dc3545;}.btn-outline-danger:focus,.btn-outline-danger.focus {-webkit-box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);}.btn-outline-danger.disabled,.btn-outline-danger:disabled {color: #dc3545;background-color: transparent;}.btn-outline-danger:not(:disabled):not(.disabled):active,.btn-outline-danger:not(:disabled):not(.disabled).active,.show > .btn-outline-danger.dropdown-toggle {color: #fff;background-color: #dc3545;border-color: #dc3545;}.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.show > .btn-outline-danger.dropdown-toggle:focus {-webkit-box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);}.btn-outline-light {color: #f8f9fa;border-color: #f8f9fa;}.btn-outline-light:hover {color: #212529;background-color: #f8f9fa;border-color: #f8f9fa;}.btn-outline-light:focus,.btn-outline-light.focus {-webkit-box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);}.btn-outline-light.disabled,.btn-outline-light:disabled {color: #f8f9fa;background-color: transparent;}.btn-outline-light:not(:disabled):not(.disabled):active,.btn-outline-light:not(:disabled):not(.disabled).active,.show > .btn-outline-light.dropdown-toggle {color: #212529;background-color: #f8f9fa;border-color: #f8f9fa;}.btn-outline-light:not(:disabled):not(.disabled):active:focus,.btn-outline-light:not(:disabled):not(.disabled).active:focus,.show > .btn-outline-light.dropdown-toggle:focus {-webkit-box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);}.btn-outline-dark {color: #333333;border-color: #333333;}.btn-outline-dark:hover {color: #fff;background-color: #333333;border-color: #333333;}.btn-outline-dark:focus,.btn-outline-dark.focus {-webkit-box-shadow: 0 0 0 0.2rem rgba(51, 51, 51, 0.5);box-shadow: 0 0 0 0.2rem rgba(51, 51, 51, 0.5);}.btn-outline-dark.disabled,.btn-outline-dark:disabled {color: #333333;background-color: transparent;}.btn-outline-dark:not(:disabled):not(.disabled):active,.btn-outline-dark:not(:disabled):not(.disabled).active,.show > .btn-outline-dark.dropdown-toggle {color: #fff;background-color: #333333;border-color: #333333;}.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.show > .btn-outline-dark.dropdown-toggle:focus {-webkit-box-shadow: 0 0 0 0.2rem rgba(51, 51, 51, 0.5);box-shadow: 0 0 0 0.2rem rgba(51, 51, 51, 0.5);}.btn-link {font-weight: 400;color: #333333;text-decoration: none;}.btn-link:hover {color: #009EC0;text-decoration: underline;}.btn-link:focus,.btn-link.focus {text-decoration: underline;-webkit-box-shadow: none;box-shadow: none;}.btn-link:disabled,.btn-link.disabled {color: #6c757d;pointer-events: none;}.btn-lg,.btn-group-lg > .btn {padding: 0.5rem 1rem;font-size: 1.25rem;line-height: 1.5;border-radius: 0.3rem;}.btn-sm,.btn-group-sm > .btn {padding: 0.25rem 0.5rem;font-size: 0.875rem;line-height: 1.5;border-radius: 0.2rem;}.btn-block {display: block;width: 100%;}.btn-block + .btn-block {margin-top: 0.5rem;}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block {width: 100%;}.fade {-webkit-transition: opacity 0.15s linear;transition: opacity 0.15s linear;}.fade:not(.show) {opacity: 0;}.collapse:not(.show) {display: none;}.collapsing {position: relative;height: 0;overflow: hidden;-webkit-transition: height 0.35s ease;transition: height 0.35s ease;}.dropup,.dropright,.dropdown,.dropleft {position: relative;}.dropdown-toggle {white-space: nowrap;}.dropdown-toggle::after {display: inline-block;margin-left: 0.255em;vertical-align: 0.255em;content: "";border-top: 0.3em solid;border-right: 0.3em solid transparent;border-bottom: 0;border-left: 0.3em solid transparent;}.dropdown-toggle:empty::after {margin-left: 0;}.dropdown-menu {position: absolute;top: 100%;left: 0;z-index: 1000;display: none;float: left;min-width: 10rem;padding: 0.5rem 0;margin: 0.125rem 0 0;font-size: 1rem;color: #212529;text-align: left;list-style: none;background-color: #fff;background-clip: padding-box;border: 1px solid rgba(0, 0, 0, 0.15);border-radius: 0.25rem;}.dropdown-menu-left {right: auto;left: 0;}.dropdown-menu-right {right: 0;left: auto;}.dropup .dropdown-menu {top: auto;bottom: 100%;margin-top: 0;margin-bottom: 0.125rem;}.dropup .dropdown-toggle::after {display: inline-block;margin-left: 0.255em;vertical-align: 0.255em;content: "";border-top: 0;border-right: 0.3em solid transparent;border-bottom: 0.3em solid;border-left: 0.3em solid transparent;}.dropup .dropdown-toggle:empty::after {margin-left: 0;}.dropright .dropdown-menu {top: 0;right: auto;left: 100%;margin-top: 0;margin-left: 0.125rem;}.dropright .dropdown-toggle::after {display: inline-block;margin-left: 0.255em;vertical-align: 0.255em;content: "";border-top: 0.3em solid transparent;border-right: 0;border-bottom: 0.3em solid transparent;border-left: 0.3em solid;}.dropright .dropdown-toggle:empty::after {margin-left: 0;}.dropright .dropdown-toggle::after {vertical-align: 0;}.dropleft .dropdown-menu {top: 0;right: 100%;left: auto;margin-top: 0;margin-right: 0.125rem;}.dropleft .dropdown-toggle::after {display: inline-block;margin-left: 0.255em;vertical-align: 0.255em;content: "";}.dropleft .dropdown-toggle::after {display: none;}.dropleft .dropdown-toggle::before {display: inline-block;margin-right: 0.255em;vertical-align: 0.255em;content: "";border-top: 0.3em solid transparent;border-right: 0.3em solid;border-bottom: 0.3em solid transparent;}.dropleft .dropdown-toggle:empty::after {margin-left: 0;}.dropleft .dropdown-toggle::before {vertical-align: 0;}.dropdown-menu[x-placement^="top"],.dropdown-menu[x-placement^="right"],.dropdown-menu[x-placement^="bottom"],.dropdown-menu[x-placement^="left"] {right: auto;bottom: auto;}.dropdown-divider {height: 0;margin: 0.5rem 0;overflow: hidden;border-top: 1px solid #e9ecef;}.dropdown-item {display: block;width: 100%;padding: 0.25rem 1.5rem;clear: both;font-weight: 400;color: #212529;text-align: inherit;white-space: nowrap;background-color: transparent;border: 0;}.dropdown-item:hover,.dropdown-item:focus {color: #16181b;text-decoration: none;background-color: #f8f9fa;}.dropdown-item.active,.dropdown-item:active {color: #fff;text-decoration: none;background-color: #009EC0;}.dropdown-item.disabled,.dropdown-item:disabled {color: #6c757d;pointer-events: none;background-color: transparent;}.dropdown-menu.show {display: block;}.dropdown-header {display: block;padding: 0.5rem 1.5rem;margin-bottom: 0;font-size: 0.875rem;color: #6c757d;white-space: nowrap;}.dropdown-item-text {display: block;padding: 0.25rem 1.5rem;color: #212529;}.btn-group,.btn-group-vertical {position: relative;display: -webkit-inline-box;display: -ms-inline-flexbox;display: inline-flex;vertical-align: middle;}.btn-group > .btn,.btn-group-vertical > .btn {position: relative;-webkit-box-flex: 1;-ms-flex: 1 1 auto;flex: 1 1 auto;}.btn-group > .btn:hover,.btn-group-vertical > .btn:hover {z-index: 1;}.btn-group > .btn:focus,.btn-group > .btn:active,.btn-group > .btn.active,.btn-group-vertical > .btn:focus,.btn-group-vertical > .btn:active,.btn-group-vertical > .btn.active {z-index: 1;}.btn-toolbar {display: -webkit-box;display: -ms-flexbox;display: flex;-ms-flex-wrap: wrap;flex-wrap: wrap;-webkit-box-pack: start;-ms-flex-pack: start;justify-content: flex-start;}.btn-toolbar .input-group {width: auto;}.btn-group > .btn:not(:first-child),.btn-group > .btn-group:not(:first-child) {margin-left: -1px;}.btn-group > .btn:not(:last-child):not(.dropdown-toggle),.btn-group > .btn-group:not(:last-child) > .btn {border-top-right-radius: 0;border-bottom-right-radius: 0;}.btn-group > .btn:not(:first-child),.btn-group > .btn-group:not(:first-child) > .btn {border-top-left-radius: 0;border-bottom-left-radius: 0;}.dropdown-toggle-split {padding-right: 0.5625rem;padding-left: 0.5625rem;}.dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after {margin-left: 0;}.dropleft .dropdown-toggle-split::before {margin-right: 0;}.btn-sm + .dropdown-toggle-split,.btn-group-sm > .btn + .dropdown-toggle-split {padding-right: 0.375rem;padding-left: 0.375rem;}.btn-lg + .dropdown-toggle-split,.btn-group-lg > .btn + .dropdown-toggle-split {padding-right: 0.75rem;padding-left: 0.75rem;}.btn-group-vertical {-webkit-box-orient: vertical;-webkit-box-direction: normal;-ms-flex-direction: column;flex-direction: column;-webkit-box-align: start;-ms-flex-align: start;align-items: flex-start;-webkit-box-pack: center;-ms-flex-pack: center;justify-content: center;}.btn-group-vertical > .btn,.btn-group-vertical > .btn-group {width: 100%;}.btn-group-vertical > .btn:not(:first-child),.btn-group-vertical > .btn-group:not(:first-child) {margin-top: -1px;}.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle),.btn-group-vertical > .btn-group:not(:last-child) > .btn {border-bottom-right-radius: 0;border-bottom-left-radius: 0;}.btn-group-vertical > .btn:not(:first-child),.btn-group-vertical > .btn-group:not(:first-child) > .btn {border-top-left-radius: 0;border-top-right-radius: 0;}.btn-group-toggle > .btn,.btn-group-toggle > .btn-group > .btn {margin-bottom: 0;}.btn-group-toggle > .btn input[type="radio"],.btn-group-toggle > .btn input[type="checkbox"],.btn-group-toggle > .btn-group > .btn input[type="radio"],.btn-group-toggle > .btn-group > .btn input[type="checkbox"] {position: absolute;clip: rect(0, 0, 0, 0);pointer-events: none;}.input-group {position: relative;display: -webkit-box;display: -ms-flexbox;display: flex;-ms-flex-wrap: wrap;flex-wrap: wrap;-webkit-box-align: stretch;-ms-flex-align: stretch;align-items: stretch;width: 100%;}.input-group > .form-control,.input-group > .form-control-plaintext,.input-group > .custom-select,.input-group > .custom-file {position: relative;-webkit-box-flex: 1;-ms-flex: 1 1 auto;flex: 1 1 auto;width: 1%;margin-bottom: 0;}.input-group > .form-control + .form-control,.input-group > .form-control + .custom-select,.input-group > .form-control + .custom-file,.input-group > .form-control-plaintext + .form-control,.input-group > .form-control-plaintext + .custom-select,.input-group > .form-control-plaintext + .custom-file,.input-group > .custom-select + .form-control,.input-group > .custom-select + .custom-select,.input-group > .custom-select + .custom-file,.input-group > .custom-file + .form-control,.input-group > .custom-file + .custom-select,.input-group > .custom-file + .custom-file {margin-left: -1px;}.input-group > .form-control:focus,.input-group > .custom-select:focus,.input-group > .custom-file .custom-file-input:focus ~ .custom-file-label {z-index: 3;}.input-group > .custom-file .custom-file-input:focus {z-index: 4;}.input-group > .form-control:not(:last-child),.input-group > .custom-select:not(:last-child) {border-top-right-radius: 0;border-bottom-right-radius: 0;}.input-group > .form-control:not(:first-child),.input-group > .custom-select:not(:first-child) {border-top-left-radius: 0;border-bottom-left-radius: 0;}.input-group > .custom-file {display: -webkit-box;display: -ms-flexbox;display: flex;-webkit-box-align: center;-ms-flex-align: center;align-items: center;}.input-group > .custom-file:not(:last-child) .custom-file-label,.input-group > .custom-file:not(:last-child) .custom-file-label::after {border-top-right-radius: 0;border-bottom-right-radius: 0;}.input-group > .custom-file:not(:first-child) .custom-file-label {border-top-left-radius: 0;border-bottom-left-radius: 0;}.input-group-prepend,.input-group-append {display: -webkit-box;display: -ms-flexbox;display: flex;}.input-group-prepend .btn,.input-group-append .btn {position: relative;z-index: 2;}.input-group-prepend .btn:focus,.input-group-append .btn:focus {z-index: 3;}.input-group-prepend .btn + .btn,.input-group-prepend .btn + .input-group-text,.input-group-prepend .input-group-text + .input-group-text,.input-group-prepend .input-group-text + .btn,.input-group-append .btn + .btn,.input-group-append .btn + .input-group-text,.input-group-append .input-group-text + .input-group-text,.input-group-append .input-group-text + .btn {margin-left: -1px;}.input-group-prepend {margin-right: -1px;}.input-group-append {margin-left: -1px;}.input-group-text {display: -webkit-box;display: -ms-flexbox;display: flex;-webkit-box-align: center;-ms-flex-align: center;align-items: center;padding: 0.375rem 0.75rem;margin-bottom: 0;font-size: 1rem;font-weight: 400;line-height: 1.5;color: #495057;text-align: center;white-space: nowrap;background-color: #e9ecef;border: 1px solid #ced4da;border-radius: 0.25rem;}.input-group-text input[type="radio"],.input-group-text input[type="checkbox"] {margin-top: 0;}.input-group-lg > .form-control:not(textarea),.input-group-lg > .custom-select {height: calc(1.5em + 1rem + 2px);}.input-group-lg > .form-control,.input-group-lg > .custom-select,.input-group-lg > .input-group-prepend > .input-group-text,.input-group-lg > .input-group-append > .input-group-text,.input-group-lg > .input-group-prepend > .btn,.input-group-lg > .input-group-append > .btn {padding: 0.5rem 1rem;font-size: 1.25rem;line-height: 1.5;border-radius: 0.3rem;}.input-group-sm > .form-control:not(textarea),.input-group-sm > .custom-select {height: calc(1.5em + 0.5rem + 2px);}.input-group-sm > .form-control,.input-group-sm > .custom-select,.input-group-sm > .input-group-prepend > .input-group-text,.input-group-sm > .input-group-append > .input-group-text,.input-group-sm > .input-group-prepend > .btn,.input-group-sm > .input-group-append > .btn {padding: 0.25rem 0.5rem;font-size: 0.875rem;line-height: 1.5;border-radius: 0.2rem;}.input-group-lg > .custom-select,.input-group-sm > .custom-select {padding-right: 1.75rem;}.input-group > .input-group-prepend > .btn,.input-group > .input-group-prepend > .input-group-text,.input-group > .input-group-append:not(:last-child) > .btn,.input-group > .input-group-append:not(:last-child) > .input-group-text,.input-group > .input-group-append:last-child > .btn:not(:last-child):not(.dropdown-toggle),.input-group > .input-group-append:last-child > .input-group-text:not(:last-child) {border-top-right-radius: 0;border-bottom-right-radius: 0;}.input-group > .input-group-append > .btn,.input-group > .input-group-append > .input-group-text,.input-group > .input-group-prepend:not(:first-child) > .btn,.input-group > .input-group-prepend:not(:first-child) > .input-group-text,.input-group > .input-group-prepend:first-child > .btn:not(:first-child),.input-group > .input-group-prepend:first-child > .input-group-text:not(:first-child) {border-top-left-radius: 0;border-bottom-left-radius: 0;}.custom-control {position: relative;display: block;min-height: 1.5rem;padding-left: 1.5rem;}.custom-control-inline {display: -webkit-inline-box;display: -ms-inline-flexbox;display: inline-flex;margin-right: 1rem;}.custom-control-input {position: absolute;z-index: -1;opacity: 0;}.custom-control-input:checked ~ .custom-control-label::before {color: #fff;border-color: #009EC0;background-color: #009EC0;}.custom-control-input:focus ~ .custom-control-label::before {-webkit-box-shadow: 0 0 0 0.2rem rgba(0, 158, 192, 0.25);box-shadow: 0 0 0 0.2rem rgba(0, 158, 192, 0.25);}.custom-control-input:focus:not(:checked) ~ .custom-control-label::before {border-color: #41ddff;}.custom-control-input:not(:disabled):active ~ .custom-control-label::before {color: #fff;background-color: #74e6ff;border-color: #74e6ff;}.custom-control-input:disabled ~ .custom-control-label {color: #6c757d;}.custom-control-input:disabled ~ .custom-control-label::before {background-color: #e9ecef;}.custom-control-label {position: relative;margin-bottom: 0;vertical-align: top;}.custom-control-label::before {position: absolute;top: 0.25rem;left: -1.5rem;display: block;width: 1rem;height: 1rem;pointer-events: none;content: "";background-color: #fff;border: #adb5bd solid 1px;}.custom-control-label::after {position: absolute;top: 0.25rem;left: -1.5rem;display: block;width: 1rem;height: 1rem;content: "";background: no-repeat 50% / 50% 50%;}.custom-checkbox .custom-control-label::before {border-radius: 0.25rem;}.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after {background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3e%3c/svg%3e");}.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before {border-color: #009EC0;background-color: #009EC0;}.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::after {background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e");}.custom-checkbox .custom-control-input:disabled:checked ~ .custom-control-label::before {background-color: rgba(0, 158, 192, 0.5);}.custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before {background-color: rgba(0, 158, 192, 0.5);}.custom-radio .custom-control-label::before {border-radius: 50%;}.custom-radio .custom-control-input:checked ~ .custom-control-label::after {background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e");}.custom-radio .custom-control-input:disabled:checked ~ .custom-control-label::before {background-color: rgba(0, 158, 192, 0.5);}.custom-switch {padding-left: 2.25rem;}.custom-switch .custom-control-label::before {left: -2.25rem;width: 1.75rem;pointer-events: all;border-radius: 0.5rem;}.custom-switch .custom-control-label::after {top: calc(0.25rem + 2px);left: calc(-2.25rem + 2px);width: calc(1rem - 4px);height: calc(1rem - 4px);background-color: #adb5bd;border-radius: 0.5rem;-webkit-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition: transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition: transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;}.custom-switch .custom-control-input:checked ~ .custom-control-label::after {background-color: #fff;-webkit-transform: translateX(0.75rem);transform: translateX(0.75rem);}.custom-switch .custom-control-input:disabled:checked ~ .custom-control-label::before {background-color: rgba(0, 158, 192, 0.5);}.custom-select {display: inline-block;width: 100%;height: calc(1.5em + 0.75rem + 2px);padding: 0.375rem 1.75rem 0.375rem 0.75rem;font-size: 1rem;font-weight: 400;line-height: 1.5;color: #495057;vertical-align: middle;background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23333333' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px;background-color: #fff;border: 1px solid #ced4da;border-radius: 0.25rem;-webkit-appearance: none;-moz-appearance: none;appearance: none;}.custom-select:focus {border-color: #41ddff;outline: 0;-webkit-box-shadow: 0 0 0 0.2rem rgba(0, 158, 192, 0.25);box-shadow: 0 0 0 0.2rem rgba(0, 158, 192, 0.25);}.custom-select:focus::-ms-value {color: #495057;background-color: #fff;}.custom-select[multiple],.custom-select[size]:not([size="1"]) {height: auto;padding-right: 0.75rem;background-image: none;}.custom-select:disabled {color: #6c757d;background-color: #e9ecef;}.custom-select::-ms-expand {display: none;}.custom-select-sm {height: calc(1.5em + 0.5rem + 2px);padding-top: 0.25rem;padding-bottom: 0.25rem;padding-left: 0.5rem;font-size: 0.875rem;}.custom-select-lg {height: calc(1.5em + 1rem + 2px);padding-top: 0.5rem;padding-bottom: 0.5rem;padding-left: 1rem;font-size: 1.25rem;}.custom-file {position: relative;display: inline-block;width: 100%;height: calc(1.5em + 0.75rem + 2px);margin-bottom: 0;}.custom-file-input {position: relative;z-index: 2;width: 100%;height: calc(1.5em + 0.75rem + 2px);margin: 0;opacity: 0;}.custom-file-input:focus ~ .custom-file-label {border-color: #41ddff;-webkit-box-shadow: 0 0 0 0.2rem rgba(0, 158, 192, 0.25);box-shadow: 0 0 0 0.2rem rgba(0, 158, 192, 0.25);}.custom-file-input:disabled ~ .custom-file-label {background-color: #e9ecef;}.custom-file-input:lang(en) ~ .custom-file-label::after {content: "Browse";}.custom-file-input ~ .custom-file-label[data-browse]::after {content: attr(data-browse);}.custom-file-label {position: absolute;top: 0;right: 0;left: 0;z-index: 1;height: calc(1.5em + 0.75rem + 2px);padding: 0.375rem 0.75rem;font-weight: 400;line-height: 1.5;color: #495057;background-color: #fff;border: 1px solid #ced4da;border-radius: 0.25rem;}.custom-file-label::after {position: absolute;top: 0;right: 0;bottom: 0;z-index: 3;display: block;height: calc(1.5em + 0.75rem);padding: 0.375rem 0.75rem;line-height: 1.5;color: #495057;content: "Browse";background-color: #e9ecef;border-left: inherit;border-radius: 0 0.25rem 0.25rem 0;}.custom-range {width: 100%;height: calc(1rem + 0.4rem);padding: 0;background-color: transparent;-webkit-appearance: none;-moz-appearance: none;appearance: none;}.custom-range:focus {outline: none;}.custom-range:focus::-webkit-slider-thumb {-webkit-box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 158, 192, 0.25);box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 158, 192, 0.25);}.custom-range:focus::-moz-range-thumb {box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 158, 192, 0.25);}.custom-range:focus::-ms-thumb {box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 158, 192, 0.25);}.custom-range::-moz-focus-outer {border: 0;}.custom-range::-webkit-slider-thumb {width: 1rem;height: 1rem;margin-top: -0.25rem;background-color: #009EC0;border: 0;border-radius: 1rem;-webkit-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;-webkit-appearance: none;appearance: none;}.custom-range::-webkit-slider-thumb:active {background-color: #74e6ff;}.custom-range::-webkit-slider-runnable-track {width: 100%;height: 0.5rem;color: transparent;cursor: pointer;background-color: #dee2e6;border-color: transparent;border-radius: 1rem;}.custom-range::-moz-range-thumb {width: 1rem;height: 1rem;background-color: #009EC0;border: 0;border-radius: 1rem;-webkit-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;-moz-appearance: none;appearance: none;}.custom-range::-moz-range-thumb:active {background-color: #74e6ff;}.custom-range::-moz-range-track {width: 100%;height: 0.5rem;color: transparent;cursor: pointer;background-color: #dee2e6;border-color: transparent;border-radius: 1rem;}.custom-range::-ms-thumb {width: 1rem;height: 1rem;margin-top: 0;margin-right: 0.2rem;margin-left: 0.2rem;background-color: #009EC0;border: 0;border-radius: 1rem;-webkit-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;appearance: none;}.custom-range::-ms-thumb:active {background-color: #74e6ff;}.custom-range::-ms-track {width: 100%;height: 0.5rem;color: transparent;cursor: pointer;background-color: transparent;border-color: transparent;border-width: 0.5rem;}.custom-range::-ms-fill-lower {background-color: #dee2e6;border-radius: 1rem;}.custom-range::-ms-fill-upper {margin-right: 15px;background-color: #dee2e6;border-radius: 1rem;}.custom-range:disabled::-webkit-slider-thumb {background-color: #adb5bd;}.custom-range:disabled::-webkit-slider-runnable-track {cursor: default;}.custom-range:disabled::-moz-range-thumb {background-color: #adb5bd;}.custom-range:disabled::-moz-range-track {cursor: default;}.custom-range:disabled::-ms-thumb {background-color: #adb5bd;}.custom-control-label::before,.custom-file-label,.custom-select {-webkit-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;}.nav {display: -webkit-box;display: -ms-flexbox;display: flex;-ms-flex-wrap: wrap;flex-wrap: wrap;padding-left: 0;margin-bottom: 0;list-style: none;}.nav-link {display: block;padding: 0.5rem 1rem;}.nav-link:hover,.nav-link:focus {text-decoration: none;}.nav-link.disabled {color: #6c757d;pointer-events: none;cursor: default;}.nav-tabs {border-bottom: 1px solid #dee2e6;}.nav-tabs .nav-item {margin-bottom: -1px;}.nav-tabs .nav-link {border: 1px solid transparent;border-top-left-radius: 0.25rem;border-top-right-radius: 0.25rem;}.nav-tabs .nav-link:hover,.nav-tabs .nav-link:focus {border-color: #e9ecef #e9ecef #dee2e6;}.nav-tabs .nav-link.disabled {color: #6c757d;background-color: transparent;border-color: transparent;}.nav-tabs .nav-link.active,.nav-tabs .nav-item.show .nav-link {color: #495057;background-color: #fff;border-color: #dee2e6 #dee2e6 #fff;}.nav-tabs .dropdown-menu {margin-top: -1px;border-top-left-radius: 0;border-top-right-radius: 0;}.nav-pills .nav-link {border-radius: 0.25rem;}.nav-pills .nav-link.active,.nav-pills .show > .nav-link {color: #fff;background-color: #009EC0;}.nav-fill .nav-item {-webkit-box-flex: 1;-ms-flex: 1 1 auto;flex: 1 1 auto;text-align: center;}.nav-justified .nav-item {-ms-flex-preferred-size: 0;flex-basis: 0;-webkit-box-flex: 1;-ms-flex-positive: 1;flex-grow: 1;text-align: center;}.tab-content > .tab-pane {display: none;}.tab-content > .active {display: block;}.navbar {position: relative;display: -webkit-box;display: -ms-flexbox;display: flex;-ms-flex-wrap: wrap;flex-wrap: wrap;-webkit-box-align: center;-ms-flex-align: center;align-items: center;-webkit-box-pack: justify;-ms-flex-pack: justify;justify-content: space-between;padding: 0.5rem 1rem;}.navbar > .container,.navbar > .container-fluid {display: -webkit-box;display: -ms-flexbox;display: flex;-ms-flex-wrap: wrap;flex-wrap: wrap;-webkit-box-align: center;-ms-flex-align: center;align-items: center;-webkit-box-pack: justify;-ms-flex-pack: justify;justify-content: space-between;}.navbar-brand {display: inline-block;padding-top: 0.3125rem;padding-bottom: 0.3125rem;margin-right: 1rem;font-size: 1.25rem;line-height: inherit;white-space: nowrap;}.navbar-brand:hover,.navbar-brand:focus {text-decoration: none;}.navbar-nav {display: -webkit-box;display: -ms-flexbox;display: flex;-webkit-box-orient: vertical;-webkit-box-direction: normal;-ms-flex-direction: column;flex-direction: column;padding-left: 0;margin-bottom: 0;list-style: none;}.navbar-nav .nav-link {padding-right: 0;padding-left: 0;}.navbar-nav .dropdown-menu {position: static;float: none;}.navbar-text {display: inline-block;padding-top: 0.5rem;padding-bottom: 0.5rem;}.navbar-collapse {-ms-flex-preferred-size: 100%;flex-basis: 100%;-webkit-box-flex: 1;-ms-flex-positive: 1;flex-grow: 1;-webkit-box-align: center;-ms-flex-align: center;align-items: center;}.navbar-toggler {padding: 0.25rem 0.75rem;font-size: 1.25rem;line-height: 1;background-color: transparent;border: 1px solid transparent;border-radius: 0.25rem;}.navbar-toggler:hover,.navbar-toggler:focus {text-decoration: none;}.navbar-toggler-icon {display: inline-block;width: 1.5em;height: 1.5em;vertical-align: middle;content: "";background: no-repeat center center;background-size: 100% 100%;}.navbar-expand {-webkit-box-orient: horizontal;-webkit-box-direction: normal;-ms-flex-flow: row nowrap;flex-flow: row nowrap;-webkit-box-pack: start;-ms-flex-pack: start;justify-content: flex-start;}.navbar-expand > .container,.navbar-expand > .container-fluid {padding-right: 0;padding-left: 0;}.navbar-expand .navbar-nav {-webkit-box-orient: horizontal;-webkit-box-direction: normal;-ms-flex-direction: row;flex-direction: row;}.navbar-expand .navbar-nav .dropdown-menu {position: absolute;}.navbar-expand .navbar-nav .nav-link {padding-right: 0.5rem;padding-left: 0.5rem;}.navbar-expand > .container,.navbar-expand > .container-fluid {-ms-flex-wrap: nowrap;flex-wrap: nowrap;}.navbar-expand .navbar-collapse {display: -webkit-box !important;display: -ms-flexbox !important;display: flex !important;-ms-flex-preferred-size: auto;flex-basis: auto;}.navbar-expand .navbar-toggler {display: none;}.navbar-light .navbar-brand {color: rgba(0, 0, 0, 0.9);}.navbar-light .navbar-brand:hover,.navbar-light .navbar-brand:focus {color: rgba(0, 0, 0, 0.9);}.navbar-light .navbar-nav .nav-link {color: rgba(0, 0, 0, 0.5);}.navbar-light .navbar-nav .nav-link:hover,.navbar-light .navbar-nav .nav-link:focus {color: rgba(0, 0, 0, 0.7);}.navbar-light .navbar-nav .nav-link.disabled {color: rgba(0, 0, 0, 0.3);}.navbar-light .navbar-nav .show > .nav-link,.navbar-light .navbar-nav .active > .nav-link,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .nav-link.active {color: rgba(0, 0, 0, 0.9);}.navbar-light .navbar-toggler {color: rgba(0, 0, 0, 0.5);border-color: rgba(0, 0, 0, 0.1);}.navbar-light .navbar-toggler-icon {background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");}.navbar-light .navbar-text {color: rgba(0, 0, 0, 0.5);}.navbar-light .navbar-text a {color: rgba(0, 0, 0, 0.9);}.navbar-light .navbar-text a:hover,.navbar-light .navbar-text a:focus {color: rgba(0, 0, 0, 0.9);}.navbar-dark .navbar-brand {color: #fff;}.navbar-dark .navbar-brand:hover,.navbar-dark .navbar-brand:focus {color: #fff;}.navbar-dark .navbar-nav .nav-link {color: rgba(255, 255, 255, 0.5);}.navbar-dark .navbar-nav .nav-link:hover,.navbar-dark .navbar-nav .nav-link:focus {color: rgba(255, 255, 255, 0.75);}.navbar-dark .navbar-nav .nav-link.disabled {color: rgba(255, 255, 255, 0.25);}.navbar-dark .navbar-nav .show > .nav-link,.navbar-dark .navbar-nav .active > .nav-link,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .nav-link.active {color: #fff;}.navbar-dark .navbar-toggler {color: rgba(255, 255, 255, 0.5);border-color: rgba(255, 255, 255, 0.1);}.navbar-dark .navbar-toggler-icon {background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");}.navbar-dark .navbar-text {color: rgba(255, 255, 255, 0.5);}.navbar-dark .navbar-text a {color: #fff;}.navbar-dark .navbar-text a:hover,.navbar-dark .navbar-text a:focus {color: #fff;}.card {position: relative;display: -webkit-box;display: -ms-flexbox;display: flex;-webkit-box-orient: vertical;-webkit-box-direction: normal;-ms-flex-direction: column;flex-direction: column;min-width: 0;word-wrap: break-word;background-color: #fff;background-clip: border-box;border: 1px solid rgba(0, 0, 0, 0.125);border-radius: 0.25rem;}.card > hr {margin-right: 0;margin-left: 0;}.card > .list-group:first-child .list-group-item:first-child {border-top-left-radius: 0.25rem;border-top-right-radius: 0.25rem;}.card > .list-group:last-child .list-group-item:last-child {border-bottom-right-radius: 0.25rem;border-bottom-left-radius: 0.25rem;}.card-body {-webkit-box-flex: 1;-ms-flex: 1 1 auto;flex: 1 1 auto;padding: 1.25rem;}.card-title {margin-bottom: 0.75rem;}.card-subtitle {margin-top: -0.375rem;margin-bottom: 0;}.card-text:last-child {margin-bottom: 0;}.card-link:hover {text-decoration: none;}.card-link + .card-link {margin-left: 1.25rem;}.card-header {padding: 0.75rem 1.25rem;margin-bottom: 0;background-color: rgba(0, 0, 0, 0.03);border-bottom: 1px solid rgba(0, 0, 0, 0.125);}.card-header:first-child {border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0;}.card-header + .list-group .list-group-item:first-child {border-top: 0;}.card-footer {padding: 0.75rem 1.25rem;background-color: rgba(0, 0, 0, 0.03);border-top: 1px solid rgba(0, 0, 0, 0.125);}.card-footer:last-child {border-radius: 0 0 calc(0.25rem - 1px) calc(0.25rem - 1px);}.card-header-tabs {margin-right: -0.625rem;margin-bottom: -0.75rem;margin-left: -0.625rem;border-bottom: 0;}.card-header-pills {margin-right: -0.625rem;margin-left: -0.625rem;}.card-img-overlay {position: absolute;top: 0;right: 0;bottom: 0;left: 0;padding: 1.25rem;}.card-img {width: 100%;border-radius: calc(0.25rem - 1px);}.card-img-top {width: 100%;border-top-left-radius: calc(0.25rem - 1px);border-top-right-radius: calc(0.25rem - 1px);}.card-img-bottom {width: 100%;border-bottom-right-radius: calc(0.25rem - 1px);border-bottom-left-radius: calc(0.25rem - 1px);}.card-deck {display: -webkit-box;display: -ms-flexbox;display: flex;-webkit-box-orient: vertical;-webkit-box-direction: normal;-ms-flex-direction: column;flex-direction: column;}.card-deck .card {margin-bottom: 15px;}.card-group {display: -webkit-box;display: -ms-flexbox;display: flex;-webkit-box-orient: vertical;-webkit-box-direction: normal;-ms-flex-direction: column;flex-direction: column;}.card-group > .card {margin-bottom: 15px;}.card-columns .card {margin-bottom: 0.75rem;}.accordion > .card {overflow: hidden;}.accordion > .card:not(:first-of-type) .card-header:first-child {border-radius: 0;}.accordion > .card:not(:first-of-type):not(:last-of-type) {border-bottom: 0;border-radius: 0;}.accordion > .card:first-of-type {border-bottom: 0;border-bottom-right-radius: 0;border-bottom-left-radius: 0;}.accordion > .card:last-of-type {border-top-left-radius: 0;border-top-right-radius: 0;}.accordion > .card .card-header {margin-bottom: -1px;}.breadcrumb {display: -webkit-box;display: -ms-flexbox;display: flex;-ms-flex-wrap: wrap;flex-wrap: wrap;padding: 0.75rem 1rem;margin-bottom: 1rem;list-style: none;background-color: transparent;border-radius: 0.25rem;}.breadcrumb-item + .breadcrumb-item {padding-left: 0.5rem;}.breadcrumb-item + .breadcrumb-item::before {display: inline-block;padding-right: 0.5rem;color: #6c757d;content: "/";}.breadcrumb-item + .breadcrumb-item:hover::before {text-decoration: underline;}.breadcrumb-item + .breadcrumb-item:hover::before {text-decoration: none;}.breadcrumb-item.active {color: #FC6621;}.pagination {display: -webkit-box;display: -ms-flexbox;display: flex;padding-left: 0;list-style: none;border-radius: 0.25rem;}.page-link {position: relative;display: block;padding: 0.5rem 0.75rem;margin-left: -1px;line-height: 1.25;color: #333333;background-color: #fff;border: 1px solid #dee2e6;}.page-link:hover {z-index: 2;color: #009EC0;text-decoration: none;background-color: #e9ecef;border-color: #dee2e6;}.page-link:focus {z-index: 2;outline: 0;-webkit-box-shadow: 0 0 0 0.2rem rgba(0, 158, 192, 0.25);box-shadow: 0 0 0 0.2rem rgba(0, 158, 192, 0.25);}.page-item:first-child .page-link {margin-left: 0;border-top-left-radius: 0.25rem;border-bottom-left-radius: 0.25rem;}.page-item:last-child .page-link {border-top-right-radius: 0.25rem;border-bottom-right-radius: 0.25rem;}.page-item.active .page-link {z-index: 1;color: #fff;background-color: #009EC0;border-color: #009EC0;}.page-item.disabled .page-link {color: #6c757d;pointer-events: none;cursor: auto;background-color: #fff;border-color: #dee2e6;}.pagination-lg .page-link {padding: 0.75rem 1.5rem;font-size: 1.25rem;line-height: 1.5;}.pagination-lg .page-item:first-child .page-link {border-top-left-radius: 0.3rem;border-bottom-left-radius: 0.3rem;}.pagination-lg .page-item:last-child .page-link {border-top-right-radius: 0.3rem;border-bottom-right-radius: 0.3rem;}.pagination-sm .page-link {padding: 0.25rem 0.5rem;font-size: 0.875rem;line-height: 1.5;}.pagination-sm .page-item:first-child .page-link {border-top-left-radius: 0.2rem;border-bottom-left-radius: 0.2rem;}.pagination-sm .page-item:last-child .page-link {border-top-right-radius: 0.2rem;border-bottom-right-radius: 0.2rem;}.badge {display: inline-block;padding: 0.25em 0.4em;font-size: 75%;font-weight: 700;line-height: 1;text-align: center;white-space: nowrap;vertical-align: baseline;border-radius: 0.25rem;-webkit-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;}a.badge:hover,a.badge:focus {text-decoration: none;}.badge:empty {display: none;}.btn .badge {position: relative;top: -1px;}.badge-pill {padding-right: 0.6em;padding-left: 0.6em;border-radius: 10rem;}.badge-primary {color: #fff;background-color: #009EC0;}a.badge-primary:hover,a.badge-primary:focus {color: #fff;background-color: #00748d;}a.badge-primary:focus,a.badge-primary.focus {outline: 0;-webkit-box-shadow: 0 0 0 0.2rem rgba(0, 158, 192, 0.5);box-shadow: 0 0 0 0.2rem rgba(0, 158, 192, 0.5);}.badge-secondary {color: #fff;background-color: #FC6621;}a.badge-secondary:hover,a.badge-secondary:focus {color: #fff;background-color: #e74b03;}a.badge-secondary:focus,a.badge-secondary.focus {outline: 0;-webkit-box-shadow: 0 0 0 0.2rem rgba(252, 102, 33, 0.5);box-shadow: 0 0 0 0.2rem rgba(252, 102, 33, 0.5);}.badge-success {color: #fff;background-color: #28a745;}a.badge-success:hover,a.badge-success:focus {color: #fff;background-color: #1e7e34;}a.badge-success:focus,a.badge-success.focus {outline: 0;-webkit-box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);}.badge-info {color: #fff;background-color: #17a2b8;}a.badge-info:hover,a.badge-info:focus {color: #fff;background-color: #117a8b;}a.badge-info:focus,a.badge-info.focus {outline: 0;-webkit-box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);}.badge-warning {color: #212529;background-color: #ffc107;}a.badge-warning:hover,a.badge-warning:focus {color: #212529;background-color: #d39e00;}a.badge-warning:focus,a.badge-warning.focus {outline: 0;-webkit-box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);}.badge-danger {color: #fff;background-color: #dc3545;}a.badge-danger:hover,a.badge-danger:focus {color: #fff;background-color: #bd2130;}a.badge-danger:focus,a.badge-danger.focus {outline: 0;-webkit-box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);}.badge-light {color: #212529;background-color: #f8f9fa;}a.badge-light:hover,a.badge-light:focus {color: #212529;background-color: #dae0e5;}a.badge-light:focus,a.badge-light.focus {outline: 0;-webkit-box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);}.badge-dark {color: #fff;background-color: #333333;}a.badge-dark:hover,a.badge-dark:focus {color: #fff;background-color: #1a1919;}a.badge-dark:focus,a.badge-dark.focus {outline: 0;-webkit-box-shadow: 0 0 0 0.2rem rgba(51, 51, 51, 0.5);box-shadow: 0 0 0 0.2rem rgba(51, 51, 51, 0.5);}.jumbotron {padding: 2rem 1rem;margin-bottom: 2rem;background-color: #e9ecef;border-radius: 0.3rem;}.jumbotron-fluid {padding-right: 0;padding-left: 0;border-radius: 0;}.alert,.paypalHelpBox {position: relative;padding: 0.75rem 1.25rem;margin-bottom: 1rem;border: 1px solid transparent;border-radius: 0.25rem;}.alert-heading {color: inherit;}.alert-link {font-weight: 700;}.alert-dismissible {padding-right: 4rem;}.alert-dismissible .close {position: absolute;top: 0;right: 0;padding: 0.75rem 1.25rem;color: inherit;}.alert-primary {color: #005264;background-color: #ccecf2;border-color: #b8e4ed;}.alert-primary hr {border-top-color: #a4dce8;}.alert-primary .alert-link {color: #002831;}.alert-secondary {color: #833511;background-color: #fee0d3;border-color: #fed4c1;}.alert-secondary hr {border-top-color: #fec3a8;}.alert-secondary .alert-link {color: #56230b;}.alert-success {color: #155724;background-color: #d4edda;border-color: #c3e6cb;}.alert-success hr {border-top-color: #b1dfbb;}.alert-success .alert-link {color: #0b2e13;}.alert-info,.paypalHelpBox {color: #0c5460;background-color: #d1ecf1;border-color: #bee5eb;}.alert-info hr,.paypalHelpBox hr {border-top-color: #abdde5;}.alert-info .alert-link,.paypalHelpBox .alert-link {color: #062c33;}.alert-warning {color: #856404;background-color: #fff3cd;border-color: #ffeeba;}.alert-warning hr {border-top-color: #ffe8a1;}.alert-warning .alert-link {color: #533f03;}.alert-danger {color: #721c24;background-color: #f8d7da;border-color: #f5c6cb;}.alert-danger hr {border-top-color: #f1b0b7;}.alert-danger .alert-link {color: #491217;}.alert-light {color: #818182;background-color: #fefefe;border-color: #fdfdfe;}.alert-light hr {border-top-color: #ececf6;}.alert-light .alert-link {color: #686868;}.alert-dark {color: #1b1b1b;background-color: #d6d6d6;border-color: #c6c6c6;}.alert-dark hr {border-top-color: #b9b9b9;}.alert-dark .alert-link {color: #020101;}.progress {display: -webkit-box;display: -ms-flexbox;display: flex;height: 1rem;overflow: hidden;font-size: 0.75rem;background-color: #e9ecef;border-radius: 0.25rem;}.progress-bar {display: -webkit-box;display: -ms-flexbox;display: flex;-webkit-box-orient: vertical;-webkit-box-direction: normal;-ms-flex-direction: column;flex-direction: column;-webkit-box-pack: center;-ms-flex-pack: center;justify-content: center;color: #fff;text-align: center;white-space: nowrap;background-color: #009EC0;-webkit-transition: width 0.6s ease;transition: width 0.6s ease;}.progress-bar-striped {background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-size: 1rem 1rem;}.progress-bar-animated {-webkit-animation: progress-bar-stripes 1s linear infinite;animation: progress-bar-stripes 1s linear infinite;}.media {display: -webkit-box;display: -ms-flexbox;display: flex;-webkit-box-align: start;-ms-flex-align: start;align-items: flex-start;}.media-body {-webkit-box-flex: 1;-ms-flex: 1;flex: 1;}.list-group {display: -webkit-box;display: -ms-flexbox;display: flex;-webkit-box-orient: vertical;-webkit-box-direction: normal;-ms-flex-direction: column;flex-direction: column;padding-left: 0;margin-bottom: 0;}.list-group-item-action {width: 100%;color: #495057;text-align: inherit;}.list-group-item-action:hover,.list-group-item-action:focus {z-index: 1;color: #495057;text-decoration: none;background-color: #f8f9fa;}.list-group-item-action:active {color: #212529;background-color: #e9ecef;}.list-group-item {position: relative;display: block;padding: 0.75rem 1.25rem;margin-bottom: -1px;background-color: #fff;border: 1px solid rgba(0, 0, 0, 0.125);}.list-group-item:first-child {border-top-left-radius: 0.25rem;border-top-right-radius: 0.25rem;}.list-group-item:last-child {margin-bottom: 0;border-bottom-right-radius: 0.25rem;border-bottom-left-radius: 0.25rem;}.list-group-item.disabled,.list-group-item:disabled {color: #6c757d;pointer-events: none;background-color: #fff;}.list-group-item.active {z-index: 2;color: #fff;background-color: #009EC0;border-color: #009EC0;}.list-group-horizontal {-webkit-box-orient: horizontal;-webkit-box-direction: normal;-ms-flex-direction: row;flex-direction: row;}.list-group-horizontal .list-group-item {margin-right: -1px;margin-bottom: 0;}.list-group-horizontal .list-group-item:first-child {border-top-left-radius: 0.25rem;border-bottom-left-radius: 0.25rem;border-top-right-radius: 0;}.list-group-horizontal .list-group-item:last-child {margin-right: 0;border-top-right-radius: 0.25rem;border-bottom-right-radius: 0.25rem;border-bottom-left-radius: 0;}.list-group-flush .list-group-item {border-right: 0;border-left: 0;border-radius: 0;}.list-group-flush .list-group-item:last-child {margin-bottom: -1px;}.list-group-flush:first-child .list-group-item:first-child {border-top: 0;}.list-group-flush:last-child .list-group-item:last-child {margin-bottom: 0;border-bottom: 0;}.list-group-item-primary {color: #005264;background-color: #b8e4ed;}.list-group-item-primary.list-group-item-action:hover,.list-group-item-primary.list-group-item-action:focus {color: #005264;background-color: #a4dce8;}.list-group-item-primary.list-group-item-action.active {color: #fff;background-color: #005264;border-color: #005264;}.list-group-item-secondary {color: #833511;background-color: #fed4c1;}.list-group-item-secondary.list-group-item-action:hover,.list-group-item-secondary.list-group-item-action:focus {color: #833511;background-color: #fec3a8;}.list-group-item-secondary.list-group-item-action.active {color: #fff;background-color: #833511;border-color: #833511;}.list-group-item-success {color: #155724;background-color: #c3e6cb;}.list-group-item-success.list-group-item-action:hover,.list-group-item-success.list-group-item-action:focus {color: #155724;background-color: #b1dfbb;}.list-group-item-success.list-group-item-action.active {color: #fff;background-color: #155724;border-color: #155724;}.list-group-item-info {color: #0c5460;background-color: #bee5eb;}.list-group-item-info.list-group-item-action:hover,.list-group-item-info.list-group-item-action:focus {color: #0c5460;background-color: #abdde5;}.list-group-item-info.list-group-item-action.active {color: #fff;background-color: #0c5460;border-color: #0c5460;}.list-group-item-warning {color: #856404;background-color: #ffeeba;}.list-group-item-warning.list-group-item-action:hover,.list-group-item-warning.list-group-item-action:focus {color: #856404;background-color: #ffe8a1;}.list-group-item-warning.list-group-item-action.active {color: #fff;background-color: #856404;border-color: #856404;}.list-group-item-danger {color: #721c24;background-color: #f5c6cb;}.list-group-item-danger.list-group-item-action:hover,.list-group-item-danger.list-group-item-action:focus {color: #721c24;background-color: #f1b0b7;}.list-group-item-danger.list-group-item-action.active {color: #fff;background-color: #721c24;border-color: #721c24;}.list-group-item-light {color: #818182;background-color: #fdfdfe;}.list-group-item-light.list-group-item-action:hover,.list-group-item-light.list-group-item-action:focus {color: #818182;background-color: #ececf6;}.list-group-item-light.list-group-item-action.active {color: #fff;background-color: #818182;border-color: #818182;}.list-group-item-dark {color: #1b1b1b;background-color: #c6c6c6;}.list-group-item-dark.list-group-item-action:hover,.list-group-item-dark.list-group-item-action:focus {color: #1b1b1b;background-color: #b9b9b9;}.list-group-item-dark.list-group-item-action.active {color: #fff;background-color: #1b1b1b;border-color: #1b1b1b;}.close {float: right;font-size: 1.5rem;font-weight: 700;line-height: 1;color: #000;text-shadow: 0 1px 0 #fff;opacity: .5;}.close:hover {color: #000;text-decoration: none;}.close:not(:disabled):not(.disabled):hover,.close:not(:disabled):not(.disabled):focus {opacity: .75;}button.close {padding: 0;background-color: transparent;border: 0;-webkit-appearance: none;-moz-appearance: none;appearance: none;}a.close.disabled {pointer-events: none;}.toast {max-width: 350px;overflow: hidden;font-size: 0.875rem;background-color: rgba(255, 255, 255, 0.85);background-clip: padding-box;border: 1px solid rgba(0, 0, 0, 0.1);-webkit-box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.1);box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.1);-webkit-backdrop-filter: blur(10px);backdrop-filter: blur(10px);opacity: 0;border-radius: 0.25rem;}.toast:not(:last-child) {margin-bottom: 0.75rem;}.toast.showing {opacity: 1;}.toast.show {display: block;opacity: 1;}.toast.hide {display: none;}.toast-header {display: -webkit-box;display: -ms-flexbox;display: flex;-webkit-box-align: center;-ms-flex-align: center;align-items: center;padding: 0.25rem 0.75rem;color: #6c757d;background-color: rgba(255, 255, 255, 0.85);background-clip: padding-box;border-bottom: 1px solid rgba(0, 0, 0, 0.05);}.toast-body {padding: 0.75rem;}.modal-open {overflow: hidden;}.modal-open .modal {overflow-x: hidden;overflow-y: auto;}.modal {position: fixed;top: 0;left: 0;z-index: 1050;display: none;width: 100%;height: 100%;overflow: hidden;outline: 0;}.modal-dialog {position: relative;width: auto;margin: 0.5rem;pointer-events: none;}.modal.fade .modal-dialog {-webkit-transition: -webkit-transform 0.3s ease-out;transition: -webkit-transform 0.3s ease-out;transition: transform 0.3s ease-out;transition: transform 0.3s ease-out, -webkit-transform 0.3s ease-out;-webkit-transform: translate(0, -50px);transform: translate(0, -50px);}.modal.show .modal-dialog {-webkit-transform: none;transform: none;}.modal-dialog-scrollable {display: -webkit-box;display: -ms-flexbox;display: flex;max-height: calc(100% - 1rem);}.modal-dialog-scrollable .modal-content {max-height: calc(100vh - 1rem);overflow: hidden;}.modal-dialog-scrollable .modal-header,.modal-dialog-scrollable .modal-footer {-ms-flex-negative: 0;flex-shrink: 0;}.modal-dialog-scrollable .modal-body {overflow-y: auto;}.modal-dialog-centered {display: -webkit-box;display: -ms-flexbox;display: flex;-webkit-box-align: center;-ms-flex-align: center;align-items: center;min-height: calc(100% - 1rem);}.modal-dialog-centered::before {display: block;height: calc(100vh - 1rem);content: "";}.modal-dialog-centered.modal-dialog-scrollable {-webkit-box-orient: vertical;-webkit-box-direction: normal;-ms-flex-direction: column;flex-direction: column;-webkit-box-pack: center;-ms-flex-pack: center;justify-content: center;height: 100%;}.modal-dialog-centered.modal-dialog-scrollable .modal-content {max-height: none;}.modal-dialog-centered.modal-dialog-scrollable::before {content: none;}.modal-content {position: relative;display: -webkit-box;display: -ms-flexbox;display: flex;-webkit-box-orient: vertical;-webkit-box-direction: normal;-ms-flex-direction: column;flex-direction: column;width: 100%;pointer-events: auto;background-color: #fff;background-clip: padding-box;border: 1px solid rgba(0, 0, 0, 0.2);border-radius: 0.3rem;outline: 0;}.modal-backdrop {position: fixed;top: 0;left: 0;z-index: 1040;width: 100vw;height: 100vh;background-color: #000;}.modal-backdrop.fade {opacity: 0;}.modal-backdrop.show {opacity: 0.5;}.modal-header {display: -webkit-box;display: -ms-flexbox;display: flex;-webkit-box-align: start;-ms-flex-align: start;align-items: flex-start;-webkit-box-pack: justify;-ms-flex-pack: justify;justify-content: space-between;padding: 1rem 1rem;border-bottom: 1px solid #dee2e6;border-top-left-radius: 0.3rem;border-top-right-radius: 0.3rem;}.modal-header .close {padding: 1rem 1rem;margin: -1rem -1rem -1rem auto;}.modal-title {margin-bottom: 0;line-height: 1.5;}.modal-body {position: relative;-webkit-box-flex: 1;-ms-flex: 1 1 auto;flex: 1 1 auto;padding: 1rem;}.modal-footer {display: -webkit-box;display: -ms-flexbox;display: flex;-webkit-box-align: center;-ms-flex-align: center;align-items: center;-webkit-box-pack: end;-ms-flex-pack: end;justify-content: flex-end;padding: 1rem;border-top: 1px solid #dee2e6;border-bottom-right-radius: 0.3rem;border-bottom-left-radius: 0.3rem;}.modal-footer > :not(:first-child) {margin-left: .25rem;}.modal-footer > :not(:last-child) {margin-right: .25rem;}.modal-scrollbar-measure {position: absolute;top: -9999px;width: 50px;height: 50px;overflow: scroll;}.tooltip {position: absolute;z-index: 1070;display: block;margin: 0;font-family: Raleway, "Helvetica Neue", Helvetica, Arial, sans-serif;font-style: normal;font-weight: 400;line-height: 1.5;text-align: left;text-align: start;text-decoration: none;text-shadow: none;text-transform: none;letter-spacing: normal;word-break: normal;word-spacing: normal;white-space: normal;line-break: auto;font-size: 0.875rem;word-wrap: break-word;opacity: 0;}.tooltip.show {opacity: 0.9;}.tooltip .arrow {position: absolute;display: block;width: 0.8rem;height: 0.4rem;}.tooltip .arrow::before {position: absolute;content: "";border-color: transparent;border-style: solid;}.bs-tooltip-top,.bs-tooltip-auto[x-placement^="top"] {padding: 0.4rem 0;}.bs-tooltip-top .arrow,.bs-tooltip-auto[x-placement^="top"] .arrow {bottom: 0;}.bs-tooltip-top .arrow::before,.bs-tooltip-auto[x-placement^="top"] .arrow::before {top: 0;border-width: 0.4rem 0.4rem 0;border-top-color: #000;}.bs-tooltip-right,.bs-tooltip-auto[x-placement^="right"] {padding: 0 0.4rem;}.bs-tooltip-right .arrow,.bs-tooltip-auto[x-placement^="right"] .arrow {left: 0;width: 0.4rem;height: 0.8rem;}.bs-tooltip-right .arrow::before,.bs-tooltip-auto[x-placement^="right"] .arrow::before {right: 0;border-width: 0.4rem 0.4rem 0.4rem 0;border-right-color: #000;}.bs-tooltip-bottom,.bs-tooltip-auto[x-placement^="bottom"] {padding: 0.4rem 0;}.bs-tooltip-bottom .arrow,.bs-tooltip-auto[x-placement^="bottom"] .arrow {top: 0;}.bs-tooltip-bottom .arrow::before,.bs-tooltip-auto[x-placement^="bottom"] .arrow::before {bottom: 0;border-width: 0 0.4rem 0.4rem;border-bottom-color: #000;}.bs-tooltip-left,.bs-tooltip-auto[x-placement^="left"] {padding: 0 0.4rem;}.bs-tooltip-left .arrow,.bs-tooltip-auto[x-placement^="left"] .arrow {right: 0;width: 0.4rem;height: 0.8rem;}.bs-tooltip-left .arrow::before,.bs-tooltip-auto[x-placement^="left"] .arrow::before {left: 0;border-width: 0.4rem 0 0.4rem 0.4rem;border-left-color: #000;}.tooltip-inner {max-width: 200px;padding: 0.25rem 0.5rem;color: #fff;text-align: center;background-color: #000;border-radius: 0.25rem;}.popover {position: absolute;top: 0;left: 0;z-index: 1060;display: block;max-width: 276px;font-family: Raleway, "Helvetica Neue", Helvetica, Arial, sans-serif;font-style: normal;font-weight: 400;line-height: 1.5;text-align: left;text-align: start;text-decoration: none;text-shadow: none;text-transform: none;letter-spacing: normal;word-break: normal;word-spacing: normal;white-space: normal;line-break: auto;font-size: 0.875rem;word-wrap: break-word;background-color: #fff;background-clip: padding-box;border: 1px solid rgba(0, 0, 0, 0.2);border-radius: 0.3rem;}.popover .arrow {position: absolute;display: block;width: 1rem;height: 0.5rem;margin: 0 0.3rem;}.popover .arrow::before,.popover .arrow::after {position: absolute;display: block;content: "";border-color: transparent;border-style: solid;}.bs-popover-top,.bs-popover-auto[x-placement^="top"] {margin-bottom: 0.5rem;}.bs-popover-top > .arrow,.bs-popover-auto[x-placement^="top"] > .arrow {bottom: calc((0.5rem + 1px) * -1);}.bs-popover-top > .arrow::before,.bs-popover-auto[x-placement^="top"] > .arrow::before {bottom: 0;border-width: 0.5rem 0.5rem 0;border-top-color: rgba(0, 0, 0, 0.25);}.bs-popover-top > .arrow::after,.bs-popover-auto[x-placement^="top"] > .arrow::after {bottom: 1px;border-width: 0.5rem 0.5rem 0;border-top-color: #fff;}.bs-popover-right,.bs-popover-auto[x-placement^="right"] {margin-left: 0.5rem;}.bs-popover-right > .arrow,.bs-popover-auto[x-placement^="right"] > .arrow {left: calc((0.5rem + 1px) * -1);width: 0.5rem;height: 1rem;margin: 0.3rem 0;}.bs-popover-right > .arrow::before,.bs-popover-auto[x-placement^="right"] > .arrow::before {left: 0;border-width: 0.5rem 0.5rem 0.5rem 0;border-right-color: rgba(0, 0, 0, 0.25);}.bs-popover-right > .arrow::after,.bs-popover-auto[x-placement^="right"] > .arrow::after {left: 1px;border-width: 0.5rem 0.5rem 0.5rem 0;border-right-color: #fff;}.bs-popover-bottom,.bs-popover-auto[x-placement^="bottom"] {margin-top: 0.5rem;}.bs-popover-bottom > .arrow,.bs-popover-auto[x-placement^="bottom"] > .arrow {top: calc((0.5rem + 1px) * -1);}.bs-popover-bottom > .arrow::before,.bs-popover-auto[x-placement^="bottom"] > .arrow::before {top: 0;border-width: 0 0.5rem 0.5rem 0.5rem;border-bottom-color: rgba(0, 0, 0, 0.25);}.bs-popover-bottom > .arrow::after,.bs-popover-auto[x-placement^="bottom"] > .arrow::after {top: 1px;border-width: 0 0.5rem 0.5rem 0.5rem;border-bottom-color: #fff;}.bs-popover-bottom .popover-header::before,.bs-popover-auto[x-placement^="bottom"] .popover-header::before {position: absolute;top: 0;left: 50%;display: block;width: 1rem;margin-left: -0.5rem;content: "";border-bottom: 1px solid #f7f7f7;}.bs-popover-left,.bs-popover-auto[x-placement^="left"] {margin-right: 0.5rem;}.bs-popover-left > .arrow,.bs-popover-auto[x-placement^="left"] > .arrow {right: calc((0.5rem + 1px) * -1);width: 0.5rem;height: 1rem;margin: 0.3rem 0;}.bs-popover-left > .arrow::before,.bs-popover-auto[x-placement^="left"] > .arrow::before {right: 0;border-width: 0.5rem 0 0.5rem 0.5rem;border-left-color: rgba(0, 0, 0, 0.25);}.bs-popover-left > .arrow::after,.bs-popover-auto[x-placement^="left"] > .arrow::after {right: 1px;border-width: 0.5rem 0 0.5rem 0.5rem;border-left-color: #fff;}.popover-header {padding: 0.5rem 0.75rem;margin-bottom: 0;font-size: 1rem;background-color: #f7f7f7;border-bottom: 1px solid #ebebeb;border-top-left-radius: calc(0.3rem - 1px);border-top-right-radius: calc(0.3rem - 1px);}.popover-header:empty {display: none;}.popover-body {padding: 0.5rem 0.75rem;color: #212529;}.carousel {position: relative;}.carousel.pointer-event {-ms-touch-action: pan-y;touch-action: pan-y;}.carousel-inner {position: relative;width: 100%;overflow: hidden;}.carousel-inner::after {display: block;clear: both;content: "";}.carousel-item {position: relative;display: none;float: left;width: 100%;margin-right: -100%;-webkit-backface-visibility: hidden;backface-visibility: hidden;-webkit-transition: -webkit-transform 0.6s ease-in-out;transition: -webkit-transform 0.6s ease-in-out;transition: transform 0.6s ease-in-out;transition: transform 0.6s ease-in-out, -webkit-transform 0.6s ease-in-out;}.carousel-item.active,.carousel-item-next,.carousel-item-prev {display: block;}.carousel-item-next:not(.carousel-item-left),.active.carousel-item-right {-webkit-transform: translateX(100%);transform: translateX(100%);}.carousel-item-prev:not(.carousel-item-right),.active.carousel-item-left {-webkit-transform: translateX(-100%);transform: translateX(-100%);}.carousel-fade .carousel-item {opacity: 0;-webkit-transition-property: opacity;transition-property: opacity;-webkit-transform: none;transform: none;}.carousel-fade .carousel-item.active,.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right {z-index: 1;opacity: 1;}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right {z-index: 0;opacity: 0;-webkit-transition: 0s 0.6s opacity;transition: 0s 0.6s opacity;}.carousel-control-prev,.carousel-control-next {position: absolute;top: 0;bottom: 0;z-index: 1;display: -webkit-box;display: -ms-flexbox;display: flex;-webkit-box-align: center;-ms-flex-align: center;align-items: center;-webkit-box-pack: center;-ms-flex-pack: center;justify-content: center;width: 15%;color: #fff;text-align: center;opacity: 0.5;-webkit-transition: opacity 0.15s ease;transition: opacity 0.15s ease;}.carousel-control-prev:hover,.carousel-control-prev:focus,.carousel-control-next:hover,.carousel-control-next:focus {color: #fff;text-decoration: none;outline: 0;opacity: 0.9;}.carousel-control-prev {left: 0;}.carousel-control-next {right: 0;}.carousel-control-prev-icon,.carousel-control-next-icon {display: inline-block;width: 20px;height: 20px;background: no-repeat 50% / 100% 100%;}.carousel-control-prev-icon {background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3e%3c/svg%3e");}.carousel-control-next-icon {background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3e%3c/svg%3e");}.carousel-indicators {position: absolute;right: 0;bottom: 0;left: 0;z-index: 15;display: -webkit-box;display: -ms-flexbox;display: flex;-webkit-box-pack: center;-ms-flex-pack: center;justify-content: center;padding-left: 0;margin-right: 15%;margin-left: 15%;list-style: none;}.carousel-indicators li {-webkit-box-sizing: content-box;box-sizing: content-box;-webkit-box-flex: 0;-ms-flex: 0 1 auto;flex: 0 1 auto;width: 30px;height: 3px;margin-right: 3px;margin-left: 3px;text-indent: -999px;cursor: pointer;background-color: #fff;background-clip: padding-box;border-top: 10px solid transparent;border-bottom: 10px solid transparent;opacity: .5;-webkit-transition: opacity 0.6s ease;transition: opacity 0.6s ease;}.carousel-indicators .active {opacity: 1;}.carousel-caption {position: absolute;right: 15%;bottom: 20px;left: 15%;z-index: 10;padding-top: 20px;padding-bottom: 20px;color: #fff;text-align: center;}.spinner-border {display: inline-block;width: 2rem;height: 2rem;vertical-align: text-bottom;border: 0.25em solid currentColor;border-right-color: transparent;border-radius: 50%;-webkit-animation: spinner-border .75s linear infinite;animation: spinner-border .75s linear infinite;}.spinner-border-sm {width: 1rem;height: 1rem;border-width: 0.2em;}.spinner-grow {display: inline-block;width: 2rem;height: 2rem;vertical-align: text-bottom;background-color: currentColor;border-radius: 50%;opacity: 0;-webkit-animation: spinner-grow .75s linear infinite;animation: spinner-grow .75s linear infinite;}.spinner-grow-sm {width: 1rem;height: 1rem;}.align-baseline {vertical-align: baseline !important;}.align-top {vertical-align: top !important;}.align-middle {vertical-align: middle !important;}.align-bottom {vertical-align: bottom !important;}.align-text-bottom {vertical-align: text-bottom !important;}.align-text-top {vertical-align: text-top !important;}.bg-primary {background-color: #009EC0 !important;}a.bg-primary:hover,a.bg-primary:focus,button.bg-primary:hover,button.bg-primary:focus {background-color: #00748d !important;}.bg-secondary {background-color: #FC6621 !important;}a.bg-secondary:hover,a.bg-secondary:focus,button.bg-secondary:hover,button.bg-secondary:focus {background-color: #e74b03 !important;}.bg-success {background-color: #28a745 !important;}a.bg-success:hover,a.bg-success:focus,button.bg-success:hover,button.bg-success:focus {background-color: #1e7e34 !important;}.bg-info {background-color: #17a2b8 !important;}a.bg-info:hover,a.bg-info:focus,button.bg-info:hover,button.bg-info:focus {background-color: #117a8b !important;}.bg-warning {background-color: #ffc107 !important;}a.bg-warning:hover,a.bg-warning:focus,button.bg-warning:hover,button.bg-warning:focus {background-color: #d39e00 !important;}.bg-danger {background-color: #dc3545 !important;}a.bg-danger:hover,a.bg-danger:focus,button.bg-danger:hover,button.bg-danger:focus {background-color: #bd2130 !important;}.bg-light {background-color: #f8f9fa !important;}a.bg-light:hover,a.bg-light:focus,button.bg-light:hover,button.bg-light:focus {background-color: #dae0e5 !important;}.bg-dark {background-color: #333333 !important;}a.bg-dark:hover,a.bg-dark:focus,button.bg-dark:hover,button.bg-dark:focus {background-color: #1a1919 !important;}.bg-white {background-color: #fff !important;}.bg-transparent {background-color: transparent !important;}.border {border: 1px solid #dee2e6 !important;}.border-top {border-top: 1px solid #dee2e6 !important;}.border-right {border-right: 1px solid #dee2e6 !important;}.border-bottom {border-bottom: 1px solid #dee2e6 !important;}.border-left {border-left: 1px solid #dee2e6 !important;}.border-0 {border: 0 !important;}.border-top-0 {border-top: 0 !important;}.border-right-0 {border-right: 0 !important;}.border-bottom-0 {border-bottom: 0 !important;}.border-left-0 {border-left: 0 !important;}.border-primary {border-color: #009EC0 !important;}.border-secondary {border-color: #FC6621 !important;}.border-success {border-color: #28a745 !important;}.border-info {border-color: #17a2b8 !important;}.border-warning {border-color: #ffc107 !important;}.border-danger {border-color: #dc3545 !important;}.border-light {border-color: #f8f9fa !important;}.border-dark {border-color: #333333 !important;}.border-white {border-color: #fff !important;}.rounded-sm {border-radius: 0.2rem !important;}.rounded {border-radius: 0.25rem !important;}.rounded-top {border-top-left-radius: 0.25rem !important;border-top-right-radius: 0.25rem !important;}.rounded-right {border-top-right-radius: 0.25rem !important;border-bottom-right-radius: 0.25rem !important;}.rounded-bottom {border-bottom-right-radius: 0.25rem !important;border-bottom-left-radius: 0.25rem !important;}.rounded-left {border-top-left-radius: 0.25rem !important;border-bottom-left-radius: 0.25rem !important;}.rounded-lg {border-radius: 0.3rem !important;}.rounded-circle {border-radius: 50% !important;}.rounded-pill {border-radius: 50rem !important;}.rounded-0 {border-radius: 0 !important;}.clearfix::after {display: block;clear: both;content: "";}.d-none {display: none !important;}.d-inline {display: inline !important;}.d-inline-block {display: inline-block !important;}.d-block {display: block !important;}.d-table {display: table !important;}.d-table-row {display: table-row !important;}.d-table-cell {display: table-cell !important;}.d-flex {display: -webkit-box !important;display: -ms-flexbox !important;display: flex !important;}.d-inline-flex {display: -webkit-inline-box !important;display: -ms-inline-flexbox !important;display: inline-flex !important;}.embed-responsive {position: relative;display: block;width: 100%;padding: 0;overflow: hidden;}.embed-responsive::before {display: block;content: "";}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object,.embed-responsive video {position: absolute;top: 0;bottom: 0;left: 0;width: 100%;height: 100%;border: 0;}.embed-responsive-21by9::before {padding-top: 42.8571428571%;}.embed-responsive-16by9::before {padding-top: 56.25%;}.embed-responsive-4by3::before {padding-top: 75%;}.embed-responsive-1by1::before {padding-top: 100%;}.flex-row {-webkit-box-orient: horizontal !important;-webkit-box-direction: normal !important;-ms-flex-direction: row !important;flex-direction: row !important;}.flex-column {-webkit-box-orient: vertical !important;-webkit-box-direction: normal !important;-ms-flex-direction: column !important;flex-direction: column !important;}.flex-row-reverse {-webkit-box-orient: horizontal !important;-webkit-box-direction: reverse !important;-ms-flex-direction: row-reverse !important;flex-direction: row-reverse !important;}.flex-column-reverse {-webkit-box-orient: vertical !important;-webkit-box-direction: reverse !important;-ms-flex-direction: column-reverse !important;flex-direction: column-reverse !important;}.flex-wrap {-ms-flex-wrap: wrap !important;flex-wrap: wrap !important;}.flex-nowrap {-ms-flex-wrap: nowrap !important;flex-wrap: nowrap !important;}.flex-wrap-reverse {-ms-flex-wrap: wrap-reverse !important;flex-wrap: wrap-reverse !important;}.flex-fill {-webkit-box-flex: 1 !important;-ms-flex: 1 1 auto !important;flex: 1 1 auto !important;}.flex-grow-0 {-webkit-box-flex: 0 !important;-ms-flex-positive: 0 !important;flex-grow: 0 !important;}.flex-grow-1 {-webkit-box-flex: 1 !important;-ms-flex-positive: 1 !important;flex-grow: 1 !important;}.flex-shrink-0 {-ms-flex-negative: 0 !important;flex-shrink: 0 !important;}.flex-shrink-1 {-ms-flex-negative: 1 !important;flex-shrink: 1 !important;}.justify-content-start {-webkit-box-pack: start !important;-ms-flex-pack: start !important;justify-content: flex-start !important;}.justify-content-end {-webkit-box-pack: end !important;-ms-flex-pack: end !important;justify-content: flex-end !important;}.justify-content-center {-webkit-box-pack: center !important;-ms-flex-pack: center !important;justify-content: center !important;}.justify-content-between {-webkit-box-pack: justify !important;-ms-flex-pack: justify !important;justify-content: space-between !important;}.justify-content-around {-ms-flex-pack: distribute !important;justify-content: space-around !important;}.align-items-start {-webkit-box-align: start !important;-ms-flex-align: start !important;align-items: flex-start !important;}.align-items-end {-webkit-box-align: end !important;-ms-flex-align: end !important;align-items: flex-end !important;}.align-items-center {-webkit-box-align: center !important;-ms-flex-align: center !important;align-items: center !important;}.align-items-baseline {-webkit-box-align: baseline !important;-ms-flex-align: baseline !important;align-items: baseline !important;}.align-items-stretch {-webkit-box-align: stretch !important;-ms-flex-align: stretch !important;align-items: stretch !important;}.align-content-start {-ms-flex-line-pack: start !important;align-content: flex-start !important;}.align-content-end {-ms-flex-line-pack: end !important;align-content: flex-end !important;}.align-content-center {-ms-flex-line-pack: center !important;align-content: center !important;}.align-content-between {-ms-flex-line-pack: justify !important;align-content: space-between !important;}.align-content-around {-ms-flex-line-pack: distribute !important;align-content: space-around !important;}.align-content-stretch {-ms-flex-line-pack: stretch !important;align-content: stretch !important;}.align-self-auto {-ms-flex-item-align: auto !important;align-self: auto !important;}.align-self-start {-ms-flex-item-align: start !important;align-self: flex-start !important;}.align-self-end {-ms-flex-item-align: end !important;align-self: flex-end !important;}.align-self-center {-ms-flex-item-align: center !important;align-self: center !important;}.align-self-baseline {-ms-flex-item-align: baseline !important;align-self: baseline !important;}.align-self-stretch {-ms-flex-item-align: stretch !important;align-self: stretch !important;}.float-left {float: left !important;}.float-right {float: right !important;}.float-none {float: none !important;}.overflow-auto {overflow: auto !important;}.overflow-hidden {overflow: hidden !important;}.position-static {position: static !important;}.position-relative {position: relative !important;}.position-absolute {position: absolute !important;}.position-fixed {position: fixed !important;}.position-sticky {position: -webkit-sticky !important;position: sticky !important;}.fixed-top {position: fixed;top: 0;right: 0;left: 0;z-index: 1030;}.fixed-bottom {position: fixed;right: 0;bottom: 0;left: 0;z-index: 1030;}.sr-only {position: absolute;width: 1px;height: 1px;padding: 0;overflow: hidden;clip: rect(0, 0, 0, 0);white-space: nowrap;border: 0;}.sr-only-focusable:active,.sr-only-focusable:focus {position: static;width: auto;height: auto;overflow: visible;clip: auto;white-space: normal;}.shadow-sm {-webkit-box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important;box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important;}.shadow {-webkit-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;}.shadow-lg {-webkit-box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important;box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important;}.shadow-none {-webkit-box-shadow: none !important;box-shadow: none !important;}.w-25 {width: 25% !important;}.w-50 {width: 50% !important;}.w-75 {width: 75% !important;}.w-100 {width: 100% !important;}.w-auto {width: auto !important;}.h-25 {height: 25% !important;}.h-50 {height: 50% !important;}.h-75 {height: 75% !important;}.h-100 {height: 100% !important;}.h-auto {height: auto !important;}.mw-100 {max-width: 100% !important;}.mh-100 {max-height: 100% !important;}.min-vw-100 {min-width: 100vw !important;}.min-vh-100 {min-height: 100vh !important;}.vw-100 {width: 100vw !important;}.vh-100 {height: 100vh !important;}.stretched-link::after {position: absolute;top: 0;right: 0;bottom: 0;left: 0;z-index: 1;pointer-events: auto;content: "";background-color: rgba(0, 0, 0, 0);}.m-0 {margin: 0 !important;}.mt-0,.my-0 {margin-top: 0 !important;}.mr-0,.mx-0 {margin-right: 0 !important;}.mb-0,.my-0 {margin-bottom: 0 !important;}.ml-0,.mx-0 {margin-left: 0 !important;}.m-1 {margin: 0.25rem !important;}.mt-1,.my-1 {margin-top: 0.25rem !important;}.mr-1,.mx-1 {margin-right: 0.25rem !important;}.mb-1,.my-1 {margin-bottom: 0.25rem !important;}.ml-1,.mx-1 {margin-left: 0.25rem !important;}.m-2 {margin: 0.5rem !important;}.mt-2,.my-2 {margin-top: 0.5rem !important;}.mr-2,.mx-2 {margin-right: 0.5rem !important;}.mb-2,.my-2 {margin-bottom: 0.5rem !important;}.ml-2,.mx-2 {margin-left: 0.5rem !important;}.m-3 {margin: 1rem !important;}.mt-3,.my-3 {margin-top: 1rem !important;}.mr-3,.mx-3 {margin-right: 1rem !important;}.mb-3,.my-3 {margin-bottom: 1rem !important;}.ml-3,.mx-3 {margin-left: 1rem !important;}.m-4 {margin: 1.5rem !important;}.mt-4,.my-4 {margin-top: 1.5rem !important;}.mr-4,.mx-4 {margin-right: 1.5rem !important;}.mb-4,.my-4 {margin-bottom: 1.5rem !important;}.ml-4,.mx-4 {margin-left: 1.5rem !important;}.m-5 {margin: 3rem !important;}.mt-5,.my-5 {margin-top: 3rem !important;}.mr-5,.mx-5 {margin-right: 3rem !important;}.mb-5,.my-5 {margin-bottom: 3rem !important;}.ml-5,.mx-5 {margin-left: 3rem !important;}.p-0 {padding: 0 !important;}.pt-0,.py-0 {padding-top: 0 !important;}.pr-0,.px-0 {padding-right: 0 !important;}.pb-0,.py-0 {padding-bottom: 0 !important;}.pl-0,.px-0 {padding-left: 0 !important;}.p-1 {padding: 0.25rem !important;}.pt-1,.py-1 {padding-top: 0.25rem !important;}.pr-1,.px-1 {padding-right: 0.25rem !important;}.pb-1,.py-1 {padding-bottom: 0.25rem !important;}.pl-1,.px-1 {padding-left: 0.25rem !important;}.p-2 {padding: 0.5rem !important;}.pt-2,.py-2 {padding-top: 0.5rem !important;}.pr-2,.px-2 {padding-right: 0.5rem !important;}.pb-2,.py-2 {padding-bottom: 0.5rem !important;}.pl-2,.px-2 {padding-left: 0.5rem !important;}.p-3 {padding: 1rem !important;}.pt-3,.py-3 {padding-top: 1rem !important;}.pr-3,.px-3 {padding-right: 1rem !important;}.pb-3,.py-3 {padding-bottom: 1rem !important;}.pl-3,.px-3 {padding-left: 1rem !important;}.p-4 {padding: 1.5rem !important;}.pt-4,.py-4 {padding-top: 1.5rem !important;}.pr-4,.px-4 {padding-right: 1.5rem !important;}.pb-4,.py-4 {padding-bottom: 1.5rem !important;}.pl-4,.px-4 {padding-left: 1.5rem !important;}.p-5 {padding: 3rem !important;}.pt-5,.py-5 {padding-top: 3rem !important;}.pr-5,.px-5 {padding-right: 3rem !important;}.pb-5,.py-5 {padding-bottom: 3rem !important;}.pl-5,.px-5 {padding-left: 3rem !important;}.m-n1 {margin: -0.25rem !important;}.mt-n1,.my-n1 {margin-top: -0.25rem !important;}.mr-n1,.mx-n1 {margin-right: -0.25rem !important;}.mb-n1,.my-n1 {margin-bottom: -0.25rem !important;}.ml-n1,.mx-n1 {margin-left: -0.25rem !important;}.m-n2 {margin: -0.5rem !important;}.mt-n2,.my-n2 {margin-top: -0.5rem !important;}.mr-n2,.mx-n2 {margin-right: -0.5rem !important;}.mb-n2,.my-n2 {margin-bottom: -0.5rem !important;}.ml-n2,.mx-n2 {margin-left: -0.5rem !important;}.m-n3 {margin: -1rem !important;}.mt-n3,.my-n3 {margin-top: -1rem !important;}.mr-n3,.mx-n3 {margin-right: -1rem !important;}.mb-n3,.my-n3 {margin-bottom: -1rem !important;}.ml-n3,.mx-n3 {margin-left: -1rem !important;}.m-n4 {margin: -1.5rem !important;}.mt-n4,.my-n4 {margin-top: -1.5rem !important;}.mr-n4,.mx-n4 {margin-right: -1.5rem !important;}.mb-n4,.my-n4 {margin-bottom: -1.5rem !important;}.ml-n4,.mx-n4 {margin-left: -1.5rem !important;}.m-n5 {margin: -3rem !important;}.mt-n5,.my-n5 {margin-top: -3rem !important;}.mr-n5,.mx-n5 {margin-right: -3rem !important;}.mb-n5,.my-n5 {margin-bottom: -3rem !important;}.ml-n5,.mx-n5 {margin-left: -3rem !important;}.m-auto {margin: auto !important;}.mt-auto,.my-auto {margin-top: auto !important;}.mr-auto,.mx-auto {margin-right: auto !important;}.mb-auto,.my-auto {margin-bottom: auto !important;}.ml-auto,.mx-auto {margin-left: auto !important;}.text-monospace {font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important;}.text-justify {text-align: justify !important;}.text-wrap {white-space: normal !important;}.text-nowrap {white-space: nowrap !important;}.text-truncate {overflow: hidden;text-overflow: ellipsis;white-space: nowrap;}.text-left {text-align: left !important;}.text-right {text-align: right !important;}.text-center {text-align: center !important;}.text-lowercase {text-transform: lowercase !important;}.text-uppercase {text-transform: uppercase !important;}.text-capitalize {text-transform: capitalize !important;}.font-weight-light {font-weight: 300 !important;}.font-weight-lighter {font-weight: lighter !important;}.font-weight-normal {font-weight: 400 !important;}.font-weight-bold {font-weight: 700 !important;}.font-weight-bolder {font-weight: bolder !important;}.font-italic {font-style: italic !important;}.text-white {color: #fff !important;}.text-primary {color: #009EC0 !important;}a.text-primary:hover,a.text-primary:focus {color: #005f74 !important;}.text-secondary {color: #FC6621 !important;}a.text-secondary:hover,a.text-secondary:focus {color: #ce4303 !important;}.text-success {color: #28a745 !important;}a.text-success:hover,a.text-success:focus {color: #19692c !important;}.text-info {color: #17a2b8 !important;}a.text-info:hover,a.text-info:focus {color: #0f6674 !important;}.text-warning {color: #ffc107 !important;}a.text-warning:hover,a.text-warning:focus {color: #ba8b00 !important;}.text-danger {color: #dc3545 !important;}a.text-danger:hover,a.text-danger:focus {color: #a71d2a !important;}.text-light {color: #f8f9fa !important;}a.text-light:hover,a.text-light:focus {color: #cbd3da !important;}.text-dark {color: #333333 !important;}a.text-dark:hover,a.text-dark:focus {color: #0d0d0d !important;}.text-body {color: #212529 !important;}.text-muted {color: #6c757d !important;}.text-black-50 {color: rgba(0, 0, 0, 0.5) !important;}.text-white-50 {color: rgba(255, 255, 255, 0.5) !important;}.text-hide {font: 0/0 a;color: transparent;text-shadow: none;background-color: transparent;border: 0;}.text-decoration-none {text-decoration: none !important;}.text-break {word-break: break-word !important;overflow-wrap: break-word !important;}.text-reset {color: inherit !important;}.visible {visibility: visible !important;}.invisible {visibility: hidden !important;}/*! + * Font Awesome Free 5.0.10 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */.fa,.fas,.far,.fal,.fab {-moz-osx-font-smoothing: grayscale;-webkit-font-smoothing: antialiased;display: inline-block;font-style: normal;font-variant: normal;text-rendering: auto;line-height: 1;}.fa-lg {font-size: 1.3333333333em;line-height: 0.75em;vertical-align: -.0667em;}.fa-xs {font-size: .75em;}.fa-sm {font-size: .875em;}.fa-1x {font-size: 1em;}.fa-2x {font-size: 2em;}.fa-3x {font-size: 3em;}.fa-4x {font-size: 4em;}.fa-5x {font-size: 5em;}.fa-6x {font-size: 6em;}.fa-7x {font-size: 7em;}.fa-8x {font-size: 8em;}.fa-9x {font-size: 9em;}.fa-10x {font-size: 10em;}.fa-fw {text-align: center;width: 1.25em;}.fa-ul {list-style-type: none;margin-left: 2.5em;padding-left: 0;}.fa-ul > li {position: relative;}.fa-li {left: -2em;position: absolute;text-align: center;width: 2em;line-height: inherit;}.fa-border {border: solid 0.08em #eee;border-radius: .1em;padding: .2em .25em .15em;}.fa-pull-left {float: left;}.fa-pull-right {float: right;}.fa.fa-pull-left,.fas.fa-pull-left,.far.fa-pull-left,.fal.fa-pull-left,.fab.fa-pull-left {margin-right: .3em;}.fa.fa-pull-right,.fas.fa-pull-right,.far.fa-pull-right,.fal.fa-pull-right,.fab.fa-pull-right {margin-left: .3em;}.fa-spin {-webkit-animation: fa-spin 2s infinite linear;animation: fa-spin 2s infinite linear;}.fa-pulse {-webkit-animation: fa-spin 1s infinite steps(8);animation: fa-spin 1s infinite steps(8);}.fa-rotate-90 {-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform: rotate(90deg);transform: rotate(90deg);}.fa-rotate-180 {-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform: rotate(180deg);transform: rotate(180deg);}.fa-rotate-270 {-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform: rotate(270deg);transform: rotate(270deg);}.fa-flip-horizontal {-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform: scale(-1, 1);transform: scale(-1, 1);}.fa-flip-vertical {-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform: scale(1, -1);transform: scale(1, -1);}.fa-flip-horizontal.fa-flip-vertical {-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform: scale(-1, -1);transform: scale(-1, -1);}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical {-webkit-filter: none;filter: none;}.fa-stack {display: inline-block;height: 2em;line-height: 2em;position: relative;vertical-align: middle;width: 2em;}.fa-stack-1x,.fa-stack-2x {left: 0;position: absolute;text-align: center;width: 100%;}.fa-stack-1x {line-height: inherit;}.fa-stack-2x {font-size: 2em;}.fa-inverse {color: #fff;}/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen +readers do not read off random characters that represent icons */.fa-500px:before {content: "\f26e";}.fa-accessible-icon:before {content: "\f368";}.fa-accusoft:before {content: "\f369";}.fa-address-book:before {content: "\f2b9";}.fa-address-card:before {content: "\f2bb";}.fa-adjust:before {content: "\f042";}.fa-adn:before {content: "\f170";}.fa-adversal:before {content: "\f36a";}.fa-affiliatetheme:before {content: "\f36b";}.fa-algolia:before {content: "\f36c";}.fa-align-center:before {content: "\f037";}.fa-align-justify:before {content: "\f039";}.fa-align-left:before {content: "\f036";}.fa-align-right:before {content: "\f038";}.fa-allergies:before {content: "\f461";}.fa-amazon:before {content: "\f270";}.fa-amazon-pay:before {content: "\f42c";}.fa-ambulance:before {content: "\f0f9";}.fa-american-sign-language-interpreting:before {content: "\f2a3";}.fa-amilia:before {content: "\f36d";}.fa-anchor:before {content: "\f13d";}.fa-android:before {content: "\f17b";}.fa-angellist:before {content: "\f209";}.fa-angle-double-down:before {content: "\f103";}.fa-angle-double-left:before {content: "\f100";}.fa-angle-double-right:before {content: "\f101";}.fa-angle-double-up:before {content: "\f102";}.fa-angle-down:before {content: "\f107";}.fa-angle-left:before {content: "\f104";}.fa-angle-right:before {content: "\f105";}.fa-angle-up:before {content: "\f106";}.fa-angrycreative:before {content: "\f36e";}.fa-angular:before {content: "\f420";}.fa-app-store:before {content: "\f36f";}.fa-app-store-ios:before {content: "\f370";}.fa-apper:before {content: "\f371";}.fa-apple:before {content: "\f179";}.fa-apple-pay:before {content: "\f415";}.fa-archive:before {content: "\f187";}.fa-arrow-alt-circle-down:before {content: "\f358";}.fa-arrow-alt-circle-left:before {content: "\f359";}.fa-arrow-alt-circle-right:before {content: "\f35a";}.fa-arrow-alt-circle-up:before {content: "\f35b";}.fa-arrow-circle-down:before {content: "\f0ab";}.fa-arrow-circle-left:before {content: "\f0a8";}.fa-arrow-circle-right:before {content: "\f0a9";}.fa-arrow-circle-up:before {content: "\f0aa";}.fa-arrow-down:before {content: "\f063";}.fa-arrow-left:before {content: "\f060";}.fa-arrow-right:before {content: "\f061";}.fa-arrow-up:before {content: "\f062";}.fa-arrows-alt:before {content: "\f0b2";}.fa-arrows-alt-h:before {content: "\f337";}.fa-arrows-alt-v:before {content: "\f338";}.fa-assistive-listening-systems:before {content: "\f2a2";}.fa-asterisk:before {content: "\f069";}.fa-asymmetrik:before {content: "\f372";}.fa-at:before {content: "\f1fa";}.fa-audible:before {content: "\f373";}.fa-audio-description:before {content: "\f29e";}.fa-autoprefixer:before {content: "\f41c";}.fa-avianex:before {content: "\f374";}.fa-aviato:before {content: "\f421";}.fa-aws:before {content: "\f375";}.fa-backward:before {content: "\f04a";}.fa-balance-scale:before {content: "\f24e";}.fa-ban:before {content: "\f05e";}.fa-band-aid:before {content: "\f462";}.fa-bandcamp:before {content: "\f2d5";}.fa-barcode:before {content: "\f02a";}.fa-bars:before {content: "\f0c9";}.fa-baseball-ball:before {content: "\f433";}.fa-basketball-ball:before {content: "\f434";}.fa-bath:before {content: "\f2cd";}.fa-battery-empty:before {content: "\f244";}.fa-battery-full:before {content: "\f240";}.fa-battery-half:before {content: "\f242";}.fa-battery-quarter:before {content: "\f243";}.fa-battery-three-quarters:before {content: "\f241";}.fa-bed:before {content: "\f236";}.fa-beer:before {content: "\f0fc";}.fa-behance:before {content: "\f1b4";}.fa-behance-square:before {content: "\f1b5";}.fa-bell:before {content: "\f0f3";}.fa-bell-slash:before {content: "\f1f6";}.fa-bicycle:before {content: "\f206";}.fa-bimobject:before {content: "\f378";}.fa-binoculars:before {content: "\f1e5";}.fa-birthday-cake:before {content: "\f1fd";}.fa-bitbucket:before {content: "\f171";}.fa-bitcoin:before {content: "\f379";}.fa-bity:before {content: "\f37a";}.fa-black-tie:before {content: "\f27e";}.fa-blackberry:before {content: "\f37b";}.fa-blind:before {content: "\f29d";}.fa-blogger:before {content: "\f37c";}.fa-blogger-b:before {content: "\f37d";}.fa-bluetooth:before {content: "\f293";}.fa-bluetooth-b:before {content: "\f294";}.fa-bold:before {content: "\f032";}.fa-bolt:before {content: "\f0e7";}.fa-bomb:before {content: "\f1e2";}.fa-book:before {content: "\f02d";}.fa-bookmark:before {content: "\f02e";}.fa-bowling-ball:before {content: "\f436";}.fa-box:before {content: "\f466";}.fa-box-open:before {content: "\f49e";}.fa-boxes:before {content: "\f468";}.fa-braille:before {content: "\f2a1";}.fa-briefcase:before {content: "\f0b1";}.fa-briefcase-medical:before {content: "\f469";}.fa-btc:before {content: "\f15a";}.fa-bug:before {content: "\f188";}.fa-building:before {content: "\f1ad";}.fa-bullhorn:before {content: "\f0a1";}.fa-bullseye:before {content: "\f140";}.fa-burn:before {content: "\f46a";}.fa-buromobelexperte:before {content: "\f37f";}.fa-bus:before {content: "\f207";}.fa-buysellads:before {content: "\f20d";}.fa-calculator:before {content: "\f1ec";}.fa-calendar:before {content: "\f133";}.fa-calendar-alt:before {content: "\f073";}.fa-calendar-check:before {content: "\f274";}.fa-calendar-minus:before {content: "\f272";}.fa-calendar-plus:before {content: "\f271";}.fa-calendar-times:before {content: "\f273";}.fa-camera:before {content: "\f030";}.fa-camera-retro:before {content: "\f083";}.fa-capsules:before {content: "\f46b";}.fa-car:before {content: "\f1b9";}.fa-caret-down:before {content: "\f0d7";}.fa-caret-left:before {content: "\f0d9";}.fa-caret-right:before {content: "\f0da";}.fa-caret-square-down:before {content: "\f150";}.fa-caret-square-left:before {content: "\f191";}.fa-caret-square-right:before {content: "\f152";}.fa-caret-square-up:before {content: "\f151";}.fa-caret-up:before {content: "\f0d8";}.fa-cart-arrow-down:before {content: "\f218";}.fa-cart-plus:before {content: "\f217";}.fa-cc-amazon-pay:before {content: "\f42d";}.fa-cc-amex:before {content: "\f1f3";}.fa-cc-apple-pay:before {content: "\f416";}.fa-cc-diners-club:before {content: "\f24c";}.fa-cc-discover:before {content: "\f1f2";}.fa-cc-jcb:before {content: "\f24b";}.fa-cc-mastercard:before {content: "\f1f1";}.fa-cc-paypal:before {content: "\f1f4";}.fa-cc-stripe:before {content: "\f1f5";}.fa-cc-visa:before {content: "\f1f0";}.fa-centercode:before {content: "\f380";}.fa-certificate:before {content: "\f0a3";}.fa-chart-area:before {content: "\f1fe";}.fa-chart-bar:before {content: "\f080";}.fa-chart-line:before {content: "\f201";}.fa-chart-pie:before {content: "\f200";}.fa-check:before {content: "\f00c";}.fa-check-circle:before {content: "\f058";}.fa-check-square:before {content: "\f14a";}.fa-chess:before {content: "\f439";}.fa-chess-bishop:before {content: "\f43a";}.fa-chess-board:before {content: "\f43c";}.fa-chess-king:before {content: "\f43f";}.fa-chess-knight:before {content: "\f441";}.fa-chess-pawn:before {content: "\f443";}.fa-chess-queen:before {content: "\f445";}.fa-chess-rook:before {content: "\f447";}.fa-chevron-circle-down:before {content: "\f13a";}.fa-chevron-circle-left:before {content: "\f137";}.fa-chevron-circle-right:before {content: "\f138";}.fa-chevron-circle-up:before {content: "\f139";}.fa-chevron-down:before {content: "\f078";}.fa-chevron-left:before {content: "\f053";}.fa-chevron-right:before {content: "\f054";}.fa-chevron-up:before {content: "\f077";}.fa-child:before {content: "\f1ae";}.fa-chrome:before {content: "\f268";}.fa-circle:before {content: "\f111";}.fa-circle-notch:before {content: "\f1ce";}.fa-clipboard:before {content: "\f328";}.fa-clipboard-check:before {content: "\f46c";}.fa-clipboard-list:before {content: "\f46d";}.fa-clock:before {content: "\f017";}.fa-clone:before {content: "\f24d";}.fa-closed-captioning:before {content: "\f20a";}.fa-cloud:before {content: "\f0c2";}.fa-cloud-download-alt:before {content: "\f381";}.fa-cloud-upload-alt:before {content: "\f382";}.fa-cloudscale:before {content: "\f383";}.fa-cloudsmith:before {content: "\f384";}.fa-cloudversify:before {content: "\f385";}.fa-code:before {content: "\f121";}.fa-code-branch:before {content: "\f126";}.fa-codepen:before {content: "\f1cb";}.fa-codiepie:before {content: "\f284";}.fa-coffee:before {content: "\f0f4";}.fa-cog:before {content: "\f013";}.fa-cogs:before {content: "\f085";}.fa-columns:before {content: "\f0db";}.fa-comment:before {content: "\f075";}.fa-comment-alt:before {content: "\f27a";}.fa-comment-dots:before {content: "\f4ad";}.fa-comment-slash:before {content: "\f4b3";}.fa-comments:before {content: "\f086";}.fa-compass:before {content: "\f14e";}.fa-compress:before {content: "\f066";}.fa-connectdevelop:before {content: "\f20e";}.fa-contao:before {content: "\f26d";}.fa-copy:before {content: "\f0c5";}.fa-copyright:before {content: "\f1f9";}.fa-couch:before {content: "\f4b8";}.fa-cpanel:before {content: "\f388";}.fa-creative-commons:before {content: "\f25e";}.fa-credit-card:before {content: "\f09d";}.fa-crop:before {content: "\f125";}.fa-crosshairs:before {content: "\f05b";}.fa-css3:before {content: "\f13c";}.fa-css3-alt:before {content: "\f38b";}.fa-cube:before {content: "\f1b2";}.fa-cubes:before {content: "\f1b3";}.fa-cut:before {content: "\f0c4";}.fa-cuttlefish:before {content: "\f38c";}.fa-d-and-d:before {content: "\f38d";}.fa-dashcube:before {content: "\f210";}.fa-database:before {content: "\f1c0";}.fa-deaf:before {content: "\f2a4";}.fa-delicious:before {content: "\f1a5";}.fa-deploydog:before {content: "\f38e";}.fa-deskpro:before {content: "\f38f";}.fa-desktop:before {content: "\f108";}.fa-deviantart:before {content: "\f1bd";}.fa-diagnoses:before {content: "\f470";}.fa-digg:before {content: "\f1a6";}.fa-digital-ocean:before {content: "\f391";}.fa-discord:before {content: "\f392";}.fa-discourse:before {content: "\f393";}.fa-dna:before {content: "\f471";}.fa-dochub:before {content: "\f394";}.fa-docker:before {content: "\f395";}.fa-dollar-sign:before {content: "\f155";}.fa-dolly:before {content: "\f472";}.fa-dolly-flatbed:before {content: "\f474";}.fa-donate:before {content: "\f4b9";}.fa-dot-circle:before {content: "\f192";}.fa-dove:before {content: "\f4ba";}.fa-download:before {content: "\f019";}.fa-draft2digital:before {content: "\f396";}.fa-dribbble:before {content: "\f17d";}.fa-dribbble-square:before {content: "\f397";}.fa-dropbox:before {content: "\f16b";}.fa-drupal:before {content: "\f1a9";}.fa-dyalog:before {content: "\f399";}.fa-earlybirds:before {content: "\f39a";}.fa-edge:before {content: "\f282";}.fa-edit:before {content: "\f044";}.fa-eject:before {content: "\f052";}.fa-elementor:before {content: "\f430";}.fa-ellipsis-h:before {content: "\f141";}.fa-ellipsis-v:before {content: "\f142";}.fa-ember:before {content: "\f423";}.fa-empire:before {content: "\f1d1";}.fa-envelope:before {content: "\f0e0";}.fa-envelope-open:before {content: "\f2b6";}.fa-envelope-square:before {content: "\f199";}.fa-envira:before {content: "\f299";}.fa-eraser:before {content: "\f12d";}.fa-erlang:before {content: "\f39d";}.fa-ethereum:before {content: "\f42e";}.fa-etsy:before {content: "\f2d7";}.fa-euro-sign:before {content: "\f153";}.fa-exchange-alt:before {content: "\f362";}.fa-exclamation:before {content: "\f12a";}.fa-exclamation-circle:before {content: "\f06a";}.fa-exclamation-triangle:before {content: "\f071";}.fa-expand:before {content: "\f065";}.fa-expand-arrows-alt:before {content: "\f31e";}.fa-expeditedssl:before {content: "\f23e";}.fa-external-link-alt:before {content: "\f35d";}.fa-external-link-square-alt:before {content: "\f360";}.fa-eye:before {content: "\f06e";}.fa-eye-dropper:before {content: "\f1fb";}.fa-eye-slash:before {content: "\f070";}.fa-facebook:before {content: "\f09a";}.fa-facebook-f:before {content: "\f39e";}.fa-facebook-messenger:before {content: "\f39f";}.fa-facebook-square:before {content: "\f082";}.fa-fast-backward:before {content: "\f049";}.fa-fast-forward:before {content: "\f050";}.fa-fax:before {content: "\f1ac";}.fa-female:before {content: "\f182";}.fa-fighter-jet:before {content: "\f0fb";}.fa-file:before {content: "\f15b";}.fa-file-alt:before {content: "\f15c";}.fa-file-archive:before {content: "\f1c6";}.fa-file-audio:before {content: "\f1c7";}.fa-file-code:before {content: "\f1c9";}.fa-file-excel:before {content: "\f1c3";}.fa-file-image:before {content: "\f1c5";}.fa-file-medical:before {content: "\f477";}.fa-file-medical-alt:before {content: "\f478";}.fa-file-pdf:before {content: "\f1c1";}.fa-file-powerpoint:before {content: "\f1c4";}.fa-file-video:before {content: "\f1c8";}.fa-file-word:before {content: "\f1c2";}.fa-film:before {content: "\f008";}.fa-filter:before {content: "\f0b0";}.fa-fire:before {content: "\f06d";}.fa-fire-extinguisher:before {content: "\f134";}.fa-firefox:before {content: "\f269";}.fa-first-aid:before {content: "\f479";}.fa-first-order:before {content: "\f2b0";}.fa-firstdraft:before {content: "\f3a1";}.fa-flag:before {content: "\f024";}.fa-flag-checkered:before {content: "\f11e";}.fa-flask:before {content: "\f0c3";}.fa-flickr:before {content: "\f16e";}.fa-flipboard:before {content: "\f44d";}.fa-fly:before {content: "\f417";}.fa-folder:before {content: "\f07b";}.fa-folder-open:before {content: "\f07c";}.fa-font:before {content: "\f031";}.fa-font-awesome:before {content: "\f2b4";}.fa-font-awesome-alt:before {content: "\f35c";}.fa-font-awesome-flag:before {content: "\f425";}.fa-fonticons:before {content: "\f280";}.fa-fonticons-fi:before {content: "\f3a2";}.fa-football-ball:before {content: "\f44e";}.fa-fort-awesome:before {content: "\f286";}.fa-fort-awesome-alt:before {content: "\f3a3";}.fa-forumbee:before {content: "\f211";}.fa-forward:before {content: "\f04e";}.fa-foursquare:before {content: "\f180";}.fa-free-code-camp:before {content: "\f2c5";}.fa-freebsd:before {content: "\f3a4";}.fa-frown:before {content: "\f119";}.fa-futbol:before {content: "\f1e3";}.fa-gamepad:before {content: "\f11b";}.fa-gavel:before {content: "\f0e3";}.fa-gem:before {content: "\f3a5";}.fa-genderless:before {content: "\f22d";}.fa-get-pocket:before {content: "\f265";}.fa-gg:before {content: "\f260";}.fa-gg-circle:before {content: "\f261";}.fa-gift:before {content: "\f06b";}.fa-git:before {content: "\f1d3";}.fa-git-square:before {content: "\f1d2";}.fa-github:before {content: "\f09b";}.fa-github-alt:before {content: "\f113";}.fa-github-square:before {content: "\f092";}.fa-gitkraken:before {content: "\f3a6";}.fa-gitlab:before {content: "\f296";}.fa-gitter:before {content: "\f426";}.fa-glass-martini:before {content: "\f000";}.fa-glide:before {content: "\f2a5";}.fa-glide-g:before {content: "\f2a6";}.fa-globe:before {content: "\f0ac";}.fa-gofore:before {content: "\f3a7";}.fa-golf-ball:before {content: "\f450";}.fa-goodreads:before {content: "\f3a8";}.fa-goodreads-g:before {content: "\f3a9";}.fa-google:before {content: "\f1a0";}.fa-google-drive:before {content: "\f3aa";}.fa-google-play:before {content: "\f3ab";}.fa-google-plus:before {content: "\f2b3";}.fa-google-plus-g:before {content: "\f0d5";}.fa-google-plus-square:before {content: "\f0d4";}.fa-google-wallet:before {content: "\f1ee";}.fa-graduation-cap:before {content: "\f19d";}.fa-gratipay:before {content: "\f184";}.fa-grav:before {content: "\f2d6";}.fa-gripfire:before {content: "\f3ac";}.fa-grunt:before {content: "\f3ad";}.fa-gulp:before {content: "\f3ae";}.fa-h-square:before {content: "\f0fd";}.fa-hacker-news:before {content: "\f1d4";}.fa-hacker-news-square:before {content: "\f3af";}.fa-hand-holding:before {content: "\f4bd";}.fa-hand-holding-heart:before {content: "\f4be";}.fa-hand-holding-usd:before {content: "\f4c0";}.fa-hand-lizard:before {content: "\f258";}.fa-hand-paper:before {content: "\f256";}.fa-hand-peace:before {content: "\f25b";}.fa-hand-point-down:before {content: "\f0a7";}.fa-hand-point-left:before {content: "\f0a5";}.fa-hand-point-right:before {content: "\f0a4";}.fa-hand-point-up:before {content: "\f0a6";}.fa-hand-pointer:before {content: "\f25a";}.fa-hand-rock:before {content: "\f255";}.fa-hand-scissors:before {content: "\f257";}.fa-hand-spock:before {content: "\f259";}.fa-hands:before {content: "\f4c2";}.fa-hands-helping:before {content: "\f4c4";}.fa-handshake:before {content: "\f2b5";}.fa-hashtag:before {content: "\f292";}.fa-hdd:before {content: "\f0a0";}.fa-heading:before {content: "\f1dc";}.fa-headphones:before {content: "\f025";}.fa-heart:before {content: "\f004";}.fa-heartbeat:before {content: "\f21e";}.fa-hips:before {content: "\f452";}.fa-hire-a-helper:before {content: "\f3b0";}.fa-history:before {content: "\f1da";}.fa-hockey-puck:before {content: "\f453";}.fa-home:before {content: "\f015";}.fa-hooli:before {content: "\f427";}.fa-hospital:before {content: "\f0f8";}.fa-hospital-alt:before {content: "\f47d";}.fa-hospital-symbol:before {content: "\f47e";}.fa-hotjar:before {content: "\f3b1";}.fa-hourglass:before {content: "\f254";}.fa-hourglass-end:before {content: "\f253";}.fa-hourglass-half:before {content: "\f252";}.fa-hourglass-start:before {content: "\f251";}.fa-houzz:before {content: "\f27c";}.fa-html5:before {content: "\f13b";}.fa-hubspot:before {content: "\f3b2";}.fa-i-cursor:before {content: "\f246";}.fa-id-badge:before {content: "\f2c1";}.fa-id-card:before {content: "\f2c2";}.fa-id-card-alt:before {content: "\f47f";}.fa-image:before {content: "\f03e";}.fa-images:before {content: "\f302";}.fa-imdb:before {content: "\f2d8";}.fa-inbox:before {content: "\f01c";}.fa-indent:before {content: "\f03c";}.fa-industry:before {content: "\f275";}.fa-info:before {content: "\f129";}.fa-info-circle:before {content: "\f05a";}.fa-instagram:before {content: "\f16d";}.fa-internet-explorer:before {content: "\f26b";}.fa-ioxhost:before {content: "\f208";}.fa-italic:before {content: "\f033";}.fa-itunes:before {content: "\f3b4";}.fa-itunes-note:before {content: "\f3b5";}.fa-java:before {content: "\f4e4";}.fa-jenkins:before {content: "\f3b6";}.fa-joget:before {content: "\f3b7";}.fa-joomla:before {content: "\f1aa";}.fa-js:before {content: "\f3b8";}.fa-js-square:before {content: "\f3b9";}.fa-jsfiddle:before {content: "\f1cc";}.fa-key:before {content: "\f084";}.fa-keyboard:before {content: "\f11c";}.fa-keycdn:before {content: "\f3ba";}.fa-kickstarter:before {content: "\f3bb";}.fa-kickstarter-k:before {content: "\f3bc";}.fa-korvue:before {content: "\f42f";}.fa-language:before {content: "\f1ab";}.fa-laptop:before {content: "\f109";}.fa-laravel:before {content: "\f3bd";}.fa-lastfm:before {content: "\f202";}.fa-lastfm-square:before {content: "\f203";}.fa-leaf:before {content: "\f06c";}.fa-leanpub:before {content: "\f212";}.fa-lemon:before {content: "\f094";}.fa-less:before {content: "\f41d";}.fa-level-down-alt:before {content: "\f3be";}.fa-level-up-alt:before {content: "\f3bf";}.fa-life-ring:before {content: "\f1cd";}.fa-lightbulb:before {content: "\f0eb";}.fa-line:before {content: "\f3c0";}.fa-link:before {content: "\f0c1";}.fa-linkedin:before {content: "\f08c";}.fa-linkedin-in:before {content: "\f0e1";}.fa-linode:before {content: "\f2b8";}.fa-linux:before {content: "\f17c";}.fa-lira-sign:before {content: "\f195";}.fa-list:before {content: "\f03a";}.fa-list-alt:before {content: "\f022";}.fa-list-ol:before {content: "\f0cb";}.fa-list-ul:before {content: "\f0ca";}.fa-location-arrow:before {content: "\f124";}.fa-lock:before {content: "\f023";}.fa-lock-open:before {content: "\f3c1";}.fa-long-arrow-alt-down:before {content: "\f309";}.fa-long-arrow-alt-left:before {content: "\f30a";}.fa-long-arrow-alt-right:before {content: "\f30b";}.fa-long-arrow-alt-up:before {content: "\f30c";}.fa-low-vision:before {content: "\f2a8";}.fa-lyft:before {content: "\f3c3";}.fa-magento:before {content: "\f3c4";}.fa-magic:before {content: "\f0d0";}.fa-magnet:before {content: "\f076";}.fa-male:before {content: "\f183";}.fa-map:before {content: "\f279";}.fa-map-marker:before {content: "\f041";}.fa-map-marker-alt:before {content: "\f3c5";}.fa-map-pin:before {content: "\f276";}.fa-map-signs:before {content: "\f277";}.fa-mars:before {content: "\f222";}.fa-mars-double:before {content: "\f227";}.fa-mars-stroke:before {content: "\f229";}.fa-mars-stroke-h:before {content: "\f22b";}.fa-mars-stroke-v:before {content: "\f22a";}.fa-maxcdn:before {content: "\f136";}.fa-medapps:before {content: "\f3c6";}.fa-medium:before {content: "\f23a";}.fa-medium-m:before {content: "\f3c7";}.fa-medkit:before {content: "\f0fa";}.fa-medrt:before {content: "\f3c8";}.fa-meetup:before {content: "\f2e0";}.fa-meh:before {content: "\f11a";}.fa-mercury:before {content: "\f223";}.fa-microchip:before {content: "\f2db";}.fa-microphone:before {content: "\f130";}.fa-microphone-slash:before {content: "\f131";}.fa-microsoft:before {content: "\f3ca";}.fa-minus:before {content: "\f068";}.fa-minus-circle:before {content: "\f056";}.fa-minus-square:before {content: "\f146";}.fa-mix:before {content: "\f3cb";}.fa-mixcloud:before {content: "\f289";}.fa-mizuni:before {content: "\f3cc";}.fa-mobile:before {content: "\f10b";}.fa-mobile-alt:before {content: "\f3cd";}.fa-modx:before {content: "\f285";}.fa-monero:before {content: "\f3d0";}.fa-money-bill-alt:before {content: "\f3d1";}.fa-moon:before {content: "\f186";}.fa-motorcycle:before {content: "\f21c";}.fa-mouse-pointer:before {content: "\f245";}.fa-music:before {content: "\f001";}.fa-napster:before {content: "\f3d2";}.fa-neuter:before {content: "\f22c";}.fa-newspaper:before {content: "\f1ea";}.fa-nintendo-switch:before {content: "\f418";}.fa-node:before {content: "\f419";}.fa-node-js:before {content: "\f3d3";}.fa-notes-medical:before {content: "\f481";}.fa-npm:before {content: "\f3d4";}.fa-ns8:before {content: "\f3d5";}.fa-nutritionix:before {content: "\f3d6";}.fa-object-group:before {content: "\f247";}.fa-object-ungroup:before {content: "\f248";}.fa-odnoklassniki:before {content: "\f263";}.fa-odnoklassniki-square:before {content: "\f264";}.fa-opencart:before {content: "\f23d";}.fa-openid:before {content: "\f19b";}.fa-opera:before {content: "\f26a";}.fa-optin-monster:before {content: "\f23c";}.fa-osi:before {content: "\f41a";}.fa-outdent:before {content: "\f03b";}.fa-page4:before {content: "\f3d7";}.fa-pagelines:before {content: "\f18c";}.fa-paint-brush:before {content: "\f1fc";}.fa-palfed:before {content: "\f3d8";}.fa-pallet:before {content: "\f482";}.fa-paper-plane:before {content: "\f1d8";}.fa-paperclip:before {content: "\f0c6";}.fa-parachute-box:before {content: "\f4cd";}.fa-paragraph:before {content: "\f1dd";}.fa-paste:before {content: "\f0ea";}.fa-patreon:before {content: "\f3d9";}.fa-pause:before {content: "\f04c";}.fa-pause-circle:before {content: "\f28b";}.fa-paw:before {content: "\f1b0";}.fa-paypal:before {content: "\f1ed";}.fa-pen-square:before {content: "\f14b";}.fa-pencil-alt:before {content: "\f303";}.fa-people-carry:before {content: "\f4ce";}.fa-percent:before {content: "\f295";}.fa-periscope:before {content: "\f3da";}.fa-phabricator:before {content: "\f3db";}.fa-phoenix-framework:before {content: "\f3dc";}.fa-phone:before {content: "\f095";}.fa-phone-slash:before {content: "\f3dd";}.fa-phone-square:before {content: "\f098";}.fa-phone-volume:before {content: "\f2a0";}.fa-php:before {content: "\f457";}.fa-pied-piper:before {content: "\f2ae";}.fa-pied-piper-alt:before {content: "\f1a8";}.fa-pied-piper-hat:before {content: "\f4e5";}.fa-pied-piper-pp:before {content: "\f1a7";}.fa-piggy-bank:before {content: "\f4d3";}.fa-pills:before {content: "\f484";}.fa-pinterest:before {content: "\f0d2";}.fa-pinterest-p:before {content: "\f231";}.fa-pinterest-square:before {content: "\f0d3";}.fa-plane:before {content: "\f072";}.fa-play:before {content: "\f04b";}.fa-play-circle:before {content: "\f144";}.fa-playstation:before {content: "\f3df";}.fa-plug:before {content: "\f1e6";}.fa-plus:before {content: "\f067";}.fa-plus-circle:before {content: "\f055";}.fa-plus-square:before {content: "\f0fe";}.fa-podcast:before {content: "\f2ce";}.fa-poo:before {content: "\f2fe";}.fa-pound-sign:before {content: "\f154";}.fa-power-off:before {content: "\f011";}.fa-prescription-bottle:before {content: "\f485";}.fa-prescription-bottle-alt:before {content: "\f486";}.fa-print:before {content: "\f02f";}.fa-procedures:before {content: "\f487";}.fa-product-hunt:before {content: "\f288";}.fa-pushed:before {content: "\f3e1";}.fa-puzzle-piece:before {content: "\f12e";}.fa-python:before {content: "\f3e2";}.fa-qq:before {content: "\f1d6";}.fa-qrcode:before {content: "\f029";}.fa-question:before {content: "\f128";}.fa-question-circle:before {content: "\f059";}.fa-quidditch:before {content: "\f458";}.fa-quinscape:before {content: "\f459";}.fa-quora:before {content: "\f2c4";}.fa-quote-left:before {content: "\f10d";}.fa-quote-right:before {content: "\f10e";}.fa-random:before {content: "\f074";}.fa-ravelry:before {content: "\f2d9";}.fa-react:before {content: "\f41b";}.fa-readme:before {content: "\f4d5";}.fa-rebel:before {content: "\f1d0";}.fa-recycle:before {content: "\f1b8";}.fa-red-river:before {content: "\f3e3";}.fa-reddit:before {content: "\f1a1";}.fa-reddit-alien:before {content: "\f281";}.fa-reddit-square:before {content: "\f1a2";}.fa-redo:before {content: "\f01e";}.fa-redo-alt:before {content: "\f2f9";}.fa-registered:before {content: "\f25d";}.fa-rendact:before {content: "\f3e4";}.fa-renren:before {content: "\f18b";}.fa-reply:before {content: "\f3e5";}.fa-reply-all:before {content: "\f122";}.fa-replyd:before {content: "\f3e6";}.fa-resolving:before {content: "\f3e7";}.fa-retweet:before {content: "\f079";}.fa-ribbon:before {content: "\f4d6";}.fa-road:before {content: "\f018";}.fa-rocket:before {content: "\f135";}.fa-rocketchat:before {content: "\f3e8";}.fa-rockrms:before {content: "\f3e9";}.fa-rss:before {content: "\f09e";}.fa-rss-square:before {content: "\f143";}.fa-ruble-sign:before {content: "\f158";}.fa-rupee-sign:before {content: "\f156";}.fa-safari:before {content: "\f267";}.fa-sass:before {content: "\f41e";}.fa-save:before {content: "\f0c7";}.fa-schlix:before {content: "\f3ea";}.fa-scribd:before {content: "\f28a";}.fa-search:before {content: "\f002";}.fa-search-minus:before {content: "\f010";}.fa-search-plus:before {content: "\f00e";}.fa-searchengin:before {content: "\f3eb";}.fa-seedling:before {content: "\f4d8";}.fa-sellcast:before {content: "\f2da";}.fa-sellsy:before {content: "\f213";}.fa-server:before {content: "\f233";}.fa-servicestack:before {content: "\f3ec";}.fa-share:before {content: "\f064";}.fa-share-alt:before {content: "\f1e0";}.fa-share-alt-square:before {content: "\f1e1";}.fa-share-square:before {content: "\f14d";}.fa-shekel-sign:before {content: "\f20b";}.fa-shield-alt:before {content: "\f3ed";}.fa-ship:before {content: "\f21a";}.fa-shipping-fast:before {content: "\f48b";}.fa-shirtsinbulk:before {content: "\f214";}.fa-shopping-bag:before {content: "\f290";}.fa-shopping-basket:before {content: "\f291";}.fa-shopping-cart:before {content: "\f07a";}.fa-shower:before {content: "\f2cc";}.fa-sign:before {content: "\f4d9";}.fa-sign-in-alt:before {content: "\f2f6";}.fa-sign-language:before {content: "\f2a7";}.fa-sign-out-alt:before {content: "\f2f5";}.fa-signal:before {content: "\f012";}.fa-simplybuilt:before {content: "\f215";}.fa-sistrix:before {content: "\f3ee";}.fa-sitemap:before {content: "\f0e8";}.fa-skyatlas:before {content: "\f216";}.fa-skype:before {content: "\f17e";}.fa-slack:before {content: "\f198";}.fa-slack-hash:before {content: "\f3ef";}.fa-sliders-h:before {content: "\f1de";}.fa-slideshare:before {content: "\f1e7";}.fa-smile:before {content: "\f118";}.fa-smoking:before {content: "\f48d";}.fa-snapchat:before {content: "\f2ab";}.fa-snapchat-ghost:before {content: "\f2ac";}.fa-snapchat-square:before {content: "\f2ad";}.fa-snowflake:before {content: "\f2dc";}.fa-sort:before {content: "\f0dc";}.fa-sort-alpha-down:before {content: "\f15d";}.fa-sort-alpha-up:before {content: "\f15e";}.fa-sort-amount-down:before {content: "\f160";}.fa-sort-amount-up:before {content: "\f161";}.fa-sort-down:before {content: "\f0dd";}.fa-sort-numeric-down:before {content: "\f162";}.fa-sort-numeric-up:before {content: "\f163";}.fa-sort-up:before {content: "\f0de";}.fa-soundcloud:before {content: "\f1be";}.fa-space-shuttle:before {content: "\f197";}.fa-speakap:before {content: "\f3f3";}.fa-spinner:before {content: "\f110";}.fa-spotify:before {content: "\f1bc";}.fa-square:before {content: "\f0c8";}.fa-square-full:before {content: "\f45c";}.fa-stack-exchange:before {content: "\f18d";}.fa-stack-overflow:before {content: "\f16c";}.fa-star:before {content: "\f005";}.fa-star-half:before {content: "\f089";}.fa-staylinked:before {content: "\f3f5";}.fa-steam:before {content: "\f1b6";}.fa-steam-square:before {content: "\f1b7";}.fa-steam-symbol:before {content: "\f3f6";}.fa-step-backward:before {content: "\f048";}.fa-step-forward:before {content: "\f051";}.fa-stethoscope:before {content: "\f0f1";}.fa-sticker-mule:before {content: "\f3f7";}.fa-sticky-note:before {content: "\f249";}.fa-stop:before {content: "\f04d";}.fa-stop-circle:before {content: "\f28d";}.fa-stopwatch:before {content: "\f2f2";}.fa-strava:before {content: "\f428";}.fa-street-view:before {content: "\f21d";}.fa-strikethrough:before {content: "\f0cc";}.fa-stripe:before {content: "\f429";}.fa-stripe-s:before {content: "\f42a";}.fa-studiovinari:before {content: "\f3f8";}.fa-stumbleupon:before {content: "\f1a4";}.fa-stumbleupon-circle:before {content: "\f1a3";}.fa-subscript:before {content: "\f12c";}.fa-subway:before {content: "\f239";}.fa-suitcase:before {content: "\f0f2";}.fa-sun:before {content: "\f185";}.fa-superpowers:before {content: "\f2dd";}.fa-superscript:before {content: "\f12b";}.fa-supple:before {content: "\f3f9";}.fa-sync:before {content: "\f021";}.fa-sync-alt:before {content: "\f2f1";}.fa-syringe:before {content: "\f48e";}.fa-table:before {content: "\f0ce";}.fa-table-tennis:before {content: "\f45d";}.fa-tablet:before {content: "\f10a";}.fa-tablet-alt:before {content: "\f3fa";}.fa-tablets:before {content: "\f490";}.fa-tachometer-alt:before {content: "\f3fd";}.fa-tag:before {content: "\f02b";}.fa-tags:before {content: "\f02c";}.fa-tape:before {content: "\f4db";}.fa-tasks:before {content: "\f0ae";}.fa-taxi:before {content: "\f1ba";}.fa-telegram:before {content: "\f2c6";}.fa-telegram-plane:before {content: "\f3fe";}.fa-tencent-weibo:before {content: "\f1d5";}.fa-terminal:before {content: "\f120";}.fa-text-height:before {content: "\f034";}.fa-text-width:before {content: "\f035";}.fa-th:before {content: "\f00a";}.fa-th-large:before {content: "\f009";}.fa-th-list:before {content: "\f00b";}.fa-themeisle:before {content: "\f2b2";}.fa-thermometer:before {content: "\f491";}.fa-thermometer-empty:before {content: "\f2cb";}.fa-thermometer-full:before {content: "\f2c7";}.fa-thermometer-half:before {content: "\f2c9";}.fa-thermometer-quarter:before {content: "\f2ca";}.fa-thermometer-three-quarters:before {content: "\f2c8";}.fa-thumbs-down:before {content: "\f165";}.fa-thumbs-up:before {content: "\f164";}.fa-thumbtack:before {content: "\f08d";}.fa-ticket-alt:before {content: "\f3ff";}.fa-times:before {content: "\f00d";}.fa-times-circle:before {content: "\f057";}.fa-tint:before {content: "\f043";}.fa-toggle-off:before {content: "\f204";}.fa-toggle-on:before {content: "\f205";}.fa-trademark:before {content: "\f25c";}.fa-train:before {content: "\f238";}.fa-transgender:before {content: "\f224";}.fa-transgender-alt:before {content: "\f225";}.fa-trash:before {content: "\f1f8";}.fa-trash-alt:before {content: "\f2ed";}.fa-tree:before {content: "\f1bb";}.fa-trello:before {content: "\f181";}.fa-tripadvisor:before {content: "\f262";}.fa-trophy:before {content: "\f091";}.fa-truck:before {content: "\f0d1";}.fa-truck-loading:before {content: "\f4de";}.fa-truck-moving:before {content: "\f4df";}.fa-tty:before {content: "\f1e4";}.fa-tumblr:before {content: "\f173";}.fa-tumblr-square:before {content: "\f174";}.fa-tv:before {content: "\f26c";}.fa-twitch:before {content: "\f1e8";}.fa-twitter:before {content: "\f099";}.fa-twitter-square:before {content: "\f081";}.fa-typo3:before {content: "\f42b";}.fa-uber:before {content: "\f402";}.fa-uikit:before {content: "\f403";}.fa-umbrella:before {content: "\f0e9";}.fa-underline:before {content: "\f0cd";}.fa-undo:before {content: "\f0e2";}.fa-undo-alt:before {content: "\f2ea";}.fa-uniregistry:before {content: "\f404";}.fa-universal-access:before {content: "\f29a";}.fa-university:before {content: "\f19c";}.fa-unlink:before {content: "\f127";}.fa-unlock:before {content: "\f09c";}.fa-unlock-alt:before {content: "\f13e";}.fa-untappd:before {content: "\f405";}.fa-upload:before {content: "\f093";}.fa-usb:before {content: "\f287";}.fa-user:before {content: "\f007";}.fa-user-circle:before {content: "\f2bd";}.fa-user-md:before {content: "\f0f0";}.fa-user-plus:before {content: "\f234";}.fa-user-secret:before {content: "\f21b";}.fa-user-times:before {content: "\f235";}.fa-users:before {content: "\f0c0";}.fa-ussunnah:before {content: "\f407";}.fa-utensil-spoon:before {content: "\f2e5";}.fa-utensils:before {content: "\f2e7";}.fa-vaadin:before {content: "\f408";}.fa-venus:before {content: "\f221";}.fa-venus-double:before {content: "\f226";}.fa-venus-mars:before {content: "\f228";}.fa-viacoin:before {content: "\f237";}.fa-viadeo:before {content: "\f2a9";}.fa-viadeo-square:before {content: "\f2aa";}.fa-vial:before {content: "\f492";}.fa-vials:before {content: "\f493";}.fa-viber:before {content: "\f409";}.fa-video:before {content: "\f03d";}.fa-video-slash:before {content: "\f4e2";}.fa-vimeo:before {content: "\f40a";}.fa-vimeo-square:before {content: "\f194";}.fa-vimeo-v:before {content: "\f27d";}.fa-vine:before {content: "\f1ca";}.fa-vk:before {content: "\f189";}.fa-vnv:before {content: "\f40b";}.fa-volleyball-ball:before {content: "\f45f";}.fa-volume-down:before {content: "\f027";}.fa-volume-off:before {content: "\f026";}.fa-volume-up:before {content: "\f028";}.fa-vuejs:before {content: "\f41f";}.fa-warehouse:before {content: "\f494";}.fa-weibo:before {content: "\f18a";}.fa-weight:before {content: "\f496";}.fa-weixin:before {content: "\f1d7";}.fa-whatsapp:before {content: "\f232";}.fa-whatsapp-square:before {content: "\f40c";}.fa-wheelchair:before {content: "\f193";}.fa-whmcs:before {content: "\f40d";}.fa-wifi:before {content: "\f1eb";}.fa-wikipedia-w:before {content: "\f266";}.fa-window-close:before {content: "\f410";}.fa-window-maximize:before {content: "\f2d0";}.fa-window-minimize:before {content: "\f2d1";}.fa-window-restore:before {content: "\f2d2";}.fa-windows:before {content: "\f17a";}.fa-wine-glass:before {content: "\f4e3";}.fa-won-sign:before {content: "\f159";}.fa-wordpress:before {content: "\f19a";}.fa-wordpress-simple:before {content: "\f411";}.fa-wpbeginner:before {content: "\f297";}.fa-wpexplorer:before {content: "\f2de";}.fa-wpforms:before {content: "\f298";}.fa-wrench:before {content: "\f0ad";}.fa-x-ray:before {content: "\f497";}.fa-xbox:before {content: "\f412";}.fa-xing:before {content: "\f168";}.fa-xing-square:before {content: "\f169";}.fa-y-combinator:before {content: "\f23b";}.fa-yahoo:before {content: "\f19e";}.fa-yandex:before {content: "\f413";}.fa-yandex-international:before {content: "\f414";}.fa-yelp:before {content: "\f1e9";}.fa-yen-sign:before {content: "\f157";}.fa-yoast:before {content: "\f2b1";}.fa-youtube:before {content: "\f167";}.fa-youtube-square:before {content: "\f431";}.sr-only {border: 0;clip: rect(0, 0, 0, 0);height: 1px;margin: -1px;overflow: hidden;padding: 0;position: absolute;width: 1px;}.sr-only-focusable:active,.sr-only-focusable:focus {clip: auto;height: auto;margin: 0;overflow: visible;position: static;width: auto;}.fa,.fas {font-family: 'Font Awesome 5 Free';font-weight: 900;}.far {font-family: 'Font Awesome 5 Free';font-weight: 400;}.fab {font-family: 'Font Awesome 5 Brands';}/*! PhotoSwipe main CSS by Dmitry Semenov | photoswipe.com | MIT license *//* + Styles for basic PhotoSwipe functionality (sliding area, open/close transitions) +*//* pswp = photoswipe */.pswp {display: none;position: absolute;width: 100%;height: 100%;left: 0;top: 0;overflow: hidden;-ms-touch-action: none;touch-action: none;z-index: 1500;-webkit-text-size-adjust: 100%; /* create separate layer, to avoid paint on window.onscroll in webkit/blink */-webkit-backface-visibility: hidden;outline: none;}.pswp * {-webkit-box-sizing: border-box;box-sizing: border-box;}.pswp img {max-width: none;}/* style is added when JS option showHideOpacity is set to true */.pswp--animate_opacity { /* 0.001, because opacity:0 doesn't trigger Paint action, which causes lag at start of transition */opacity: 0.001;will-change: opacity; /* for open/close transition */-webkit-transition: opacity 333ms cubic-bezier(0.4, 0, 0.22, 1);transition: opacity 333ms cubic-bezier(0.4, 0, 0.22, 1);}.pswp--open {display: block;}.pswp--zoom-allowed .pswp__img { /* autoprefixer: off */cursor: -webkit-zoom-in;cursor: -moz-zoom-in;cursor: zoom-in;}.pswp--zoomed-in .pswp__img { /* autoprefixer: off */cursor: -webkit-grab;cursor: -moz-grab;cursor: grab;}.pswp--dragging .pswp__img { /* autoprefixer: off */cursor: -webkit-grabbing;cursor: -moz-grabbing;cursor: grabbing;}/* + Background is added as a separate element. + As animating opacity is much faster than animating rgba() background-color. +*/.pswp__bg {position: absolute;left: 0;top: 0;width: 100%;height: 100%;background: #000;opacity: 0;-webkit-backface-visibility: hidden;will-change: opacity;}.pswp__scroll-wrap {position: absolute;left: 0;top: 0;width: 100%;height: 100%;overflow: hidden;}.pswp__container,.pswp__zoom-wrap {-ms-touch-action: none;touch-action: none;position: absolute;left: 0;right: 0;top: 0;bottom: 0;}/* Prevent selection and tap highlights */.pswp__container,.pswp__img {-webkit-user-select: none;-moz-user-select: none;-ms-user-select: none;user-select: none;-webkit-tap-highlight-color: transparent;-webkit-touch-callout: none;}.pswp__zoom-wrap {position: absolute;width: 100%;-webkit-transform-origin: left top;transform-origin: left top; /* for open/close transition */-webkit-transition: -webkit-transform 333ms cubic-bezier(0.4, 0, 0.22, 1);transition: -webkit-transform 333ms cubic-bezier(0.4, 0, 0.22, 1);transition: transform 333ms cubic-bezier(0.4, 0, 0.22, 1);transition: transform 333ms cubic-bezier(0.4, 0, 0.22, 1), -webkit-transform 333ms cubic-bezier(0.4, 0, 0.22, 1);}.pswp__bg {will-change: opacity; /* for open/close transition */-webkit-transition: opacity 333ms cubic-bezier(0.4, 0, 0.22, 1);transition: opacity 333ms cubic-bezier(0.4, 0, 0.22, 1);}.pswp--animated-in .pswp__bg,.pswp--animated-in .pswp__zoom-wrap {-webkit-transition: none;transition: none;}.pswp__container,.pswp__zoom-wrap {-webkit-backface-visibility: hidden;}.pswp__item {position: absolute;left: 0;right: 0;top: 0;bottom: 0;overflow: hidden;}.pswp__img {position: absolute;width: auto;height: auto;top: 0;left: 0;}/* + stretched thumbnail or div placeholder element (see below) + style is added to avoid flickering in webkit/blink when layers overlap +*/.pswp__img--placeholder {-webkit-backface-visibility: hidden;}/* + div element that matches size of large image + large image loads on top of it +*/.pswp__img--placeholder--blank {background: #222;}.pswp--ie .pswp__img {width: 100% !important;height: auto !important;left: 0;top: 0;}/* + Error message appears when image is not loaded + (JS option errorMsg controls markup) +*/.pswp__error-msg {position: absolute;left: 0;top: 50%;width: 100%;text-align: center;font-size: 14px;line-height: 16px;margin-top: -8px;color: #CCC;}.pswp__error-msg a {color: #CCC;text-decoration: underline;}/*! PhotoSwipe Default UI CSS by Dmitry Semenov | photoswipe.com | MIT license *//* + + Contents: + + 1. Buttons + 2. Share modal and links + 3. Index indicator ("1 of X" counter) + 4. Caption + 5. Loading indicator + 6. Additional styles (root element, top bar, idle state, hidden state, etc.) + +*//* + + 1. Buttons + + *//* - -[{include file="bottomitem.tpl"}] \ No newline at end of file + + +[{assign var="oViewConf" value=$oView->getViewConfig()}] +[{assign var="oConf" value=$oView->getConfig()}] +[{oxstyle include=$oViewConf->getModuleUrl('oxpspasswordpolicy','out/src/css/style.css')}] +[{oxstyle include=$oViewConf->getModuleUrl('oxpspasswordpolicy','out/src/css/styles.css')}] +[{oxstyle}] +[{oxscript}] + \ No newline at end of file diff --git a/views/admin/tpl/admin_twofactorbackupcode.tpl b/views/admin/tpl/admin_twofactorbackupcode.tpl new file mode 100644 index 00000000..9c6b021d --- /dev/null +++ b/views/admin/tpl/admin_twofactorbackupcode.tpl @@ -0,0 +1,32 @@ + +
    +
    +

    [{oxmultilang ident="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_BACKUPCODE"}]

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

    [{$oView->generateBackupCode()}]

    +
    + +
    + +
    +
    +
    +
    + [{assign var="oViewConf" value=$oView->getViewConfig()}] + [{assign var="oConf" value=$oView->getConfig()}] + [{oxstyle include=$oViewConf->getModuleUrl('oxpspasswordpolicy','out/src/css/style.css')}] + [{oxscript include=$oViewConf->getModuleUrl('oxpspasswordpolicy','out/src/js/otpField.js')}] + [{oxstyle include=$oViewConf->getModuleUrl('oxpspasswordpolicy','out/src/css/styles.css')}] + [{oxstyle}] + [{oxscript}] + diff --git a/views/admin/tpl/admin_twofactorconfirmation.tpl b/views/admin/tpl/admin_twofactorconfirmation.tpl new file mode 100644 index 00000000..cbab820b --- /dev/null +++ b/views/admin/tpl/admin_twofactorconfirmation.tpl @@ -0,0 +1,35 @@ +
    +
    +
    + [{include file="message/errors.tpl"}] +
    +

    [{oxmultilang ident="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_LOGIN"}]

    + [{oxmultilang ident="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_HELP_LOGIN"}] +
    + +
    +
    + +
    + +
    +
    +
    +
    + +[{assign var="oViewConf" value=$oView->getViewConfig()}] +[{assign var="oConf" value=$oView->getConfig()}] +[{oxstyle include=$oViewConf->getModuleUrl('oxpspasswordpolicy','out/src/css/style.css')}] +[{oxscript include=$oViewConf->getModuleUrl('oxpspasswordpolicy','out/src/js/otpField.js')}] +[{oxstyle include=$oViewConf->getModuleUrl('oxpspasswordpolicy','out/src/css/styles.css')}] +[{oxstyle}] +[{oxscript}] + \ No newline at end of file diff --git a/views/admin/tpl/admin_twofactorregister.tpl b/views/admin/tpl/admin_twofactorregister.tpl index ed3f0710..661acbf2 100644 --- a/views/admin/tpl/admin_twofactorregister.tpl +++ b/views/admin/tpl/admin_twofactorregister.tpl @@ -1,8 +1,11 @@
    -
    +
    +
    + [{include file="message/errors.tpl"}] +

    [{oxmultilang ident="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_REGISTER"}]

    [{oxmultilang ident="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_HELP_REGISTER"}] -
    +
    - [{oxstyle include=$oViewConf->getModuleUrl('oxpspasswordpolicy','out/src/css/style.css')}] - [{oxscript include=$oViewConf->getModuleUrl('oxpspasswordpolicy','out/src/js/otpField.js')}] - +[{assign var="oViewConf" value=$oView->getViewConfig()}] +[{assign var="oConf" value=$oView->getConfig()}] +[{oxstyle include=$oViewConf->getModuleUrl('oxpspasswordpolicy','out/src/css/style.css')}] +[{oxscript include=$oViewConf->getModuleUrl('oxpspasswordpolicy','out/src/js/otpField.js')}] +[{oxstyle include=$oViewConf->getModuleUrl('oxpspasswordpolicy','out/src/css/styles.css')}] +[{oxstyle}] +[{oxscript}] + \ No newline at end of file diff --git a/views/admin/tpl/message/errors.tpl b/views/admin/tpl/message/errors.tpl new file mode 100644 index 00000000..54a3fb33 --- /dev/null +++ b/views/admin/tpl/message/errors.tpl @@ -0,0 +1,8 @@ +[{if $Errors|is_array && $Errors.default|is_array && !empty($Errors.default)}] + [{foreach from=$Errors.default item=oEr key=key}] +

    [{$oEr->getOxMessage()}]

    + [{/foreach}] +[{/if}] +[{if $Errors.popup|is_array && !empty($Errors.popup)}] + [{include file="message/errors_modal.tpl"}] +[{/if}] \ No newline at end of file diff --git a/views/admin/tpl/message/errors_modal.tpl b/views/admin/tpl/message/errors_modal.tpl new file mode 100644 index 00000000..426470bc --- /dev/null +++ b/views/admin/tpl/message/errors_modal.tpl @@ -0,0 +1,21 @@ + +[{oxscript add="$('#error-popup').modal('show');"}] \ No newline at end of file From 8e642936807383e76bd33757ead1ce56c20c086f Mon Sep 17 00:00:00 2001 From: moritz Date: Thu, 7 Oct 2021 14:41:11 +0200 Subject: [PATCH 157/199] using random_int as a more secure way to generate backupcode --- src/Controller/Admin/PasswordPolicyTwoFactorBackupCodeAdmin.php | 2 +- src/Controller/PasswordPolicyTwoFactorBackupCode.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Controller/Admin/PasswordPolicyTwoFactorBackupCodeAdmin.php b/src/Controller/Admin/PasswordPolicyTwoFactorBackupCodeAdmin.php index cff187a0..a6cbe22a 100644 --- a/src/Controller/Admin/PasswordPolicyTwoFactorBackupCodeAdmin.php +++ b/src/Controller/Admin/PasswordPolicyTwoFactorBackupCodeAdmin.php @@ -25,7 +25,7 @@ public function generateBackupCode() { $result = ''; for($i = 0; $i < 20; $i++) { - $result .= mt_rand(0, 9); + $result .= random_int(0, 9); } $backupCode = password_hash($result, PASSWORD_BCRYPT); $user = $this->getUser(); diff --git a/src/Controller/PasswordPolicyTwoFactorBackupCode.php b/src/Controller/PasswordPolicyTwoFactorBackupCode.php index 7db9bc8a..f983f37a 100644 --- a/src/Controller/PasswordPolicyTwoFactorBackupCode.php +++ b/src/Controller/PasswordPolicyTwoFactorBackupCode.php @@ -24,7 +24,7 @@ public function generateBackupCode() { $result = ''; for($i = 0; $i < 20; $i++) { - $result .= mt_rand(0, 9); + $result .= random_int(0,9); } $backupCode = password_hash($result, PASSWORD_BCRYPT); $user = $this->getUser(); From 931de53092a1dfd6c2a4dbebd5e24cbfc3210ba2 Mon Sep 17 00:00:00 2001 From: moritz Date: Wed, 13 Oct 2021 14:43:19 +0200 Subject: [PATCH 158/199] fixed replay attack database entry --- src/Core/PasswordPolicyEvents.php | 5 +++-- src/Model/PasswordPolicyUser.php | 21 +++------------------ 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/src/Core/PasswordPolicyEvents.php b/src/Core/PasswordPolicyEvents.php index 470239a0..9ec443a5 100644 --- a/src/Core/PasswordPolicyEvents.php +++ b/src/Core/PasswordPolicyEvents.php @@ -21,9 +21,10 @@ public static function onActivate() { $user = oxNew(User::class); - if (!(in_array('oxpstotpsecret', $user->getFieldNames()) && in_array('oxpsbackupcode', $user->getFieldNames()))) { + if (!(in_array('oxpstotpsecret', $user->getFieldNames()) && in_array('oxpsbackupcode', $user->getFieldNames()) && in_array('oxpsotp', $user->getFieldNames()))) { $query = "ALTER TABLE oxuser ADD OXPSTOTPSECRET varchar(255) NOT NULL, - ADD OXPSBACKUPCODE varchar(255) NOT NULL;"; + ADD OXPSBACKUPCODE varchar(255) NOT NULL, + ADD OXPSOTP varchar(255) NOT NULL;"; try { DatabaseProvider::getDb()->execute($query); self::regenerateViews(); diff --git a/src/Model/PasswordPolicyUser.php b/src/Model/PasswordPolicyUser.php index 49cb1b10..eeb6a84e 100644 --- a/src/Model/PasswordPolicyUser.php +++ b/src/Model/PasswordPolicyUser.php @@ -36,12 +36,6 @@ public function onLogin($userName, $password) $forgotPass->forgotPassword(); $errorMessage = $err->getMessage() . ' ' . Registry::getLang()->translateString('REQUEST_PASSWORD_AFTERCLICK'); throw oxNew(UserException::class, $errorMessage); - - // redirects admin to password reset page *temporary solution - /* $oViewConf = oxNew(ViewConfig::class); - $passwordResetLink = $oViewConf->getBaseDir() . 'index.php?cl=forgotpwd&uid=' . $this->getUpdateId() . '&lang=' . $oViewConf->getActLanguageId() . '&shp=' . $this->getShopId(); - Registry::getUtils()->redirect($passwordResetLink, true);*/ - } parent::onLogin($userName, $password); } @@ -88,27 +82,18 @@ public function finalizeLogin($otp, $setsessioncookie = false) { $container = ContainerFactory::getInstance()->getContainer(); $totp = $container->get(PasswordPolicyTOTP::class); - $passwordpolicyConfig = $container->get(PasswordPolicyConfig::class); $config = oxNew(Config::class); $session = Registry::getSession(); $usr = $session->getVariable('tmpusr'); $this->load($usr); $secret = $this->oxuser__oxpstotpsecret->value; - if ($passwordpolicyConfig->isRateLimiting()) { - $driverName = $passwordpolicyConfig->getSelectedDriver(); - $rateLimiter = (new PasswordPolicyRateLimiterFactory())->getRateLimiter($driverName)->getLimiter(); - // checks whether rate limit is exceeded - try { - $rateLimiter->limit($secret, Rate::perMinute($passwordpolicyConfig->getRateLimit())); - } catch (LimitExceeded $exception) { - throw oxNew(UserException::class, 'OXPS_PASSWORDPOLICY_RATELIMIT_TWOFACTOR_EXCEEDED'); - } - } $decryptedSecret = $totp->decryptSecret($secret); $totp->verifyOTP($decryptedSecret, $otp, $this); - $this->oxuser__oxpsotp = new Field($otp, Field::T_TEXT); $session->deleteVariable('tmpusr'); $session->setVariable('usr', $usr); + $session->setVariable('auth', $usr); + // to prevent replay attacks + $this->oxuser__oxpsotp = new \OxidEsales\EshopCommunity\Core\Field($otp, Field::T_TEXT); // in case user wants to stay logged in, set user cookie again if ($setsessioncookie && $config->getConfigParam('blShowRememberMe')) { Registry::getUtilsServer()->setUserCookie( From fe48a17d7a863502f5d9f1f9617085dfdb26381c Mon Sep 17 00:00:00 2001 From: moritz Date: Wed, 13 Oct 2021 14:43:46 +0200 Subject: [PATCH 159/199] adds rate limiting to verifyOTP method --- src/TwoFactorAuth/PasswordPolicyTOTP.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/TwoFactorAuth/PasswordPolicyTOTP.php b/src/TwoFactorAuth/PasswordPolicyTOTP.php index 13134a28..85608c4f 100644 --- a/src/TwoFactorAuth/PasswordPolicyTOTP.php +++ b/src/TwoFactorAuth/PasswordPolicyTOTP.php @@ -8,6 +8,11 @@ use OxidEsales\Eshop\Core\Exception\UserException; use OxidEsales\Eshop\Core\Registry; use OxidEsales\Eshop\Core\ViewConfig; +use OxidEsales\EshopCommunity\Internal\Container\ContainerFactory; +use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyConfig; +use OxidProfessionalServices\PasswordPolicy\Factory\PasswordPolicyRateLimiterFactory; +use RateLimit\Exception\LimitExceeded; +use RateLimit\Rate; class PasswordPolicyTOTP extends Base { @@ -34,6 +39,18 @@ public function getTOTPQrUrl(): string */ public function verifyOTP(string $secret, string $auth, $user = null) { + $container = ContainerFactory::getInstance()->getContainer(); + $passwordpolicyConfig = $container->get(PasswordPolicyConfig::class); + if ($passwordpolicyConfig->isRateLimiting()) { + $driverName = $passwordpolicyConfig->getSelectedDriver(); + $rateLimiter = (new PasswordPolicyRateLimiterFactory())->getRateLimiter($driverName)->getLimiter(); + // checks whether rate limit is exceeded + try { + $rateLimiter->limit($secret, Rate::perMinute($passwordpolicyConfig->getRateLimit())); + } catch (LimitExceeded $exception) { + throw oxNew(UserException::class, 'OXPS_PASSWORDPOLICY_RATELIMIT_TWOFACTOR_EXCEEDED'); + } + } $totp = TOTP::create($secret); if (!$totp->verify($auth, null, 1) || $this->isOTPUsed($user, $auth)) { throw oxNew(UserException::class, 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_ERROR_WRONGOTP'); From 1b91c6d51e5d6cd9dd0ac395dc813a8f6e895ab1 Mon Sep 17 00:00:00 2001 From: moritz Date: Wed, 13 Oct 2021 14:44:03 +0200 Subject: [PATCH 160/199] added new translations --- views/admin/de/passwordpolicy_lang.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/views/admin/de/passwordpolicy_lang.php b/views/admin/de/passwordpolicy_lang.php index a67620e3..0bd6b415 100644 --- a/views/admin/de/passwordpolicy_lang.php +++ b/views/admin/de/passwordpolicy_lang.php @@ -107,6 +107,7 @@ 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_REGISTER_ERROR' => 'Die 2-Faktor Einrichtung kann derzeit nicht durchgeführt werden, bitten versuchen Sie es später nochmal oder kontaktieren Sie uns.', 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_RESET_SUCCESS' => 'Die 2FA wurde erfolgreich resettet. ', 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_YES' => "Aktivieren", - 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_NO' => 'Deaktivieren' + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_NO' => 'Deaktivieren', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_ADMIN_LOGIN' => 'Anmelden' ); From 068741dea2cc9c0ebb6508d0149d9b061bf32582 Mon Sep 17 00:00:00 2001 From: moritz Date: Wed, 13 Oct 2021 14:44:55 +0200 Subject: [PATCH 161/199] added working 2FA login for admins --- .../Admin/PasswordPolicyLoginController.php | 25 ++++++++++ .../PasswordPolicyTwoFactorLoginAdmin.php | 46 +++++++++++++++++++ views/admin/tpl/admin_twofactorlogin.tpl | 35 ++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 src/Controller/Admin/PasswordPolicyLoginController.php create mode 100644 src/Controller/Admin/PasswordPolicyTwoFactorLoginAdmin.php create mode 100644 views/admin/tpl/admin_twofactorlogin.tpl diff --git a/src/Controller/Admin/PasswordPolicyLoginController.php b/src/Controller/Admin/PasswordPolicyLoginController.php new file mode 100644 index 00000000..ffb70612 --- /dev/null +++ b/src/Controller/Admin/PasswordPolicyLoginController.php @@ -0,0 +1,25 @@ +getVariable('auth'); + $user = oxNew(User::class); + $user->load($sessionuser); + $secret = $user->oxuser__oxpstotpsecret->value; + if($secret) + { + Registry::getSession()->setVariable('tmpusr', $sessionuser); + Registry::getSession()->deleteVariable('auth'); + return 'admin_twofactorlogin'; + } + return 'admin_start'; + } +} \ No newline at end of file diff --git a/src/Controller/Admin/PasswordPolicyTwoFactorLoginAdmin.php b/src/Controller/Admin/PasswordPolicyTwoFactorLoginAdmin.php new file mode 100644 index 00000000..8fb9ab7f --- /dev/null +++ b/src/Controller/Admin/PasswordPolicyTwoFactorLoginAdmin.php @@ -0,0 +1,46 @@ +getRequestEscapedParameter('otp'); + try { + $user = oxNew(User::class); + $user->finalizeLogin($otp, false); + }catch(UserException $ex) + { + return Registry::getUtilsView()->addErrorToDisplay($ex); + } + return 'admin_start'; + } + + protected function _authorize() + { + return true; + } + +} \ No newline at end of file diff --git a/views/admin/tpl/admin_twofactorlogin.tpl b/views/admin/tpl/admin_twofactorlogin.tpl new file mode 100644 index 00000000..8e6dfe98 --- /dev/null +++ b/views/admin/tpl/admin_twofactorlogin.tpl @@ -0,0 +1,35 @@ +
    +
    +
    + [{include file="message/errors.tpl"}] +
    +

    [{oxmultilang ident="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_LOGIN"}]

    + [{oxmultilang ident="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_HELP_LOGIN"}] +
    + +
    +
    + [{oxmultilang ident="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_HELP_LOST"}] + +
    + +
    +
    +
    +
    +[{assign var="oViewConf" value=$oView->getViewConfig()}] +[{assign var="oConf" value=$oView->getConfig()}] +[{oxstyle include=$oViewConf->getModuleUrl('oxpspasswordpolicy','out/src/css/style.css')}] +[{oxscript include=$oViewConf->getModuleUrl('oxpspasswordpolicy','out/src/js/otpField.js')}] +[{oxstyle include=$oViewConf->getModuleUrl('oxpspasswordpolicy','out/src/css/styles.css')}] +[{oxstyle}] +[{oxscript}] + \ No newline at end of file From 4e44462a21c1e90cd6316f7317305c698c9b72e2 Mon Sep 17 00:00:00 2001 From: moritz Date: Wed, 13 Oct 2021 16:59:01 +0200 Subject: [PATCH 162/199] language changes --- translations/de/passwordpolicy_lang.php | 3 ++- views/admin/de/passwordpolicy_lang.php | 5 +++-- views/admin/tpl/admin_twofactoraccount.tpl | 6 +++--- views/tpl/twofactoraccount.tpl | 6 +++--- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/translations/de/passwordpolicy_lang.php b/translations/de/passwordpolicy_lang.php index 9ba0534c..d9059845 100644 --- a/translations/de/passwordpolicy_lang.php +++ b/translations/de/passwordpolicy_lang.php @@ -62,8 +62,9 @@ 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_BACKUPCODE' => 'Backup-Code', 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_RECOVERY' => '2-Faktor-Authentifizierung Reset', 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_LOGIN' => '2-Faktor-Authentifizierung', - 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_ACTIVATE' => '2FA aktivieren', 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_DEACTIVATE' => '2FA deaktivieren', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_ACTIVE' => 'Aktiviert', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_INACTIVE' => 'Deaktiviert', 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_CONTINUE' => 'Weiter', 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_CONFIRM' => 'Verstanden', 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_RESET' => 'Zurücksetzen', diff --git a/views/admin/de/passwordpolicy_lang.php b/views/admin/de/passwordpolicy_lang.php index 0bd6b415..eb87961e 100644 --- a/views/admin/de/passwordpolicy_lang.php +++ b/views/admin/de/passwordpolicy_lang.php @@ -90,8 +90,9 @@ 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_BACKUPCODE' => 'Backup-Code', 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_RECOVERY' => '2-Faktor-Authentifizierung Reset', 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_LOGIN' => '2-Faktor-Authentifizierung', - 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_ACTIVATE' => '2FA aktivieren', - 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_DEACTIVATE' => '2FA deaktivieren', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_ACTIVE' => 'Aktiviert', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_INACTIVE' => 'Deaktiviert', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_DEACTIVATE' => 'Deaktivieren', 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_CONTINUE' => 'Weiter', 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_CONFIRM' => 'Verstanden', 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_RESET' => 'Zurücksetzen', diff --git a/views/admin/tpl/admin_twofactoraccount.tpl b/views/admin/tpl/admin_twofactoraccount.tpl index 1e12fac8..47fcc7c6 100644 --- a/views/admin/tpl/admin_twofactoraccount.tpl +++ b/views/admin/tpl/admin_twofactoraccount.tpl @@ -16,11 +16,11 @@
    - +
    diff --git a/views/tpl/twofactoraccount.tpl b/views/tpl/twofactoraccount.tpl index 7c05b890..fa87d8ae 100644 --- a/views/tpl/twofactoraccount.tpl +++ b/views/tpl/twofactoraccount.tpl @@ -15,11 +15,11 @@
    - +
    From 3459f7c6134f1dfebaef3a5a0f013c6bc1369686 Mon Sep 17 00:00:00 2001 From: moritz Date: Wed, 13 Oct 2021 17:00:01 +0200 Subject: [PATCH 163/199] style changes --- views/admin/tpl/admin_twofactorbackupcode.tpl | 2 +- views/admin/tpl/admin_twofactorconfirmation.tpl | 3 ++- views/admin/tpl/admin_twofactorlogin.tpl | 2 +- views/admin/tpl/admin_twofactorregister.tpl | 4 ++-- views/tpl/twofactorconfirmation.tpl | 3 ++- views/tpl/twofactorrecovery.tpl | 2 +- 6 files changed, 9 insertions(+), 7 deletions(-) diff --git a/views/admin/tpl/admin_twofactorbackupcode.tpl b/views/admin/tpl/admin_twofactorbackupcode.tpl index 9c6b021d..28438353 100644 --- a/views/admin/tpl/admin_twofactorbackupcode.tpl +++ b/views/admin/tpl/admin_twofactorbackupcode.tpl @@ -13,7 +13,7 @@
    - +
    diff --git a/views/admin/tpl/admin_twofactorconfirmation.tpl b/views/admin/tpl/admin_twofactorconfirmation.tpl index cbab820b..b4db2225 100644 --- a/views/admin/tpl/admin_twofactorconfirmation.tpl +++ b/views/admin/tpl/admin_twofactorconfirmation.tpl @@ -14,8 +14,9 @@
    + [{oxmultilang ident="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_HELP_LOST"}]
    - +
    diff --git a/views/admin/tpl/admin_twofactorlogin.tpl b/views/admin/tpl/admin_twofactorlogin.tpl index 8e6dfe98..9a07051a 100644 --- a/views/admin/tpl/admin_twofactorlogin.tpl +++ b/views/admin/tpl/admin_twofactorlogin.tpl @@ -13,7 +13,7 @@
    - [{oxmultilang ident="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_HELP_LOST"}] + [{oxmultilang ident="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_HELP_LOST"}]
    diff --git a/views/admin/tpl/admin_twofactorregister.tpl b/views/admin/tpl/admin_twofactorregister.tpl index 661acbf2..b4fef67e 100644 --- a/views/admin/tpl/admin_twofactorregister.tpl +++ b/views/admin/tpl/admin_twofactorregister.tpl @@ -19,8 +19,8 @@
    -
    - +
    +
    diff --git a/views/tpl/twofactorconfirmation.tpl b/views/tpl/twofactorconfirmation.tpl index 38bf891d..4e00b443 100644 --- a/views/tpl/twofactorconfirmation.tpl +++ b/views/tpl/twofactorconfirmation.tpl @@ -17,8 +17,9 @@
    + [{oxmultilang ident="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_HELP_LOST"}]
    - +
    diff --git a/views/tpl/twofactorrecovery.tpl b/views/tpl/twofactorrecovery.tpl index 26e5af0d..68ae17ad 100644 --- a/views/tpl/twofactorrecovery.tpl +++ b/views/tpl/twofactorrecovery.tpl @@ -18,7 +18,7 @@
    - +
    From e8b58324645d6c2060051a2e2b57c41aec28b2c2 Mon Sep 17 00:00:00 2001 From: moritz Date: Wed, 13 Oct 2021 17:00:12 +0200 Subject: [PATCH 164/199] fix --- metadata.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/metadata.php b/metadata.php index a86fc0de..5e28be2e 100644 --- a/metadata.php +++ b/metadata.php @@ -27,13 +27,17 @@ use OxidEsales\Eshop\Application\Component\UserComponent; use OxidEsales\Eshop\Application\Controller\AccountPasswordController; +use OxidEsales\Eshop\Application\Controller\Admin\LoginController; use OxidEsales\Eshop\Core\InputValidator; use OxidEsales\Eshop\Core\ViewConfig; use OxidEsales\Eshop\Application\Model\User; use OxidEsales\Eshop\Application\Controller\Admin\ModuleConfiguration; +use OxidProfessionalServices\PasswordPolicy\Controller\Admin\PasswordPolicyTwoFactorRecoveryAdmin; use OxidProfessionalServices\PasswordPolicy\Component\PasswordPolicyUserComponent; use OxidProfessionalServices\PasswordPolicy\Controller\Admin\PasswordPolicyAccountTOTPAdmin; +use OxidProfessionalServices\PasswordPolicy\Controller\Admin\PasswordPolicyLoginController; use OxidProfessionalServices\PasswordPolicy\Controller\Admin\PasswordPolicyTwoFactorBackupCodeAdmin; +use OxidProfessionalServices\PasswordPolicy\Controller\Admin\PasswordPolicyTwoFactorLoginAdmin; use OxidProfessionalServices\PasswordPolicy\Controller\Admin\PasswordPolicyTwoFactorRegisterAdmin; use OxidProfessionalServices\PasswordPolicy\Controller\PasswordPolicyAccountTOTP; use OxidProfessionalServices\PasswordPolicy\Controller\PasswordPolicyTwoFactorConfirmation; @@ -74,7 +78,8 @@ AccountPasswordController::class => PasswordPolicyAccountPasswordController::class, User::class => PasswordPolicyUser::class, ModuleConfiguration::class => PasswordPolicyModuleConfiguration::class, - UserComponent::class => PasswordPolicyUserComponent::class + UserComponent::class => PasswordPolicyUserComponent::class, + LoginController::class => PasswordPolicyLoginController::class ], 'controllers' => [ 'twofactorregister' => PasswordPolicyTwoFactorRegister::class, @@ -87,6 +92,8 @@ 'admin_twofactorregister' => PasswordPolicyTwoFactorRegisterAdmin::class, 'admin_twofactorconfirmation' => PasswordPolicyTwoFactorConfirmationAdmin::class, 'admin_twofactorbackup' => PasswordPolicyTwoFactorBackupCodeAdmin::class, + 'admin_twofactorlogin' => PasswordPolicyTwoFactorLoginAdmin::class, + 'admin_twofactorrecovery' => PasswordPolicyTwoFactorRecoveryAdmin::class, ], @@ -101,6 +108,8 @@ 'admin_twofactorregister.tpl' => 'oxps/passwordpolicy/views/admin/tpl/admin_twofactorregister.tpl', 'admin_twofactorconfirmation.tpl' => 'oxps/passwordpolicy/views/admin/tpl/admin_twofactorconfirmation.tpl', 'admin_twofactorbackupcode.tpl' => 'oxps/passwordpolicy/views/admin/tpl/admin_twofactorbackupcode.tpl', + 'admin_twofactorlogin.tpl' => 'oxps/passwordpolicy/views/admin/tpl/admin_twofactorlogin.tpl', + 'admin_twofactorrecovery.tpl' => 'oxps/passwordpolicy/views/admin/tpl/admin_twofactorrecovery.tpl', 'layout/page.tpl' => 'oxps/passwordpolicy/views/admin/tpl/layout/page.tpl', 'message/errors.tpl' => 'oxps/passwordpolicy/views/admin/tpl/message/errors.tpl', 'message/error.tpl' => 'oxps/passwordpolicy/views/admin/tpl/message/error.tpl', From 36b6378e39c514b4c8934c2ff955cfd829c523da Mon Sep 17 00:00:00 2001 From: moritz Date: Wed, 13 Oct 2021 17:00:41 +0200 Subject: [PATCH 165/199] added recovery functionality to admin --- .../PasswordPolicyTwoFactorRecoveryAdmin.php | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 src/Controller/Admin/PasswordPolicyTwoFactorRecoveryAdmin.php diff --git a/src/Controller/Admin/PasswordPolicyTwoFactorRecoveryAdmin.php b/src/Controller/Admin/PasswordPolicyTwoFactorRecoveryAdmin.php new file mode 100644 index 00000000..2a40ab07 --- /dev/null +++ b/src/Controller/Admin/PasswordPolicyTwoFactorRecoveryAdmin.php @@ -0,0 +1,63 @@ +getRequestEscapedParameter('recoveryCode'); + $session = Registry::getSession(); + $user = $this->getUser(); + $usr = $session->getVariable('tmpusr') ?: $user->getId(); + if(!$user) + { + $user = oxNew(User::class); + $user->load($usr); + $redirect = 'admin_start'; + } + if($this->checkCode($user, $recoveryCode)) + { + $this->resetCode($user); + $session->setVariable('auth', $usr); + return $redirect; + } + Registry::getUtilsView()->addErrorToDisplay('OXPS_PASSWORDPOLICY_TWOFACTORAUTH_ERROR_WRONGBACKUPCODE'); + } + + public function resetCode($user) + { + $user->oxuser__oxpstotpsecret = new Field("", Field::T_TEXT); + $user->oxuser__oxpsbackupcode = new Field("", Field::T_TEXT); + $user->save(); + } + + public function checkCode($user, $recoveryCode) + { + $userRecoveryCode = $user->oxuser__oxpsbackupcode->value; + if(password_verify($recoveryCode, $userRecoveryCode)) + { + return true; + } + return false; + } + + protected function _authorize() + { + return true; + } +} \ No newline at end of file From 8329a3656e5e8354a98f3a10f5eb99d33527b140 Mon Sep 17 00:00:00 2001 From: moritz Date: Wed, 13 Oct 2021 17:02:50 +0200 Subject: [PATCH 166/199] added recovery template for admin --- views/admin/tpl/admin_twofactorrecovery.tpl | 36 +++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 views/admin/tpl/admin_twofactorrecovery.tpl diff --git a/views/admin/tpl/admin_twofactorrecovery.tpl b/views/admin/tpl/admin_twofactorrecovery.tpl new file mode 100644 index 00000000..e0dc6340 --- /dev/null +++ b/views/admin/tpl/admin_twofactorrecovery.tpl @@ -0,0 +1,36 @@ +
    +
    +
    + [{include file="message/errors.tpl"}] +
    +

    [{oxmultilang ident="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_RECOVERY"}]

    + [{oxmultilang ident="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_HELP_RECOVERY"}] +
    + +
    + +
    +
    + +
    +
    +
    +
    +[{assign var="oViewConf" value=$oView->getViewConfig()}] +[{assign var="oConf" value=$oView->getConfig()}] +[{oxstyle include=$oViewConf->getModuleUrl('oxpspasswordpolicy','out/src/css/style.css')}] +[{oxscript include=$oViewConf->getModuleUrl('oxpspasswordpolicy','out/src/js/otpField.js')}] +[{oxstyle include=$oViewConf->getModuleUrl('oxpspasswordpolicy','out/src/css/styles.css')}] +[{oxstyle}] +[{oxscript}] + + + From d2776e703e5c708d958085799dc6773437eda7da Mon Sep 17 00:00:00 2001 From: moritz Date: Wed, 13 Oct 2021 17:03:00 +0200 Subject: [PATCH 167/199] style changes --- views/admin/tpl/admin_twofactorlogin.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/admin/tpl/admin_twofactorlogin.tpl b/views/admin/tpl/admin_twofactorlogin.tpl index 9a07051a..a2aa9f68 100644 --- a/views/admin/tpl/admin_twofactorlogin.tpl +++ b/views/admin/tpl/admin_twofactorlogin.tpl @@ -16,7 +16,7 @@ [{oxmultilang ident="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_HELP_LOST"}]
    - +
    From b4a0a98064be9af36ae83bda8d566424c2641921 Mon Sep 17 00:00:00 2001 From: moritz Date: Wed, 13 Oct 2021 17:07:36 +0200 Subject: [PATCH 168/199] added language for my data --- views/admin/de/passwordpolicy_lang.php | 1 + 1 file changed, 1 insertion(+) diff --git a/views/admin/de/passwordpolicy_lang.php b/views/admin/de/passwordpolicy_lang.php index eb87961e..b53408b5 100644 --- a/views/admin/de/passwordpolicy_lang.php +++ b/views/admin/de/passwordpolicy_lang.php @@ -87,6 +87,7 @@ 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_ERROR_WRONGBACKUPCODE' => 'Der eingebene Backup Code war falsch.', 'OXPS_PASSWORDPOLICY_RATELIMIT_TWOFACTOR_EXCEEDED' => 'Sie haben zu oft den falschen Code für die Zwei-Faktor-Authentifizierung eingegeben. Bitte versuchen Sie es später erneut.', 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_REGISTER' => '2-Faktor-Authentifizierung Einrichtung', + 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_MYDATA' => 'Meine Daten', 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_BACKUPCODE' => 'Backup-Code', 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_RECOVERY' => '2-Faktor-Authentifizierung Reset', 'OXPS_PASSWORDPOLICY_TWOFACTORAUTH_LOGIN' => '2-Faktor-Authentifizierung', From b1c9bf4ae90fdde4758e97a1dfbb24d8cd39c0a2 Mon Sep 17 00:00:00 2001 From: moritz Date: Wed, 13 Oct 2021 17:07:50 +0200 Subject: [PATCH 169/199] style changes --- menu.xml | 4 ++-- views/admin/tpl/admin_twofactorrecovery.tpl | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/menu.xml b/menu.xml index ee01e950..8f03e294 100644 --- a/menu.xml +++ b/menu.xml @@ -1,8 +1,8 @@ - - + + diff --git a/views/admin/tpl/admin_twofactorrecovery.tpl b/views/admin/tpl/admin_twofactorrecovery.tpl index e0dc6340..dc2e256d 100644 --- a/views/admin/tpl/admin_twofactorrecovery.tpl +++ b/views/admin/tpl/admin_twofactorrecovery.tpl @@ -15,7 +15,7 @@
    - +
    From 20aa5a3b19dce1927e598b0d24739e74850e928b Mon Sep 17 00:00:00 2001 From: moritz Date: Wed, 13 Oct 2021 17:11:56 +0200 Subject: [PATCH 170/199] fixxed anti replay attack for admin --- src/Controller/Admin/PasswordPolicyTwoFactorLoginAdmin.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Controller/Admin/PasswordPolicyTwoFactorLoginAdmin.php b/src/Controller/Admin/PasswordPolicyTwoFactorLoginAdmin.php index 8fb9ab7f..795339b2 100644 --- a/src/Controller/Admin/PasswordPolicyTwoFactorLoginAdmin.php +++ b/src/Controller/Admin/PasswordPolicyTwoFactorLoginAdmin.php @@ -31,6 +31,7 @@ public function finalizeLogin() try { $user = oxNew(User::class); $user->finalizeLogin($otp, false); + $user->save(); }catch(UserException $ex) { return Registry::getUtilsView()->addErrorToDisplay($ex); From 67385bc66c189827c4b90b791376177ab4bea417 Mon Sep 17 00:00:00 2001 From: moritz Date: Thu, 14 Oct 2021 20:10:21 +0200 Subject: [PATCH 171/199] now takes language keys from frontend & backend --- src/Core/PasswordPolicyLanguage.php | 39 +++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/Core/PasswordPolicyLanguage.php diff --git a/src/Core/PasswordPolicyLanguage.php b/src/Core/PasswordPolicyLanguage.php new file mode 100644 index 00000000..71c0f3e8 --- /dev/null +++ b/src/Core/PasswordPolicyLanguage.php @@ -0,0 +1,39 @@ +. + * + * @author OXID Professional services + * @link http://www.oxid-esales.com + * @copyright (C) OXID eSales AG 2003-2019 + */ + +declare(strict_types=1); + +namespace OxidProfessionalServices\PasswordPolicy\Core; + +/** + * Password policy config helpers used in controllers mostly + */ +class PasswordPolicyLanguage extends PasswordPolicyLanguage_parent +{ + protected function _getAdminLangFilesPathArray($iLang) + { + + $aLangFiles = parent::_getLangFilesPathArray($iLang); + return array_merge($aLangFiles,parent::_getAdminLangFilesPathArray($iLang)); + } +} From 95d15ffd6438c45cef276b6e26b3897f233feee3 Mon Sep 17 00:00:00 2001 From: moritz Date: Thu, 14 Oct 2021 20:11:22 +0200 Subject: [PATCH 172/199] added new (non working) admin password forgot controller and templates --- ...ordPolicyForgotPasswordControllerAdmin.php | 149 +++ views/admin/tpl/email/html/footer.tpl | 102 +++ views/admin/tpl/email/html/forgotpwd.tpl | 10 + views/admin/tpl/email/html/header.tpl | 855 ++++++++++++++++++ views/admin/tpl/email/plain/forgotpwd.tpl | 3 + views/admin/tpl/form/forgotpwd_change_pwd.tpl | 24 + views/admin/tpl/form/forgotpwd_email.tpl | 39 + views/admin/tpl/page/account/forgotpwd.tpl | 61 ++ 8 files changed, 1243 insertions(+) create mode 100644 src/Controller/Admin/PasswordPolicyForgotPasswordControllerAdmin.php create mode 100644 views/admin/tpl/email/html/footer.tpl create mode 100644 views/admin/tpl/email/html/forgotpwd.tpl create mode 100644 views/admin/tpl/email/html/header.tpl create mode 100644 views/admin/tpl/email/plain/forgotpwd.tpl create mode 100644 views/admin/tpl/form/forgotpwd_change_pwd.tpl create mode 100644 views/admin/tpl/form/forgotpwd_email.tpl create mode 100644 views/admin/tpl/page/account/forgotpwd.tpl diff --git a/src/Controller/Admin/PasswordPolicyForgotPasswordControllerAdmin.php b/src/Controller/Admin/PasswordPolicyForgotPasswordControllerAdmin.php new file mode 100644 index 00000000..d639440b --- /dev/null +++ b/src/Controller/Admin/PasswordPolicyForgotPasswordControllerAdmin.php @@ -0,0 +1,149 @@ +getRequestParameter('user'); + $this->_sForgotEmail = $sEmail; + $oEmail = oxNew(\OxidEsales\Eshop\Core\Email::class); + + // problems sending passwd reminder ? + $iSuccess = false; + if ($sEmail) { + $iSuccess = $oEmail->sendForgotPwdEmail($sEmail); + } + if ($iSuccess !== true) { + $sError = ($iSuccess === false) ? 'ERROR_MESSAGE_PASSWORD_EMAIL_INVALID' : 'MESSAGE_NOT_ABLE_TO_SEND_EMAIL'; + Registry::getUtilsView()->addErrorToDisplay($sError); + $this->_sForgotEmail = false; + } + } + public function updatePassword() + { + $sNewPass = Registry::getConfig()->getRequestParameter('password_new', true); + $sConfPass = Registry::getConfig()->getRequestParameter('password_new_confirm', true); + + $oUser = oxNew(\OxidEsales\Eshop\Application\Model\User::class); + + /** @var \OxidEsales\Eshop\Core\InputValidator $oInputValidator */ + $oInputValidator = Registry::getInputValidator(); + if (($oExcp = $oInputValidator->checkPassword($oUser, $sNewPass, $sConfPass, true))) { + Registry::getUtilsView()->addErrorToDisplay($oExcp->getMessage(), false, true); + return 'admin_forgotpwd'; + } + + // passwords are fine - updating and loggin user in + if ($oUser->loadUserByUpdateId($this->getUpdateId())) { + // setting new pass .. + $oUser->setPassword($sNewPass); + + // resetting update pass params + $oUser->setUpdateKey(true); + + // saving .. + $oUser->save(); + + // forcing user login + Registry::getSession()->setVariable('auth', $oUser->getId()); + + return 'admin_start'; + } else { + // expired reminder + $oUtilsView = Registry::getUtilsView(); + + return $oUtilsView->addErrorToDisplay('ERROR_MESSAGE_PASSWORD_LINK_EXPIRED', false, true); + } + } + + /** + * If user password update was successfull - setting success status + * + * @return bool + */ + public function updateSuccess() + { + return (bool) Registry::getConfig()->getRequestParameter('success'); + } + + /** + * Notifies that password update form must be shown + * + * @return bool + */ + public function showUpdateScreen() + { + return (bool) $this->getUpdateId(); + } + + /** + * Returns special id used for password update functionality + * + * @return string + */ + public function getUpdateId() + { + return Registry::getConfig()->getRequestParameter('uid'); + } + + /** + * Returns password update link expiration status + * + * @return bool + */ + public function isExpiredLink() + { + if (($sKey = $this->getUpdateId())) { + $blExpired = oxNew(\OxidEsales\Eshop\Application\Model\User::class)->isExpiredUpdateId($sKey); + } + + return $blExpired; + } + + /** + * Template variable getter. Returns searched article list + * + * @return string + */ + public function getForgotEmail() + { + return $this->_sForgotEmail; + } + + + /** + * Get password reminder page title + * + * @return string + */ + public function getTitle() + { + $sTitle = 'FORGOT_PASSWORD'; + + if ($this->showUpdateScreen()) { + $sTitle = 'NEW_PASSWORD'; + } elseif ($this->updateSuccess()) { + $sTitle = 'CHANGE_PASSWORD'; + } + + return Registry::getLang()->translateString($sTitle, Registry::getLang()->getBaseLanguage(), false); + } + + protected function _authorize() + { + return true; + } +} \ No newline at end of file diff --git a/views/admin/tpl/email/html/footer.tpl b/views/admin/tpl/email/html/footer.tpl new file mode 100644 index 00000000..35f61e34 --- /dev/null +++ b/views/admin/tpl/email/html/footer.tpl @@ -0,0 +1,102 @@ + + + + + [{if $oViewConf->getViewThemeParam('sFacebookUrl') || $oViewConf->getViewThemeParam('sGooglePlusUrl') || $oViewConf->getViewThemeParam('sTwitterUrl') || $oViewConf->getViewThemeParam('sYouTubeUrl') || $oViewConf->getViewThemeParam('sBlogUrl')}] + + [{/if}] + + + + + + + + +
    + + + + + + +
    + [{*ToDo*}] +
    + +
    + + + + + + + + + + + \ No newline at end of file diff --git a/views/admin/tpl/email/html/forgotpwd.tpl b/views/admin/tpl/email/html/forgotpwd.tpl new file mode 100644 index 00000000..f13bae22 --- /dev/null +++ b/views/admin/tpl/email/html/forgotpwd.tpl @@ -0,0 +1,10 @@ +[{assign var="shop" value=$oEmailView->getShop()}] +[{assign var="oViewConf" value=$oEmailView->getViewConfig()}] +[{assign var="user" value=$oEmailView->getUser()}] + + +[{include file="email/html/header.tpl" title="DD_FORGOT_PASSWORD_HEADING"|oxmultilangassign}] + +

    [{oxcontent ident="oxpsadminupdatepassinfoemail"}]

    + +[{include file="email/html/footer.tpl"}] diff --git a/views/admin/tpl/email/html/header.tpl b/views/admin/tpl/email/html/header.tpl new file mode 100644 index 00000000..ee723409 --- /dev/null +++ b/views/admin/tpl/email/html/header.tpl @@ -0,0 +1,855 @@ + + + + + + + + + + [{block name="email_html_header"}][{/block}] + [{assign var="oConfig" value=$oViewConf->getConfig()}] + + +
    +
    + + + + + +
    +
    + + + + +
    + + + + + + + + +
    + [{assign var="sEmailLogo" value=$oViewConf->getViewThemeParam('sEmailLogo')}] + + [{$shop->oxshops__oxname->value}] + + + [{$title}] +
    +
    +
    +
    + + + + + + + \ No newline at end of file From 748eb043d26aaf70e073fba1e9108ba81b73b4f1 Mon Sep 17 00:00:00 2001 From: moritz Date: Wed, 20 Oct 2021 18:43:33 +0200 Subject: [PATCH 185/199] added validation from frontend --- .../Admin/PasswordPolicyForgotPasswordControllerAdmin.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Controller/Admin/PasswordPolicyForgotPasswordControllerAdmin.php b/src/Controller/Admin/PasswordPolicyForgotPasswordControllerAdmin.php index a5e59914..960e73f5 100644 --- a/src/Controller/Admin/PasswordPolicyForgotPasswordControllerAdmin.php +++ b/src/Controller/Admin/PasswordPolicyForgotPasswordControllerAdmin.php @@ -151,4 +151,9 @@ protected function _authorize() { return true; } + + public function getFieldValidationErrors() + { + return Registry::getInputValidator()->getFieldValidationErrors(); + } } \ No newline at end of file From 2a7cc786e39fb12cd45c3b4acbb32bd1028e1fb8 Mon Sep 17 00:00:00 2001 From: moritz Date: Wed, 20 Oct 2021 18:43:59 +0200 Subject: [PATCH 186/199] added passwort strength indicator to backend --- views/admin/tpl/form/forgotpwd_change_pwd.tpl | 55 ++++++++++++++++++- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/views/admin/tpl/form/forgotpwd_change_pwd.tpl b/views/admin/tpl/form/forgotpwd_change_pwd.tpl index 87dd717e..35fe9175 100644 --- a/views/admin/tpl/form/forgotpwd_change_pwd.tpl +++ b/views/admin/tpl/form/forgotpwd_change_pwd.tpl @@ -1,6 +1,50 @@ -[{oxscript add="$('input,select,textarea').not('[type=submit]').jqBootstrapValidation();"}] -[{include file="message/errors.tpl"}] + + +
    + [{assign var="aErrors" value=$oView->getFieldValidationErrors()}] + [{include file="message/errors.tpl"}]
    + [{block name="user_account_password"}] +

    + [{/block}]
    - +


    + +[{oxscript}] \ No newline at end of file From 0a19c3940ea0519f41c2d487eedaecf0179d554c Mon Sep 17 00:00:00 2001 From: moritz Date: Wed, 20 Oct 2021 19:16:52 +0200 Subject: [PATCH 187/199] prevents submitting when error messages still exist --- views/admin/tpl/form/forgotpwd_change_pwd.tpl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/views/admin/tpl/form/forgotpwd_change_pwd.tpl b/views/admin/tpl/form/forgotpwd_change_pwd.tpl index 35fe9175..a9644a87 100644 --- a/views/admin/tpl/form/forgotpwd_change_pwd.tpl +++ b/views/admin/tpl/form/forgotpwd_change_pwd.tpl @@ -36,6 +36,12 @@ helpblock.append(str); return error_messages; }); + $('.btn').click(function(e) { + if(error_messages.length) + { + e.preventDefault(); + } + }); }); From d3224fec1fc418c61073648e5c18aad2ed1a58b2 Mon Sep 17 00:00:00 2001 From: moritz Date: Wed, 20 Oct 2021 19:57:48 +0200 Subject: [PATCH 188/199] code style fixxes --- .../Admin/PasswordPolicyForgotPasswordControllerAdmin.php | 4 ++-- src/Controller/Admin/PasswordPolicyLoginController.php | 5 ++--- .../Admin/PasswordPolicyTwoFactorConfirmationAdmin.php | 4 ---- .../Admin/PasswordPolicyTwoFactorRegisterAdmin.php | 3 +-- src/Controller/PasswordPolicyAccountPasswordController.php | 2 +- src/Controller/PasswordPolicyTwoFactorRegister.php | 3 +-- src/Core/PasswordPolicyEvents.php | 5 ++--- src/TwoFactorAuth/PasswordPolicyTOTP.php | 6 ++---- src/Validators/PasswordPolicyDigits.php | 2 +- src/Validators/PasswordPolicySpecialCharacter.php | 2 +- src/Validators/PasswordPolicyUpperLowerCase.php | 4 ++-- 11 files changed, 15 insertions(+), 25 deletions(-) diff --git a/src/Controller/Admin/PasswordPolicyForgotPasswordControllerAdmin.php b/src/Controller/Admin/PasswordPolicyForgotPasswordControllerAdmin.php index 960e73f5..01aa4648 100644 --- a/src/Controller/Admin/PasswordPolicyForgotPasswordControllerAdmin.php +++ b/src/Controller/Admin/PasswordPolicyForgotPasswordControllerAdmin.php @@ -43,7 +43,7 @@ public function updatePassword() /** @var \OxidEsales\Eshop\Core\InputValidator $oInputValidator */ $oInputValidator = Registry::getInputValidator(); - if (($oExcp = $oInputValidator->checkPassword($oUser, $sNewPass, $sConfPass, true))) { + if ($oExcp = $oInputValidator->checkPassword($oUser, $sNewPass, $sConfPass, true)) { return Registry::getUtilsView()->addErrorToDisplay($oExcp->getMessage(), false, true); } @@ -111,7 +111,7 @@ public function getUpdateId() */ public function isExpiredLink() { - if (($sKey = $this->getUpdateId())) { + if ($sKey = $this->getUpdateId()) { $blExpired = oxNew(\OxidEsales\Eshop\Application\Model\User::class)->isExpiredUpdateId($sKey); } diff --git a/src/Controller/Admin/PasswordPolicyLoginController.php b/src/Controller/Admin/PasswordPolicyLoginController.php index 1645b3c0..85f95fce 100644 --- a/src/Controller/Admin/PasswordPolicyLoginController.php +++ b/src/Controller/Admin/PasswordPolicyLoginController.php @@ -46,7 +46,7 @@ public function checklogin() return; } catch (\OxidEsales\Eshop\Core\Exception\CookieException $oEx) { - $myUtilsView->addErrorToDisplay($oEx->getMessage()); + $myUtilsView->addErrorToDisplay($oEx); $oStr = getStr(); $this->addTplParam('user', $oStr->htmlspecialchars($sUser)); $this->addTplParam('pwd', $oStr->htmlspecialchars($sPass)); @@ -83,8 +83,7 @@ public function checklogin() $myUtilsServer->setOxCookie("oxidadminlanguage", $aLanguages[$iLang]->abbr, time() + 31536000, "/"); - //P - //\OxidEsales\Eshop\Core\Registry::getSession()->setVariable( "blAdminTemplateLanguage", $iLang ); + \OxidEsales\Eshop\Core\Registry::getLang()->setTplLanguage($iLang); diff --git a/src/Controller/Admin/PasswordPolicyTwoFactorConfirmationAdmin.php b/src/Controller/Admin/PasswordPolicyTwoFactorConfirmationAdmin.php index 61110976..d690d52c 100644 --- a/src/Controller/Admin/PasswordPolicyTwoFactorConfirmationAdmin.php +++ b/src/Controller/Admin/PasswordPolicyTwoFactorConfirmationAdmin.php @@ -17,10 +17,6 @@ class PasswordPolicyTwoFactorConfirmationAdmin extends AdminController public function render() { parent::render(); - $oUser = $this->getUser(); - if (!$oUser) { - return 'page/account/login.tpl'; - } return 'admin_twofactorconfirmation.tpl'; } diff --git a/src/Controller/Admin/PasswordPolicyTwoFactorRegisterAdmin.php b/src/Controller/Admin/PasswordPolicyTwoFactorRegisterAdmin.php index d5ed11a3..d6d4e782 100644 --- a/src/Controller/Admin/PasswordPolicyTwoFactorRegisterAdmin.php +++ b/src/Controller/Admin/PasswordPolicyTwoFactorRegisterAdmin.php @@ -62,8 +62,7 @@ public function finalizeRegistration() public function getTOTPQrCode() { $TOTPurl = $this->TOTP->getTotpQrUrl(); - $qrcode = $this->qrCodeRenderer->generateQrCode($TOTPurl); - return $qrcode; + return $this->qrCodeRenderer->generateQrCode($TOTPurl); } diff --git a/src/Controller/PasswordPolicyAccountPasswordController.php b/src/Controller/PasswordPolicyAccountPasswordController.php index 9be2ec00..fabe8508 100644 --- a/src/Controller/PasswordPolicyAccountPasswordController.php +++ b/src/Controller/PasswordPolicyAccountPasswordController.php @@ -36,7 +36,7 @@ public function changePassword() /** @var \OxidEsales\Eshop\Core\InputValidator $oInputValidator */ $oInputValidator = \OxidEsales\Eshop\Core\Registry::getInputValidator(); - if (($oExcp = $oInputValidator->checkPassword($oUser, $sNewPass, $sConfPass, true))) { + if ($oExcp = $oInputValidator->checkPassword($oUser, $sNewPass, $sConfPass, true)) { $tmpInputValidator = oxNew(InputValidator::class); \OxidEsales\Eshop\Core\Registry::set(InputValidator::class, $tmpInputValidator); return \OxidEsales\Eshop\Core\Registry::getUtilsView()->addErrorToDisplay( diff --git a/src/Controller/PasswordPolicyTwoFactorRegister.php b/src/Controller/PasswordPolicyTwoFactorRegister.php index 22811142..b6962cee 100644 --- a/src/Controller/PasswordPolicyTwoFactorRegister.php +++ b/src/Controller/PasswordPolicyTwoFactorRegister.php @@ -38,8 +38,7 @@ public function render() public function getTOTPQrCode() { $TOTPurl = $this->TOTP->getTotpQrUrl(); - $qrcode = $this->qrCodeRenderer->generateQrCode($TOTPurl); - return $qrcode; + return $this->qrCodeRenderer->generateQrCode($TOTPurl); } public function getBreadCrumb() diff --git a/src/Core/PasswordPolicyEvents.php b/src/Core/PasswordPolicyEvents.php index 0317b9d7..37546d30 100644 --- a/src/Core/PasswordPolicyEvents.php +++ b/src/Core/PasswordPolicyEvents.php @@ -56,8 +56,7 @@ public static function onActivate() protected static function generateKey(): string { $key = random_bytes(32); - $hashedKey = base64_encode($key); - return $hashedKey; + return base64_encode($key); } protected static function regenerateViews() @@ -87,7 +86,7 @@ protected static function _getFolderToClear($clearFolderPath = '') { $templateFolderPath = (string)\OxidEsales\Eshop\Core\Registry::getConfig()->getConfigParam('sCompileDir'); - if (!empty($clearFolderPath) and (strpos($clearFolderPath, $templateFolderPath) !== false)) { + if (!empty($clearFolderPath) && (strpos($clearFolderPath, $templateFolderPath) !== false)) { $folderPath = $clearFolderPath; } else { $folderPath = $templateFolderPath; diff --git a/src/TwoFactorAuth/PasswordPolicyTOTP.php b/src/TwoFactorAuth/PasswordPolicyTOTP.php index 85608c4f..b631f116 100644 --- a/src/TwoFactorAuth/PasswordPolicyTOTP.php +++ b/src/TwoFactorAuth/PasswordPolicyTOTP.php @@ -28,8 +28,7 @@ public function getTOTPQrUrl(): string Registry::getSession()->setVariable('otp_secret', $this->encryptSecret($secret)); $otp->setLabel($user->oxuser__oxusername->value); $otp->setIssuer(Registry::getConfig()->getActiveShop()->getFieldData('oxname')); - $dataUrl = $otp->getProvisioningUri(); - return $dataUrl; + return $otp->getProvisioningUri(); } /** @@ -81,8 +80,7 @@ public function encryptSecret($secret) $key = base64_decode($twofactorconfig->getVar('key')); $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('aes-256-cbc')); $encrypted = openssl_encrypt($secret, 'aes-256-cbc', $key, 0, $iv); - $result = base64_encode($encrypted . '::' . $iv); - return $result; + return base64_encode($encrypted . '::' . $iv); } /** diff --git a/src/Validators/PasswordPolicyDigits.php b/src/Validators/PasswordPolicyDigits.php index 51299213..f9672186 100644 --- a/src/Validators/PasswordPolicyDigits.php +++ b/src/Validators/PasswordPolicyDigits.php @@ -17,7 +17,7 @@ public function __construct(PasswordPolicyConfig $config) public function validate(string $sUsername, string $sPassword) { - if ($this->config->getPasswordNeedDigits() and !preg_match('(\d+)', $sPassword)) { + if ($this->config->getPasswordNeedDigits() && !preg_match('(\d+)', $sPassword)) { return 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESDIGITS'; } return true; diff --git a/src/Validators/PasswordPolicySpecialCharacter.php b/src/Validators/PasswordPolicySpecialCharacter.php index 58c7ebe4..0f7a89f5 100644 --- a/src/Validators/PasswordPolicySpecialCharacter.php +++ b/src/Validators/PasswordPolicySpecialCharacter.php @@ -18,7 +18,7 @@ public function validate(string $sUsername, string $sPassword) { if ( - $this->config->getPasswordNeedSpecialCharacter() and + $this->config->getPasswordNeedSpecialCharacter() && !preg_match('([\.,_@\~\(\)\!\#\$%\^\&\*\+=\-\\\/|:;`]+)', $sPassword) ) { return 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESSPECIAL'; diff --git a/src/Validators/PasswordPolicyUpperLowerCase.php b/src/Validators/PasswordPolicyUpperLowerCase.php index 7816d537..0e33547e 100644 --- a/src/Validators/PasswordPolicyUpperLowerCase.php +++ b/src/Validators/PasswordPolicyUpperLowerCase.php @@ -17,11 +17,11 @@ public function __construct(PasswordPolicyConfig $config) public function validate(string $sUsername, string $sPassword) { $settings = $this->config; - if ($settings->getPasswordNeedUpperCase() and !preg_match('(\p{Lu}+)', $sPassword)) { + if ($settings->getPasswordNeedUpperCase() && !preg_match('(\p{Lu}+)', $sPassword)) { return 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESUPPERCASE'; } - if ($settings->getPasswordNeedLowerCase() and !preg_match('(\p{Ll}+)', $sPassword)) { + if ($settings->getPasswordNeedLowerCase() && !preg_match('(\p{Ll}+)', $sPassword)) { return 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESLOWERCASE'; } return true; From d7e5d1acb3794db4908af761e94816b8eb2a2317 Mon Sep 17 00:00:00 2001 From: moritz Date: Fri, 22 Oct 2021 15:34:00 +0200 Subject: [PATCH 189/199] codestyle fixes --- src/Core/PasswordPolicyConfig.php | 74 +++++++++++++++---------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/src/Core/PasswordPolicyConfig.php b/src/Core/PasswordPolicyConfig.php index 84602ca1..b540e66c 100644 --- a/src/Core/PasswordPolicyConfig.php +++ b/src/Core/PasswordPolicyConfig.php @@ -32,54 +32,54 @@ */ class PasswordPolicyConfig { - private const SettingsPrefix = 'oxpspasswordpolicy'; - public const SettingGoodPasswordLength = self::SettingsPrefix . 'GoodPasswordLength'; - public const SettingMinPasswordLength = self::SettingsPrefix . 'MinPasswordLength'; - public const SettingDigits = self::SettingsPrefix . 'Digits'; - public const SettingSpecial = self::SettingsPrefix . 'Special'; - public const SettingAPI = self::SettingsPrefix . 'API'; - public const SettingEnzoicAPIKey = self::SettingsPrefix . 'EnzoicAPIKey'; - public const SettingEnzoicSecretKey = self::SettingsPrefix . 'EnzoicSecretKey'; - public const SettingEnzoic = self::SettingsPrefix . 'Enzoic'; - public const SettingHaveIBeenPwned = self::SettingsPrefix . 'HaveIBeenPwned'; - public const SettingUpper = self::SettingsPrefix . 'UpperCase'; - public const SettingLower = self::SettingsPrefix . 'LowerCase'; - public const SettingDrivers = self::SettingsPrefix . 'RateLimitingDrivers'; - public const SettingLimit = self::SettingsPrefix . 'RateLimitingLimit'; - public const SettingRateLimiting = self::SettingsPrefix . 'RateLimiting'; - public const SettingMemcachedHost = self::SettingsPrefix . 'MemcachedHost'; - public const SettingMemcachedPort = self::SettingsPrefix . 'MemcachedPort'; - public const SettingTOTP = self::SettingsPrefix . 'TOTP'; - public const SettingAdminUser = self::SettingsPrefix . 'admin'; + private const SETTINGS_PREFIX = 'oxpspasswordpolicy'; + public const SETTING_GOOD_PASSWORD_LENGTH = self::SETTINGS_PREFIX . 'GoodPasswordLength'; + public const SETTING_MIN_PASSWORD_LENGTH = self::SETTINGS_PREFIX . 'MinPasswordLength'; + public const SETTING_DIGITS = self::SETTINGS_PREFIX . 'Digits'; + public const SETTING_SPECIAL = self::SETTINGS_PREFIX . 'Special'; + public const SETTING_API = self::SETTINGS_PREFIX . 'API'; + public const SETTING_ENZOIC_API_KEY = self::SETTINGS_PREFIX . 'EnzoicAPIKey'; + public const SETTING_ENZOIC_SECRET_KEY = self::SETTINGS_PREFIX . 'EnzoicSecretKey'; + public const SETTING_ENZOIC = self::SETTINGS_PREFIX . 'Enzoic'; + public const SETTING_HAVE_I_BEEN_PWNED = self::SETTINGS_PREFIX . 'HaveIBeenPwned'; + public const SETTING_UPPER = self::SETTINGS_PREFIX . 'UpperCase'; + public const SETTING_LOWER = self::SETTINGS_PREFIX . 'LowerCase'; + public const SETTING_DRIVER = self::SETTINGS_PREFIX . 'RateLimitingDrivers'; + public const SETTING_LIMIT = self::SETTINGS_PREFIX . 'RateLimitingLimit'; + public const SETTING_RATELIMITING = self::SETTINGS_PREFIX . 'RateLimiting'; + public const SETTING_MEMCACHED_HOST = self::SETTINGS_PREFIX . 'MemcachedHost'; + public const SETTING_MEMCACHED_PORT = self::SETTINGS_PREFIX . 'MemcachedPort'; + public const SETTING_TOTP = self::SETTINGS_PREFIX . 'TOTP'; + public const SETTING_ADMIN_USER = self::SETTINGS_PREFIX . 'admin'; public function getMinPasswordLength(): int { - return (int) Registry::getConfig()->getConfigParam(self::SettingMinPasswordLength, 8); + return (int) Registry::getConfig()->getConfigParam(self::SETTING_MIN_PASSWORD_LENGTH, 8); } public function getGoodPasswordLength(): int { - return (int) Registry::getConfig()->getConfigParam(self::SettingGoodPasswordLength, 12); + return (int) Registry::getConfig()->getConfigParam(self::SETTING_GOOD_PASSWORD_LENGTH, 12); } public function getPasswordNeedDigits(): bool { - return $this->isConfigParam(self::SettingDigits); + return $this->isConfigParam(self::SETTING_DIGITS); } public function getPasswordNeedUpperCase(): bool { - return $this->isConfigParam(self::SettingUpper); + return $this->isConfigParam(self::SETTING_UPPER); } public function getPasswordNeedLowerCase(): bool { - return $this->isConfigParam(self::SettingLower); + return $this->isConfigParam(self::SETTING_LOWER); } public function getPasswordNeedSpecialCharacter(): bool { - return $this->isConfigParam(self::SettingSpecial); + return $this->isConfigParam(self::SETTING_SPECIAL); } /** @@ -97,62 +97,62 @@ public function getMaxPasswordLength(): int public function getAPIKey(): string { - return (string) Registry::getConfig()->getConfigParam(self::SettingEnzoicAPIKey); + return (string) Registry::getConfig()->getConfigParam(self::SETTING_ENZOIC_API_KEY); } public function getSecretKey(): string { - return (string) Registry::getConfig()->getConfigParam(self::SettingEnzoicSecretKey); + return (string) Registry::getConfig()->getConfigParam(self::SETTING_ENZOIC_SECRET_KEY); } public function isAPI(): bool { - return $this->isConfigParam(self::SettingAPI); + return $this->isConfigParam(self::SETTING_API); } public function isEnzoic(): bool { - return $this->isConfigParam(self::SettingEnzoic); + return $this->isConfigParam(self::SETTING_ENZOIC); } public function isHaveIBeenPwned(): bool { - return $this->isConfigParam(self::SettingHaveIBeenPwned); + return $this->isConfigParam(self::SETTING_HAVE_I_BEEN_PWNED); } public function isRateLimiting(): bool { - return $this->isConfigParam(self::SettingRateLimiting); + return $this->isConfigParam(self::SETTING_RATELIMITING); } public function isTOTP(): bool { - return $this->isConfigParam(self::SettingTOTP); + return $this->isConfigParam(self::SETTING_TOTP); } public function isAdminUsers(): bool { - return $this->isConfigParam(self::SettingAdminUser); + return $this->isConfigParam(self::SETTING_ADMIN_USER); } public function getSelectedDriver(): string { - return (string) Registry::getConfig()->getConfigParam(self::SettingDrivers); + return (string) Registry::getConfig()->getConfigParam(self::SETTING_DRIVER); } public function getRateLimit(): int { - return (int) Registry::getConfig()->getConfigParam(self::SettingLimit, 60); + return (int) Registry::getConfig()->getConfigParam(self::SETTING_LIMIT, 60); } public function getMemcachedHost(): string { - return (string) Registry::getConfig()->getConfigParam(self::SettingMemcachedHost); + return (string) Registry::getConfig()->getConfigParam(self::SETTING_MEMCACHED_HOST); } public function getMemcachedPort(): int { - return (int) Registry::getConfig()->getConfigParam(self::SettingMemcachedPort, 11211); + return (int) Registry::getConfig()->getConfigParam(self::SETTING_MEMCACHED_PORT, 11211); } private function isConfigParam(string $name): bool { From 5f75e33e0e5667badf488e20289b528f5a771fbb Mon Sep 17 00:00:00 2001 From: moritz Date: Fri, 22 Oct 2021 16:15:39 +0200 Subject: [PATCH 190/199] fixed codestyle --- out/src/js/otpField.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/out/src/js/otpField.js b/out/src/js/otpField.js index ddcb6f94..a83b11b4 100644 --- a/out/src/js/otpField.js +++ b/out/src/js/otpField.js @@ -37,10 +37,9 @@ for (let i = 0; i < inputs.length; i++) { } document.getElementById('accUserSaveTop').addEventListener("click", function () { - const inputs = document.querySelectorAll('#OTPInput > *[id]'); let compiledOtp = ''; - for (let i = 0; i < inputs.length; i++) { - compiledOtp += inputs[i].value; + for (let input of inputs) { + compiledOtp += input.value; } document.getElementById('otp').value = compiledOtp; return true; From 28726e3d009a52cf060e25a22352340bc3cda3f6 Mon Sep 17 00:00:00 2001 From: moritz Date: Fri, 22 Oct 2021 16:15:52 +0200 Subject: [PATCH 191/199] renamed constant --- src/Core/PasswordPolicyValidator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/PasswordPolicyValidator.php b/src/Core/PasswordPolicyValidator.php index 592d6647..e484d245 100644 --- a/src/Core/PasswordPolicyValidator.php +++ b/src/Core/PasswordPolicyValidator.php @@ -80,6 +80,6 @@ public function validatePassword(string $sUsername, string $sPassword): ?Standar */ public function getPasswordLength() { - return (int) Registry::getConfig()->getConfigParam(PasswordPolicyConfig::SettingMinPasswordLength, 8); + return (int) Registry::getConfig()->getConfigParam(PasswordPolicyConfig::SETTING_MIN_PASSWORD_LENGTH, 8); } } From f57fe5435336cd36bf5412f201898905bc6d6e91 Mon Sep 17 00:00:00 2001 From: moritz Date: Fri, 22 Oct 2021 17:57:13 +0200 Subject: [PATCH 192/199] fixed codestyle --- src/Api/PasswordCheck.php | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/Api/PasswordCheck.php b/src/Api/PasswordCheck.php index 80d80b21..f6c61829 100644 --- a/src/Api/PasswordCheck.php +++ b/src/Api/PasswordCheck.php @@ -35,12 +35,30 @@ public function __construct(PasswordPolicyConfig $config, PasswordExposedChecker */ public function isPasswordKnown(string $username, string $password): bool { - if ($this->config->isHaveIBeenPwned() && $this->haveIBeenPwned->passwordExposed($password) == "exposed") { + if ($this->isListedOnHaveIBeenPwned($password) || $this->isListedOnEnzoic($username,$password)) + { return true; - } elseif ($this->config->isEnzoic() && ($this->enzoicApiCon->checkPassword($password) !== null || $this->enzoicApiCon->checkCredentials($username, $password))) { + } + return false; + } + + public function isListedOnEnzoic(string $username, string $password): bool + { + if($this->config->isEnzoic() && ($this->enzoicApiCon->checkPassword($password) !== null || $this->enzoicApiCon->checkCredentials($username, $password))) + { + return true; + } + return false; + } + + public function isListedOnHaveIBeenPwned(string $password): bool + { + if($this->config->isHaveIBeenPwned() && $this->haveIBeenPwned->passwordExposed($password) == "exposed") + { return true; } return false; } -} \ No newline at end of file +} + From f9a16ed1b319a2c6a9e024f7d6af640e30da33a7 Mon Sep 17 00:00:00 2001 From: moritz Date: Fri, 22 Oct 2021 18:28:54 +0200 Subject: [PATCH 193/199] renamed variables --- .../Admin/PasswordPolicyModuleConfiguration.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Controller/Admin/PasswordPolicyModuleConfiguration.php b/src/Controller/Admin/PasswordPolicyModuleConfiguration.php index c9838371..7f6eae26 100644 --- a/src/Controller/Admin/PasswordPolicyModuleConfiguration.php +++ b/src/Controller/Admin/PasswordPolicyModuleConfiguration.php @@ -12,9 +12,9 @@ class PasswordPolicyModuleConfiguration extends PasswordPolicyModuleConfiguratio public function saveConfVars() { $variables = $this->getConfigVariablesFromRequest(); - if($variables[PasswordPolicyConfig::SettingEnzoic] == "true") { - $enzoicAPIKey = $variables[PasswordPolicyConfig::SettingEnzoicAPIKey]; - $enzoicSecretKey = $variables[PasswordPolicyConfig::SettingEnzoicSecretKey]; + if($variables[PasswordPolicyConfig::SETTING_ENZOIC] == "true") { + $enzoicAPIKey = $variables[PasswordPolicyConfig::SETTING_ENZOIC_API_KEY]; + $enzoicSecretKey = $variables[PasswordPolicyConfig::SETTING_ENZOIC_SECRET_KEY]; $enzoicApiCon = new Enzoic($enzoicAPIKey, $enzoicSecretKey); try { @@ -22,10 +22,10 @@ public function saveConfVars() } catch (\RuntimeException $ex) { Registry::getUtilsView()->addErrorToDisplay("OXPS_PASSWORDPOLICY_ENZOICERROR" . $ex->getCode()); # needs better solution - $_POST["confbools"][PasswordPolicyConfig::SettingEnzoic] = "false"; + $_POST["confbools"][PasswordPolicyConfig::SETTING_ENZOIC] = "false"; } } - if($variables[PasswordPolicyConfig::SettingAdminUser] == "true") + if($variables[PasswordPolicyConfig::SETTING_ADMIN_USER] == "true") { $query = "Select oxusername from oxuser where oxrights != 'user'"; $result = DatabaseProvider::getDb()->getCol($query); @@ -39,7 +39,7 @@ public function saveConfVars() } if(!empty($invalidMails)) { - $_POST["confbools"][PasswordPolicyConfig::SettingAdminUser] = "false"; + $_POST["confbools"][PasswordPolicyConfig::SETTING_ADMIN_USER] = "false"; Registry::getUtilsView()->addErrorToDisplay("OXPS_PASSWORDPOLICY_INVALIDADMINUSERS"); foreach ($invalidMails as $invalidUser) { From ab38bc2ebe53963ea53a46bc83d899ec8b1f3d32 Mon Sep 17 00:00:00 2001 From: moritz Date: Fri, 22 Oct 2021 18:31:06 +0200 Subject: [PATCH 194/199] fixed language error --- views/admin/de/passwordpolicy_lang.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/admin/de/passwordpolicy_lang.php b/views/admin/de/passwordpolicy_lang.php index 0d29a9d0..38eb5e1a 100644 --- a/views/admin/de/passwordpolicy_lang.php +++ b/views/admin/de/passwordpolicy_lang.php @@ -54,7 +54,7 @@ 'SHOP_MODULE_oxpspasswordpolicyRateLimitingLimit' => 'Einlogversuche pro Minute', 'SHOP_MODULE_oxpspasswordpolicyMemcachedHost' => 'Memcached Host', 'SHOP_MODULE_oxpspasswordpolicyMemcachedPort' => 'Memcached Port', - 'SHOP_MODULE_GROUP_passwordpolicy_twofactor' => '2FA Authentifizierung', + 'SHOP_MODULE_GROUP_passwordpolicy_twofactor' => '2-Faktor-Authentifizierung', 'SHOP_MODULE_oxpspasswordpolicyTOTP' => '2FA aktivieren', 'OXPS_PASSWORDPOLICY_ACCOUNTBLOCKED_TITLE' => 'Ihr Account wurde blockiert.', 'OXPS_PASSWORDPOLICY_ACCOUNTBLOCKED_INFO' => 'Ihr Account wurde blockiert. Sie haben das Passwort zu oft hintereinander falsch eingegeben.', From 2358ac85f5d566a81f157ed1f38e48440079525f Mon Sep 17 00:00:00 2001 From: moritz Date: Mon, 25 Oct 2021 17:16:11 +0200 Subject: [PATCH 195/199] admin login box style changes [temporary] --- metadata.php | 5 +++++ out/src/css/style.css | 17 +++++++++++++++++ views/admin/blocks/login.tpl | 3 +++ 3 files changed, 25 insertions(+) create mode 100644 views/admin/blocks/login.tpl diff --git a/metadata.php b/metadata.php index f1589b35..5690e4cf 100644 --- a/metadata.php +++ b/metadata.php @@ -165,6 +165,11 @@ 'block' => 'admin_user_main_form', 'file' => 'views/admin/blocks/user_main.tpl', ], + [ + 'template' => 'login.tpl', + 'block' => 'admin_login_form', + 'file' => 'views/admin/blocks/login.tpl', + ], ], 'settings' => [ ['group' => 'passwordpolicy', 'name' => 'oxpspasswordpolicyGoodPasswordLength', 'type' => 'num', 'value' => 12], diff --git a/out/src/css/style.css b/out/src/css/style.css index 695dd858..ece18576 100644 --- a/out/src/css/style.css +++ b/out/src/css/style.css @@ -1,3 +1,20 @@ +div.admin-login-box { + border: 0; + padding: 0; + border-radius: 0; + background: #fff; + box-shadow: 0 20px 40px rgba(0,0,0,0.1); + width: 340px; + top: 50%; + -ms-transform: translate(-50%, -50%); + transform: translate(-50%, -50%); + margin: auto; + height: unset; + right: unset; + bottom: unset; + left: 50%; +} + input.w-12 { width: 3rem; } diff --git a/views/admin/blocks/login.tpl b/views/admin/blocks/login.tpl new file mode 100644 index 00000000..29350fb7 --- /dev/null +++ b/views/admin/blocks/login.tpl @@ -0,0 +1,3 @@ +[{$smarty.block.parent}] +[{oxstyle include=$oViewConf->getModuleUrl('oxpspasswordpolicy','out/src/css/style.css')}] +[{oxstyle}] From f306be3b782fd82b20a4c4f328d2df0c3ff3df73 Mon Sep 17 00:00:00 2001 From: moritz Date: Mon, 25 Oct 2021 17:28:40 +0200 Subject: [PATCH 196/199] saving is now disabled when 2FA is deactivated for admins --- .../Admin/PasswordPolicyAccountTOTPAdmin.php | 13 +++++++++++++ views/admin/tpl/admin_twofactoraccount.tpl | 5 +++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Controller/Admin/PasswordPolicyAccountTOTPAdmin.php b/src/Controller/Admin/PasswordPolicyAccountTOTPAdmin.php index be9cb265..d1b707bb 100644 --- a/src/Controller/Admin/PasswordPolicyAccountTOTPAdmin.php +++ b/src/Controller/Admin/PasswordPolicyAccountTOTPAdmin.php @@ -7,6 +7,8 @@ use OxidEsales\Eshop\Application\Controller\Admin\AdminController; use OxidEsales\Eshop\Core\Registry; use OxidEsales\Eshop\Core\Request; +use OxidEsales\EshopCommunity\Internal\Container\ContainerFactory; +use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyConfig; class PasswordPolicyAccountTOTPAdmin extends AdminController { @@ -27,6 +29,17 @@ public function isTOTP(): bool return $secret != null; } + public function isAdminUsers(): bool + { + $container = ContainerFactory::getInstance()->getContainer(); + $config = $container->get(PasswordPolicyConfig::class); + if($config->isAdminUsers()) + { + return true; + } + return false; + } + public function redirect() { $totpenabled = (new Request())->getRequestEscapedParameter('status'); diff --git a/views/admin/tpl/admin_twofactoraccount.tpl b/views/admin/tpl/admin_twofactoraccount.tpl index 47fcc7c6..f680c6b4 100644 --- a/views/admin/tpl/admin_twofactoraccount.tpl +++ b/views/admin/tpl/admin_twofactoraccount.tpl @@ -1,5 +1,6 @@ [{oxscript include="js/libs/jquery.min.js"}] [{assign var="template_title" value="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_LOGIN"|oxmultilangassign}] +[{assign var="oConf" value=$oView->getConfig()}]
    [{if $success == '1'}] @@ -26,13 +27,13 @@
    - +
    [{assign var="oViewConf" value=$oView->getViewConfig()}] -[{assign var="oConf" value=$oView->getConfig()}] + [{oxstyle include=$oViewConf->getModuleUrl('oxpspasswordpolicy','out/src/css/style.css')}] [{oxstyle include=$oViewConf->getModuleUrl('oxpspasswordpolicy','out/src/css/styles.css')}] [{oxstyle}] From 970db27e8d5fa3765dc61acab62acc0bb09cb222 Mon Sep 17 00:00:00 2001 From: moritz Date: Mon, 25 Oct 2021 17:38:52 +0200 Subject: [PATCH 197/199] now shows 2FA disabled when its deactivated in module settings --- src/Controller/Admin/PasswordPolicyAccountTOTPAdmin.php | 2 +- views/admin/tpl/admin_twofactoraccount.tpl | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Controller/Admin/PasswordPolicyAccountTOTPAdmin.php b/src/Controller/Admin/PasswordPolicyAccountTOTPAdmin.php index d1b707bb..25a9c7c5 100644 --- a/src/Controller/Admin/PasswordPolicyAccountTOTPAdmin.php +++ b/src/Controller/Admin/PasswordPolicyAccountTOTPAdmin.php @@ -33,7 +33,7 @@ public function isAdminUsers(): bool { $container = ContainerFactory::getInstance()->getContainer(); $config = $container->get(PasswordPolicyConfig::class); - if($config->isAdminUsers()) + if($config->isTOTP() && $config->isAdminUsers()) { return true; } diff --git a/views/admin/tpl/admin_twofactoraccount.tpl b/views/admin/tpl/admin_twofactoraccount.tpl index f680c6b4..5cf27414 100644 --- a/views/admin/tpl/admin_twofactoraccount.tpl +++ b/views/admin/tpl/admin_twofactoraccount.tpl @@ -20,14 +20,14 @@
    - +
    From be248f0bbe222a831c04cdd671cd71516ed0e0f0 Mon Sep 17 00:00:00 2001 From: moritz Date: Tue, 26 Oct 2021 15:26:27 +0200 Subject: [PATCH 198/199] code clean up --- src/Controller/Admin/PasswordPolicyLoginController.php | 8 +++++--- src/Core/PasswordPolicyConfig.php | 6 +++++- src/Model/PasswordPolicyUser.php | 7 ++----- views/admin/tpl/admin_twofactoraccount.tpl | 8 ++++---- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/Controller/Admin/PasswordPolicyLoginController.php b/src/Controller/Admin/PasswordPolicyLoginController.php index 85f95fce..7b092f60 100644 --- a/src/Controller/Admin/PasswordPolicyLoginController.php +++ b/src/Controller/Admin/PasswordPolicyLoginController.php @@ -1,9 +1,10 @@ setTplLanguage($iLang); - + $container = ContainerFactory::getInstance()->getContainer(); + $config = $container->get(PasswordPolicyConfig::class); $secret = $oUser->oxuser__oxpstotpsecret->value; - if($secret) + if($config->isTOTP() && $secret) { Registry::getSession()->setVariable('tmpusr', $oUser->getId()); Registry::getSession()->deleteVariable('auth'); diff --git a/src/Core/PasswordPolicyConfig.php b/src/Core/PasswordPolicyConfig.php index b540e66c..b586f95a 100644 --- a/src/Core/PasswordPolicyConfig.php +++ b/src/Core/PasswordPolicyConfig.php @@ -132,7 +132,7 @@ public function isTOTP(): bool public function isAdminUsers(): bool { - return $this->isConfigParam(self::SETTING_ADMIN_USER); + return Registry::getConfig()->getConfigParam(self::SETTING_ADMIN_USER); } public function getSelectedDriver(): string @@ -156,6 +156,10 @@ public function getMemcachedPort(): int } private function isConfigParam(string $name): bool { + if(isAdmin() && !$this->isAdminUsers()) + { + return false; + } return (bool) Registry::getConfig()->getConfigParam($name, true); } } diff --git a/src/Model/PasswordPolicyUser.php b/src/Model/PasswordPolicyUser.php index c062118a..a26985b9 100644 --- a/src/Model/PasswordPolicyUser.php +++ b/src/Model/PasswordPolicyUser.php @@ -32,12 +32,9 @@ public function onLogin($userName, $password) { /** @var PasswordPolicyValidator $passValidator */ $passValidator = oxNew(InputValidator::class); - $forgotPass = oxNew(ForgotPasswordController::class); + $container = ContainerFactory::getInstance()->getContainer(); if ($this->isLoaded() && $err = $passValidator->validatePassword($userName, $password)) { - if($this->isAdmin()) - { - $forgotPass = oxNew(PasswordPolicyForgotPasswordControllerAdmin::class); - } + $forgotPass = $this->isAdmin() ? oxNew(PasswordPolicyForgotPasswordControllerAdmin::class) : oxNew(ForgotPasswordController::class); $forgotPass->forgotPassword(); $errorMessage = $err->getMessage() . ' ' . Registry::getLang()->translateString('REQUEST_PASSWORD_AFTERCLICK'); throw oxNew(UserException::class, $errorMessage); diff --git a/views/admin/tpl/admin_twofactoraccount.tpl b/views/admin/tpl/admin_twofactoraccount.tpl index 5cf27414..39b8dfcc 100644 --- a/views/admin/tpl/admin_twofactoraccount.tpl +++ b/views/admin/tpl/admin_twofactoraccount.tpl @@ -1,6 +1,7 @@ [{oxscript include="js/libs/jquery.min.js"}] [{assign var="template_title" value="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_LOGIN"|oxmultilangassign}] [{assign var="oConf" value=$oView->getConfig()}] +[{assign var="oViewConf" value=$oView->getViewConfig()}]
    [{if $success == '1'}] @@ -20,19 +21,18 @@
    - +
    -[{assign var="oViewConf" value=$oView->getViewConfig()}] [{oxstyle include=$oViewConf->getModuleUrl('oxpspasswordpolicy','out/src/css/style.css')}] [{oxstyle include=$oViewConf->getModuleUrl('oxpspasswordpolicy','out/src/css/styles.css')}] From 4cee62ec4d0fb1e177e1ea47291362e6eb3677bd Mon Sep 17 00:00:00 2001 From: moritz Date: Tue, 26 Oct 2021 16:50:45 +0200 Subject: [PATCH 199/199] sets 2FA rate limiting to 5 per minute --- src/TwoFactorAuth/PasswordPolicyTOTP.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/TwoFactorAuth/PasswordPolicyTOTP.php b/src/TwoFactorAuth/PasswordPolicyTOTP.php index b631f116..c61197d6 100644 --- a/src/TwoFactorAuth/PasswordPolicyTOTP.php +++ b/src/TwoFactorAuth/PasswordPolicyTOTP.php @@ -45,7 +45,7 @@ public function verifyOTP(string $secret, string $auth, $user = null) $rateLimiter = (new PasswordPolicyRateLimiterFactory())->getRateLimiter($driverName)->getLimiter(); // checks whether rate limit is exceeded try { - $rateLimiter->limit($secret, Rate::perMinute($passwordpolicyConfig->getRateLimit())); + $rateLimiter->limit($secret, Rate::perMinute(5)); } catch (LimitExceeded $exception) { throw oxNew(UserException::class, 'OXPS_PASSWORDPOLICY_RATELIMIT_TWOFACTOR_EXCEEDED'); } @@ -56,7 +56,7 @@ public function verifyOTP(string $secret, string $auth, $user = null) } } - public function isOTPUsed($user, string $auth) + public function isOTPUsed($user, string $auth): bool { $otp = $user->oxuser__oxpsotp->value; if($otp == $auth)
    + +
    \ No newline at end of file diff --git a/views/admin/tpl/email/plain/forgotpwd.tpl b/views/admin/tpl/email/plain/forgotpwd.tpl new file mode 100644 index 00000000..aa080d54 --- /dev/null +++ b/views/admin/tpl/email/plain/forgotpwd.tpl @@ -0,0 +1,3 @@ +[{oxcontent ident="oxpsadminupdatepassinfoplainmail"}] + +[{oxcontent ident="oxemailfooterplain"}] \ No newline at end of file diff --git a/views/admin/tpl/form/forgotpwd_change_pwd.tpl b/views/admin/tpl/form/forgotpwd_change_pwd.tpl new file mode 100644 index 00000000..04b23900 --- /dev/null +++ b/views/admin/tpl/form/forgotpwd_change_pwd.tpl @@ -0,0 +1,24 @@ +[{oxscript add="$('input,select,textarea').not('[type=submit]').jqBootstrapValidation();"}] + +
    + + +
    + + +
    +
    + + + +
    + +
    + +
    diff --git a/views/admin/tpl/form/forgotpwd_email.tpl b/views/admin/tpl/form/forgotpwd_email.tpl new file mode 100644 index 00000000..cc9e86aa --- /dev/null +++ b/views/admin/tpl/form/forgotpwd_email.tpl @@ -0,0 +1,39 @@ +[{oxscript add="$('input,select,textarea').not('[type=submit]').jqBootstrapValidation();"}] + +

    + [{oxmultilang ident="HAVE_YOU_FORGOTTEN_PASSWORD"}]
    + [{oxmultilang ident="HERE_YOU_SET_UP_NEW_PASSWORD"}] +

    + +
    +
    +
    + + +
    + + + +

    +
    + + [{block name="captcha_form"}][{/block}] + +
    + +
    +
    +
    +
    + +[{oxmultilang ident="REQUEST_PASSWORD_AFTERCLICK"}]

    +[{oxifcontent ident="oxforgotpwd" object="oCont"}] + [{$oCont->oxcontents__oxcontent->value}] + [{/oxifcontent}] \ No newline at end of file diff --git a/views/admin/tpl/page/account/forgotpwd.tpl b/views/admin/tpl/page/account/forgotpwd.tpl new file mode 100644 index 00000000..e335ee36 --- /dev/null +++ b/views/admin/tpl/page/account/forgotpwd.tpl @@ -0,0 +1,61 @@ + + + + \ No newline at end of file From 188640308fb3bcbe6f080f3fa372a1a4909cb350 Mon Sep 17 00:00:00 2001 From: moritz Date: Thu, 14 Oct 2021 20:12:10 +0200 Subject: [PATCH 173/199] displays error messages when logging in --- .../Admin/PasswordPolicyLoginController.php | 87 +++++++++++++++++-- 1 file changed, 81 insertions(+), 6 deletions(-) diff --git a/src/Controller/Admin/PasswordPolicyLoginController.php b/src/Controller/Admin/PasswordPolicyLoginController.php index ffb70612..1645b3c0 100644 --- a/src/Controller/Admin/PasswordPolicyLoginController.php +++ b/src/Controller/Admin/PasswordPolicyLoginController.php @@ -2,6 +2,7 @@ namespace OxidProfessionalServices\PasswordPolicy\Controller\Admin; use OxidEsales\Eshop\Application\Model\User; +use OxidEsales\Eshop\Core\Exception\UserException; use OxidEsales\Eshop\Core\Registry; @@ -9,14 +10,88 @@ class PasswordPolicyLoginController extends PasswordPolicyLoginController_parent { public function checklogin() { - parent::checklogin(); - $sessionuser = Registry::getSession()->getVariable('auth'); - $user = oxNew(User::class); - $user->load($sessionuser); - $secret = $user->oxuser__oxpstotpsecret->value; + $myUtilsServer = \OxidEsales\Eshop\Core\Registry::getUtilsServer(); + $myUtilsView = \OxidEsales\Eshop\Core\Registry::getUtilsView(); + + $sUser = \OxidEsales\Eshop\Core\Registry::getConfig()->getRequestParameter('user', true); + $sPass = \OxidEsales\Eshop\Core\Registry::getConfig()->getRequestParameter('pwd', true); + $sProfile = \OxidEsales\Eshop\Core\Registry::getConfig()->getRequestParameter('profile'); + + try { // trying to login + $session = \OxidEsales\Eshop\Core\Registry::getSession(); + $adminProfiles = $session->getVariable("aAdminProfiles"); + $session->initNewSession(); + $session->setVariable("aAdminProfiles", $adminProfiles); + + /** @var \OxidEsales\Eshop\Application\Model\User $oUser */ + $oUser = oxNew(\OxidEsales\Eshop\Application\Model\User::class); + $oUser->login($sUser, $sPass); + + if ($oUser->oxuser__oxrights->value === 'user') { + throw oxNew(UserException::class, 'ERROR_MESSAGE_USER_NOVALIDLOGIN'); + } + + $iSubshop = (int) $oUser->oxuser__oxrights->value; + if ($iSubshop) { + \OxidEsales\Eshop\Core\Registry::getSession()->setVariable("shp", $iSubshop); + \OxidEsales\Eshop\Core\Registry::getSession()->setVariable('currentadminshop', $iSubshop); + \OxidEsales\Eshop\Core\Registry::getConfig()->setShopId($iSubshop); + } + } catch (UserException $oEx) { + $myUtilsView->addErrorToDisplay($oEx->getMessage()); + $oStr = getStr(); + $this->addTplParam('user', $oStr->htmlspecialchars($sUser)); + $this->addTplParam('pwd', $oStr->htmlspecialchars($sPass)); + $this->addTplParam('profile', $oStr->htmlspecialchars($sProfile)); + + return; + } catch (\OxidEsales\Eshop\Core\Exception\CookieException $oEx) { + $myUtilsView->addErrorToDisplay($oEx->getMessage()); + $oStr = getStr(); + $this->addTplParam('user', $oStr->htmlspecialchars($sUser)); + $this->addTplParam('pwd', $oStr->htmlspecialchars($sPass)); + $this->addTplParam('profile', $oStr->htmlspecialchars($sProfile)); + + return; + } catch (\OxidEsales\Eshop\Core\Exception\ConnectionException $oEx) { + $myUtilsView->addErrorToDisplay($oEx); + } + + //execute onAdminLogin() event + $oEvenHandler = oxNew(\OxidEsales\Eshop\Core\SystemEventHandler::class); + $oEvenHandler->onAdminLogin(\OxidEsales\Eshop\Core\Registry::getConfig()->getShopId()); + + // #533 + if (isset($sProfile)) { + $aProfiles = \OxidEsales\Eshop\Core\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, "/"); + \OxidEsales\Eshop\Core\Registry::getSession()->setVariable("profile", $aProfiles[$sProfile]); + } + } else { + //deleting cookie info, as setting profile to default + $myUtilsServer->setOxCookie("oxidadminprofile", "", time() - 3600, "/"); + } + + // languages + $iLang = \OxidEsales\Eshop\Core\Registry::getConfig()->getRequestParameter("chlanguage"); + $aLanguages = \OxidEsales\Eshop\Core\Registry::getLang()->getAdminTplLanguageArray(); + if (!isset($aLanguages[$iLang])) { + $iLang = key($aLanguages); + } + + $myUtilsServer->setOxCookie("oxidadminlanguage", $aLanguages[$iLang]->abbr, time() + 31536000, "/"); + + //P + //\OxidEsales\Eshop\Core\Registry::getSession()->setVariable( "blAdminTemplateLanguage", $iLang ); + \OxidEsales\Eshop\Core\Registry::getLang()->setTplLanguage($iLang); + + + $secret = $oUser->oxuser__oxpstotpsecret->value; if($secret) { - Registry::getSession()->setVariable('tmpusr', $sessionuser); + Registry::getSession()->setVariable('tmpusr', $oUser->getId()); Registry::getSession()->deleteVariable('auth'); return 'admin_twofactorlogin'; } From b2154c3eb902cb7c2c8ee43540049ae4cfe137ca Mon Sep 17 00:00:00 2001 From: moritz Date: Thu, 14 Oct 2021 20:12:56 +0200 Subject: [PATCH 174/199] added 2 new content messages for password forgot mail --- src/Core/PasswordPolicyEvents.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Core/PasswordPolicyEvents.php b/src/Core/PasswordPolicyEvents.php index 9ec443a5..0317b9d7 100644 --- a/src/Core/PasswordPolicyEvents.php +++ b/src/Core/PasswordPolicyEvents.php @@ -4,6 +4,7 @@ namespace OxidProfessionalServices\PasswordPolicy\Core; +use Monolog\Registry; use OxidEsales\Eshop\Application\Model\User; use OxidEsales\Eshop\Core\DatabaseProvider; use OxidEsales\Eshop\Core\DbMetaDataHandler; @@ -19,14 +20,21 @@ class PasswordPolicyEvents */ public static function onActivate() { - $user = oxNew(User::class); if (!(in_array('oxpstotpsecret', $user->getFieldNames()) && in_array('oxpsbackupcode', $user->getFieldNames()) && in_array('oxpsotp', $user->getFieldNames()))) { $query = "ALTER TABLE oxuser ADD OXPSTOTPSECRET varchar(255) NOT NULL, ADD OXPSBACKUPCODE varchar(255) NOT NULL, - ADD OXPSOTP varchar(255) NOT NULL;"; + ADD OXPSOTP varchar(255) NOT NULL; "; + $queryContent = <<<'SQL' +INSERT INTO `oxcontents` (`OXID`, `OXLOADID`, `OXSHOPID`, `OXSNIPPET`, `OXTYPE`, `OXACTIVE`, `OXACTIVE_1`, `OXPOSITION`, `OXTITLE`, `OXCONTENT`, `OXTITLE_1`, `OXCONTENT_1`, `OXACTIVE_2`, `OXTITLE_2`, `OXCONTENT_2`, `OXACTIVE_3`, `OXTITLE_3`, `OXCONTENT_3`, `OXCATID`, `OXFOLDER`, `OXTERMVERSION`) VALUES +('ac542e43541c1add', 'oxpsadminupdatepassinfoemail', ?, 1, 0, 1, 1, '', 'Ihr Passwort im eShop', 'Hallo [{ $user->oxuser__oxsal->value|oxmultilangsal }] [{ $user->oxuser__oxfname->value }] [{ $user->oxuser__oxlname->value }],\r\n

    \r\nöffnen Sie den folgenden Link, um ein neues Passwort für [{ $shop->oxshops__oxname->value }] einzurichten:\r\n

    \r\n[{ $oViewConf->getSelfLink() }]cl=admin_forgotpwd&uid=[{ $user->getUpdateId()}]&lang=[{ $oViewConf->getActLanguageId() }]&shp=[{ $shop->oxshops__oxid->value }]\r\n

    \r\nDiesen Link können Sie innerhalb der nächsten [{ $user->getUpdateLinkTerm()/3600 }] Stunden aufrufen.\r\n

    \r\nIhr [{ $shop->oxshops__oxname->value }] Team\r\n
    ', 'password update info', 'Hello [{ $user->oxuser__oxsal->value|oxmultilangsal }] [{ $user->oxuser__oxfname->value }] [{ $user->oxuser__oxlname->value }],
    \r\n
    \r\nfollow this link to generate a new password for [{ $shop->oxshops__oxname->value }]:
    \r\n
    [{ $oViewConf->getBaseDir() }]index.php?cl=forgotpwd&uid=[{ $user->getUpdateId()}]&lang=[{ $oViewConf->getActLanguageId() }]&shp=[{ $shop->oxshops__oxid->value }]
    \r\n
    \r\nYou can use this link within the next [{ $user->getUpdateLinkTerm()/3600 }] hours.
    \r\n
    \r\nYour [{ $shop->oxshops__oxname->value }] team
    ', 1, '', '', 1, '', '', '30e44ab83fdee7564.23264141', 'CMSFOLDER_EMAILS', ''), +('ac542e295c394c6f', 'oxpsadminupdatepassinfoplainmail', ?, 1, 0, 1, 1, '', 'Ihr Passwort im eShop Plain', 'Hallo [{ $user->oxuser__oxsal->value|oxmultilangsal }] [{ $user->oxuser__oxfname->getRawValue() }] [{ $user->oxuser__oxlname->getRawValue() }],\r\n\r\nöffnen Sie den folgenden Link, um ein neues Passwort für [{ $shop->oxshops__oxname->getRawValue() }] einzurichten:\r\n\r\n[{ $oViewConf->getSelfLink() }]cl=admin_forgotpwd&uid=[{ $user->getUpdateId()}]&lang=[{ $oViewConf->getActLanguageId() }]&shp=[{ $shop->oxshops__oxid->value }]\r\n\r\nDiesen Link können Sie innerhalb der nächsten [{ $user->getUpdateLinkTerm()/3600 }] Stunden aufrufen.\r\n\r\nIhr [{ $shop->oxshops__oxname->getRawValue() }] Team', 'password update info plain', 'Hello [{ $user->oxuser__oxsal->value|oxmultilangsal }] [{ $user->oxuser__oxfname->getRawValue() }] [{ $user->oxuser__oxlname->getRawValue() }],\r\n\r\nfollow this link to generate a new password for [{ $shop->oxshops__oxname->getRawValue() }]:\r\n\r\n[{ $oViewConf->getSelfLink() }]index.php?cl=admin_forgotpwd&uid=[{ $user->getUpdateId()}]&lang=[{ $oViewConf->getActLanguageId() }]&shp=[{ $shop->oxshops__oxid->value }]\r\n\r\nYou can use this link within the next [{ $user->getUpdateLinkTerm()/3600 }] hours.\r\n\r\nYour [{ $shop->oxshops__oxname->getRawValue() }] team', 1, '', '', 1, '', '', '30e44ab83fdee7564.23264141', 'CMSFOLDER_EMAILS', ''); +SQL; + + try { DatabaseProvider::getDb()->execute($query); + DatabaseProvider::getDb()->execute($queryContent, [\OxidEsales\Eshop\Core\Registry::getConfig()->getShopId(), \OxidEsales\Eshop\Core\Registry::getConfig()->getShopId()]); self::regenerateViews(); } catch (\Exception $exception) { throw oxNew(UserException::class, "Ein Fehler ist bei der Erzeugung der neuen Datenbankspalten aufgetreten: \n" . $exception); From 380fad9e2fa5cb71ff01525c18072aeed6f70d90 Mon Sep 17 00:00:00 2001 From: moritz Date: Thu, 14 Oct 2021 20:13:38 +0200 Subject: [PATCH 175/199] added demo password recovery --- src/Model/PasswordPolicyUser.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Model/PasswordPolicyUser.php b/src/Model/PasswordPolicyUser.php index eeb6a84e..7439016d 100644 --- a/src/Model/PasswordPolicyUser.php +++ b/src/Model/PasswordPolicyUser.php @@ -10,6 +10,7 @@ use OxidEsales\Eshop\Core\InputValidator; use OxidEsales\Eshop\Core\Registry; use OxidEsales\EshopCommunity\Internal\Container\ContainerFactory; +use OxidProfessionalServices\PasswordPolicy\Controller\Admin\PasswordPolicyForgotPasswordControllerAdmin; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyConfig; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyValidator; use OxidProfessionalServices\PasswordPolicy\Exception\LimiterNotFound; @@ -31,8 +32,12 @@ public function onLogin($userName, $password) { /** @var PasswordPolicyValidator $passValidator */ $passValidator = oxNew(InputValidator::class); - if (!$this->isAdmin() && $this->isLoaded() && $err = $passValidator->validatePassword($userName, $password)) { - $forgotPass = new ForgotPasswordController(); + $forgotPass = oxNew(ForgotPasswordController::class); + if ($this->isLoaded() && $err = $passValidator->validatePassword($userName, $password)) { + if($this->isAdmin()) + { + $forgotPass = oxNew(PasswordPolicyForgotPasswordControllerAdmin::class); + } $forgotPass->forgotPassword(); $errorMessage = $err->getMessage() . ' ' . Registry::getLang()->translateString('REQUEST_PASSWORD_AFTERCLICK'); throw oxNew(UserException::class, $errorMessage); @@ -89,9 +94,9 @@ public function finalizeLogin($otp, $setsessioncookie = false) $secret = $this->oxuser__oxpstotpsecret->value; $decryptedSecret = $totp->decryptSecret($secret); $totp->verifyOTP($decryptedSecret, $otp, $this); + $name = $this->isAdmin() ? 'auth': 'usr'; $session->deleteVariable('tmpusr'); - $session->setVariable('usr', $usr); - $session->setVariable('auth', $usr); + $session->setVariable($name, $usr); // to prevent replay attacks $this->oxuser__oxpsotp = new \OxidEsales\EshopCommunity\Core\Field($otp, Field::T_TEXT); // in case user wants to stay logged in, set user cookie again From ab3340d58a535fa40728d4cc5a14d2882305797d Mon Sep 17 00:00:00 2001 From: moritz Date: Thu, 14 Oct 2021 20:13:48 +0200 Subject: [PATCH 176/199] small language changes --- views/admin/de/passwordpolicy_lang.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/views/admin/de/passwordpolicy_lang.php b/views/admin/de/passwordpolicy_lang.php index b53408b5..0d29a9d0 100644 --- a/views/admin/de/passwordpolicy_lang.php +++ b/views/admin/de/passwordpolicy_lang.php @@ -76,6 +76,8 @@ 'OXPS_PASSWORDPOLICY_PASSWORDVALIDATION_LOWERCASE' => 'Das Passwort enthält keine Kleinbuchstaben.', 'OXPS_PASSWORDPOLICY_PASSWORDVALIDATION_DIGITS' => 'Das Passwort enthält keine Ziffer.', 'OXPS_PASSWORDPOLICY_PASSWORDVALIDATION_SPECIAL' => 'Das Passwort muss mindestens eines der folgenden Zeichen enthalten: ! @ # $ % ^ & * ? _ ~ - ( ) ', + 'ERROR_MESSAGE_PASSWORD_TOO_SHORT' => 'Das Password ist zu kurz, bitte benutzen Sie ein längeres.', + 'REQUEST_PASSWORD_AFTERCLICK' => 'Sie erhalten eine E-Mail mit einem Link, um ein neues Passwort zu vergeben.', 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_TOOLONG' => 'Das Passwort ist zu lang, bitte benutzen Sie ein kürzeres.', 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESDIGITS' => 'Das Passwort muss mindestens eine Zahl enthalten.', 'OXPS_PASSWORDPOLICY_PASSWORDSTRENGTH_ERROR_REQUIRESUPPERCASE' => 'Das Passwort muss mindestens einen Großbuchstaben enthalten.', From b923bf7bf872428562cbc2b8adb761ace2a0e51f Mon Sep 17 00:00:00 2001 From: moritz Date: Thu, 14 Oct 2021 20:14:06 +0200 Subject: [PATCH 177/199] added new controller and templates --- metadata.php | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/metadata.php b/metadata.php index 5e28be2e..9cc7c013 100644 --- a/metadata.php +++ b/metadata.php @@ -32,6 +32,7 @@ use OxidEsales\Eshop\Core\ViewConfig; use OxidEsales\Eshop\Application\Model\User; use OxidEsales\Eshop\Application\Controller\Admin\ModuleConfiguration; +use OxidProfessionalServices\PasswordPolicy\Controller\Admin\PasswordPolicyForgotPasswordControllerAdmin; use OxidProfessionalServices\PasswordPolicy\Controller\Admin\PasswordPolicyTwoFactorRecoveryAdmin; use OxidProfessionalServices\PasswordPolicy\Component\PasswordPolicyUserComponent; use OxidProfessionalServices\PasswordPolicy\Controller\Admin\PasswordPolicyAccountTOTPAdmin; @@ -46,6 +47,7 @@ use OxidProfessionalServices\PasswordPolicy\Controller\PasswordPolicyTwoFactorRecovery; use OxidProfessionalServices\PasswordPolicy\Controller\PasswordPolicyTwoFactorRegister; use OxidProfessionalServices\PasswordPolicy\Controller\PasswordPolicyTwoFactorLogin; +use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyLanguage; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyValidator; use OxidProfessionalServices\PasswordPolicy\Core\PasswordPolicyViewConfig; use OxidProfessionalServices\PasswordPolicy\Controller\PasswordPolicyAccountPasswordController; @@ -79,7 +81,8 @@ User::class => PasswordPolicyUser::class, ModuleConfiguration::class => PasswordPolicyModuleConfiguration::class, UserComponent::class => PasswordPolicyUserComponent::class, - LoginController::class => PasswordPolicyLoginController::class + LoginController::class => PasswordPolicyLoginController::class, + \OxidEsales\Eshop\Core\Language::class => PasswordPolicyLanguage::class ], 'controllers' => [ 'twofactorregister' => PasswordPolicyTwoFactorRegister::class, @@ -94,6 +97,7 @@ 'admin_twofactorbackup' => PasswordPolicyTwoFactorBackupCodeAdmin::class, 'admin_twofactorlogin' => PasswordPolicyTwoFactorLoginAdmin::class, 'admin_twofactorrecovery' => PasswordPolicyTwoFactorRecoveryAdmin::class, + 'admin_forgotpwd' => PasswordPolicyForgotPasswordControllerAdmin::class, ], @@ -110,9 +114,17 @@ 'admin_twofactorbackupcode.tpl' => 'oxps/passwordpolicy/views/admin/tpl/admin_twofactorbackupcode.tpl', 'admin_twofactorlogin.tpl' => 'oxps/passwordpolicy/views/admin/tpl/admin_twofactorlogin.tpl', 'admin_twofactorrecovery.tpl' => 'oxps/passwordpolicy/views/admin/tpl/admin_twofactorrecovery.tpl', - 'layout/page.tpl' => 'oxps/passwordpolicy/views/admin/tpl/layout/page.tpl', 'message/errors.tpl' => 'oxps/passwordpolicy/views/admin/tpl/message/errors.tpl', 'message/error.tpl' => 'oxps/passwordpolicy/views/admin/tpl/message/error.tpl', + 'email/html/forgotpwd.tpl' => 'oxps/passwordpolicy/views/admin/tpl/email/html/forgotpwd.tpl', + 'email/html/header.tpl' => 'oxps/passwordpolicy/views/admin/tpl/email/html/header.tpl', + 'email/html/footer.tpl' => 'oxps/passwordpolicy/views/admin/tpl/email/html/footer.tpl', + 'email/plain/forgotpwd.tpl' => 'oxps/passwordpolicy/views/admin/tpl/email/plain/forgotpwd.tpl', + 'forgotpwd.tpl' => 'oxps/passwordpolicy/views/admin/tpl/page/account/forgotpwd.tpl', + 'layout/page.tpl' => 'oxps/passwordpolicy/views/admin/tpl/layout/page.tpl', + 'form/forgotpwd_email.tpl' => 'oxps/passwordpolicy/views/admin/tpl/form/forgotpwd_email.tpl', + 'form/forgotpwd_change_pwd.tpl' => 'oxps/passwordpolicy/views/admin/tpl/form/forgotpwd_change_pwd.tpl' + ], 'blocks' => [ From 281bc7c6f7bd8b22533b9d604e1fb5ebb1c61634 Mon Sep 17 00:00:00 2001 From: moritz Date: Fri, 15 Oct 2021 14:45:35 +0200 Subject: [PATCH 178/199] design changes --- views/admin/tpl/form/forgotpwd_change_pwd.tpl | 4 +- views/admin/tpl/form/forgotpwd_email.tpl | 39 ------------------- views/admin/tpl/page/account/forgotpwd.tpl | 4 +- 3 files changed, 3 insertions(+), 44 deletions(-) delete mode 100644 views/admin/tpl/form/forgotpwd_email.tpl diff --git a/views/admin/tpl/form/forgotpwd_change_pwd.tpl b/views/admin/tpl/form/forgotpwd_change_pwd.tpl index 04b23900..87dd717e 100644 --- a/views/admin/tpl/form/forgotpwd_change_pwd.tpl +++ b/views/admin/tpl/form/forgotpwd_change_pwd.tpl @@ -1,6 +1,6 @@ [{oxscript add="$('input,select,textarea').not('[type=submit]').jqBootstrapValidation();"}] - -
    +[{include file="message/errors.tpl"}] +
    + [{oxmultilang ident="OXPS_PASSWORDPOLICY_TWOFACTORAUTH_LOGIN"}] + + oxuser__oxpstotpsecret->value != ""}]checked[{/if}] [{if $edit->oxuser__oxpstotpsecret->value == ""}]disabled[{/if}]> + +