diff --git a/TwoFactorAuth/Api/AdminTokenServiceInterface.php b/TwoFactorAuth/Api/AdminTokenServiceInterface.php new file mode 100644 index 00000000..5482feba --- /dev/null +++ b/TwoFactorAuth/Api/AdminTokenServiceInterface.php @@ -0,0 +1,19 @@ +userResource = $userResource; + $this->userFactory = $userFactory; + $this->google = $google; + $this->configManager = $configManager; + } + + /** + * @inheritDoc + */ + protected function configure() + { + $this->setName('security:tfa:google:set-secret'); + $this->setDescription('Set the secret used for Google OTP generation.'); + + $this->addArgument('user', InputArgument::REQUIRED, __('Username')); + $this->addArgument('secret', InputArgument::REQUIRED, __('Secret')); + + parent::configure(); + } + + /** + * Set the secret used for google otp generation + * + * @SuppressWarnings("PHPMD.UnusedFormalParameter") + * @param InputInterface $input + * @param OutputInterface $output + * @throws LocalizedException + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $userName = $input->getArgument('user'); + $secret = $input->getArgument('secret'); + + $user = $this->userFactory->create(); + + $this->userResource->load($user, $userName, 'username'); + if (!$user->getId()) { + throw new LocalizedException(__('Unknown user %1', $userName)); + } + + $this->google->setSharedSecret((int)$user->getId(), $secret); + $this->configManager->addProviderConfig( + (int)$user->getId(), + Google::CODE, + [ + UserConfigManagerInterface::ACTIVE_CONFIG_KEY => true + ] + ); + + $output->writeln((string)__('Google OTP secret has been set')); + } +} diff --git a/TwoFactorAuth/Command/TfaProviders.php b/TwoFactorAuth/Command/TfaProviders.php index d076b76e..a2aeebe7 100644 --- a/TwoFactorAuth/Command/TfaProviders.php +++ b/TwoFactorAuth/Command/TfaProviders.php @@ -44,6 +44,8 @@ protected function configure() } /** + * @inheritDoc + * * @SuppressWarnings("PHPMD.UnusedFormalParameter") */ protected function execute(InputInterface $input, OutputInterface $output) @@ -53,5 +55,7 @@ protected function execute(InputInterface $input, OutputInterface $output) foreach ($providers as $provider) { $output->writeln(sprintf("%16s: %s", $provider->getCode(), $provider->getName())); } + + return 0; } } diff --git a/TwoFactorAuth/Command/TfaReset.php b/TwoFactorAuth/Command/TfaReset.php index 67050c45..c3020ee0 100644 --- a/TwoFactorAuth/Command/TfaReset.php +++ b/TwoFactorAuth/Command/TfaReset.php @@ -76,6 +76,8 @@ protected function configure() } /** + * @inheritDoc + * * @SuppressWarnings("PHPMD.UnusedFormalParameter") * @throws LocalizedException */ @@ -96,5 +98,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $this->userConfigManager->resetProviderConfig((int) $user->getId(), $providerCode); $output->writeln('' . __('Provider %1 has been reset for user %2', $provider->getName(), $userName)); + + return 0; } } diff --git a/TwoFactorAuth/Controller/Adminhtml/AbstractAction.php b/TwoFactorAuth/Controller/Adminhtml/AbstractAction.php index d6c9cee7..f3a3868f 100644 --- a/TwoFactorAuth/Controller/Adminhtml/AbstractAction.php +++ b/TwoFactorAuth/Controller/Adminhtml/AbstractAction.php @@ -12,6 +12,8 @@ /** * Abstraction for 2FA controllers + * + * @SuppressWarnings(PHPMD.NumberOfChildren) */ abstract class AbstractAction extends Action { diff --git a/TwoFactorAuth/Controller/Adminhtml/Authy/Auth.php b/TwoFactorAuth/Controller/Adminhtml/Authy/Auth.php index 7c68ea64..e4f25cd8 100644 --- a/TwoFactorAuth/Controller/Adminhtml/Authy/Auth.php +++ b/TwoFactorAuth/Controller/Adminhtml/Authy/Auth.php @@ -66,6 +66,7 @@ public function __construct( /** * Get current user + * * @return User|null */ private function getUser() @@ -75,6 +76,7 @@ private function getUser() /** * @inheritdoc + * * @throws NoSuchEntityException */ public function execute() diff --git a/TwoFactorAuth/Controller/Adminhtml/Authy/Authpost.php b/TwoFactorAuth/Controller/Adminhtml/Authy/Authpost.php index 08c17fc0..1fd18f93 100644 --- a/TwoFactorAuth/Controller/Adminhtml/Authy/Authpost.php +++ b/TwoFactorAuth/Controller/Adminhtml/Authy/Authpost.php @@ -92,6 +92,7 @@ public function __construct( /** * Get current user + * * @return User|null */ private function getUser(): ?User diff --git a/TwoFactorAuth/Controller/Adminhtml/Authy/Configure.php b/TwoFactorAuth/Controller/Adminhtml/Authy/Configure.php index a35c52ab..07356384 100644 --- a/TwoFactorAuth/Controller/Adminhtml/Authy/Configure.php +++ b/TwoFactorAuth/Controller/Adminhtml/Authy/Configure.php @@ -59,6 +59,7 @@ public function __construct( /** * Get current user + * * @return User|null */ private function getUser(): ?User diff --git a/TwoFactorAuth/Controller/Adminhtml/Authy/Configurepost.php b/TwoFactorAuth/Controller/Adminhtml/Authy/Configurepost.php index 63efa4f6..aa0bab38 100644 --- a/TwoFactorAuth/Controller/Adminhtml/Authy/Configurepost.php +++ b/TwoFactorAuth/Controller/Adminhtml/Authy/Configurepost.php @@ -21,6 +21,8 @@ use Magento\TwoFactorAuth\Model\UserConfig\HtmlAreaTokenVerifier; /** + * Configure authy + * * @SuppressWarnings(PHPMD.CamelCaseMethodName) */ class Configurepost extends AbstractConfigureAction implements HttpPostActionInterface @@ -78,6 +80,7 @@ public function __construct( /** * Get current user + * * @return User|null */ private function getUser(): ?User diff --git a/TwoFactorAuth/Controller/Adminhtml/Authy/Configureverifypost.php b/TwoFactorAuth/Controller/Adminhtml/Authy/Configureverifypost.php index 6b6c091e..c94fffd9 100644 --- a/TwoFactorAuth/Controller/Adminhtml/Authy/Configureverifypost.php +++ b/TwoFactorAuth/Controller/Adminhtml/Authy/Configureverifypost.php @@ -21,6 +21,8 @@ use Magento\TwoFactorAuth\Model\UserConfig\HtmlAreaTokenVerifier; /** + * Verify authy + * * @SuppressWarnings(PHPMD.CamelCaseMethodName) */ class Configureverifypost extends AbstractConfigureAction implements HttpPostActionInterface @@ -94,6 +96,7 @@ public function __construct( /** * Get current user + * * @return User|null */ private function getUser(): ?User diff --git a/TwoFactorAuth/Controller/Adminhtml/Authy/Onetouch.php b/TwoFactorAuth/Controller/Adminhtml/Authy/Onetouch.php index 45c9fd5c..52da562d 100644 --- a/TwoFactorAuth/Controller/Adminhtml/Authy/Onetouch.php +++ b/TwoFactorAuth/Controller/Adminhtml/Authy/Onetouch.php @@ -18,6 +18,8 @@ use Magento\User\Model\User; /** + * One touch process + * * @SuppressWarnings(PHPMD.CamelCaseMethodName) */ class Onetouch extends AbstractAction implements HttpGetActionInterface @@ -65,6 +67,7 @@ public function __construct( /** * Get current user + * * @return User|null */ private function getUser(): ?User @@ -80,8 +83,8 @@ public function execute() $result = $this->jsonFactory->create(); try { - $approvalCode = $this->oneTouch->request($this->getUser()); - $res = ['success' => true, 'code' => $approvalCode]; + $this->oneTouch->request($this->getUser()); + $res = ['success' => true]; } catch (Exception $e) { $result->setHttpResponseCode(500); $res = ['success' => false, 'message' => $e->getMessage()]; diff --git a/TwoFactorAuth/Controller/Adminhtml/Authy/Token.php b/TwoFactorAuth/Controller/Adminhtml/Authy/Token.php index b1aa7700..f4fca06f 100644 --- a/TwoFactorAuth/Controller/Adminhtml/Authy/Token.php +++ b/TwoFactorAuth/Controller/Adminhtml/Authy/Token.php @@ -19,6 +19,8 @@ use Magento\User\Model\User; /** + * Verify with authy token + * * @SuppressWarnings(PHPMD.CamelCaseMethodName) */ class Token extends AbstractAction implements HttpGetActionInterface, HttpPostActionInterface @@ -66,6 +68,7 @@ public function __construct( /** * Get current user + * * @return User|null */ private function getUser(): ?User diff --git a/TwoFactorAuth/Controller/Adminhtml/Authy/Verify.php b/TwoFactorAuth/Controller/Adminhtml/Authy/Verify.php index 21b8f4c0..badb4e45 100644 --- a/TwoFactorAuth/Controller/Adminhtml/Authy/Verify.php +++ b/TwoFactorAuth/Controller/Adminhtml/Authy/Verify.php @@ -21,6 +21,8 @@ use Magento\User\Model\User; /** + * Verify authy code + * * @SuppressWarnings(PHPMD.CamelCaseMethodName) */ class Verify extends AbstractAction implements HttpPostActionInterface, HttpGetActionInterface @@ -77,6 +79,7 @@ public function __construct( /** * Get current user + * * @return User|null */ private function getUser(): ?User @@ -86,6 +89,7 @@ private function getUser(): ?User /** * Get verify information + * * @return verify payload * @throws NoSuchEntityException */ diff --git a/TwoFactorAuth/Controller/Adminhtml/Authy/Verifyonetouch.php b/TwoFactorAuth/Controller/Adminhtml/Authy/Verifyonetouch.php index ee0befce..1812c1e6 100644 --- a/TwoFactorAuth/Controller/Adminhtml/Authy/Verifyonetouch.php +++ b/TwoFactorAuth/Controller/Adminhtml/Authy/Verifyonetouch.php @@ -21,6 +21,8 @@ use Magento\User\Model\User; /** + * Verify one touch response + * * @SuppressWarnings(PHPMD.CamelCaseMethodName) */ class Verifyonetouch extends AbstractAction implements HttpGetActionInterface, HttpPostActionInterface @@ -85,6 +87,7 @@ public function __construct( /** * Get current user + * * @return User|null */ private function getUser(): ?User diff --git a/TwoFactorAuth/Controller/Adminhtml/Duo/Auth.php b/TwoFactorAuth/Controller/Adminhtml/Duo/Auth.php index 6c9bb734..e5fbfbaa 100644 --- a/TwoFactorAuth/Controller/Adminhtml/Duo/Auth.php +++ b/TwoFactorAuth/Controller/Adminhtml/Duo/Auth.php @@ -48,6 +48,14 @@ class Auth extends AbstractAction implements HttpGetActionInterface */ private $tokenVerifier; + /** + * @param Action\Context $context + * @param Session $session + * @param PageFactory $pageFactory + * @param UserConfigManagerInterface $userConfigManager + * @param TfaInterface $tfa + * @param HtmlAreaTokenVerifier $tokenVerifier + */ public function __construct( Action\Context $context, Session $session, @@ -66,6 +74,7 @@ public function __construct( /** * Get current user + * * @return \Magento\User\Model\User|null */ private function getUser() diff --git a/TwoFactorAuth/Controller/Adminhtml/Duo/Authpost.php b/TwoFactorAuth/Controller/Adminhtml/Duo/Authpost.php index f21d5a8a..1928332d 100644 --- a/TwoFactorAuth/Controller/Adminhtml/Duo/Authpost.php +++ b/TwoFactorAuth/Controller/Adminhtml/Duo/Authpost.php @@ -21,6 +21,7 @@ /** * Duo security authentication post controller + * * @SuppressWarnings(PHPMD.CamelCaseMethodName) */ class Authpost extends AbstractAction implements HttpPostActionInterface @@ -107,6 +108,7 @@ public function __construct( /** * Get current user + * * @return \Magento\User\Model\User|null */ private function getUser() @@ -140,9 +142,7 @@ public function execute() } /** - * Check if admin has permissions to visit related pages - * - * @return bool + * @inheritDoc */ protected function _isAllowed() { diff --git a/TwoFactorAuth/Controller/Adminhtml/Duo/Configure.php b/TwoFactorAuth/Controller/Adminhtml/Duo/Configure.php index a02debaf..aa1c690f 100644 --- a/TwoFactorAuth/Controller/Adminhtml/Duo/Configure.php +++ b/TwoFactorAuth/Controller/Adminhtml/Duo/Configure.php @@ -18,7 +18,7 @@ class Configure extends AbstractAction implements HttpGetActionInterface /** * @see _isAllowed() */ - const ADMIN_RESOURCE = 'Magento_TwoFactorAuth::config'; + const ADMIN_RESOURCE = 'Magento_TwoFactorAuth::tfa'; /** * @inheritdoc diff --git a/TwoFactorAuth/Controller/Adminhtml/Google/Auth.php b/TwoFactorAuth/Controller/Adminhtml/Google/Auth.php index 75a788ad..df652239 100644 --- a/TwoFactorAuth/Controller/Adminhtml/Google/Auth.php +++ b/TwoFactorAuth/Controller/Adminhtml/Google/Auth.php @@ -19,6 +19,7 @@ /** * Google authenticator page + * * @SuppressWarnings(PHPMD.CamelCaseMethodName) */ class Auth extends AbstractAction implements HttpGetActionInterface @@ -66,6 +67,7 @@ public function __construct( /** * Get current user + * * @return User|null */ private function getUser(): ?User diff --git a/TwoFactorAuth/Controller/Adminhtml/Google/Authpost.php b/TwoFactorAuth/Controller/Adminhtml/Google/Authpost.php index 75eac2e3..f0437f1d 100644 --- a/TwoFactorAuth/Controller/Adminhtml/Google/Authpost.php +++ b/TwoFactorAuth/Controller/Adminhtml/Google/Authpost.php @@ -22,6 +22,7 @@ /** * Google authenticator post controller + * * @SuppressWarnings(PHPMD.CamelCaseMethodName) */ class Authpost extends AbstractAction implements HttpPostActionInterface @@ -92,6 +93,7 @@ public function __construct( /** * @inheritdoc + * * @throws NoSuchEntityException */ public function execute() diff --git a/TwoFactorAuth/Controller/Adminhtml/Google/Configure.php b/TwoFactorAuth/Controller/Adminhtml/Google/Configure.php index 121469b5..1b3e6743 100644 --- a/TwoFactorAuth/Controller/Adminhtml/Google/Configure.php +++ b/TwoFactorAuth/Controller/Adminhtml/Google/Configure.php @@ -19,6 +19,7 @@ /** * Google authenticator configuration page + * * @SuppressWarnings(PHPMD.CamelCaseMethodName) */ class Configure extends AbstractConfigureAction implements HttpGetActionInterface @@ -60,6 +61,7 @@ public function __construct( /** * Get current user + * * @return User|null */ private function getUser(): ?User diff --git a/TwoFactorAuth/Controller/Adminhtml/Google/Configurepost.php b/TwoFactorAuth/Controller/Adminhtml/Google/Configurepost.php index 7467660b..e4d823d3 100644 --- a/TwoFactorAuth/Controller/Adminhtml/Google/Configurepost.php +++ b/TwoFactorAuth/Controller/Adminhtml/Google/Configurepost.php @@ -25,6 +25,7 @@ /** * Google authenticator configuration post controller + * * @SuppressWarnings(PHPMD.CamelCaseMethodName) */ class Configurepost extends AbstractConfigureAction implements HttpPostActionInterface @@ -98,6 +99,7 @@ public function __construct( /** * Get current user + * * @return User|null */ private function getUser(): ?User @@ -107,6 +109,7 @@ private function getUser(): ?User /** * @inheritdoc + * * @return ResponseInterface|ResultInterface * @throws NoSuchEntityException */ diff --git a/TwoFactorAuth/Controller/Adminhtml/Google/Qr.php b/TwoFactorAuth/Controller/Adminhtml/Google/Qr.php index 53f30bdf..f5eb03e6 100644 --- a/TwoFactorAuth/Controller/Adminhtml/Google/Qr.php +++ b/TwoFactorAuth/Controller/Adminhtml/Google/Qr.php @@ -18,6 +18,7 @@ /** * QR code generator for google authenticator + * * @SuppressWarnings(PHPMD.CamelCaseMethodName) */ class Qr extends AbstractAction implements HttpGetActionInterface @@ -65,6 +66,7 @@ public function __construct( /** * Get current user + * * @return User|null */ private function getUser(): ?User diff --git a/TwoFactorAuth/Controller/Adminhtml/Tfa/AccessDenied.php b/TwoFactorAuth/Controller/Adminhtml/Tfa/AccessDenied.php index e064bcc1..bce6590a 100644 --- a/TwoFactorAuth/Controller/Adminhtml/Tfa/AccessDenied.php +++ b/TwoFactorAuth/Controller/Adminhtml/Tfa/AccessDenied.php @@ -23,4 +23,12 @@ public function execute() { return $this->resultFactory->create(ResultFactory::TYPE_PAGE); } + + /** + * @inheritDoc + */ + protected function _isAllowed() + { + return true; + } } diff --git a/TwoFactorAuth/Controller/Adminhtml/Tfa/Index.php b/TwoFactorAuth/Controller/Adminhtml/Tfa/Index.php index 1b57203c..f428d6c4 100644 --- a/TwoFactorAuth/Controller/Adminhtml/Tfa/Index.php +++ b/TwoFactorAuth/Controller/Adminhtml/Tfa/Index.php @@ -87,7 +87,10 @@ public function __construct( /** * @inheritdoc + * * @throws LocalizedException + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) */ public function execute() { diff --git a/TwoFactorAuth/Controller/Adminhtml/Tfa/Reset.php b/TwoFactorAuth/Controller/Adminhtml/Tfa/Reset.php index 57a99c04..6f2d3a9d 100644 --- a/TwoFactorAuth/Controller/Adminhtml/Tfa/Reset.php +++ b/TwoFactorAuth/Controller/Adminhtml/Tfa/Reset.php @@ -18,6 +18,7 @@ /** * Reset 2FA configuration controller + * * @SuppressWarnings(PHPMD.CamelCaseMethodName) */ class Reset extends AbstractAction implements HttpGetActionInterface, HttpPostActionInterface @@ -57,6 +58,7 @@ public function __construct( /** * @inheritdoc + * * @throws LocalizedException */ public function execute() diff --git a/TwoFactorAuth/Controller/Adminhtml/U2f/Configure.php b/TwoFactorAuth/Controller/Adminhtml/U2f/Configure.php index 072492f2..7c33c2c6 100644 --- a/TwoFactorAuth/Controller/Adminhtml/U2f/Configure.php +++ b/TwoFactorAuth/Controller/Adminhtml/U2f/Configure.php @@ -18,7 +18,8 @@ use Magento\TwoFactorAuth\Model\UserConfig\HtmlAreaTokenVerifier; /** - * CUbiKey configuration page controller + * Configuration page for U2f + * * @SuppressWarnings(PHPMD.CamelCaseMethodName) */ class Configure extends AbstractConfigureAction implements HttpGetActionInterface @@ -68,6 +69,8 @@ public function execute() } /** + * Get the current user + * * @return User|null */ private function getUser(): ?User diff --git a/TwoFactorAuth/Controller/Adminhtml/U2f/Configurepost.php b/TwoFactorAuth/Controller/Adminhtml/U2f/Configurepost.php index ecc8ef7e..91978915 100644 --- a/TwoFactorAuth/Controller/Adminhtml/U2f/Configurepost.php +++ b/TwoFactorAuth/Controller/Adminhtml/U2f/Configurepost.php @@ -142,6 +142,8 @@ public function execute() } /** + * Get the current user + * * @return User|null */ private function getUser(): ?User diff --git a/TwoFactorAuth/Model/AdminAccessTokenService.php b/TwoFactorAuth/Model/AdminAccessTokenService.php new file mode 100644 index 00000000..bac1f3a0 --- /dev/null +++ b/TwoFactorAuth/Model/AdminAccessTokenService.php @@ -0,0 +1,137 @@ +tfa = $tfa; + $this->configRequestManager = $configRequestManager; + $this->userFactory = $userFactory; + $this->adminTokenService = $adminTokenService; + } + + /** + * Prevent the admin token from being created via the token service + * + * @param string $username + * @param string $password + * @return string + * @throws AuthenticationException + * @throws LocalizedException + * @throws InputException + */ + public function createAdminAccessToken($username, $password): string + { + // No exception means valid input. Ignore the created token. + $this->adminTokenService->createAdminAccessToken($username, $password); + $user = $this->userFactory->create(); + $user->loadByUsername($username); + $userId = (int)$user->getId(); + + $providerCodes = []; + $activeProviderCodes = []; + foreach ($this->tfa->getUserProviders($userId) as $provider) { + $providerCodes[] = $provider->getCode(); + if ($provider->isActive($userId)) { + $activeProviderCodes[] = $provider->getCode(); + } + } + + if (!$this->configRequestManager->isConfigurationRequiredFor($userId)) { + // @codingStandardsIgnoreStart + throw new LocalizedException( + __( + 'Please use the 2fa provider-specific endpoints to obtain a token.', + [ + 'active_providers' => $activeProviderCodes + ] + ) + ); + // @codingStandardsIgnoreEnd + } elseif (empty($this->tfa->getUserProviders($userId))) { + // It is expected that available 2fa providers are selected via db or admin ui + throw new LocalizedException( + __('Please ask an administrator with sufficient access to configure 2FA first') + ); + } + + try { + $this->configRequestManager->sendConfigRequestTo($user); + } catch (AuthorizationException|NotificationExceptionInterface $exception) { + throw new LocalizedException( + __('Failed to send the message. Please contact the administrator') + ); + } + + throw new LocalizedException( + __( + 'You are required to configure personal Two-Factor Authorization in order to login. ' + . 'Please check your email.', + [ + 'providers' => $providerCodes, + 'active_providers' => $activeProviderCodes + ] + ) + ); + } + + /** + * @inheritDoc + */ + public function revokeAdminAccessToken($adminId): bool + { + return $this->adminTokenService->revokeAdminAccessToken($adminId); + } +} diff --git a/TwoFactorAuth/Model/Alert.php b/TwoFactorAuth/Model/Alert.php index f07efd55..f6d28044 100644 --- a/TwoFactorAuth/Model/Alert.php +++ b/TwoFactorAuth/Model/Alert.php @@ -30,6 +30,7 @@ public function __construct( /** * Trigger a security suite event + * * @param string $module * @param string $message * @param string $level diff --git a/TwoFactorAuth/Model/AlertInterface.php b/TwoFactorAuth/Model/AlertInterface.php index 1d48f900..ce07377d 100644 --- a/TwoFactorAuth/Model/AlertInterface.php +++ b/TwoFactorAuth/Model/AlertInterface.php @@ -59,6 +59,7 @@ interface AlertInterface /** * Trigger a security suite event + * * @param string $module * @param string $message * @param string $level diff --git a/TwoFactorAuth/Model/Config/Backend/ForceProviders.php b/TwoFactorAuth/Model/Config/Backend/ForceProviders.php index 2009dae3..dc093b52 100644 --- a/TwoFactorAuth/Model/Config/Backend/ForceProviders.php +++ b/TwoFactorAuth/Model/Config/Backend/ForceProviders.php @@ -72,6 +72,9 @@ public function beforeSave() } $value = $this->getValue(); + if (is_string($value)) { + $value = explode(',', $value); + } $validValues = is_array($value) ? array_intersect($codes, $value) : []; if (empty($value) || !$validValues) { throw new ValidatorException(__('You have to select at least one Two-Factor Authorization provider')); diff --git a/TwoFactorAuth/Model/Config/UserNotifier.php b/TwoFactorAuth/Model/Config/UserNotifier.php new file mode 100644 index 00000000..6564b1b9 --- /dev/null +++ b/TwoFactorAuth/Model/Config/UserNotifier.php @@ -0,0 +1,73 @@ +scopeConfig = $scopeConfig; + $this->url = $url; + } + + /** + * Get the url to send to the user for configuring personal 2fa settings + * + * @param string $tfaToken + * @return string + */ + public function getPersonalRequestConfigUrl(string $tfaToken): string + { + return $this->getRequestConfigUrl($tfaToken); + } + + /** + * Get the url to send to the user for configuring global 2fa settings + * + * @param string $tfaToken + * @return string + */ + public function getAppRequestConfigUrl(string $tfaToken): string + { + return $this->getRequestConfigUrl($tfaToken); + } + + /** + * Get the default config url + * + * @param string $tfaToken + * @return string + */ + private function getRequestConfigUrl(string $tfaToken) + { + return $this->url->getUrl('tfa/tfa/index', ['tfat' => $tfaToken]); + } +} diff --git a/TwoFactorAuth/Model/Config/WebApiUserNotifier.php b/TwoFactorAuth/Model/Config/WebApiUserNotifier.php new file mode 100644 index 00000000..2713f064 --- /dev/null +++ b/TwoFactorAuth/Model/Config/WebApiUserNotifier.php @@ -0,0 +1,53 @@ +scopeConfig = $scopeConfig; + } + + /** + * Get the url to send to the user for configuring personal 2fa settings + * + * @param string $tfaToken + * @return string + */ + public function getPersonalRequestConfigUrl(string $tfaToken): string + { + $userUrl = $this->scopeConfig->getValue(UserNotifierInterface::XML_PATH_WEBAPI_NOTIFICATION_URL); + + if ($userUrl) { + return str_replace(':tfat', $tfaToken, $userUrl); + } + + return parent::getPersonalRequestConfigUrl($tfaToken); + } +} diff --git a/TwoFactorAuth/Model/CountryRegistry.php b/TwoFactorAuth/Model/CountryRegistry.php index da6364ba..d45f11cd 100644 --- a/TwoFactorAuth/Model/CountryRegistry.php +++ b/TwoFactorAuth/Model/CountryRegistry.php @@ -30,6 +30,7 @@ class CountryRegistry /** * Remove registry entity by id + * * @param int $id */ public function removeById(int $id): void @@ -48,6 +49,7 @@ public function removeById(int $id): void /** * Push one object into registry + * * @param int $id * @return CountryInterface|null */ @@ -62,6 +64,7 @@ public function retrieveById(int $id): ?CountryInterface /** * Retrieve by Code value + * * @param string $value * @return CountryInterface|null */ @@ -76,6 +79,7 @@ public function retrieveByCode(string $value): ?CountryInterface /** * Push one object into registry + * * @param Country $country */ public function push(Country $country): void diff --git a/TwoFactorAuth/Model/Data/AdminTokenResponse.php b/TwoFactorAuth/Model/Data/AdminTokenResponse.php new file mode 100644 index 00000000..956ca842 --- /dev/null +++ b/TwoFactorAuth/Model/Data/AdminTokenResponse.php @@ -0,0 +1,82 @@ +_get(self::USER_ID); + } + + /** + * @inheritDoc + */ + public function setUserId(int $value): void + { + $this->setData(self::USER_ID, $value); + } + + /** + * @inheritDoc + */ + public function getMessage(): string + { + return (string)$this->_get(self::MESSAGE); + } + + /** + * @inheritDoc + */ + public function setMessage(string $value): void + { + $this->setData(self::MESSAGE, $value); + } + + /** + * @inheritDoc + */ + public function getActiveProviders(): array + { + return $this->_get(self::ACTIVE_PROVIDERS); + } + + /** + * @inheritDoc + */ + public function setActiveProviders(array $value): void + { + $this->setData(self::ACTIVE_PROVIDERS, $value); + } + + /** + * @inheritdoc + */ + public function getExtensionAttributes(): ?AdminTokenResponseExtensionInterface + { + return $this->_get(self::EXTENSION_ATTRIBUTES_KEY); + } + + /** + * @inheritDoc + */ + public function setExtensionAttributes(AdminTokenResponseExtensionInterface $extensionAttributes): void + { + $this->setData(self::EXTENSION_ATTRIBUTES_KEY, $extensionAttributes); + } +} diff --git a/TwoFactorAuth/Model/Data/Country.php b/TwoFactorAuth/Model/Data/Country.php index 6a93dc58..35c43b79 100644 --- a/TwoFactorAuth/Model/Data/Country.php +++ b/TwoFactorAuth/Model/Data/Country.php @@ -7,41 +7,41 @@ namespace Magento\TwoFactorAuth\Model\Data; -use Magento\Framework\Api\AbstractExtensibleObject; +use Magento\Framework\Model\AbstractExtensibleModel; use Magento\TwoFactorAuth\Api\Data\CountryExtensionInterface; use Magento\TwoFactorAuth\Api\Data\CountryInterface; /** * @inheritDoc */ -class Country extends AbstractExtensibleObject implements CountryInterface +class Country extends AbstractExtensibleModel implements CountryInterface { /** - * {@inheritdoc} + * @inheritdoc */ public function getId(): int { - return (int) $this->_get(self::ID); + return (int) $this->getData(self::ID); } /** - * {@inheritdoc} + * @inheritdoc */ - public function setId(int $value): void + public function setId($value): void { $this->setData(self::ID, $value); } /** - * {@inheritdoc} + * @inheritdoc */ public function getCode(): string { - return (string) $this->_get(self::CODE); + return (string) $this->getData(self::CODE); } /** - * {@inheritdoc} + * @inheritdoc */ public function setCode(string $value): void { @@ -49,15 +49,15 @@ public function setCode(string $value): void } /** - * {@inheritdoc} + * @inheritdoc */ public function getName(): string { - return (string) $this->_get(self::NAME); + return (string) $this->getData(self::NAME); } /** - * {@inheritdoc} + * @inheritdoc */ public function setName(string $value): void { @@ -65,15 +65,15 @@ public function setName(string $value): void } /** - * {@inheritdoc} + * @inheritdoc */ public function getDialCode(): string { - return (string) $this->_get(self::DIAL_CODE); + return (string) $this->getData(self::DIAL_CODE); } /** - * {@inheritdoc} + * @inheritdoc */ public function setDialCode(string $value): void { @@ -81,15 +81,15 @@ public function setDialCode(string $value): void } /** - * {@inheritdoc} + * @inheritdoc */ public function getExtensionAttributes(): ?CountryExtensionInterface { - return $this->_get(self::EXTENSION_ATTRIBUTES_KEY); + return $this->getData(self::EXTENSION_ATTRIBUTES_KEY); } /** - * {@inheritdoc} + * @inheritdoc */ public function setExtensionAttributes(CountryExtensionInterface $extensionAttributes): void { diff --git a/TwoFactorAuth/Model/Data/Provider/Engine/Authy/Device.php b/TwoFactorAuth/Model/Data/Provider/Engine/Authy/Device.php new file mode 100644 index 00000000..68eddac3 --- /dev/null +++ b/TwoFactorAuth/Model/Data/Provider/Engine/Authy/Device.php @@ -0,0 +1,90 @@ +getData(self::COUNTRY); + } + + /** + * @inheritDoc + */ + public function setCountry(string $value): void + { + $this->setData(self::COUNTRY, $value); + } + + /** + * @inheritDoc + */ + public function getPhoneNumber(): string + { + return $this->getData(self::PHONE); + } + + /** + * @inheritDoc + */ + public function setPhoneNumber(string $value): void + { + $this->setData(self::PHONE, $value); + } + + /** + * @inheritDoc + */ + public function getMethod(): string + { + return $this->getData(self::METHOD); + } + + /** + * @inheritDoc + */ + public function setMethod(string $value): void + { + $this->setData(self::METHOD, $value); + } + + /** + * Retrieve existing extension attributes object or create a new one + * + * Used fully qualified namespaces in annotations for proper work of extension interface/class code generation + * + * @return \Magento\TwoFactorAuth\Api\Data\AuthyDeviceExtensionInterface|null + */ + public function getExtensionAttributes(): ?AuthyDeviceExtensionInterface + { + return $this->getData(self::EXTENSION_ATTRIBUTES_KEY); + } + + /** + * Set an extension attributes object + * + * @param \Magento\TwoFactorAuth\Api\Data\AuthyDeviceExtensionInterface $extensionAttributes + * @return void + */ + public function setExtensionAttributes(AuthyDeviceExtensionInterface $extensionAttributes): void + { + $this->setData(self::EXTENSION_ATTRIBUTES_KEY, $extensionAttributes); + } +} diff --git a/TwoFactorAuth/Model/Data/Provider/Engine/Authy/RegistrationResponse.php b/TwoFactorAuth/Model/Data/Provider/Engine/Authy/RegistrationResponse.php new file mode 100644 index 00000000..99b7f5a6 --- /dev/null +++ b/TwoFactorAuth/Model/Data/Provider/Engine/Authy/RegistrationResponse.php @@ -0,0 +1,74 @@ +getData(self::MESSAGE); + } + + /** + * @inheritDoc + */ + public function setMessage(string $value): void + { + $this->setData(self::MESSAGE, $value); + } + + /** + * @inheritDoc + */ + public function getExpirationSeconds(): int + { + return (int)$this->getData(self::EXPIRATION_SECONDS); + } + + /** + * @inheritDoc + */ + public function setExpirationSeconds(int $value): void + { + $this->setData(self::EXPIRATION_SECONDS, $value); + } + + /** + * Retrieve existing extension attributes object or create a new one + * + * Used fully qualified namespaces in annotations for proper work of extension interface/class code generation + * + * @return \Magento\TwoFactorAuth\Api\Data\AuthyRegistrationPromptResponseExtensionInterface|null + */ + public function getExtensionAttributes(): ?AuthyRegistrationPromptResponseExtensionInterface + { + return $this->getData(self::EXTENSION_ATTRIBUTES_KEY); + } + + /** + * Set an extension attributes object + * + * @param \Magento\TwoFactorAuth\Api\Data\AuthyRegistrationPromptResponseExtensionInterface $extensionAttributes + */ + public function setExtensionAttributes( + AuthyRegistrationPromptResponseExtensionInterface $extensionAttributes + ): void { + $this->setData(self::EXTENSION_ATTRIBUTES_KEY, $extensionAttributes); + } +} diff --git a/TwoFactorAuth/Model/Data/Provider/Engine/DuoSecurity/Data.php b/TwoFactorAuth/Model/Data/Provider/Engine/DuoSecurity/Data.php new file mode 100644 index 00000000..a2bcda14 --- /dev/null +++ b/TwoFactorAuth/Model/Data/Provider/Engine/DuoSecurity/Data.php @@ -0,0 +1,74 @@ +getData(self::SIGNATURE); + } + + /** + * @inheritDoc + */ + public function setSignature(string $value): void + { + $this->setData(self::SIGNATURE, $value); + } + + /** + * @inheritDoc + */ + public function getApiHostname(): string + { + return (string)$this->getData(self::API_HOSTNAME); + } + + /** + * @inheritDoc + */ + public function setApiHostname(string $value): void + { + $this->setData(self::API_HOSTNAME, $value); + } + + /** + * Retrieve existing extension attributes object or create a new one + * + * Used fully qualified namespaces in annotations for proper work of extension interface/class code generation + * + * @return \Magento\TwoFactorAuth\Api\Data\DuoDataExtensionInterface|null + */ + public function getExtensionAttributes(): ?DuoDataExtensionInterface + { + return $this->getData(self::EXTENSION_ATTRIBUTES_KEY); + } + + /** + * Set an extension attributes object + * + * @param \Magento\TwoFactorAuth\Api\Data\DuoDataExtensionInterface $extensionAttributes + * @return void + */ + public function setExtensionAttributes(DuoDataExtensionInterface $extensionAttributes): void + { + $this->setData(self::EXTENSION_ATTRIBUTES_KEY, $extensionAttributes); + } +} diff --git a/TwoFactorAuth/Model/Data/Provider/Engine/Google/AuthenticateData.php b/TwoFactorAuth/Model/Data/Provider/Engine/Google/AuthenticateData.php new file mode 100644 index 00000000..e01cade1 --- /dev/null +++ b/TwoFactorAuth/Model/Data/Provider/Engine/Google/AuthenticateData.php @@ -0,0 +1,57 @@ +getData(self::OTP); + } + + /** + * @inheritDoc + */ + public function setOtp(string $value): void + { + $this->setData(self::OTP, $value); + } + + /** + * Retrieve existing extension attributes object or create a new one + * + * Used fully qualified namespaces in annotations for proper work of extension interface/class code generation + * + * @return \Magento\TwoFactorAuth\Api\Data\GoogleAuthenticateExtensionInterface|null + */ + public function getExtensionAttributes(): ?GoogleAuthenticateExtensionInterface + { + return $this->getData(self::EXTENSION_ATTRIBUTES_KEY); + } + + /** + * Set an extension attributes object + * + * @param \Magento\TwoFactorAuth\Api\Data\GoogleAuthenticateExtensionInterface $extensionAttributes + */ + public function setExtensionAttributes(GoogleAuthenticateExtensionInterface $extensionAttributes): void + { + $this->setData(self::EXTENSION_ATTRIBUTES_KEY, $extensionAttributes); + } +} diff --git a/TwoFactorAuth/Model/Data/Provider/Engine/Google/ConfigurationData.php b/TwoFactorAuth/Model/Data/Provider/Engine/Google/ConfigurationData.php new file mode 100644 index 00000000..44378481 --- /dev/null +++ b/TwoFactorAuth/Model/Data/Provider/Engine/Google/ConfigurationData.php @@ -0,0 +1,83 @@ +getData(self::QR_CODE_BASE64); + } + + /** + * Set value for QR code base 64 + * + * @param string $value + * @return void + */ + public function setQrCodeBase64(string $value): void + { + $this->setData(self::QR_CODE_BASE64, $value); + } + + /** + * Get value for secret code + * + * @return string + */ + public function getSecretCode(): string + { + return (string)$this->getData(self::SECRET_CODE); + } + + /** + * Set value for secret code + * + * @param string $value + * @return void + */ + public function setSecretCode(string $value): void + { + $this->setData(self::SECRET_CODE, $value); + } + + /** + * Retrieve existing extension attributes object or create a new one + * + * Used fully qualified namespaces in annotations for proper work of extension interface/class code generation + * + * @return \Magento\TwoFactorAuth\Api\Data\GoogleConfigureExtensionInterface|null + */ + public function getExtensionAttributes(): ?GoogleConfigureExtensionInterface + { + return $this->getData(self::EXTENSION_ATTRIBUTES_KEY); + } + + /** + * Set an extension attributes object + * + * @param \Magento\TwoFactorAuth\Api\Data\GoogleConfigureExtensionInterface $extensionAttributes + */ + public function setExtensionAttributes(GoogleConfigureExtensionInterface $extensionAttributes): void + { + $this->setData(self::EXTENSION_ATTRIBUTES_KEY, $extensionAttributes); + } +} diff --git a/TwoFactorAuth/Model/Data/Provider/Engine/U2fkey/WebAuthnRequest.php b/TwoFactorAuth/Model/Data/Provider/Engine/U2fkey/WebAuthnRequest.php new file mode 100644 index 00000000..dc5de8a3 --- /dev/null +++ b/TwoFactorAuth/Model/Data/Provider/Engine/U2fkey/WebAuthnRequest.php @@ -0,0 +1,51 @@ +getData(self::CREDENTIAL_REQUEST_OPTIONS_JSON); + } + + /** + * @inheritDoc + */ + public function setCredentialRequestOptionsJson(string $value): void + { + $this->setData(self::CREDENTIAL_REQUEST_OPTIONS_JSON, $value); + } + + /** + * @inheritDoc + */ + public function getExtensionAttributes(): ?U2fWebAuthnRequestExtensionInterface + { + return $this->getData(self::EXTENSION_ATTRIBUTES_KEY); + } + + /** + * @inheritDoc + */ + public function setExtensionAttributes(U2fWebAuthnRequestExtensionInterface $extensionAttributes): void + { + $this->setData(self::EXTENSION_ATTRIBUTES_KEY, $extensionAttributes); + } +} diff --git a/TwoFactorAuth/Model/Data/UserConfig.php b/TwoFactorAuth/Model/Data/UserConfig.php index 487e3c2d..f67e25f6 100644 --- a/TwoFactorAuth/Model/Data/UserConfig.php +++ b/TwoFactorAuth/Model/Data/UserConfig.php @@ -7,27 +7,27 @@ namespace Magento\TwoFactorAuth\Model\Data; -use Magento\Framework\Api\AbstractExtensibleObject; +use Magento\Framework\Model\AbstractExtensibleModel; use Magento\TwoFactorAuth\Api\Data\UserConfigExtensionInterface; use Magento\TwoFactorAuth\Api\Data\UserConfigInterface; /** * @inheritDoc */ -class UserConfig extends AbstractExtensibleObject implements UserConfigInterface +class UserConfig extends AbstractExtensibleModel implements UserConfigInterface { /** * @inheritDoc */ public function getId(): int { - return (int) $this->_get(self::ID); + return (int) $this->getData(self::ID); } /** * @inheritDoc */ - public function setId(int $value): void + public function setId($value): void { $this->setData(self::ID, $value); } @@ -37,7 +37,7 @@ public function setId(int $value): void */ public function getUserId(): int { - return (int) $this->_get(self::USER_ID); + return (int) $this->getData(self::USER_ID); } /** @@ -53,7 +53,7 @@ public function setUserId(int $value): void */ public function getEncodedProviders(): string { - return (string) $this->_get(self::ENCODED_PROVIDERS); + return (string) $this->getData(self::ENCODED_PROVIDERS); } /** @@ -69,7 +69,7 @@ public function setEncodedProviders(string $value): void */ public function getDefaultProvider(): string { - return (string) $this->_get(self::DEFAULT_PROVIDER); + return (string) $this->getData(self::DEFAULT_PROVIDER); } /** @@ -85,7 +85,7 @@ public function setDefaultProvider(string $value): void */ public function getExtensionAttributes(): ?UserConfigExtensionInterface { - return $this->_get(self::EXTENSION_ATTRIBUTES_KEY); + return $this->getData(self::EXTENSION_ATTRIBUTES_KEY); } /** diff --git a/TwoFactorAuth/Model/EmailUserNotifier.php b/TwoFactorAuth/Model/EmailUserNotifier.php index f1cc1801..7ab01949 100644 --- a/TwoFactorAuth/Model/EmailUserNotifier.php +++ b/TwoFactorAuth/Model/EmailUserNotifier.php @@ -9,10 +9,9 @@ namespace Magento\TwoFactorAuth\Model; use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\Framework\UrlInterface; use Magento\Store\Model\StoreManagerInterface; +use Magento\TwoFactorAuth\Model\Config\UserNotifier as UserNotifierConfig; use Magento\User\Model\User; -use Magento\TwoFactorAuth\Api\Exception\NotificationExceptionInterface; use Magento\TwoFactorAuth\Api\UserNotifierInterface; use Magento\Framework\Mail\Template\TransportBuilder; use Magento\TwoFactorAuth\Model\Exception\NotificationException; @@ -44,29 +43,29 @@ class EmailUserNotifier implements UserNotifierInterface private $logger; /** - * @var UrlInterface + * @var UserNotifierConfig */ - private $url; + private $userNotifierConfig; /** * @param ScopeConfigInterface $scopeConfig * @param TransportBuilder $transportBuilder * @param StoreManagerInterface $storeManager * @param LoggerInterface $logger - * @param UrlInterface $url + * @param UserNotifierConfig $userNotifierConfig */ public function __construct( ScopeConfigInterface $scopeConfig, TransportBuilder $transportBuilder, StoreManagerInterface $storeManager, LoggerInterface $logger, - UrlInterface $url + UserNotifierConfig $userNotifierConfig ) { $this->scopeConfig = $scopeConfig; $this->transportBuilder = $transportBuilder; $this->storeManager = $storeManager; $this->logger = $logger; - $this->url = $url; + $this->userNotifierConfig = $userNotifierConfig; } /** @@ -75,11 +74,15 @@ public function __construct( * @param User $user * @param string $token * @param string $emailTemplateId + * @param string $url * @return void - * @throws NotificationExceptionInterface */ - private function sendConfigRequired(User $user, string $token, string $emailTemplateId): void - { + private function sendConfigRequired( + User $user, + string $token, + string $emailTemplateId, + string $url + ): void { try { $transport = $this->transportBuilder ->setTemplateIdentifier($emailTemplateId) @@ -92,7 +95,7 @@ private function sendConfigRequired(User $user, string $token, string $emailTemp 'username' => $user->getFirstName() . ' ' . $user->getLastName(), 'token' => $token, 'store_name' => $this->storeManager->getStore()->getFrontendName(), - 'url' => $this->url->getUrl('tfa/tfa/index', ['tfat' => $token]) + 'url' => $url ] ) ->setFromByScope( @@ -112,7 +115,12 @@ private function sendConfigRequired(User $user, string $token, string $emailTemp */ public function sendUserConfigRequestMessage(User $user, string $token): void { - $this->sendConfigRequired($user, $token, 'tfa_admin_user_config_required'); + $this->sendConfigRequired( + $user, + $token, + 'tfa_admin_user_config_required', + $this->userNotifierConfig->getPersonalRequestConfigUrl($token) + ); } /** @@ -120,6 +128,11 @@ public function sendUserConfigRequestMessage(User $user, string $token): void */ public function sendAppConfigRequestMessage(User $user, string $token): void { - $this->sendConfigRequired($user, $token, 'tfa_admin_app_config_required'); + $this->sendConfigRequired( + $user, + $token, + 'tfa_admin_app_config_required', + $this->userNotifierConfig->getAppRequestConfigUrl($token) + ); } } diff --git a/TwoFactorAuth/Model/Provider.php b/TwoFactorAuth/Model/Provider.php index 266da84f..90195248 100644 --- a/TwoFactorAuth/Model/Provider.php +++ b/TwoFactorAuth/Model/Provider.php @@ -161,6 +161,7 @@ public function isConfigured(int $userId): bool /** * Retrieve user's configuration + * * @param int $userId * @return array|null * @throws NoSuchEntityException diff --git a/TwoFactorAuth/Model/Provider/Engine/Authy.php b/TwoFactorAuth/Model/Provider/Engine/Authy.php index 7487d381..efc59c5e 100644 --- a/TwoFactorAuth/Model/Provider/Engine/Authy.php +++ b/TwoFactorAuth/Model/Provider/Engine/Authy.php @@ -11,12 +11,12 @@ use Magento\Framework\DataObject; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\HTTP\Client\CurlFactory; +use Magento\Framework\Serialize\Serializer\Json; use Magento\User\Api\Data\UserInterface; use Magento\TwoFactorAuth\Api\UserConfigManagerInterface; use Magento\TwoFactorAuth\Api\EngineInterface; use Magento\TwoFactorAuth\Model\Provider\Engine\Authy\Service; use Magento\TwoFactorAuth\Model\Provider\Engine\Authy\Token; -use Zend\Json\Json; /** * Authy engine @@ -53,29 +53,38 @@ class Authy implements EngineInterface */ private $token; + /** + * @var Json + */ + private $json; + /** * @param UserConfigManagerInterface $userConfigManager * @param ScopeConfigInterface $scopeConfig * @param Token $token * @param Service $service * @param CurlFactory $curlFactory + * @param Json $json */ public function __construct( UserConfigManagerInterface $userConfigManager, ScopeConfigInterface $scopeConfig, Token $token, Service $service, - CurlFactory $curlFactory + CurlFactory $curlFactory, + Json $json ) { $this->userConfigManager = $userConfigManager; $this->curlFactory = $curlFactory; $this->service = $service; $this->scopeConfig = $scopeConfig; $this->token = $token; + $this->json = $json; } /** * Enroll in Authy + * * @param UserInterface $user * @return bool * @throws LocalizedException @@ -97,7 +106,7 @@ public function enroll(UserInterface $user): bool 'user[country_code]' => $providerInfo['country_code'], ]); - $response = Json::decode($curl->getBody(), Json::TYPE_ARRAY); + $response = $this->json->unserialize($curl->getBody()); $errorMessage = $this->service->getErrorFromResponse($response); if ($errorMessage) { diff --git a/TwoFactorAuth/Model/Provider/Engine/Authy/Authenticate.php b/TwoFactorAuth/Model/Provider/Engine/Authy/Authenticate.php new file mode 100644 index 00000000..6318608e --- /dev/null +++ b/TwoFactorAuth/Model/Provider/Engine/Authy/Authenticate.php @@ -0,0 +1,204 @@ +userFactory = $userFactory; + $this->authy = $authy; + $this->alert = $alert; + $this->dataObjectFactory = $dataObjectFactory; + $this->adminTokenService = $adminTokenService; + $this->authyToken = $authyToken; + $this->userAuthenticator = $userAuthenticator; + $this->oneTouch = $oneTouch; + } + + /** + * @inheritDoc + */ + public function createAdminAccessTokenWithCredentials(string $username, string $password, string $otp): string + { + $token = $this->adminTokenService->createAdminAccessToken($username, $password); + + $user = $this->getUser($username); + $this->userAuthenticator->assertProviderIsValidForUser((int)$user->getId(), Authy::CODE); + + try { + $this->authy->verify($user, $this->dataObjectFactory->create([ + 'data' => [ + 'tfa_code' => $otp + ], + ])); + + return $token; + } catch (\Exception $e) { + $this->alert->event( + 'Magento_TwoFactorAuth', + 'Authy error', + AlertInterface::LEVEL_ERROR, + $user->getUserName(), + $e->getMessage() + ); + throw $e; + } + } + + /** + * @inheritDoc + */ + public function sendToken(string $username, string $password, string $via): void + { + $this->adminTokenService->createAdminAccessToken($username, $password); + + $user = $this->getUser($username); + $this->userAuthenticator->assertProviderIsValidForUser((int)$user->getId(), Authy::CODE); + + if ($via === 'onetouch') { + $this->oneTouch->request($user); + } else { + $this->authyToken->request($user, $via); + } + } + + /** + * @inheritDoc + */ + public function creatAdminAccessTokenWithOneTouch(string $username, string $password): string + { + $token = $this->adminTokenService->createAdminAccessToken($username, $password); + + $user = $this->getUser($username); + $this->userAuthenticator->assertProviderIsValidForUser((int)$user->getId(), Authy::CODE); + + try { + $res = $this->oneTouch->verify($user); + if ($res === 'approved') { + return $token; + } else { + $this->alert->event( + 'Magento_TwoFactorAuth', + 'Authy onetouch auth denied', + AlertInterface::LEVEL_WARNING, + $user->getUserName() + ); + + throw new LocalizedException(__('Onetouch prompt was denied or timed out.')); + } + } catch (\Exception $e) { + $this->alert->event( + 'Magento_TwoFactorAuth', + 'Authy onetouch error', + AlertInterface::LEVEL_ERROR, + $user->getUserName(), + $e->getMessage() + ); + + throw $e; + } + } + + /** + * Retrieve a user using the username + * + * @param string $username + * @return UserInterface + * @throws AuthenticationException + */ + private function getUser(string $username): UserInterface + { + $user = $this->userFactory->create(); + $user->loadByUsername($username); + $userId = (int)$user->getId(); + if ($userId === 0) { + throw new AuthenticationException(__( + 'The account sign-in was incorrect or your account is disabled temporarily. ' + . 'Please wait and try again later.' + )); + } + + return $user; + } +} diff --git a/TwoFactorAuth/Model/Provider/Engine/Authy/Configure.php b/TwoFactorAuth/Model/Provider/Engine/Authy/Configure.php new file mode 100644 index 00000000..df68fcf3 --- /dev/null +++ b/TwoFactorAuth/Model/Provider/Engine/Authy/Configure.php @@ -0,0 +1,135 @@ +alert = $alert; + $this->verification = $verification; + $this->responseFactory = $responseFactory; + $this->authy = $authy; + $this->userAuthenticator = $userAuthenticator; + } + + /** + * @inheritDoc + */ + public function sendDeviceRegistrationPrompt( + string $tfaToken, + AuthyDeviceInterface + $deviceData + ): ResponseInterface { + $user = $this->userAuthenticator->authenticateWithTokenAndProvider($tfaToken, Authy::CODE); + + $response = []; + $this->verification->request( + $user, + $deviceData->getCountry(), + $deviceData->getPhoneNumber(), + $deviceData->getMethod(), + $response + ); + + $this->alert->event( + 'Magento_TwoFactorAuth', + 'New authy verification request via ' . $deviceData->getMethod(), + AlertInterface::LEVEL_INFO, + $user->getUserName() + ); + + return $this->responseFactory->create( + [ + 'data' => [ + ResponseInterface::MESSAGE => $response['message'], + ResponseInterface::EXPIRATION_SECONDS => (int)$response['seconds_to_expire'], + ] + ] + ); + } + + /** + * @inheritDoc + */ + public function activate(string $tfaToken, string $otp): void + { + $user = $this->userAuthenticator->authenticateWithTokenAndProvider($tfaToken, Authy::CODE); + + try { + $this->verification->verify($user, $otp); + $this->authy->enroll($user); + + $this->alert->event( + 'Magento_TwoFactorAuth', + 'Authy identity verified', + AlertInterface::LEVEL_INFO, + $user->getUserName() + ); + } catch (\Throwable $e) { + $this->alert->event( + 'Magento_TwoFactorAuth', + 'Authy identity verification failure', + AlertInterface::LEVEL_ERROR, + $user->getUserName(), + $e->getMessage() + ); + + throw $e; + } + } +} diff --git a/TwoFactorAuth/Model/Provider/Engine/Authy/OneTouch.php b/TwoFactorAuth/Model/Provider/Engine/Authy/OneTouch.php index bb9a3ed6..84c03d08 100644 --- a/TwoFactorAuth/Model/Provider/Engine/Authy/OneTouch.php +++ b/TwoFactorAuth/Model/Provider/Engine/Authy/OneTouch.php @@ -10,11 +10,11 @@ use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\HTTP\Client\CurlFactory; +use Magento\Framework\Serialize\Serializer\Json; use Magento\Store\Model\StoreManagerInterface; use Magento\User\Api\Data\UserInterface; use Magento\TwoFactorAuth\Api\UserConfigManagerInterface; use Magento\TwoFactorAuth\Model\Provider\Engine\Authy; -use Zend\Json\Json; /** * Authy One Touch manager class @@ -51,6 +51,11 @@ class OneTouch */ private $scopeConfig; + /** + * @var Json + */ + private $json; + /** * OneTouch constructor. * @@ -59,23 +64,27 @@ class OneTouch * @param Service $service * @param StoreManagerInterface $storeManager * @param ScopeConfigInterface $scopeConfig + * @param Json $json */ public function __construct( CurlFactory $curlFactory, UserConfigManagerInterface $userConfigManager, Service $service, StoreManagerInterface $storeManager, - ScopeConfigInterface $scopeConfig + ScopeConfigInterface $scopeConfig, + Json $json ) { $this->curlFactory = $curlFactory; $this->userConfigManager = $userConfigManager; $this->storeManager = $storeManager; $this->service = $service; $this->scopeConfig = $scopeConfig; + $this->json = $json; } /** * Request one-touch + * * @param UserInterface $user * @throws LocalizedException */ @@ -98,7 +107,7 @@ public function request(UserInterface $user): void 'seconds_to_expire' => 300, ]); - $response = Json::decode($curl->getBody(), Json::TYPE_ARRAY); + $response = $this->json->unserialize($curl->getBody()); $errorMessage = $this->service->getErrorFromResponse($response); if ($errorMessage) { @@ -112,6 +121,7 @@ public function request(UserInterface $user): void /** * Verify one-touch + * * @param UserInterface $user * @return string * @throws LocalizedException @@ -142,7 +152,7 @@ public function verify(UserInterface $user): string $curl->addHeader('X-Authy-API-Key', $this->service->getApiKey()); $curl->get($url); - $response = Json::decode($curl->getBody(), Json::TYPE_ARRAY); + $response = $this->json->unserialize($curl->getBody()); $errorMessage = $this->service->getErrorFromResponse($response); if ($errorMessage) { diff --git a/TwoFactorAuth/Model/Provider/Engine/Authy/Service.php b/TwoFactorAuth/Model/Provider/Engine/Authy/Service.php index 38fafa3e..8278a02e 100644 --- a/TwoFactorAuth/Model/Provider/Engine/Authy/Service.php +++ b/TwoFactorAuth/Model/Provider/Engine/Authy/Service.php @@ -39,6 +39,7 @@ public function __construct(ScopeConfigInterface $scopeConfig) /** * Get API key + * * @return string */ public function getApiKey(): string @@ -48,6 +49,7 @@ public function getApiKey(): string /** * Get authy API endpoint + * * @param string $path * @return string */ @@ -58,6 +60,7 @@ public function getProtectedApiEndpoint(string $path): string /** * Get authy API endpoint + * * @param string $path * @return string */ @@ -68,6 +71,7 @@ public function getOneTouchApiEndpoint(string $path): string /** * Get error from response + * * @param array|boolean $response * @return string|null */ diff --git a/TwoFactorAuth/Model/Provider/Engine/Authy/Token.php b/TwoFactorAuth/Model/Provider/Engine/Authy/Token.php index dd7d70ff..9dd58cb7 100644 --- a/TwoFactorAuth/Model/Provider/Engine/Authy/Token.php +++ b/TwoFactorAuth/Model/Provider/Engine/Authy/Token.php @@ -10,10 +10,10 @@ use Magento\Framework\DataObject; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\HTTP\Client\CurlFactory; +use Magento\Framework\Serialize\Serializer\Json; use Magento\User\Api\Data\UserInterface; use Magento\TwoFactorAuth\Api\UserConfigManagerInterface; use Magento\TwoFactorAuth\Model\Provider\Engine\Authy; -use Zend\Json\Json; /** * Authy token manager @@ -35,23 +35,32 @@ class Token */ private $service; + /** + * @var Json + */ + private $json; + /** * @param UserConfigManagerInterface $userConfigManager * @param Service $service * @param CurlFactory $curlFactory + * @param Json $json */ public function __construct( UserConfigManagerInterface $userConfigManager, Service $service, - CurlFactory $curlFactory + CurlFactory $curlFactory, + Json $json ) { $this->userConfigManager = $userConfigManager; $this->curlFactory = $curlFactory; $this->service = $service; + $this->json = $json; } /** * Request a token + * * @param UserInterface $user * @param string $via * @throws LocalizedException @@ -73,7 +82,7 @@ public function request(UserInterface $user, string $via): void $curl->addHeader('X-Authy-API-Key', $this->service->getApiKey()); $curl->get($url); - $response = Json::decode($curl->getBody(), Json::TYPE_ARRAY); + $response = $this->json->unserialize($curl->getBody()); $errorMessage = $this->service->getErrorFromResponse($response); if ($errorMessage) { @@ -83,6 +92,7 @@ public function request(UserInterface $user, string $via): void /** * Return true on token validation + * * @param UserInterface $user * @param DataObject $request * @return bool @@ -106,7 +116,7 @@ public function verify(UserInterface $user, DataObject $request): bool $curl->addHeader('X-Authy-API-Key', $this->service->getApiKey()); $curl->get($url); - $response = Json::decode($curl->getBody(), Json::TYPE_ARRAY); + $response = $this->json->unserialize($curl->getBody()); $errorMessage = $this->service->getErrorFromResponse($response); if ($errorMessage) { diff --git a/TwoFactorAuth/Model/Provider/Engine/Authy/Verification.php b/TwoFactorAuth/Model/Provider/Engine/Authy/Verification.php index 5d46664e..13730aca 100644 --- a/TwoFactorAuth/Model/Provider/Engine/Authy/Verification.php +++ b/TwoFactorAuth/Model/Provider/Engine/Authy/Verification.php @@ -9,11 +9,11 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Framework\HTTP\Client\CurlFactory; +use Magento\Framework\Serialize\Serializer\Json; use Magento\Framework\Stdlib\DateTime\DateTime; use Magento\User\Api\Data\UserInterface; use Magento\TwoFactorAuth\Api\UserConfigManagerInterface; use Magento\TwoFactorAuth\Model\Provider\Engine\Authy; -use Zend\Json\Json; /** * Authy verification management @@ -40,31 +40,40 @@ class Verification */ private $dateTime; + /** + * @var Json + */ + private $json; + /** * @param CurlFactory $curlFactory * @param DateTime $dateTime * @param UserConfigManagerInterface $userConfigManager * @param Service $service + * @param Json $json */ public function __construct( CurlFactory $curlFactory, DateTime $dateTime, UserConfigManagerInterface $userConfigManager, - Service $service + Service $service, + Json $json ) { $this->curlFactory = $curlFactory; $this->service = $service; $this->userConfigManager = $userConfigManager; $this->dateTime = $dateTime; + $this->json = $json; } /** * Verify phone number + * * @param UserInterface $user - * @param string $country - * @param string $phoneNumber - * @param string $method - * @param array &$response + * @param string $country + * @param string $phoneNumber + * @param string $method + * @param array &$response * @throws LocalizedException */ public function request( @@ -84,7 +93,7 @@ public function request( 'phone_number' => $phoneNumber ]); - $response = Json::decode($curl->getBody(), Json::TYPE_ARRAY); + $response = $this->json->unserialize($curl->getBody()); $errorMessage = $this->service->getErrorFromResponse($response); if ($errorMessage) { @@ -109,6 +118,7 @@ public function request( /** * Verify phone number + * * @param UserInterface $user * @param string $verificationCode * @throws LocalizedException @@ -130,7 +140,7 @@ public function verify(UserInterface $user, string $verificationCode): void 'verification_code' => $verificationCode, ])); - $response = Json::decode($curl->getBody(), Json::TYPE_ARRAY); + $response = $this->json->unserialize($curl->getBody()); $errorMessage = $this->service->getErrorFromResponse($response); if ($errorMessage) { diff --git a/TwoFactorAuth/Model/Provider/Engine/DuoSecurity.php b/TwoFactorAuth/Model/Provider/Engine/DuoSecurity.php index 1be0abe1..362c444c 100644 --- a/TwoFactorAuth/Model/Provider/Engine/DuoSecurity.php +++ b/TwoFactorAuth/Model/Provider/Engine/DuoSecurity.php @@ -88,6 +88,7 @@ public function __construct( /** * Get API hostname + * * @return string */ public function getApiHostname(): string @@ -97,6 +98,7 @@ public function getApiHostname(): string /** * Get application key + * * @return string */ private function getApplicationKey(): string @@ -106,6 +108,7 @@ private function getApplicationKey(): string /** * Get secret key + * * @return string */ private function getSecretKey(): string @@ -115,6 +118,7 @@ private function getSecretKey(): string /** * Get integration key + * * @return string */ private function getIntegrationKey(): string @@ -124,6 +128,7 @@ private function getIntegrationKey(): string /** * Sign values + * * @param string $key * @param string $values * @param string $prefix @@ -142,6 +147,7 @@ private function signValues(string $key, string $values, string $prefix, int $ex /** * Parse signed values and return username + * * @param string $key * @param string $val * @param string $prefix @@ -190,6 +196,7 @@ private function parseValues(string $key, string $val, string $prefix, int $time /** * Get request signature + * * @param UserInterface $user * @return string */ diff --git a/TwoFactorAuth/Model/Provider/Engine/DuoSecurity/Authenticate.php b/TwoFactorAuth/Model/Provider/Engine/DuoSecurity/Authenticate.php new file mode 100644 index 00000000..a12dc487 --- /dev/null +++ b/TwoFactorAuth/Model/Provider/Engine/DuoSecurity/Authenticate.php @@ -0,0 +1,178 @@ +userFactory = $userFactory; + $this->alert = $alert; + $this->duo = $duo; + $this->adminTokenService = $adminTokenService; + $this->dataFactory = $dataFactory; + $this->dataObjectFactory = $dataObjectFactory; + $this->userAuthenticator = $userAuthenticator; + } + + /** + * @inheritDoc + */ + public function getAuthenticateData(string $username, string $password): DuoDataInterface + { + $this->adminTokenService->createAdminAccessToken($username, $password); + + $user = $this->getUser($username); + $this->userAuthenticator->assertProviderIsValidForUser((int)$user->getId(), DuoSecurity::CODE); + + return $this->dataFactory->create( + [ + 'data' => [ + DuoDataInterface::API_HOSTNAME => $this->duo->getApiHostname(), + DuoDataInterface::SIGNATURE => $this->duo->getRequestSignature($user) + ] + ] + ); + } + + /** + * @inheritDoc + */ + public function createAdminAccessTokenWithCredentials( + string $username, + string $password, + string $signatureResponse + ): string { + $token = $this->adminTokenService->createAdminAccessToken($username, $password); + + $user = $this->getUser($username); + $this->userAuthenticator->assertProviderIsValidForUser((int)$user->getId(), DuoSecurity::CODE); + + $this->assertResponseIsValid($user, $signatureResponse); + + return $token; + } + + /** + * Assert that the given signature is valid for the user + * + * @param UserInterface $user + * @param string $signatureResponse + * @throws LocalizedException + */ + public function assertResponseIsValid(UserInterface $user, string $signatureResponse): void + { + $data = $this->dataObjectFactory->create( + [ + 'data' => [ + 'sig_response' => $signatureResponse + ] + ] + ); + if (!$this->duo->verify($user, $data)) { + $this->alert->event( + 'Magento_TwoFactorAuth', + 'DuoSecurity invalid auth', + AlertInterface::LEVEL_WARNING, + $user->getUserName() + ); + + throw new LocalizedException(__('Invalid response')); + } + } + + /** + * Retrieve a user using the username + * + * @param string $username + * @return UserInterface + * @throws AuthenticationException + */ + private function getUser(string $username): UserInterface + { + $user = $this->userFactory->create(); + $user->loadByUsername($username); + $userId = (int)$user->getId(); + if ($userId === 0) { + throw new AuthenticationException(__( + 'The account sign-in was incorrect or your account is disabled temporarily. ' + . 'Please wait and try again later.' + )); + } + + return $user; + } +} diff --git a/TwoFactorAuth/Model/Provider/Engine/DuoSecurity/Configure.php b/TwoFactorAuth/Model/Provider/Engine/DuoSecurity/Configure.php new file mode 100644 index 00000000..f842cb89 --- /dev/null +++ b/TwoFactorAuth/Model/Provider/Engine/DuoSecurity/Configure.php @@ -0,0 +1,98 @@ +userAuthenticator = $userAuthenticator; + $this->duo = $duo; + $this->dataFactory = $dataFactory; + $this->tfa = $tfa; + $this->authenticate = $authenticate; + } + + /** + * @inheritDoc + */ + public function getConfigurationData(string $tfaToken): DuoDataInterface + { + $user = $this->userAuthenticator->authenticateWithTokenAndProvider($tfaToken, DuoSecurity::CODE); + + return $this->dataFactory->create( + [ + 'data' => [ + DuoDataInterface::API_HOSTNAME => $this->duo->getApiHostname(), + DuoDataInterface::SIGNATURE => $this->duo->getRequestSignature($user) + ] + ] + ); + } + + /** + * @inheritDoc + */ + public function activate(string $tfaToken, string $signatureResponse): void + { + $user = $this->userAuthenticator->authenticateWithTokenAndProvider($tfaToken, DuoSecurity::CODE); + $userId = (int)$user->getId(); + + $this->authenticate->assertResponseIsValid($user, $signatureResponse); + $this->tfa->getProviderByCode(DuoSecurity::CODE) + ->activate($userId); + } +} diff --git a/TwoFactorAuth/Model/Provider/Engine/Google.php b/TwoFactorAuth/Model/Provider/Engine/Google.php index cfa85474..5762ab0e 100644 --- a/TwoFactorAuth/Model/Provider/Engine/Google.php +++ b/TwoFactorAuth/Model/Provider/Engine/Google.php @@ -7,24 +7,35 @@ namespace Magento\TwoFactorAuth\Model\Provider\Engine; +use Endroid\QrCode\ErrorCorrectionLevel; use Endroid\QrCode\Exception\ValidationException; use Endroid\QrCode\QrCode; use Endroid\QrCode\Writer\PngWriter; use Exception; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\DataObject; +use Magento\Framework\Encryption\EncryptorInterface; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Store\Model\StoreManagerInterface; +use Magento\TwoFactorAuth\Model\Provider\Engine\Google\TotpFactory; use Magento\User\Api\Data\UserInterface; use Magento\TwoFactorAuth\Api\UserConfigManagerInterface; use Magento\TwoFactorAuth\Api\EngineInterface; use Base32\Base32; -use OTPHP\TOTP; +use OTPHP\TOTPInterface; /** * Google authenticator engine + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Google implements EngineInterface { + /** + * Config path for the OTP window + */ + const XML_PATH_OTP_WINDOW = 'twofactorauth/google/otp_window'; + /** * Engine code * @@ -32,11 +43,6 @@ class Google implements EngineInterface */ public const CODE = 'google'; - /** - * @var null - */ - private $totp = null; - /** * @var UserConfigManagerInterface */ @@ -47,22 +53,45 @@ class Google implements EngineInterface */ private $storeManager; + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var TOTPInterfaceFactory + */ + private $totpFactory; + + /** + * @var EncryptorInterface + */ + private $encryptor; + /** * @param StoreManagerInterface $storeManager - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig + * @param ScopeConfigInterface $scopeConfig * @param UserConfigManagerInterface $configManager + * @param TotpFactory $totpFactory + * @param EncryptorInterface $encryptor */ public function __construct( StoreManagerInterface $storeManager, - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, - UserConfigManagerInterface $configManager + ScopeConfigInterface $scopeConfig, + UserConfigManagerInterface $configManager, + TotpFactory $totpFactory, + EncryptorInterface $encryptor ) { $this->configManager = $configManager; $this->storeManager = $storeManager; + $this->scopeConfig = $scopeConfig; + $this->totpFactory = $totpFactory; + $this->encryptor = $encryptor; } /** * Generate random secret + * * @return string * @throws Exception */ @@ -74,30 +103,30 @@ private function generateSecret(): string /** * Get TOTP object + * * @param UserInterface $user - * @return TOTP + * @return TOTPInterface * @throws NoSuchEntityException */ - private function getTotp(UserInterface $user): TOTP + private function getTotp(UserInterface $user): TOTPInterface { - $config = $this->configManager->getProviderConfig((int)$user->getId(), static::CODE); - if (!isset($config['secret'])) { - $config['secret'] = $this->getSecretCode($user); - } - if (!$config['secret']) { + $secret = $this->getSecretCode($user); + + if (!$secret) { throw new NoSuchEntityException(__('Secret for user with ID#%1 was not found', $user->getId())); } - $totp = new TOTP($user->getEmail(), $config['secret']); + + $totp = $this->totpFactory->create($secret); return $totp; } /** * Get the secret code used for Google Authentication + * * @param UserInterface $user * @return string|null * @throws NoSuchEntityException - * @author Konrad Skrzynski */ public function getSecretCode(UserInterface $user): ?string { @@ -105,14 +134,32 @@ public function getSecretCode(UserInterface $user): ?string if (!isset($config['secret'])) { $config['secret'] = $this->generateSecret(); - $this->configManager->setProviderConfig((int)$user->getId(), static::CODE, $config); + $this->setSharedSecret((int)$user->getId(), $config['secret']); + return $config['secret']; } - return $config['secret'] ?? null; + return $config['secret'] ? $this->encryptor->decrypt($config['secret']) : null; + } + + /** + * Set the secret used to generate OTP + * + * @param int $userId + * @param string $secret + * @throws NoSuchEntityException + */ + public function setSharedSecret(int $userId, string $secret): void + { + $this->configManager->addProviderConfig( + $userId, + static::CODE, + ['secret' => $this->encryptor->encrypt($secret)] + ); } /** * Get TFA provisioning URL + * * @param UserInterface $user * @return string * @throws NoSuchEntityException @@ -126,6 +173,7 @@ private function getProvisioningUrl(UserInterface $user): string // @codingStandardsIgnoreEnd $totp = $this->getTotp($user); + $totp->setLabel($user->getEmail()); $totp->setIssuer($issuer); return $totp->getProvisioningUri(); @@ -142,13 +190,18 @@ public function verify(UserInterface $user, DataObject $request): bool } $totp = $this->getTotp($user); - $totp->now(); + $config = $this->configManager->getProviderConfig((int)$user->getId(), static::CODE); - return $totp->verify($token); + return $totp->verify( + $token, + null, + $config['window'] ?? (int)$this->scopeConfig->getValue(self::XML_PATH_OTP_WINDOW) ?: null + ); } /** * Render TFA QrCode + * * @param UserInterface $user * @return string * @throws NoSuchEntityException @@ -159,7 +212,8 @@ public function getQrCodeAsPng(UserInterface $user): string // @codingStandardsIgnoreStart $qrCode = new QrCode($this->getProvisioningUrl($user)); $qrCode->setSize(400); - $qrCode->setErrorCorrectionLevel('high'); + $qrCode->setMargin(0); + $qrCode->setErrorCorrectionLevel(ErrorCorrectionLevel::HIGH()); $qrCode->setForegroundColor(['r' => 0, 'g' => 0, 'b' => 0, 'a' => 0]); $qrCode->setBackgroundColor(['r' => 255, 'g' => 255, 'b' => 255, 'a' => 0]); $qrCode->setLabelFontSize(16); diff --git a/TwoFactorAuth/Model/Provider/Engine/Google/Authenticate.php b/TwoFactorAuth/Model/Provider/Engine/Google/Authenticate.php new file mode 100644 index 00000000..bdfdc447 --- /dev/null +++ b/TwoFactorAuth/Model/Provider/Engine/Google/Authenticate.php @@ -0,0 +1,107 @@ +google = $google; + $this->userAuthenticator = $userAuthenticator; + $this->dataObjectFactory = $dataObjectFactory; + $this->alert = $alert; + $this->adminTokenService = $adminTokenService; + $this->userFactory = $userFactory; + } + + /** + * @inheritDoc + */ + public function createAdminAccessToken(string $username, string $password, string $otp): string + { + $token = $this->adminTokenService->createAdminAccessToken($username, $password); + $user = $this->userFactory->create(); + $user->loadByUsername($username); + $this->userAuthenticator->assertProviderIsValidForUser((int)$user->getId(), Google::CODE); + + if ($this->google->verify($user, $this->dataObjectFactory->create([ + 'data' => [ + 'tfa_code' => $otp + ], + ])) + ) { + $this->alert->event( + 'Magento_TwoFactorAuth', + 'New Google Authenticator code issued', + AlertInterface::LEVEL_INFO, + $user->getUserName() + ); + + return $token; + } else { + throw new AuthorizationException(__('Invalid code.')); + } + } +} diff --git a/TwoFactorAuth/Model/Provider/Engine/Google/Configure.php b/TwoFactorAuth/Model/Provider/Engine/Google/Configure.php new file mode 100644 index 00000000..d1ecad49 --- /dev/null +++ b/TwoFactorAuth/Model/Provider/Engine/Google/Configure.php @@ -0,0 +1,122 @@ +configurationDataFactory = $configurationDataFactory; + $this->google = $google; + $this->tfa = $tfa; + $this->dataObjectFactory = $dataObjectFactory; + $this->alert = $alert; + $this->userAuthenticator = $userAuthenticator; + } + + /** + * @inheritDoc + */ + public function getConfigurationData(string $tfaToken): GoogleConfigurationData + { + $user = $this->userAuthenticator->authenticateWithTokenAndProvider($tfaToken, Google::CODE); + + return $this->configurationDataFactory->create( + [ + 'data' => [ + GoogleConfigurationData::QR_CODE_BASE64 => base64_encode($this->google->getQrCodeAsPng($user)), + GoogleConfigurationData::SECRET_CODE => $this->google->getSecretCode($user) + ] + ] + ); + } + + /** + * @inheritDoc + */ + public function activate(string $tfaToken, string $otp): void + { + $user = $this->userAuthenticator->authenticateWithTokenAndProvider($tfaToken, Google::CODE); + + if ($this->google->verify($user, $this->dataObjectFactory->create([ + 'data' => [ + 'tfa_code' => $otp + ], + ])) + ) { + $this->tfa->getProvider(Google::CODE)->activate((int)$user->getId()); + + $this->alert->event( + 'Magento_TwoFactorAuth', + 'New Google Authenticator code issued', + AlertInterface::LEVEL_INFO, + $user->getUserName() + ); + } else { + throw new AuthorizationException(__('Invalid code.')); + } + } +} diff --git a/TwoFactorAuth/Model/Provider/Engine/Google/TotpFactory.php b/TwoFactorAuth/Model/Provider/Engine/Google/TotpFactory.php new file mode 100644 index 00000000..b0a72090 --- /dev/null +++ b/TwoFactorAuth/Model/Provider/Engine/Google/TotpFactory.php @@ -0,0 +1,29 @@ +userAuthenticator = $userAuthenticator; + $this->u2fKey = $u2fKey; + $this->alert = $alert; + $this->dataObjectFactory = $dataObjectFactory; + $this->userFactory = $userFactory; + $this->authnRequestInterfaceFactory = $authnRequestInterfaceFactory; + $this->json = $json; + $this->configManager = $configManager; + $this->adminTokenService = $adminTokenService; + } + + /** + * @inheritDoc + */ + public function getAuthenticationData(string $username, string $password): U2fWebAuthnRequestInterface + { + $this->adminTokenService->createAdminAccessToken($username, $password); + + $user = $this->getUser($username); + $userId = (int)$user->getId(); + $this->userAuthenticator->assertProviderIsValidForUser($userId, U2fKey::CODE); + + $data = $this->u2fKey->getAuthenticateData($user); + $this->configManager->addProviderConfig( + $userId, + U2fKey::CODE, + [self::AUTHENTICATION_CHALLENGE_KEY => $data['credentialRequestOptions']['challenge']] + ); + + $json = $this->json->serialize($data); + + return $this->authnRequestInterfaceFactory->create( + [ + 'data' => [ + U2fWebAuthnRequestInterface::CREDENTIAL_REQUEST_OPTIONS_JSON => $json + ] + ] + ); + } + + /** + * @inheritDoc + */ + public function createAdminAccessToken(string $username, string $password, string $publicKeyCredentialJson): string + { + $token = $this->adminTokenService->createAdminAccessToken($username, $password); + + $user = $this->getUser($username); + $userId = (int)$user->getId(); + $this->userAuthenticator->assertProviderIsValidForUser($userId, U2fKey::CODE); + + $config = $this->configManager->getProviderConfig($userId, U2fKey::CODE); + if (empty($config[self::AUTHENTICATION_CHALLENGE_KEY])) { + throw new LocalizedException(__('U2f authentication prompt not sent.')); + } + + try { + $this->u2fKey->verify($user, $this->dataObjectFactory->create( + [ + 'data' => [ + 'publicKeyCredential' => $this->json->unserialize($publicKeyCredentialJson), + 'originalChallenge' => $config[self::AUTHENTICATION_CHALLENGE_KEY] + ] + ] + )); + } catch (\Exception $e) { + $this->alert->event( + 'Magento_TwoFactorAuth', + 'U2F error', + AlertInterface::LEVEL_ERROR, + $user->getUserName(), + $e->getMessage() + ); + throw $e; + } + + $this->configManager->addProviderConfig( + $userId, + U2fKey::CODE, + [self::AUTHENTICATION_CHALLENGE_KEY => null] + ); + + return $token; + } + + /** + * Retrieve a user using the username + * + * @param string $username + * @return UserInterface + * @throws AuthenticationException + */ + private function getUser(string $username): UserInterface + { + $user = $this->userFactory->create(); + $user->loadByUsername($username); + $userId = (int)$user->getId(); + if ($userId === 0) { + throw new AuthenticationException(__( + 'The account sign-in was incorrect or your account is disabled temporarily. ' + . 'Please wait and try again later.' + )); + } + + return $user; + } +} diff --git a/TwoFactorAuth/Model/Provider/Engine/U2fKey/ConfigReader.php b/TwoFactorAuth/Model/Provider/Engine/U2fKey/ConfigReader.php new file mode 100644 index 00000000..ef4adaa2 --- /dev/null +++ b/TwoFactorAuth/Model/Provider/Engine/U2fKey/ConfigReader.php @@ -0,0 +1,46 @@ +storeManager = $storeManager; + } + + /** + * @inheritDoc + */ + public function getDomain(): string + { + $store = $this->storeManager->getStore(Store::ADMIN_CODE); + $baseUrl = $store->getBaseUrl(); + if (!preg_match('/^(https?:\/\/(?P.+?))\//', $baseUrl, $matches)) { + throw new LocalizedException(__('Could not determine domain name.')); + } + return $matches['domain']; + } +} diff --git a/TwoFactorAuth/Model/Provider/Engine/U2fKey/Configure.php b/TwoFactorAuth/Model/Provider/Engine/U2fKey/Configure.php new file mode 100644 index 00000000..1fab3fc1 --- /dev/null +++ b/TwoFactorAuth/Model/Provider/Engine/U2fKey/Configure.php @@ -0,0 +1,152 @@ +u2fKey = $u2fKey; + $this->userAuthenticator = $userAuthenticator; + $this->configManager = $configManager; + $this->authnInterfaceFactory = $authnInterfaceFactory; + $this->alert = $alert; + $this->json = $json; + } + + /** + * @inheritDoc + */ + public function getRegistrationData(string $tfaToken): U2fWebAuthnRequestInterface + { + $user = $this->userAuthenticator->authenticateWithTokenAndProvider($tfaToken, U2fKey::CODE); + $userId = (int)$user->getId(); + + $data = $this->u2fKey->getRegisterData($user); + + $this->configManager->addProviderConfig( + $userId, + U2fKey::CODE, + [self::REGISTER_CHALLENGE_KEY => $data['publicKey']['challenge']] + ); + + return $this->authnInterfaceFactory->create( + [ + 'data' => [ + U2fWebAuthnRequestInterface::CREDENTIAL_REQUEST_OPTIONS_JSON => $this->json->serialize($data) + ] + ] + ); + } + + /** + * @inheritDoc + */ + public function activate(string $tfaToken, string $publicKeyCredentialJson): void + { + $user = $this->userAuthenticator->authenticateWithTokenAndProvider($tfaToken, U2fKey::CODE); + $userId = (int)$user->getId(); + + $config = $this->configManager->getProviderConfig($userId, U2fKey::CODE); + + if (empty($config[self::REGISTER_CHALLENGE_KEY])) { + throw new LocalizedException(__('U2f key registration was not started.')); + } + + try { + $this->u2fKey->registerDevice( + $user, + [ + 'publicKeyCredential' => $this->json->unserialize($publicKeyCredentialJson), + 'challenge' => $config[self::REGISTER_CHALLENGE_KEY] + ] + ); + $this->alert->event( + 'Magento_TwoFactorAuth', + 'U2F New device registered', + AlertInterface::LEVEL_INFO, + $user->getUserName() + ); + } catch (\Exception $e) { + $this->alert->event( + 'Magento_TwoFactorAuth', + 'U2F error while adding device', + AlertInterface::LEVEL_ERROR, + $user->getUserName(), + $e->getMessage() + ); + throw $e; + } + + $this->configManager->addProviderConfig( + $userId, + U2fKey::CODE, + [self::REGISTER_CHALLENGE_KEY => null] + ); + } +} diff --git a/TwoFactorAuth/Model/Provider/Engine/U2fKey/Session.php b/TwoFactorAuth/Model/Provider/Engine/U2fKey/Session.php index 8ec49a84..7bec3c6f 100644 --- a/TwoFactorAuth/Model/Provider/Engine/U2fKey/Session.php +++ b/TwoFactorAuth/Model/Provider/Engine/U2fKey/Session.php @@ -12,6 +12,8 @@ /** * Represents u2f key session data + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class Session extends SessionManager { diff --git a/TwoFactorAuth/Model/Provider/Engine/U2fKey/WebApiConfigReader.php b/TwoFactorAuth/Model/Provider/Engine/U2fKey/WebApiConfigReader.php new file mode 100644 index 00000000..1bdc7dda --- /dev/null +++ b/TwoFactorAuth/Model/Provider/Engine/U2fKey/WebApiConfigReader.php @@ -0,0 +1,52 @@ +configReader = $configReader; + $this->scopeConfig = $scopeConfig; + } + + /** + * @inheritDoc + */ + public function getDomain(): string + { + $configValue = $this->scopeConfig->getValue(U2fKey::XML_PATH_WEBAPI_DOMAIN); + if ($configValue) { + return $configValue; + } + + return $this->configReader->getDomain(); + } +} diff --git a/TwoFactorAuth/Model/Provider/Engine/U2fKey/WebAuthn.php b/TwoFactorAuth/Model/Provider/Engine/U2fKey/WebAuthn.php index 53d0c7a7..c314f8ca 100644 --- a/TwoFactorAuth/Model/Provider/Engine/U2fKey/WebAuthn.php +++ b/TwoFactorAuth/Model/Provider/Engine/U2fKey/WebAuthn.php @@ -13,6 +13,7 @@ use Magento\Framework\Validation\ValidationException; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; +use Magento\TwoFactorAuth\Api\U2fKeyConfigReaderInterface; use Magento\User\Api\Data\UserInterface; /** @@ -27,6 +28,11 @@ class WebAuthn private const PUBKEY_LEN = 65; + /** + * @var U2fKeyConfigReaderInterface + */ + private $config; + /** * @var StoreManagerInterface */ @@ -34,10 +40,13 @@ class WebAuthn /** * @param StoreManagerInterface $storeManager + * @param U2fKeyConfigReaderInterface $u2fKeyConfig */ public function __construct( - StoreManagerInterface $storeManager + StoreManagerInterface $storeManager, + U2fKeyConfigReaderInterface $u2fKeyConfig ) { + $this->config = $u2fKeyConfig; $this->storeManager = $storeManager; } @@ -48,6 +57,7 @@ public function __construct( * @param array $publicKeys * @param array $originalChallenge * @throws LocalizedException + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function assertCredentialDataIsValid( array $credentialData, @@ -69,12 +79,13 @@ public function assertCredentialDataIsValid( throw new LocalizedException(__('Invalid U2F key.')); } - $domain = $this->getDomainName(); + $domain = $this->config->getDomain(); // Steps 7-9 if (rtrim(strtr(base64_encode($this->convertArrayToBytes($originalChallenge)), '+/', '-_'), '=') !== $credentialData['response']['clientData']['challenge'] - || 'https://' . $domain !== $credentialData['response']['clientData']['origin'] + // phpcs:ignore Magento2.Functions.DiscouragedFunction + || $domain !== parse_url($credentialData['response']['clientData']['origin'], \PHP_URL_HOST) || $credentialData['response']['clientData']['type'] !== 'webauthn.get' ) { throw new LocalizedException(__('Invalid U2F key.')); @@ -83,6 +94,7 @@ public function assertCredentialDataIsValid( // Step 10 not applicable // @see https://www.w3.org/TR/webauthn/#sec-authenticator-data + // phpcs:ignore Magento2.Functions.DiscouragedFunction $authenticatorDataBytes = base64_decode($credentialData['response']['authenticatorData']); $attestationObject = [ 'rpIdHash' => substr($authenticatorDataBytes, 0, 32), @@ -102,6 +114,7 @@ public function assertCredentialDataIsValid( $clientDataSha256 = hash('sha256', $credentialData['response']['clientDataJSON'], true); $isValidSignature = openssl_verify( $authenticatorDataBytes . $clientDataSha256, + // phpcs:ignore Magento2.Functions.DiscouragedFunction base64_decode($credentialData['response']['signature']), $key['key'], OPENSSL_ALGO_SHA256 @@ -133,6 +146,7 @@ public function getAuthenticateData(array $publicKeys): array foreach ($publicKeys as $key) { $allowedCredentials[] = [ 'type' => 'public-key', + // phpcs:ignore Magento2.Functions.DiscouragedFunction 'id' => $this->convertBytesToArray(base64_decode($key['id'])) ]; } @@ -146,7 +160,7 @@ public function getAuthenticateData(array $publicKeys): array 'extensions' => [ 'txAuthSimple' => 'Authenticate with ' . $store->getName(), ], - 'rpId' => $this->getDomainName(), + 'rpId' => $this->config->getDomain(), ] ]; @@ -162,7 +176,7 @@ public function getAuthenticateData(array $publicKeys): array */ public function getRegisterData(UserInterface $user): array { - $domain = $this->getDomainName(); + $domain = $this->config->getDomain(); try { $challenge = random_bytes(16); @@ -211,17 +225,20 @@ public function getRegisterData(UserInterface $user): array * @param array $data * @return array * @throws ValidationException + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) */ public function getPublicKeyFromRegistrationData(array $data): array { // Verification process as defined by w3 @see https://www.w3.org/TR/webauthn/#registering-a-new-credential $credentialData = $data['publicKeyCredential']; - $domain = $this->getDomainName(); + $domain = $this->config->getDomain(); if (rtrim(strtr(base64_encode($this->convertArrayToBytes($data['challenge'])), '+/', '-_'), '=') !== $credentialData['response']['clientData']['challenge'] - || 'https://' . $domain !== $credentialData['response']['clientData']['origin'] + // phpcs:ignore Magento2.Functions.DiscouragedFunction + || $domain !== parse_url($credentialData['response']['clientData']['origin'], \PHP_URL_HOST) || $credentialData['response']['clientData']['type'] !== 'webauthn.create' ) { throw new LocalizedException(__('Invalid U2F key.')); @@ -230,8 +247,11 @@ public function getPublicKeyFromRegistrationData(array $data): array if (empty($credentialData['response']['attestationObject']) || empty($credentialData['id'])) { throw new ValidationException(__('Invalid U2F key data')); } + // phpcs:ignore Magento2.Functions.DiscouragedFunction $byteString = base64_decode($credentialData['response']['attestationObject']); + //@codingStandardsIgnoreStart $attestationObject = CBOREncoder::decode($byteString); + //@codingStandardsIgnoreEnd if (empty($attestationObject['fmt']) || empty($attestationObject['authData']) ) { @@ -245,7 +265,7 @@ public function getPublicKeyFromRegistrationData(array $data): array $attestationObject['flags'] = ord(substr($byteString, 32, 1)); $attestationObject['counter'] = substr($byteString, 33, 4); - $hashId = hash('sha256', $this->getDomainName(), true); + $hashId = hash('sha256', $this->config->getDomain(), true); if ($hashId !== $attestationObject['rpIdHash']) { throw new ValidationException(__('Invalid U2F key data')); } @@ -269,6 +289,7 @@ public function getPublicKeyFromRegistrationData(array $data): array $attestationObject['attestationData']['keyBytes'] = $this->COSEECDHAtoPKCS($cborPublicKey); if (empty($attestationObject['attestationData']['keyBytes']) + // phpcs:ignore Magento2.Functions.DiscouragedFunction || $attestationObject['attestationData']['credId'] !== base64_decode($credentialData['id']) ) { throw new ValidationException(__('Invalid U2F key data')); @@ -308,38 +329,26 @@ private function convertArrayToBytes(array $bytes): string $byteString = ''; foreach ($bytes as $byte) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $byteString .= chr((int)$byte); } return $byteString; } - /** - * Get the store domain but only if it's secure - * - * @return string - * @throws LocalizedException - */ - private function getDomainName(): string - { - $store = $this->storeManager->getStore(Store::ADMIN_CODE); - $baseUrl = $store->getBaseUrl(); - if (!preg_match('/^(https?:\/\/(?P.+?))\//', $baseUrl, $matches)) { - throw new LocalizedException(__('Could not determine secure domain name.')); - } - return $matches['domain']; - } - /** * Convert a CBOR encoded public key to PKCS format * * @param string $binary * @return string|null * @throws \Exception + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ private function COSEECDHAtoPKCS(string $binary): ?string { + //@codingStandardsIgnoreStart $cosePubKey = CBOREncoder::decode($binary); + //@codingStandardsIgnoreEnd // Sections 7.1 and 13.1.1 of @see https://tools.ietf.org/html/rfc8152 if (!isset($cosePubKey[3]) diff --git a/TwoFactorAuth/Model/ResourceModel/Country.php b/TwoFactorAuth/Model/ResourceModel/Country.php index d2c7d0c6..ec5bdc26 100644 --- a/TwoFactorAuth/Model/ResourceModel/Country.php +++ b/TwoFactorAuth/Model/ResourceModel/Country.php @@ -11,10 +11,14 @@ /** * Country model + * * @SuppressWarnings(PHPMD.CamelCaseMethodName) */ class Country extends AbstractDb { + /** + * @inheritDoc + */ protected function _construct() { $this->_init('tfa_country_codes', 'country_id'); diff --git a/TwoFactorAuth/Model/ResourceModel/CountryRepository.php b/TwoFactorAuth/Model/ResourceModel/CountryRepository.php index cc5b3b49..1b4abb0c 100644 --- a/TwoFactorAuth/Model/ResourceModel/CountryRepository.php +++ b/TwoFactorAuth/Model/ResourceModel/CountryRepository.php @@ -25,6 +25,7 @@ /** * @inheritDoc + * * @SuppressWarnings(PHPMD.ShortVariable) * @SuppressWarnings(PHPMD.LongVariable) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -102,7 +103,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function save(CountryInterface $country): CountryInterface { @@ -124,7 +125,8 @@ public function save(CountryInterface $country): CountryInterface } /** - * {@inheritdoc} + * @inheritdoc + * * @throws NoSuchEntityException */ public function getById(int $id): CountryInterface @@ -145,7 +147,8 @@ public function getById(int $id): CountryInterface } /** - * {@inheritdoc} + * @inheritdoc + * * @throws NoSuchEntityException */ public function getByCode(string $value): CountryInterface @@ -166,7 +169,7 @@ public function getByCode(string $value): CountryInterface } /** - * {@inheritdoc} + * @inheritdoc */ public function delete(CountryInterface $country): void { @@ -185,7 +188,7 @@ public function delete(CountryInterface $country): void } /** - * {@inheritdoc} + * @inheritdoc */ public function getList(SearchCriteriaInterface $searchCriteria): SearchResultsInterface { diff --git a/TwoFactorAuth/Model/ResourceModel/UserConfig.php b/TwoFactorAuth/Model/ResourceModel/UserConfig.php index 08eb44a6..44da7016 100644 --- a/TwoFactorAuth/Model/ResourceModel/UserConfig.php +++ b/TwoFactorAuth/Model/ResourceModel/UserConfig.php @@ -16,6 +16,8 @@ use Magento\Framework\Serialize\SerializerInterface; /** + * User config model + * * @SuppressWarnings(PHPMD.CamelCaseMethodName) */ class UserConfig extends AbstractDb @@ -27,16 +29,12 @@ class UserConfig extends AbstractDb /** * @param Context $context - * @param null $decoder - * @param null $encoder - * @param null $connectionName + * @param string $connectionName * @param EncryptorInterface $encryptor * @param SerializerInterface $serializer */ public function __construct( Context $context, - $decoder = null, - $encoder = null, $connectionName = null, EncryptorInterface $encryptor = null, SerializerInterface $serializer = null @@ -57,6 +55,8 @@ protected function _construct() } /** + * Encode the provided config + * * @param array $config * @return string */ @@ -66,6 +66,8 @@ private function encodeConfig(array $config): string } /** + * Decode the provided config + * * @param string $config * @return array */ @@ -81,6 +83,9 @@ private function decodeConfig(string $config): array return $this->serializer->unserialize($config); } + /** + * @inheritDoc + */ public function _afterLoad(AbstractModel $object) { parent::_afterLoad($object); @@ -100,11 +105,14 @@ public function _afterLoad(AbstractModel $object) return $this; } + /** + * @inheritDoc + */ public function _beforeSave(AbstractModel $object) { $object->setData('encoded_config', $this->encodeConfig($object->getData('config') ?? [])); $object->setData('encoded_providers', $this->serializer->serialize($object->getData('providers') ?? [])); - parent::_beforeSave($object); + return parent::_beforeSave($object); } } diff --git a/TwoFactorAuth/Model/ResourceModel/UserConfigRepository.php b/TwoFactorAuth/Model/ResourceModel/UserConfigRepository.php index d47d3263..dd865ec1 100644 --- a/TwoFactorAuth/Model/ResourceModel/UserConfigRepository.php +++ b/TwoFactorAuth/Model/ResourceModel/UserConfigRepository.php @@ -25,6 +25,7 @@ /** * @inheritDoc + * * @SuppressWarnings(PHPMD.ShortVariable) * @SuppressWarnings(PHPMD.LongVariable) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -102,7 +103,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function save(UserConfigInterface $userConfig): UserConfigInterface { @@ -124,7 +125,7 @@ public function save(UserConfigInterface $userConfig): UserConfigInterface } /** - * {@inheritdoc} + * @inheritdoc */ public function getById(int $id): UserConfigInterface { @@ -144,7 +145,7 @@ public function getById(int $id): UserConfigInterface } /** - * {@inheritdoc} + * @inheritdoc */ public function getByUserId(int $value): UserConfigInterface { @@ -164,7 +165,7 @@ public function getByUserId(int $value): UserConfigInterface } /** - * {@inheritdoc} + * @inheritdoc */ public function delete(UserConfigInterface $userConfig): bool { @@ -185,7 +186,7 @@ public function delete(UserConfigInterface $userConfig): bool } /** - * {@inheritdoc} + * @inheritdoc */ public function getList(SearchCriteriaInterface $searchCriteria): SearchResultsInterface { diff --git a/TwoFactorAuth/Model/Tfa.php b/TwoFactorAuth/Model/Tfa.php index 4bb945e1..cd7c567c 100644 --- a/TwoFactorAuth/Model/Tfa.php +++ b/TwoFactorAuth/Model/Tfa.php @@ -231,6 +231,7 @@ public function isEnabled(): bool /** * Return true if a provider code is allowed + * * @param int $userId * @param string $providerCode * @throws NoSuchEntityException @@ -252,6 +253,7 @@ public function getDefaultProviderCode(int $userId): string /** * Set default provider code + * * @param int $userId * @param string $providerCode * @return boolean diff --git a/TwoFactorAuth/Model/TfaSession.php b/TwoFactorAuth/Model/TfaSession.php index 2c6a7266..67a337eb 100644 --- a/TwoFactorAuth/Model/TfaSession.php +++ b/TwoFactorAuth/Model/TfaSession.php @@ -12,6 +12,8 @@ /** * @inheritDoc + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class TfaSession extends SessionManager implements TfaSessionInterface { diff --git a/TwoFactorAuth/Model/TfatActions.php b/TwoFactorAuth/Model/TfatActions.php new file mode 100644 index 00000000..c9856f0b --- /dev/null +++ b/TwoFactorAuth/Model/TfatActions.php @@ -0,0 +1,89 @@ +tokenManager = $tokenManager; + $this->tfa = $tfa; + $this->json = $json; + } + + /** + * @inheritDoc + */ + public function getUserProviders(string $tfaToken): array + { + $userId = $this->validateTfat($tfaToken); + + return $this->tfa->getUserProviders($userId); + } + + /** + * @inheritDoc + */ + public function getProvidersToActivate(string $tfaToken): array + { + $userId = $this->validateTfat($tfaToken); + + return $this->tfa->getProvidersToActivate($userId); + } + + /** + * Validate the given 2fa token + * + * @param string $tfat + * @return int + * @throws AuthorizationException + */ + private function validateTfat(string $tfat): int + { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + ['user_id' => $userId] = $this->json->unserialize(explode('.', base64_decode($tfat))[0]); + if (!$this->tokenManager->isValidFor($userId, $tfat)) { + throw new AuthorizationException(__('Invalid token.')); + } + + return $userId; + } +} diff --git a/TwoFactorAuth/Model/UserAuthenticator.php b/TwoFactorAuth/Model/UserAuthenticator.php new file mode 100644 index 00000000..14152961 --- /dev/null +++ b/TwoFactorAuth/Model/UserAuthenticator.php @@ -0,0 +1,122 @@ +userFactory = $userFactory; + $this->userResource = $userResource; + $this->tfa = $tfa; + $this->tokenManager = $tokenManager; + $this->json = $json; + } + + /** + * Obtain a user with an id and a tfa token + * + * @param string $tfaToken + * @param string $providerCode + * @return User + * @throws AuthorizationException + * @throws LocalizedException + */ + public function authenticateWithTokenAndProvider(string $tfaToken, string $providerCode): User + { + try { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + ['user_id' => $userId] = $this->json->unserialize(explode('.', base64_decode($tfaToken))[0]); + } catch (\Throwable $e) { + throw new AuthorizationException( + __('Invalid two-factor authorization token') + ); + } + + if (!$this->tfa->getProviderIsAllowed($userId, $providerCode)) { + throw new LocalizedException(__('Provider is not allowed.')); + } elseif ($this->tfa->getProviderByCode($providerCode)->isActive($userId)) { + throw new LocalizedException(__('Provider is already configured.')); + } elseif (!$this->tokenManager->isValidFor($userId, $tfaToken)) { + throw new AuthorizationException( + __('Invalid two-factor authorization token') + ); + } + + $user = $this->userFactory->create(); + $this->userResource->load($user, $userId); + + return $user; + } + + /** + * Validate the user is allowed to use the provider + * + * @param int $userId + * @param string $providerCode + * @throws LocalizedException + */ + public function assertProviderIsValidForUser(int $userId, string $providerCode): void + { + if (!$this->tfa->getProviderIsAllowed($userId, $providerCode)) { + throw new LocalizedException(__('Provider is not allowed.')); + } elseif (!$this->tfa->getProviderByCode($providerCode)->isActive($userId)) { + throw new LocalizedException(__('Provider is not configured.')); + } + } +} diff --git a/TwoFactorAuth/Model/UserConfig/HtmlAreaTokenVerifier.php b/TwoFactorAuth/Model/UserConfig/HtmlAreaTokenVerifier.php index e461adb4..1192df29 100644 --- a/TwoFactorAuth/Model/UserConfig/HtmlAreaTokenVerifier.php +++ b/TwoFactorAuth/Model/UserConfig/HtmlAreaTokenVerifier.php @@ -19,6 +19,8 @@ * Finds and verifies token allowing users to configure 2FA. * * Works for adminhtml area. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class HtmlAreaTokenVerifier { @@ -90,6 +92,7 @@ public function isConfigTokenProvided(): bool * Read configuration token provided by user. * * @return string|null + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function readConfigToken(): ?string { diff --git a/TwoFactorAuth/Model/UserConfig/SignedTokenManager.php b/TwoFactorAuth/Model/UserConfig/SignedTokenManager.php index d0d3e93c..3d742871 100644 --- a/TwoFactorAuth/Model/UserConfig/SignedTokenManager.php +++ b/TwoFactorAuth/Model/UserConfig/SignedTokenManager.php @@ -64,8 +64,9 @@ public function issueFor(int $userId): string public function isValidFor(int $userId, string $token): bool { $isValid = false; - [$encodedData, $signatureProvided] = explode('.', base64_decode($token)); try { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + [$encodedData, $signatureProvided] = explode('.', base64_decode($token)); $data = $this->json->unserialize($encodedData); if (array_key_exists('user_id', $data) && array_key_exists('tfa_configuration', $data) diff --git a/TwoFactorAuth/Model/UserConfigManager.php b/TwoFactorAuth/Model/UserConfigManager.php index c3875747..2160d576 100644 --- a/TwoFactorAuth/Model/UserConfigManager.php +++ b/TwoFactorAuth/Model/UserConfigManager.php @@ -61,7 +61,7 @@ public function getProviderConfig(int $userId, string $providerCode): ?array /** * @inheritdoc */ - public function setProviderConfig(int $userId, string $providerCode, ?array $config=null): bool + public function setProviderConfig(int $userId, string $providerCode, ?array $config = null): bool { $userConfig = $this->getUserConfiguration($userId); $providersConfig = $userConfig->getData('config'); @@ -83,7 +83,7 @@ public function setProviderConfig(int $userId, string $providerCode, ?array $con /** * @inheritdoc */ - public function addProviderConfig(int $userId, string $providerCode, ?array $config=null): bool + public function addProviderConfig(int $userId, string $providerCode, ?array $config = null): bool { $userConfig = $this->getProviderConfig($userId, $providerCode); if ($userConfig === null) { @@ -106,6 +106,7 @@ public function resetProviderConfig(int $userId, string $providerCode): bool /** * Get user TFA config + * * @param int $userId * @return UserConfig */ diff --git a/TwoFactorAuth/Model/UserConfigRegistry.php b/TwoFactorAuth/Model/UserConfigRegistry.php index f283ba48..2cfe1e2c 100644 --- a/TwoFactorAuth/Model/UserConfigRegistry.php +++ b/TwoFactorAuth/Model/UserConfigRegistry.php @@ -30,6 +30,7 @@ class UserConfigRegistry /** * Remove registry entity by id + * * @param int $id */ public function removeById(int $id): void @@ -48,6 +49,7 @@ public function removeById(int $id): void /** * Push one object into registry + * * @param int $id * @return UserConfigInterface|null */ @@ -58,6 +60,7 @@ public function retrieveById(int $id): ?UserConfigInterface /** * Retrieve by UserId value + * * @param int $value * @return UserConfigInterface|null */ @@ -72,6 +75,7 @@ public function retrieveByUserId(int $value): ?UserConfigInterface /** * Push one object into registry + * * @param UserConfig $userConfig */ public function push(UserConfig $userConfig): void diff --git a/TwoFactorAuth/Observer/AdminUserLoadAfter.php b/TwoFactorAuth/Observer/AdminUserLoadAfter.php index 151d53b3..d9c1dbc3 100644 --- a/TwoFactorAuth/Observer/AdminUserLoadAfter.php +++ b/TwoFactorAuth/Observer/AdminUserLoadAfter.php @@ -21,6 +21,9 @@ class AdminUserLoadAfter implements ObserverInterface */ private $userConfigManager; + /** + * @param UserConfigManagerInterface $userConfigManager + */ public function __construct( UserConfigManagerInterface $userConfigManager ) { @@ -28,6 +31,8 @@ public function __construct( } /** + * @inheritDoc + * * @param Observer $observer * @return void */ diff --git a/TwoFactorAuth/Observer/ControllerActionPredispatch.php b/TwoFactorAuth/Observer/ControllerActionPredispatch.php index 784a6fd4..7923586a 100644 --- a/TwoFactorAuth/Observer/ControllerActionPredispatch.php +++ b/TwoFactorAuth/Observer/ControllerActionPredispatch.php @@ -24,6 +24,8 @@ /** * Handle redirection to 2FA page if required + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ControllerActionPredispatch implements ObserverInterface { @@ -122,7 +124,7 @@ public function execute(Observer $observer) /** @var $controllerAction AbstractAction */ $controllerAction = $observer->getEvent()->getData('controller_action'); $this->action = $controllerAction; - $fullActionName = $controllerAction->getRequest()->getFullActionName(); + $fullActionName = $observer->getEvent()->getData('request')->getFullActionName(); $userId = $this->userContext->getUserId(); $this->tokenManager->readConfigToken(); diff --git a/TwoFactorAuth/Plugin/AddTabToAdminUserEdit.php b/TwoFactorAuth/Plugin/AddTabToAdminUserEdit.php index f788c95a..99bb9f3d 100644 --- a/TwoFactorAuth/Plugin/AddTabToAdminUserEdit.php +++ b/TwoFactorAuth/Plugin/AddTabToAdminUserEdit.php @@ -40,6 +40,8 @@ public function __construct( } /** + * Check if tab should be displayed + * * @param Tabs $subject * @throws LocalizedException */ diff --git a/TwoFactorAuth/Plugin/AvoidRecursionOnPasswordChange.php b/TwoFactorAuth/Plugin/AvoidRecursionOnPasswordChange.php index 597d9688..0caa8bd9 100644 --- a/TwoFactorAuth/Plugin/AvoidRecursionOnPasswordChange.php +++ b/TwoFactorAuth/Plugin/AvoidRecursionOnPasswordChange.php @@ -41,6 +41,8 @@ public function __construct( } /** + * Prevent recursion + * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @param ForceAdminPasswordChangeObserver $subject * @param Closure $proceed diff --git a/TwoFactorAuth/Plugin/DeleteCookieOnLogout.php b/TwoFactorAuth/Plugin/DeleteCookieOnLogout.php new file mode 100644 index 00000000..cc15737d --- /dev/null +++ b/TwoFactorAuth/Plugin/DeleteCookieOnLogout.php @@ -0,0 +1,64 @@ +cookies = $cookies; + $this->cookieMetadataFactory = $cookieMetadataFactory; + $this->sessionManager = $sessionManager; + } + + /** + * Delete the tfat cookie + * + * @param \Magento\Backend\Model\Auth $subject + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeLogout(\Magento\Backend\Model\Auth $subject) + { + $metadata = $this->cookieMetadataFactory->createSensitiveCookieMetadata() + ->setPath($this->sessionManager->getCookiePath()); + $this->cookies->deleteCookie('tfa-ct', $metadata); + } +} diff --git a/TwoFactorAuth/Plugin/FirstAvailableMenu.php b/TwoFactorAuth/Plugin/FirstAvailableMenu.php new file mode 100644 index 00000000..b2841c9f --- /dev/null +++ b/TwoFactorAuth/Plugin/FirstAvailableMenu.php @@ -0,0 +1,34 @@ +moduleDataSetup->endSetup(); + + return $this; } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -97,7 +100,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/TwoFactorAuth/Setup/Patch/Data/EncryptConfiguration.php b/TwoFactorAuth/Setup/Patch/Data/EncryptConfiguration.php index f2a16736..513b652e 100644 --- a/TwoFactorAuth/Setup/Patch/Data/EncryptConfiguration.php +++ b/TwoFactorAuth/Setup/Patch/Data/EncryptConfiguration.php @@ -12,7 +12,7 @@ use Magento\Framework\Setup\Patch\DataPatchInterface; /** - * Encrypt configuration + * Encrypt the configuration */ class EncryptConfiguration implements DataPatchInterface { @@ -39,7 +39,8 @@ public function __construct( } /** - * {@inheritdoc} + * Encrypt the config + * * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) @@ -65,10 +66,12 @@ public function apply() } $this->moduleDataSetup->endSetup(); + + return $this; } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -76,7 +79,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/TwoFactorAuth/Setup/Patch/Data/EncryptGoogleSecrets.php b/TwoFactorAuth/Setup/Patch/Data/EncryptGoogleSecrets.php new file mode 100644 index 00000000..686bdf31 --- /dev/null +++ b/TwoFactorAuth/Setup/Patch/Data/EncryptGoogleSecrets.php @@ -0,0 +1,107 @@ +moduleDataSetup = $moduleDataSetup; + $this->userCollectionFactory = $userCollectionFactory; + $this->userConfigManager = $userConfigManager; + $this->encryptor = $encryptor; + } + + /** + * @inheritdoc + */ + public function apply() + { + $this->moduleDataSetup->startSetup(); + + /** @var \Magento\User\Model\ResourceModel\User\Collection $collection */ + $collection = $this->userCollectionFactory->create(); + + foreach ($collection as $user) { + /** @var $user User */ + + try { + $config = $this->userConfigManager->getProviderConfig((int)$user->getId(), Google::CODE); + if (empty($config) || empty($config['secret'])) { + continue; + } + $secret = $this->encryptor->encrypt($config['secret']); + $this->userConfigManager->addProviderConfig((int)$user->getId(), Google::CODE, ['secret' => $secret]); + } catch (NoSuchEntityException $e) { + continue; + } + } + + $this->moduleDataSetup->endSetup(); + + return $this; + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return []; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/TwoFactorAuth/Setup/Patch/Data/EncryptSecrets.php b/TwoFactorAuth/Setup/Patch/Data/EncryptSecrets.php index 15ae99f9..e28d4eac 100644 --- a/TwoFactorAuth/Setup/Patch/Data/EncryptSecrets.php +++ b/TwoFactorAuth/Setup/Patch/Data/EncryptSecrets.php @@ -73,6 +73,8 @@ public function apply() } $this->moduleDataSetup->endSetup(); + + return $this; } /** diff --git a/TwoFactorAuth/Setup/Patch/Data/GenerateDuoSecurityKey.php b/TwoFactorAuth/Setup/Patch/Data/GenerateDuoSecurityKey.php index e92e2a7e..bb2f4e68 100644 --- a/TwoFactorAuth/Setup/Patch/Data/GenerateDuoSecurityKey.php +++ b/TwoFactorAuth/Setup/Patch/Data/GenerateDuoSecurityKey.php @@ -14,7 +14,7 @@ use Magento\TwoFactorAuth\Model\Provider\Engine\DuoSecurity; /** - * Generate duo security key + * Generate initial duo security key */ class GenerateDuoSecurityKey implements DataPatchInterface { @@ -49,7 +49,8 @@ public function __construct( } /** - * {@inheritdoc} + * Create and set the duo key + * * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) @@ -71,10 +72,12 @@ public function apply() } $this->moduleDataSetup->endSetup(); + + return $this; } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -84,7 +87,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/TwoFactorAuth/Setup/Patch/Data/PopulateCountryTable.php b/TwoFactorAuth/Setup/Patch/Data/PopulateCountryTable.php index fa5c6474..d49855dd 100644 --- a/TwoFactorAuth/Setup/Patch/Data/PopulateCountryTable.php +++ b/TwoFactorAuth/Setup/Patch/Data/PopulateCountryTable.php @@ -57,7 +57,8 @@ public function __construct( } /** - * {@inheritdoc} + * Install the country table + * * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) @@ -84,10 +85,12 @@ public function apply() // @codingStandardsIgnoreEnd $this->moduleDataSetup->endSetup(); + + return $this; } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -95,7 +98,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/TwoFactorAuth/Setup/Patch/Data/ResetU2fConfig.php b/TwoFactorAuth/Setup/Patch/Data/ResetU2fConfig.php index 9b0f041a..18986427 100644 --- a/TwoFactorAuth/Setup/Patch/Data/ResetU2fConfig.php +++ b/TwoFactorAuth/Setup/Patch/Data/ResetU2fConfig.php @@ -18,7 +18,7 @@ /** * Reset the U2f data due to rewrite */ -class ResetU2FConfig implements DataPatchInterface +class ResetU2fConfig implements DataPatchInterface { /** * @var ModuleDataSetupInterface @@ -71,6 +71,8 @@ public function apply() } $this->moduleDataSetup->endSetup(); + + return $this; } /** diff --git a/TwoFactorAuth/Setup/Patch/Schema/CopyTablesFromOldModule.php b/TwoFactorAuth/Setup/Patch/Schema/CopyTablesFromOldModule.php index 81ad6b10..3392544e 100644 --- a/TwoFactorAuth/Setup/Patch/Schema/CopyTablesFromOldModule.php +++ b/TwoFactorAuth/Setup/Patch/Schema/CopyTablesFromOldModule.php @@ -12,6 +12,7 @@ /** * Copy table contents after migrating from MageSpecialist to Magento + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CopyTablesFromOldModule implements SchemaPatchInterface @@ -31,7 +32,8 @@ public function __construct( } /** - * {@inheritdoc} + * Migrate the old tables + * * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) @@ -67,7 +69,7 @@ public function apply() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -75,7 +77,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/TwoFactorAuth/Test/Api/AdminIntegrationTokenTest.php b/TwoFactorAuth/Test/Api/AdminIntegrationTokenTest.php new file mode 100644 index 00000000..801d4a1f --- /dev/null +++ b/TwoFactorAuth/Test/Api/AdminIntegrationTokenTest.php @@ -0,0 +1,212 @@ +userFactory = $objectManager->get(UserFactory::class); + $this->userConfig = $objectManager->get(UserConfigManagerInterface::class); + $this->tfa = $objectManager->get(TfaInterface::class); + $this->config = $objectManager->get(Config::class); + } + + /** + * @magentoApiDataFixture Magento/User/_files/user_with_custom_role.php + */ + public function testDefaultBehaviorForInvalidCredentials() + { + $serviceInfo = $this->buildServiceInfo(); + + try { + $this->_webApiCall( + $serviceInfo, + ['username' => 'customRoleUser', 'password' => 'bad'] + ); + self::fail('Endpoint should have thrown an exception'); + } catch (\Throwable $exception) { + $response = json_decode($exception->getMessage(), true); + if (json_last_error()) { + $message = $exception->getMessage(); + } else { + $message = $response['message']; + } + self::assertSame( + 'The account sign-in was incorrect or your account is disabled temporarily. ' + . 'Please wait and try again later.', + $message + ); + } + } + + /** + * @magentoConfigFixture twofactorauth/general/force_providers google + * @magentoApiDataFixture Magento/User/_files/user_with_custom_role.php + */ + public function testUserWithConfigured2fa() + { + $userId = $this->getUserId(); + $this->tfa->getProviderByCode(Google::CODE)->activate($userId); + $serviceInfo = $this->buildServiceInfo(); + + try { + $this->_webApiCall( + $serviceInfo, + ['username' => 'customRoleUser', 'password' => TestBootstrap::ADMIN_PASSWORD] + ); + } catch (\Exception $e) { + $response = json_decode($e->getMessage(), true); + if (json_last_error()) { + $message = $e->getMessage(); + } else { + $message = $response['message']; + self::assertCount(1, $response['parameters']['active_providers']); + self::assertSame('google', $response['parameters']['active_providers'][0]); + } + + self::assertSame( + 'Please use the 2fa provider-specific endpoints to obtain a token.', + $message + ); + } + } + + /** + * @magentoConfigFixture twofactorauth/general/force_providers google,duo_security + * @magentoConfigFixture twofactorauth/duo/integration_key abc123 + * @magentoConfigFixture twofactorauth/duo/api_hostname abc123 + * @magentoConfigFixture twofactorauth/duo/secret_key abc123 + * @magentoApiDataFixture Magento/User/_files/user_with_custom_role.php + */ + public function testUserWithAvailableUnconfigured2fa() + { + $userId = $this->getUserId(); + $this->tfa->getProviderByCode(Google::CODE)->activate($userId); + $serviceInfo = $this->buildServiceInfo(); + + try { + $this->_webApiCall( + $serviceInfo, + ['username' => 'customRoleUser', 'password' => TestBootstrap::ADMIN_PASSWORD] + ); + } catch (\Exception $e) { + $response = json_decode($e->getMessage(), true); + if (json_last_error()) { + $message = $e->getMessage(); + } else { + $message = $response['message']; + self::assertCount(1, $response['parameters']['active_providers']); + self::assertSame('google', $response['parameters']['active_providers'][0]); + } + + self::assertSame( + 'You are required to configure personal Two-Factor Authorization in order to login. ' + . 'Please check your email.', + $message + ); + } + } + + /** + * @magentoApiDataFixture Magento/User/_files/user_with_custom_role.php + */ + public function testNoAvailable2faProviders() + { + $this->config->setDataByPath('twofactorauth/general/force_providers', ''); + $this->config->save(); + $userId = $this->getUserId(); + $this->tfa->getProviderByCode(Google::CODE)->activate($userId); + $serviceInfo = $this->buildServiceInfo(); + + try { + $this->_webApiCall( + $serviceInfo, + ['username' => 'customRoleUser', 'password' => TestBootstrap::ADMIN_PASSWORD] + ); + self::fail('Endpoint should have thrown an exception'); + } catch (\Throwable $exception) { + $response = json_decode($exception->getMessage(), true); + if (json_last_error()) { + $message = $exception->getMessage(); + } else { + $message = $response['message']; + } + self::assertSame( + 'Please ask an administrator with sufficient access to configure 2FA first', + $message + ); + } + } + + /** + * @return array + */ + private function buildServiceInfo(): array + { + return [ + 'rest' => [ + // Ensure the default auth is invalidated + 'token' => 'invalid', + 'resourcePath' => self::RESOURCE_PATH, + 'httpMethod' => Request::HTTP_METHOD_POST + ], + 'soap' => [ + // Ensure the default auth is invalidated + 'token' => 'invalid', + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . self::OPERATION + ] + ]; + } + + private function getUserId(): int + { + $user = $this->userFactory->create(); + $user->loadByUsername('customRoleUser'); + + return (int)$user->getId(); + } +} diff --git a/TwoFactorAuth/Test/Api/GoogleActivateTest.php b/TwoFactorAuth/Test/Api/GoogleActivateTest.php new file mode 100644 index 00000000..3253933f --- /dev/null +++ b/TwoFactorAuth/Test/Api/GoogleActivateTest.php @@ -0,0 +1,185 @@ +userFactory = $objectManager->get(UserFactory::class); + $this->tokenManager = $objectManager->get(UserConfigTokenManagerInterface::class); + $this->tfa = $objectManager->get(TfaInterface::class); + $this->userFactory = $objectManager->get(UserFactory::class); + $this->google = $objectManager->get(Google::class); + } + + /** + * @magentoConfigFixture twofactorauth/general/force_providers google + * @magentoApiDataFixture Magento/User/_files/user_with_custom_role.php + */ + public function testInvalidTfat() + { + $serviceInfo = $this->buildServiceInfo(); + + try { + $this->_webApiCall($serviceInfo, ['tfaToken' => 'abc', 'otp' => 'invalid']); + self::fail('Endpoint should have thrown an exception'); + } catch (\Throwable $exception) { + $response = json_decode($exception->getMessage(), true); + if (json_last_error()) { + $message = $exception->getMessage(); + } else { + $message = $response['message']; + } + self::assertSame('Invalid two-factor authorization token', $message); + } + } + + /** + * @magentoConfigFixture twofactorauth/general/force_providers duo_security + * @magentoApiDataFixture Magento/User/_files/user_with_custom_role.php + */ + public function testUnavailableProvider() + { + $userId = $this->getUserId(); + $token = $this->tokenManager->issueFor($userId); + $serviceInfo = $this->buildServiceInfo(); + + try { + $this->_webApiCall($serviceInfo, ['tfaToken' => $token, 'otp' => 'invalid']); + self::fail('Endpoint should have thrown an exception'); + } catch (\Throwable $exception) { + $response = json_decode($exception->getMessage(), true); + if (json_last_error()) { + $message = $exception->getMessage(); + } else { + $message = $response['message']; + } + self::assertSame('Provider is not allowed.', $message); + } + } + + /** + * @magentoConfigFixture twofactorauth/general/force_providers google + * @magentoApiDataFixture Magento/User/_files/user_with_custom_role.php + */ + public function testAlreadyActivatedProvider() + { + $userId = $this->getUserId(); + $token = $this->tokenManager->issueFor($userId); + $serviceInfo = $this->buildServiceInfo(); + $otp = $this->getUserOtp(); + $this->tfa->getProviderByCode(Google::CODE) + ->activate($userId); + + try { + $this->_webApiCall($serviceInfo, ['tfaToken' => $token, 'otp' => $otp]); + self::fail('Endpoint should have thrown an exception'); + } catch (\Throwable $exception) { + $response = json_decode($exception->getMessage(), true); + if (json_last_error()) { + $message = $exception->getMessage(); + } else { + $message = $response['message']; + } + self::assertSame('Provider is already configured.', $message); + } + } + + /** + * @magentoConfigFixture twofactorauth/general/force_providers google + * @magentoApiDataFixture Magento/User/_files/user_with_custom_role.php + * @magentoConfigFixture twofactorauth/google/otp_window 120 + */ + public function testActivate() + { + $userId = $this->getUserId(); + $token = $this->tokenManager->issueFor($userId); + $otp = $this->getUserOtp(); + $serviceInfo = $this->buildServiceInfo(); + + $response = $this->_webApiCall( + $serviceInfo, + [ + 'tfaToken' => $token, + 'otp' => $otp + ] + ); + self::assertEmpty($response); + } + + private function getUserOtp(): string + { + $user = $this->userFactory->create(); + $user->loadByUsername('customRoleUser'); + $totp = TOTP::create($this->google->getSecretCode($user)); + + return $totp->now(); + } + + /** + * @return array + */ + private function buildServiceInfo(): array + { + return [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH, + 'httpMethod' => Request::HTTP_METHOD_POST + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . self::OPERATION + ] + ]; + } + + private function getUserId(): int + { + $user = $this->userFactory->create(); + $user->loadByUsername('customRoleUser'); + + return (int)$user->getId(); + } +} diff --git a/TwoFactorAuth/Test/Api/GoogleAuthenticateTest.php b/TwoFactorAuth/Test/Api/GoogleAuthenticateTest.php new file mode 100644 index 00000000..8fd0cd8b --- /dev/null +++ b/TwoFactorAuth/Test/Api/GoogleAuthenticateTest.php @@ -0,0 +1,236 @@ +userFactory = $objectManager->get(UserFactory::class); + $this->google = $objectManager->get(Google::class); + $this->tfa = $objectManager->get(TfaInterface::class); + } + + /** + * @magentoApiDataFixture Magento/User/_files/user_with_custom_role.php + */ + public function testInvalidCredentials() + { + $serviceInfo = $this->buildServiceInfo(); + + try { + $this->_webApiCall( + $serviceInfo, + [ + 'username' => 'customRoleUser', + 'password' => 'bad', + 'otp' => 'foo' + ] + ); + self::fail('Endpoint should have thrown an exception'); + } catch (\Throwable $exception) { + $response = json_decode($exception->getMessage(), true); + if (json_last_error()) { + $message = $exception->getMessage(); + } else { + $message = $response['message']; + } + self::assertSame( + 'The account sign-in was incorrect or your account is disabled temporarily. ' + . 'Please wait and try again later.', + $message + ); + } + } + + /** + * @magentoConfigFixture twofactorauth/general/force_providers duo_security + * @magentoApiDataFixture Magento/User/_files/user_with_custom_role.php + */ + public function testUnavailableProvider() + { + $serviceInfo = $this->buildServiceInfo(); + + try { + $this->_webApiCall( + $serviceInfo, + [ + 'username' => 'customRoleUser', + 'password' => \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD, + 'otp' => 'foo' + ] + ); + self::fail('Endpoint should have thrown an exception'); + } catch (\Throwable $exception) { + $response = json_decode($exception->getMessage(), true); + if (json_last_error()) { + $message = $exception->getMessage(); + } else { + $message = $response['message']; + } + self::assertSame('Provider is not allowed.', $message); + } + } + + /** + * @magentoConfigFixture twofactorauth/general/force_providers google + * @magentoApiDataFixture Magento/User/_files/user_with_custom_role.php + */ + public function testInvalidToken() + { + $userId = $this->getUserId(); + $serviceInfo = $this->buildServiceInfo(); + $this->tfa->getProviderByCode(Google::CODE) + ->activate($userId); + + try { + $this->_webApiCall( + $serviceInfo, + [ + 'username' => 'customRoleUser', + 'password' => \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD, + 'otp' => 'bad' + ] + ); + self::fail('Endpoint should have thrown an exception'); + } catch (\Throwable $exception) { + $response = json_decode($exception->getMessage(), true); + if (json_last_error()) { + $message = $exception->getMessage(); + } else { + $message = $response['message']; + } + self::assertSame('Invalid code.', $message); + } + } + + /** + * @magentoConfigFixture twofactorauth/general/force_providers google + * @magentoApiDataFixture Magento/User/_files/user_with_custom_role.php + */ + public function testNotConfiguredProvider() + { + $userId = $this->getUserId(); + $serviceInfo = $this->buildServiceInfo(); + $this->tfa->getProviderByCode(Google::CODE) + ->resetConfiguration($userId); + + try { + $this->_webApiCall( + $serviceInfo, + [ + 'username' => 'customRoleUser', + 'password' => \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD, + 'otp' => 'foo' + ] + ); + self::fail('Endpoint should have thrown an exception'); + } catch (\Throwable $exception) { + $response = json_decode($exception->getMessage(), true); + if (json_last_error()) { + $message = $exception->getMessage(); + } else { + $message = $response['message']; + } + self::assertSame('Provider is not configured.', $message); + } + } + + /** + * @magentoConfigFixture twofactorauth/general/force_providers google + * @magentoApiDataFixture Magento/User/_files/user_with_custom_role.php + * @magentoConfigFixture twofactorauth/google/otp_window 120 + */ + public function testValidToken() + { + $userId = $this->getUserId(); + $otp = $this->getUserOtp(); + $serviceInfo = $this->buildServiceInfo(); + $this->tfa->getProviderByCode(Google::CODE) + ->activate($userId); + + $response = $this->_webApiCall( + $serviceInfo, + [ + 'username' => 'customRoleUser', + 'password' => \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD, + 'otp' => $otp + ] + ); + self::assertNotEmpty($response); + self::assertMatchesRegularExpression('/^[a-z0-9]{32}$/', $response); + } + + /** + * @return array + */ + private function buildServiceInfo(): array + { + return [ + 'rest' => [ + // Ensure the default auth is invalidated + 'token' => 'invalid', + 'resourcePath' => self::RESOURCE_PATH, + 'httpMethod' => Request::HTTP_METHOD_POST + ], + 'soap' => [ + // Ensure the default auth is invalidated + 'token' => 'invalid', + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . self::OPERATION + ] + ]; + } + + private function getUserId(): int + { + $user = $this->userFactory->create(); + $user->loadByUsername('customRoleUser'); + + return (int)$user->getId(); + } + + private function getUserOtp(): string + { + $user = $this->userFactory->create(); + $user->loadByUsername('customRoleUser'); + $totp = TOTP::create($this->google->getSecretCode($user)); + + return $totp->now(); + } +} diff --git a/TwoFactorAuth/Test/Api/GoogleConfigureTest.php b/TwoFactorAuth/Test/Api/GoogleConfigureTest.php new file mode 100644 index 00000000..e92c856c --- /dev/null +++ b/TwoFactorAuth/Test/Api/GoogleConfigureTest.php @@ -0,0 +1,162 @@ +userFactory = $objectManager->get(UserFactory::class); + $this->tokenManager = $objectManager->get(UserConfigTokenManagerInterface::class); + $this->tfa = $objectManager->get(TfaInterface::class); + } + + /** + * @magentoConfigFixture twofactorauth/general/force_providers google + * @magentoApiDataFixture Magento/User/_files/user_with_custom_role.php + */ + public function testInvalidTfat() + { + $serviceInfo = $this->buildServiceInfo(); + + try { + $this->_webApiCall($serviceInfo, ['tfaToken' => 'abc']); + self::fail('Endpoint should have thrown an exception'); + } catch (\Throwable $exception) { + $response = json_decode($exception->getMessage(), true); + if (json_last_error()) { + $message = $exception->getMessage(); + } else { + $message = $response['message']; + } + self::assertSame('Invalid two-factor authorization token', $message); + } + } + + /** + * @magentoConfigFixture twofactorauth/general/force_providers duo_security + * @magentoApiDataFixture Magento/User/_files/user_with_custom_role.php + */ + public function testUnavailableProvider() + { + $userId = $this->getUserId(); + $token = $this->tokenManager->issueFor($userId); + $serviceInfo = $this->buildServiceInfo(); + + try { + $this->_webApiCall($serviceInfo, ['tfaToken' => $token]); + self::fail('Endpoint should have thrown an exception'); + } catch (\Throwable $exception) { + $response = json_decode($exception->getMessage(), true); + if (json_last_error()) { + $message = $exception->getMessage(); + } else { + $message = $response['message']; + } + self::assertSame('Provider is not allowed.', $message); + } + } + + /** + * @magentoConfigFixture twofactorauth/general/force_providers google + * @magentoApiDataFixture Magento/User/_files/user_with_custom_role.php + */ + public function testAlreadyConfiguredProvider() + { + $userId = $this->getUserId(); + $token = $this->tokenManager->issueFor($userId); + $serviceInfo = $this->buildServiceInfo(); + $this->tfa->getProviderByCode(Google::CODE) + ->activate($userId); + + try { + $this->_webApiCall($serviceInfo, ['tfaToken' => $token]); + self::fail('Endpoint should have thrown an exception'); + } catch (\Throwable $exception) { + $response = json_decode($exception->getMessage(), true); + if (json_last_error()) { + $message = $exception->getMessage(); + } else { + $message = $response['message']; + } + self::assertSame('Provider is already configured.', $message); + } + } + + /** + * @magentoConfigFixture twofactorauth/general/force_providers google + * @magentoApiDataFixture Magento/User/_files/user_with_custom_role.php + */ + public function testValidRequest() + { + $userId = $this->getUserId(); + $token = $this->tokenManager->issueFor($userId); + $serviceInfo = $this->buildServiceInfo(); + + $response = $this->_webApiCall($serviceInfo, ['tfaToken' => $token]); + self::assertNotEmpty($response[GoogleConfigureData::QR_CODE_BASE64]); + self::assertMatchesRegularExpression('/^[a-zA-Z0-9+\/=]+$/', $response[GoogleConfigureData::QR_CODE_BASE64]); + self::assertNotEmpty($response['secret_code']); + } + + /** + * @return array + */ + private function buildServiceInfo(): array + { + return [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH, + 'httpMethod' => Request::HTTP_METHOD_POST + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . self::OPERATION + ] + ]; + } + + private function getUserId(): int + { + $user = $this->userFactory->create(); + $user->loadByUsername('customRoleUser'); + + return (int)$user->getId(); + } +} diff --git a/TwoFactorAuth/Test/Integration/Block/ChangeProviderTest.php b/TwoFactorAuth/Test/Integration/Block/ChangeProviderTest.php index eb24c216..95507988 100644 --- a/TwoFactorAuth/Test/Integration/Block/ChangeProviderTest.php +++ b/TwoFactorAuth/Test/Integration/Block/ChangeProviderTest.php @@ -42,7 +42,7 @@ class ChangeProviderTest extends TestCase */ private $tfa; - protected function setUp() + protected function setUp(): void { $objectManager = Bootstrap::getObjectManager(); $objectManager->configure([ @@ -105,7 +105,7 @@ public function testBlockRendersWhenCurrentProviderIsActivated(): void $this->block->setData('provider', 'authy'); $html = $this->block->toHtml(); - self::assertContains('id="tfa', $html); + self::assertStringContainsString('id="tfa', $html); } /** diff --git a/TwoFactorAuth/Test/Integration/Block/ConfigureLaterTest.php b/TwoFactorAuth/Test/Integration/Block/ConfigureLaterTest.php index f76b25a7..be1ea6c8 100644 --- a/TwoFactorAuth/Test/Integration/Block/ConfigureLaterTest.php +++ b/TwoFactorAuth/Test/Integration/Block/ConfigureLaterTest.php @@ -41,7 +41,7 @@ class ConfigureLaterTest extends TestCase */ private $tfa; - protected function setUp() + protected function setUp(): void { $objectManager = Bootstrap::getObjectManager(); $objectManager->configure([ @@ -94,7 +94,7 @@ public function testBlockRendersWithCurrentInactiveAndOneOtherActive(): void $this->block->setData('provider', 'duo_security'); $html = $this->block->toHtml(); - self::assertContains('id="tfa', $html); + self::assertStringContainsString('id="tfa', $html); } /** diff --git a/TwoFactorAuth/Test/Integration/Command/GoogleSecretTest.php b/TwoFactorAuth/Test/Integration/Command/GoogleSecretTest.php new file mode 100644 index 00000000..d3d9daa9 --- /dev/null +++ b/TwoFactorAuth/Test/Integration/Command/GoogleSecretTest.php @@ -0,0 +1,98 @@ +command = $objectManager->get(GoogleSecret::class); + $this->configManager = $objectManager->get(UserConfigManagerInterface::class); + $this->userFactory = $objectManager->get(UserFactory::class); + $this->consoleInput = $this->createMock(InputInterface::class); + $this->consoleOutput = $this->createMock(OutputInterface::class); + $this->google = $objectManager->get(Google::class); + } + + /** + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testSetSecret() + { + $user = $this->userFactory->create(); + $user->loadByUsername('adminUser'); + $userId = (int)$user->getId(); + + self::assertFalse( + $this->configManager->isProviderConfigurationActive( + $userId, + Google::CODE + ) + ); + $this->command->run( + new ArgvInput(['security:tfa:google:set-secret', 'adminUser', 'MFRGGZDF']), + $this->consoleOutput + ); + self::assertTrue( + $this->configManager->isProviderConfigurationActive( + $userId, + Google::CODE + ) + ); + self::assertSame( + 'MFRGGZDF', + $this->google->getSecretCode($user) + ); + } +} diff --git a/TwoFactorAuth/Test/Integration/Controller/Adminhtml/Authy/ConfigureTest.php b/TwoFactorAuth/Test/Integration/Controller/Adminhtml/Authy/ConfigureTest.php index 7bd4bd0f..c22245b8 100644 --- a/TwoFactorAuth/Test/Integration/Controller/Adminhtml/Authy/ConfigureTest.php +++ b/TwoFactorAuth/Test/Integration/Controller/Adminhtml/Authy/ConfigureTest.php @@ -1,4 +1,9 @@ expectedNoAccessResponseCode = 302; diff --git a/TwoFactorAuth/Test/Integration/Controller/Adminhtml/Google/ConfigureTest.php b/TwoFactorAuth/Test/Integration/Controller/Adminhtml/Google/ConfigureTest.php index 17055633..a82c6348 100644 --- a/TwoFactorAuth/Test/Integration/Controller/Adminhtml/Google/ConfigureTest.php +++ b/TwoFactorAuth/Test/Integration/Controller/Adminhtml/Google/ConfigureTest.php @@ -1,4 +1,9 @@ tfa = $this->_objectManager->get(TfaInterface::class); diff --git a/TwoFactorAuth/Test/Integration/Controller/Adminhtml/Tfa/ConfigureTest.php b/TwoFactorAuth/Test/Integration/Controller/Adminhtml/Tfa/ConfigureTest.php index fe0a6cb4..3331daec 100644 --- a/TwoFactorAuth/Test/Integration/Controller/Adminhtml/Tfa/ConfigureTest.php +++ b/TwoFactorAuth/Test/Integration/Controller/Adminhtml/Tfa/ConfigureTest.php @@ -1,6 +1,10 @@ getRequest() ->setQueryValue('tfat', $this->tokenManager->issueFor((int)$this->_session->getUser()->getId())); $this->dispatch($this->uri); - $this->assertRegExp('/google/', $this->getResponse()->getBody()); + self::assertMatchesRegularExpression('/google/', $this->getResponse()->getBody()); } /** diff --git a/TwoFactorAuth/Test/Integration/Controller/Adminhtml/Tfa/ConfigurepostTest.php b/TwoFactorAuth/Test/Integration/Controller/Adminhtml/Tfa/ConfigurepostTest.php index dd0ac7a6..bcd9cdee 100644 --- a/TwoFactorAuth/Test/Integration/Controller/Adminhtml/Tfa/ConfigurepostTest.php +++ b/TwoFactorAuth/Test/Integration/Controller/Adminhtml/Tfa/ConfigurepostTest.php @@ -1,4 +1,9 @@ dispatch($this->uri); $this->assertRedirect($this->stringContains('configure')); $this->assertEmpty($this->tfa->getForcedProviders()); - $this->assertSessionMessages($this->contains(__('Please select valid providers.')->render())); + $this->assertSessionMessages($this->containsEqual(__('Please select valid providers.')->render())); } /** diff --git a/TwoFactorAuth/Test/Integration/Controller/Adminhtml/Tfa/IndexTest.php b/TwoFactorAuth/Test/Integration/Controller/Adminhtml/Tfa/IndexTest.php index d0c05429..7cba3279 100644 --- a/TwoFactorAuth/Test/Integration/Controller/Adminhtml/Tfa/IndexTest.php +++ b/TwoFactorAuth/Test/Integration/Controller/Adminhtml/Tfa/IndexTest.php @@ -1,4 +1,9 @@ dispatch($this->uri); - $this->assertRegExp('/You need to configure Two\-Factor Authorization/', $this->getResponse()->getBody()); + self::assertMatchesRegularExpression( + '/You need to configure Two\-Factor Authorization/', + $this->getResponse()->getBody() + ); } /** @@ -64,20 +71,22 @@ public function testAppConfigRequested(): void public function testUserConfigRequested(): void { $this->dispatch($this->uri); - $this->assertRegExp('/You need to configure Two\-Factor Authorization/', $this->getResponse()->getBody()); + self::assertMatchesRegularExpression( + '/You need to configure Two\-Factor Authorization/', + $this->getResponse()->getBody() + ); } - /** * Verify that 2FA config is not requested when 2FA is configured. * * @return void * @magentoConfigFixture default/twofactorauth/general/force_providers google * @magentoDbIsolation enabled - * @expectedException \Magento\Framework\Exception\AuthorizationException */ public function testNotRequested(): void { + $this->expectException(\Magento\Framework\Exception\AuthorizationException::class); $this->tfa->getProvider(Google::CODE)->activate((int)$this->_session->getUser()->getId()); $this->dispatch($this->uri); } diff --git a/TwoFactorAuth/Test/Integration/Controller/Adminhtml/U2f/ConfigureTest.php b/TwoFactorAuth/Test/Integration/Controller/Adminhtml/U2f/ConfigureTest.php index bbf35b4b..2dbe3e89 100644 --- a/TwoFactorAuth/Test/Integration/Controller/Adminhtml/U2f/ConfigureTest.php +++ b/TwoFactorAuth/Test/Integration/Controller/Adminhtml/U2f/ConfigureTest.php @@ -1,4 +1,9 @@ objectManager = Bootstrap::getObjectManager(); + $this->soapController = $this->objectManager->get(Soap::class); + } + + /** + * Get the public wsdl with anonymous credentials + * + * @return void + */ + public function testDispatchWsdlRequest(): void + { + $request = $this->objectManager->get(Request::class); + $request->setParam(Server::REQUEST_PARAM_LIST_WSDL, true); + $response = $this->soapController->dispatch($request); + $decodedWsdl = json_decode($response->getContent(), true); + + $this->assertWsdlServices($decodedWsdl); + } + + /** + * Check wsdl available methods. + * + * @param array $decodedWsdl + * + * @return void + */ + protected function assertWsdlServices(array $decodedWsdl): void + { + $this->assertArrayHasKey('customerAccountManagementV1', $decodedWsdl); + $this->assertArrayNotHasKey('integrationAdminTokenServiceV1', $decodedWsdl); + $this->assertArrayHasKey('twoFactorAuthAdminTokenServiceV1', $decodedWsdl); + } +} diff --git a/TwoFactorAuth/Test/Integration/ControllerActionPredispatchTest.php b/TwoFactorAuth/Test/Integration/ControllerActionPredispatchTest.php index 77489e9a..a52d473f 100644 --- a/TwoFactorAuth/Test/Integration/ControllerActionPredispatchTest.php +++ b/TwoFactorAuth/Test/Integration/ControllerActionPredispatchTest.php @@ -1,4 +1,9 @@ dispatch('backend/admin/user/'); //Authenticated user with 2FA configured and completed is taken to the Users page as requested. - $this->assertRegExp('/' .$this->_session->getUser()->getUserName() .'/i', $this->getResponse()->getBody()); + self::assertMatchesRegularExpression( + '/' .$this->_session->getUser()->getUserName() .'/i', + $this->getResponse()->getBody() + ); } /** @@ -89,7 +97,7 @@ public function testUnauthenticated(): void $this->getRequest()->setDispatched(false); $this->getRequest()->setUri($properUrl); $this->dispatch($properUrl); - $this->assertContains('Welcome, please sign in', $this->getResponse()->getBody()); + $this->assertStringContainsString('Welcome, please sign in', $this->getResponse()->getBody()); } /** diff --git a/TwoFactorAuth/Test/Integration/Model/Provider/Engine/Authy/AuthenticateTest.php b/TwoFactorAuth/Test/Integration/Model/Provider/Engine/Authy/AuthenticateTest.php new file mode 100644 index 00000000..28478822 --- /dev/null +++ b/TwoFactorAuth/Test/Integration/Model/Provider/Engine/Authy/AuthenticateTest.php @@ -0,0 +1,323 @@ +tfa = $objectManager->get(TfaInterface::class); + $this->authy = $this->createMock(Authy::class); + $this->userFactory = $objectManager->get(UserFactory::class); + $this->authyToken = $this->createMock(Token::class); + $this->onetouch = $this->createMock(OneTouch::class); + $this->model = $objectManager->create( + Authenticate::class, + [ + 'authy' => $this->authy, + 'authyToken' => $this->authyToken, + 'oneTouch' => $this->onetouch + ] + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers authy + * @magentoConfigFixture default/twofactorauth/authy/api_key abc + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testAuthenticateInvalidCredentials() + { + $this->expectException(\Magento\Framework\Exception\AuthenticationException::class); + $this->tfa->getProviderByCode(Authy::CODE) + ->activate($this->getUserId()); + $this->authy + ->expects($this->never()) + ->method('verify'); + $this->model->createAdminAccessTokenWithCredentials( + 'adminUser', + 'bad', + 'abc' + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers authy + * @magentoConfigFixture default/twofactorauth/authy/api_key abc + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testAuthenticateNotConfiguredProvider() + { + $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectExceptionMessage('Provider is not configured.'); + $this->authy + ->expects($this->never()) + ->method('verify'); + $this->model->createAdminAccessTokenWithCredentials( + 'adminUser', + Bootstrap::ADMIN_PASSWORD, + 'abc' + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers duo_security + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testAuthenticateUnavailableProvider() + { + $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectExceptionMessage('Provider is not allowed.'); + $this->authy + ->expects($this->never()) + ->method('verify'); + $this->model->createAdminAccessTokenWithCredentials( + 'adminUser', + Bootstrap::ADMIN_PASSWORD, + 'abc' + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers authy + * @magentoConfigFixture default/twofactorauth/authy/api_key abc + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testAuthenticateValidRequest() + { + $this->tfa->getProviderByCode(Authy::CODE) + ->activate($this->getUserId()); + $userId = $this->getUserId(); + $this->authy + ->expects($this->once()) + ->method('verify') + ->with( + $this->callback(function ($value) use ($userId) { + return (int)$value->getId() === $userId; + }), + $this->callback(function ($value) { + return $value->getData('tfa_code') === 'abc'; + }) + ); + $result = $this->model->createAdminAccessTokenWithCredentials( + 'adminUser', + Bootstrap::ADMIN_PASSWORD, + 'abc' + ); + + self::assertMatchesRegularExpression('/^[a-z0-9]{32}$/', $result); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers authy + * @magentoConfigFixture default/twofactorauth/authy/api_key abc + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testSendTokenInvalidCredentials() + { + $this->expectException(\Magento\Framework\Exception\AuthenticationException::class); + $this->tfa->getProviderByCode(Authy::CODE) + ->activate($this->getUserId()); + $this->authy + ->expects($this->never()) + ->method('verify'); + $this->model->sendToken( + 'adminUser', + 'bad', + 'sms' + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers authy + * @magentoConfigFixture default/twofactorauth/authy/api_key abc + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testSendTokenNotConfiguredProvider() + { + $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectExceptionMessage('Provider is not configured.'); + $this->authy + ->expects($this->never()) + ->method('verify'); + $this->model->sendToken( + 'adminUser', + Bootstrap::ADMIN_PASSWORD, + 'sms' + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers duo_security + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testSendTokenUnavailableProvider() + { + $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectExceptionMessage('Provider is not allowed.'); + $this->authy + ->expects($this->never()) + ->method('verify'); + $this->model->sendToken( + 'adminUser', + Bootstrap::ADMIN_PASSWORD, + 'sms' + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers authy + * @magentoConfigFixture default/twofactorauth/authy/api_key abc + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testSendTokenValidRequest() + { + $this->tfa->getProviderByCode(Authy::CODE) + ->activate($this->getUserId()); + $userId = $this->getUserId(); + $this->authyToken + ->expects($this->once()) + ->method('request') + ->with( + $this->callback(function ($value) use ($userId) { + return (int)$value->getId() === $userId; + }), + 'a method' + ); + $this->model->sendToken( + 'adminUser', + Bootstrap::ADMIN_PASSWORD, + 'a method' + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers authy + * @magentoConfigFixture default/twofactorauth/authy/api_key abc + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testSendTokenValidRequestWithOneTouch() + { + $this->tfa->getProviderByCode(Authy::CODE) + ->activate($this->getUserId()); + $userId = $this->getUserId(); + $this->onetouch + ->expects($this->once()) + ->method('request') + ->with( + $this->callback(function ($value) use ($userId) { + return (int)$value->getId() === $userId; + }) + ); + $this->model->sendToken( + 'adminUser', + Bootstrap::ADMIN_PASSWORD, + 'onetouch' + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers authy + * @magentoConfigFixture default/twofactorauth/authy/api_key abc + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testCreateTokenWithOneTouch() + { + $this->tfa->getProviderByCode(Authy::CODE) + ->activate($this->getUserId()); + $userId = $this->getUserId(); + $this->onetouch + ->method('verify') + ->with( + $this->callback(function ($value) use ($userId) { + return (int)$value->getId() === $userId; + }) + ) + ->willReturn('approved'); + $result = $this->model->creatAdminAccessTokenWithOneTouch( + 'adminUser', + Bootstrap::ADMIN_PASSWORD + ); + + self::assertMatchesRegularExpression('/^[a-z0-9]{32}$/', $result); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers authy + * @magentoConfigFixture default/twofactorauth/authy/api_key abc + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testCreateTokenWithOneTouchError() + { + $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectExceptionMessage('Onetouch prompt was denied or timed out.'); + $this->tfa->getProviderByCode(Authy::CODE) + ->activate($this->getUserId()); + $this->onetouch + ->method('verify') + ->willReturn('denied'); + $this->model->creatAdminAccessTokenWithOneTouch( + 'adminUser', + Bootstrap::ADMIN_PASSWORD + ); + } + + private function getUserId(): int + { + $user = $this->userFactory->create(); + $user->loadByUsername('adminUser'); + + return (int)$user->getId(); + } +} diff --git a/TwoFactorAuth/Test/Integration/Model/Provider/Engine/Authy/ConfigureTest.php b/TwoFactorAuth/Test/Integration/Model/Provider/Engine/Authy/ConfigureTest.php new file mode 100644 index 00000000..a4f1ff69 --- /dev/null +++ b/TwoFactorAuth/Test/Integration/Model/Provider/Engine/Authy/ConfigureTest.php @@ -0,0 +1,314 @@ +verification = $this->createMock(Verification::class); + $this->userFactory = $objectManager->get(UserFactory::class); + $this->deviceDataFactory = $objectManager->get(AuthyDeviceInterfaceFactory::class); + $this->tokenManager = $objectManager->get(UserConfigTokenManagerInterface::class); + $this->tfa = $objectManager->get(TfaInterface::class); + $this->authy = $this->createMock(Authy::class); + $this->model = $objectManager->create( + Configure::class, + [ + 'verification' => $this->verification, + 'authy' => $this->authy + ] + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers authy + * @magentoConfigFixture default/twofactorauth/authy/api_key abc + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testConfigureInvalidTfat() + { + $this->expectException(\Magento\Framework\Exception\AuthorizationException::class); + $this->expectExceptionMessage('Invalid two-factor authorization token'); + $this->verification + ->expects($this->never()) + ->method('request'); + $this->model->sendDeviceRegistrationPrompt( + 'abc', + $this->deviceDataFactory->create( + [ + 'data' => [ + AuthyDeviceInterface::COUNTRY => '1', + AuthyDeviceInterface::PHONE => '555-555-5555', + AuthyDeviceInterface::METHOD => AuthyDeviceInterface::METHOD_SMS, + ] + ] + ) + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers authy + * @magentoConfigFixture default/twofactorauth/authy/api_key abc + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testConfigureAlreadyConfiguredProvider() + { + $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectExceptionMessage('Provider is already configured.'); + $userId = $this->getUserId(); + $this->tfa->getProviderByCode(Authy::CODE) + ->activate($userId); + $this->verification + ->expects($this->never()) + ->method('request'); + $this->model->sendDeviceRegistrationPrompt( + $this->tokenManager->issueFor($userId), + $this->deviceDataFactory->create( + [ + 'data' => [ + AuthyDeviceInterface::COUNTRY => '1', + AuthyDeviceInterface::PHONE => '555-555-5555', + AuthyDeviceInterface::METHOD => AuthyDeviceInterface::METHOD_SMS, + ] + ] + ) + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers duo_security + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testConfigureUnavailableProvider() + { + $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectExceptionMessage('Provider is not allowed.'); + $userId = $this->getUserId(); + $this->verification + ->expects($this->never()) + ->method('request'); + $this->model->sendDeviceRegistrationPrompt( + $this->tokenManager->issueFor($userId), + $this->deviceDataFactory->create( + [ + 'data' => [ + AuthyDeviceInterface::COUNTRY => '1', + AuthyDeviceInterface::PHONE => '555-555-5555', + AuthyDeviceInterface::METHOD => AuthyDeviceInterface::METHOD_SMS, + ] + ] + ) + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers authy + * @magentoConfigFixture default/twofactorauth/authy/api_key abc + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testConfigureValidRequest() + { + $userId = $this->getUserId(); + + $this->verification + ->method('request') + ->with( + $this->callback(function ($value) use ($userId) { + return (int)$value->getId() === $userId; + }), + '4', + '555-555-5555', + AuthyDeviceInterface::METHOD_SMS, + $this->anything() + ) + ->willReturnCallback( + function ($userId, $country, $phone, $method, &$response) { + // These keys come from authy api not our model + $response['message'] = 'foo'; + $response['seconds_to_expire'] = 123; + // Fix static errors + unset($userId, $country, $phone, $method); + } + ); + + $result = $this->model->sendDeviceRegistrationPrompt( + $this->tokenManager->issueFor($userId), + $this->deviceDataFactory->create( + [ + 'data' => [ + AuthyDeviceInterface::COUNTRY => '4', + AuthyDeviceInterface::PHONE => '555-555-5555', + AuthyDeviceInterface::METHOD => AuthyDeviceInterface::METHOD_SMS, + ] + ] + ) + ); + + self::assertSame('foo', $result->getMessage()); + self::assertSame(123, $result->getExpirationSeconds()); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers authy + * @magentoConfigFixture default/twofactorauth/authy/api_key abc + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testActivateInvalidTfat() + { + $this->expectException(\Magento\Framework\Exception\AuthorizationException::class); + $this->expectExceptionMessage('Invalid two-factor authorization token'); + $this->verification + ->expects($this->never()) + ->method('request'); + $this->authy + ->expects($this->never()) + ->method('enroll'); + $this->model->activate( + 'abc', + 'abc' + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers authy + * @magentoConfigFixture default/twofactorauth/authy/api_key abc + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testActivateAlreadyConfiguredProvider() + { + $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectExceptionMessage('Provider is already configured.'); + $userId = $this->getUserId(); + $this->tfa->getProviderByCode(Authy::CODE) + ->activate($userId); + $this->authy + ->expects($this->never()) + ->method('enroll'); + $this->verification + ->expects($this->never()) + ->method('request'); + $this->model->activate( + $this->tokenManager->issueFor($userId), + 'abc' + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers duo_security + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testActivateUnavailableProvider() + { + $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectExceptionMessage('Provider is not allowed.'); + $userId = $this->getUserId(); + $this->authy + ->expects($this->never()) + ->method('enroll'); + $this->verification + ->expects($this->never()) + ->method('request'); + $this->model->activate( + $this->tokenManager->issueFor($userId), + 'abc' + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers authy + * @magentoConfigFixture default/twofactorauth/authy/api_key abc + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testActivateValidRequest() + { + $userId = $this->getUserId(); + $this->verification + ->method('verify') + ->with( + $this->callback(function ($value) use ($userId) { + return (int)$value->getId() === $userId; + }), + 'cba' + ); + $this->authy + ->expects($this->once()) + ->method('enroll') + ->with( + $this->callback(function ($value) use ($userId) { + return (int)$value->getId() === $userId; + }) + ); + $this->model->activate( + $this->tokenManager->issueFor($userId), + 'cba' + ); + + // Mock enroll call above is assertion of activation + } + + private function getUserId(): int + { + $user = $this->userFactory->create(); + $user->loadByUsername('adminUser'); + + return (int)$user->getId(); + } +} diff --git a/TwoFactorAuth/Test/Integration/Model/Provider/Engine/DuoSecurity/AuthenticateTest.php b/TwoFactorAuth/Test/Integration/Model/Provider/Engine/DuoSecurity/AuthenticateTest.php new file mode 100644 index 00000000..0328a383 --- /dev/null +++ b/TwoFactorAuth/Test/Integration/Model/Provider/Engine/DuoSecurity/AuthenticateTest.php @@ -0,0 +1,298 @@ +userFactory = $objectManager->get(UserFactory::class); + $this->tokenManager = $objectManager->get(UserConfigTokenManagerInterface::class); + $this->tfa = $objectManager->get(TfaInterface::class); + $this->duo = $this->createMock(DuoSecurity::class); + $this->model = $objectManager->create( + Authenticate::class, + [ + 'duo' => $this->duo, + ] + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers duo_security + * @magentoConfigFixture default/twofactorauth/duo/integration_key abc123 + * @magentoConfigFixture default/twofactorauth/duo/api_hostname abc123 + * @magentoConfigFixture default/twofactorauth/duo/secret_key abc123 + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testGetAuthenticateDataInvalidCredentials() + { + $this->expectException(\Magento\Framework\Exception\AuthenticationException::class); + $this->tfa->getProviderByCode(DuoSecurity::CODE) + ->activate($this->getUserId()); + $this->duo + ->expects($this->never()) + ->method('getRequestSignature'); + $this->model->getAuthenticateData( + 'adminUser', + 'abc' + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers duo_security + * @magentoConfigFixture default/twofactorauth/duo/integration_key abc123 + * @magentoConfigFixture default/twofactorauth/duo/api_hostname abc123 + * @magentoConfigFixture default/twofactorauth/duo/secret_key abc123 + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testGetAuthenticateDataNotConfiguredProvider() + { + $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectExceptionMessage('Provider is not configured.'); + $userId = $this->getUserId(); + $this->tfa->getProviderByCode(DuoSecurity::CODE) + ->resetConfiguration($userId); + + $this->duo + ->expects($this->never()) + ->method('getRequestSignature'); + $this->model->getAuthenticateData( + 'adminUser', + Bootstrap::ADMIN_PASSWORD + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers authy + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testGetAuthenticateDataUnavailableProvider() + { + $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectExceptionMessage('Provider is not allowed.'); + $this->duo + ->expects($this->never()) + ->method('getRequestSignature'); + $this->model->getAuthenticateData( + 'adminUser', + Bootstrap::ADMIN_PASSWORD + ); + } + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers duo_security + * @magentoConfigFixture default/twofactorauth/duo/integration_key abc123 + * @magentoConfigFixture default/twofactorauth/duo/api_hostname abc123 + * @magentoConfigFixture default/twofactorauth/duo/secret_key abc123 + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testVerifyInvalidCredentials() + { + $this->expectException(\Magento\Framework\Exception\AuthenticationException::class); + $this->tfa->getProviderByCode(DuoSecurity::CODE) + ->activate($this->getUserId()); + $this->duo + ->expects($this->never()) + ->method('getRequestSignature'); + $this->model->createAdminAccessTokenWithCredentials( + 'adminUser', + 'abc', + 'signature' + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers duo_security + * @magentoConfigFixture default/twofactorauth/duo/integration_key abc123 + * @magentoConfigFixture default/twofactorauth/duo/api_hostname abc123 + * @magentoConfigFixture default/twofactorauth/duo/secret_key abc123 + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testVerifyNotConfiguredProvider() + { + $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectExceptionMessage('Provider is not configured.'); + $userId = $this->getUserId(); + $this->tfa->getProviderByCode(DuoSecurity::CODE) + ->resetConfiguration($userId); + + $this->duo + ->expects($this->never()) + ->method('getRequestSignature'); + $this->model->createAdminAccessTokenWithCredentials( + 'adminUser', + Bootstrap::ADMIN_PASSWORD, + 'signature' + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers authy + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testVerifyUnavailableProvider() + { + $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectExceptionMessage('Provider is not allowed.'); + $this->duo + ->expects($this->never()) + ->method('getRequestSignature'); + $this->model->createAdminAccessTokenWithCredentials( + 'adminUser', + Bootstrap::ADMIN_PASSWORD, + 'signature' + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers duo_security + * @magentoConfigFixture default/twofactorauth/duo/integration_key abc123 + * @magentoConfigFixture default/twofactorauth/duo/api_hostname abc123 + * @magentoConfigFixture default/twofactorauth/duo/secret_key abc123 + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testGetAuthenticateDataValidRequest() + { + $userId = $this->getUserId(); + + $this->tfa->getProviderByCode(DuoSecurity::CODE) + ->activate($userId); + + $this->duo + ->method('getApiHostname') + ->willReturn('abc'); + $this->duo + ->method('getRequestSignature') + ->with( + $this->callback(function ($value) use ($userId) { + return (int)$value->getId() === $userId; + }) + ) + ->willReturn('cba'); + + $result = $this->model->getAuthenticateData( + 'adminUser', + Bootstrap::ADMIN_PASSWORD + ); + + self::assertInstanceOf(DuoDataInterface::class, $result); + self::assertSame('abc', $result->getApiHostname()); + self::assertSame('cba', $result->getSignature()); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers duo_security + * @magentoConfigFixture default/twofactorauth/duo/integration_key abc123 + * @magentoConfigFixture default/twofactorauth/duo/api_hostname abc123 + * @magentoConfigFixture default/twofactorauth/duo/secret_key abc123 + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testVerifyValidRequest() + { + $userId = $this->getUserId(); + $this->tfa->getProviderByCode(DuoSecurity::CODE) + ->activate($userId); + + $signature = 'a signature'; + $this->duo->method('verify') + ->with( + $this->callback(function ($value) use ($userId) { + return (int)$value->getId() === $userId; + }), + $this->callback(function ($value) use ($signature) { + return $value->getData('sig_response') === $signature; + }) + ) + ->willReturn(true); + + $token = $this->model->createAdminAccessTokenWithCredentials( + 'adminUser', + Bootstrap::ADMIN_PASSWORD, + $signature + ); + + self::assertMatchesRegularExpression('/^[a-z0-9]{32}$/', $token); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers duo_security + * @magentoConfigFixture default/twofactorauth/duo/integration_key abc123 + * @magentoConfigFixture default/twofactorauth/duo/api_hostname abc123 + * @magentoConfigFixture default/twofactorauth/duo/secret_key abc123 + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testVerifyInvalidRequest() + { + $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectExceptionMessage('Invalid response'); + $userId = $this->getUserId(); + $this->tfa->getProviderByCode(DuoSecurity::CODE) + ->activate($userId); + + $signature = 'a signature'; + $this->duo->method('verify') + ->willReturn(false); + + $token = $this->model->createAdminAccessTokenWithCredentials( + 'adminUser', + Bootstrap::ADMIN_PASSWORD, + $signature + ); + + self::assertEmpty($token); + } + + private function getUserId(): int + { + $user = $this->userFactory->create(); + $user->loadByUsername('adminUser'); + + return (int)$user->getId(); + } +} diff --git a/TwoFactorAuth/Test/Integration/Model/Provider/Engine/DuoSecurity/ConfigureTest.php b/TwoFactorAuth/Test/Integration/Model/Provider/Engine/DuoSecurity/ConfigureTest.php new file mode 100644 index 00000000..4a306f70 --- /dev/null +++ b/TwoFactorAuth/Test/Integration/Model/Provider/Engine/DuoSecurity/ConfigureTest.php @@ -0,0 +1,287 @@ +userFactory = $objectManager->get(UserFactory::class); + $this->tokenManager = $objectManager->get(UserConfigTokenManagerInterface::class); + $this->tfa = $objectManager->get(TfaInterface::class); + $this->duo = $this->createMock(DuoSecurity::class); + $this->authenticate = $this->createMock(Authenticate::class); + $this->model = $objectManager->create( + Configure::class, + [ + 'duo' => $this->duo, + 'authenticate' => $this->authenticate + ] + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers duo_security + * @magentoConfigFixture default/twofactorauth/duo/integration_key abc123 + * @magentoConfigFixture default/twofactorauth/duo/api_hostname abc123 + * @magentoConfigFixture default/twofactorauth/duo/secret_key abc123 + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testGetConfigurationDataInvalidTfat() + { + $this->expectException(\Magento\Framework\Exception\AuthorizationException::class); + $this->expectExceptionMessage('Invalid two-factor authorization token'); + $this->duo + ->expects($this->never()) + ->method('getRequestSignature'); + $this->model->getConfigurationData( + 'abc' + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers duo_security + * @magentoConfigFixture default/twofactorauth/duo/integration_key abc123 + * @magentoConfigFixture default/twofactorauth/duo/api_hostname abc123 + * @magentoConfigFixture default/twofactorauth/duo/secret_key abc123 + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testGetConfigurationDataAlreadyConfiguredProvider() + { + $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectExceptionMessage('Provider is already configured.'); + $userId = $this->getUserId(); + $this->tfa->getProviderByCode(DuoSecurity::CODE) + ->activate($userId); + + $this->duo + ->expects($this->never()) + ->method('getRequestSignature'); + $this->model->getConfigurationData( + $this->tokenManager->issueFor($userId) + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers authy + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testGetConfigurationDataUnavailableProvider() + { + $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectExceptionMessage('Provider is not allowed.'); + $this->duo + ->expects($this->never()) + ->method('getRequestSignature'); + $this->model->getConfigurationData( + $this->tokenManager->issueFor($this->getUserId()) + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers duo_security + * @magentoConfigFixture default/twofactorauth/duo/integration_key abc123 + * @magentoConfigFixture default/twofactorauth/duo/api_hostname abc123 + * @magentoConfigFixture default/twofactorauth/duo/secret_key abc123 + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testActivateInvalidTfat() + { + $this->expectException(\Magento\Framework\Exception\AuthorizationException::class); + $this->expectExceptionMessage('Invalid two-factor authorization token'); + $this->duo + ->expects($this->never()) + ->method('getRequestSignature'); + $this->model->activate( + 'abc', + 'something' + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers duo_security + * @magentoConfigFixture default/twofactorauth/duo/integration_key abc123 + * @magentoConfigFixture default/twofactorauth/duo/api_hostname abc123 + * @magentoConfigFixture default/twofactorauth/duo/secret_key abc123 + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testActivateAlreadyConfiguredProvider() + { + $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectExceptionMessage('Provider is already configured.'); + $userId = $this->getUserId(); + $this->tfa->getProviderByCode(DuoSecurity::CODE) + ->activate($userId); + $this->duo + ->expects($this->never()) + ->method('getRequestSignature'); + $this->model->activate( + $this->tokenManager->issueFor($userId), + 'something' + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers authy + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testActivateUnavailableProvider() + { + $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectExceptionMessage('Provider is not allowed.'); + $userId = $this->getUserId(); + $this->duo + ->expects($this->never()) + ->method('getRequestSignature'); + $this->model->activate( + $this->tokenManager->issueFor($userId), + 'something' + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers duo_security + * @magentoConfigFixture default/twofactorauth/duo/integration_key abc123 + * @magentoConfigFixture default/twofactorauth/duo/api_hostname abc123 + * @magentoConfigFixture default/twofactorauth/duo/secret_key abc123 + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testGetConfigurationDataValidRequest() + { + $userId = $this->getUserId(); + + $this->duo + ->method('getApiHostname') + ->willReturn('abc'); + $this->duo + ->method('getRequestSignature') + ->with( + $this->callback(function ($value) use ($userId) { + return (int)$value->getId() === $userId; + }) + ) + ->willReturn('cba'); + + $result = $this->model->getConfigurationData( + $this->tokenManager->issueFor($userId) + ); + + self::assertInstanceOf(DuoDataInterface::class, $result); + self::assertSame('abc', $result->getApiHostname()); + self::assertSame('cba', $result->getSignature()); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers duo_security + * @magentoConfigFixture default/twofactorauth/duo/integration_key abc123 + * @magentoConfigFixture default/twofactorauth/duo/api_hostname abc123 + * @magentoConfigFixture default/twofactorauth/duo/secret_key abc123 + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testActivateValidRequest() + { + $userId = $this->getUserId(); + $tfat = $this->tokenManager->issueFor($userId); + + $signature = 'a signature'; + $this->authenticate->method('assertResponseIsValid') + ->with( + $this->callback(function ($value) use ($userId) { + return (int)$value->getId() === $userId; + }), + $signature + ); + + $this->model->activate($tfat, $signature); + + self::assertTrue($this->tfa->getProviderByCode(DuoSecurity::CODE)->isActive($userId)); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers duo_security + * @magentoConfigFixture default/twofactorauth/duo/integration_key abc123 + * @magentoConfigFixture default/twofactorauth/duo/api_hostname abc123 + * @magentoConfigFixture default/twofactorauth/duo/secret_key abc123 + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testActivateInvalidDataThrowsException() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Something'); + $userId = $this->getUserId(); + $tfat = $this->tokenManager->issueFor($userId); + + $signature = 'a signature'; + $this->authenticate->method('assertResponseIsValid') + ->with( + $this->callback(function ($value) use ($userId) { + return (int)$value->getId() === $userId; + }), + $signature + ) + ->willThrowException(new \InvalidArgumentException('Something')); + + $result = $this->model->activate($tfat, $signature); + + self::assertEmpty($result); + } + + private function getUserId(): int + { + $user = $this->userFactory->create(); + $user->loadByUsername('adminUser'); + + return (int)$user->getId(); + } +} diff --git a/TwoFactorAuth/Test/Integration/Model/Provider/Engine/U2fKey/AuthenticateTest.php b/TwoFactorAuth/Test/Integration/Model/Provider/Engine/U2fKey/AuthenticateTest.php new file mode 100644 index 00000000..ca912912 --- /dev/null +++ b/TwoFactorAuth/Test/Integration/Model/Provider/Engine/U2fKey/AuthenticateTest.php @@ -0,0 +1,280 @@ +tfa = $objectManager->get(TfaInterface::class); + $this->u2fkey = $this->createMock(U2fKey::class); + $this->userFactory = $objectManager->get(UserFactory::class); + $this->model = $objectManager->create( + Authenticate::class, + [ + 'u2fKey' => $this->u2fkey + ] + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers u2fkey + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testGetDataInvalidCredentials() + { + $this->expectException(\Magento\Framework\Exception\AuthenticationException::class); + $this->tfa->getProviderByCode(U2fKey::CODE) + ->activate($this->getUserId()); + $this->u2fkey + ->expects($this->never()) + ->method('getAuthenticateData'); + $this->model->getAuthenticationData( + 'adminUser', + 'bad' + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers u2fkey + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testGetDataNotConfiguredProvider() + { + $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectExceptionMessage('Provider is not configured.'); + $this->u2fkey + ->expects($this->never()) + ->method('getAuthenticateData'); + $this->model->getAuthenticationData( + 'adminUser', + Bootstrap::ADMIN_PASSWORD + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers duo_security + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testGetDataUnavailableProvider() + { + $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectExceptionMessage('Provider is not allowed.'); + $this->u2fkey + ->expects($this->never()) + ->method('getAuthenticateData'); + $this->model->getAuthenticationData( + 'adminUser', + Bootstrap::ADMIN_PASSWORD + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers u2fkey + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testVerifyInvalidCredentials() + { + $this->expectException(\Magento\Framework\Exception\AuthenticationException::class); + $this->tfa->getProviderByCode(U2fKey::CODE) + ->activate($this->getUserId()); + $this->u2fkey + ->expects($this->never()) + ->method('verify'); + $this->u2fkey->method('getAuthenticateData') + ->willReturn(['credentialRequestOptions' => ['challenge' => [1, 2, 3]]]); + $this->model->getAuthenticationData('adminUser', Bootstrap::ADMIN_PASSWORD); + $this->model->createAdminAccessToken( + 'adminUser', + 'bad', + 'I identify as JSON' + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers u2fkey + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testVerifyNotConfiguredProvider() + { + $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectExceptionMessage('Provider is not configured.'); + $this->u2fkey + ->expects($this->never()) + ->method('verify'); + $this->model->createAdminAccessToken( + 'adminUser', + Bootstrap::ADMIN_PASSWORD, + 'I identify as JSON' + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers duo_security + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testVerifyUnavailableProvider() + { + $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectExceptionMessage('Provider is not allowed.'); + $this->u2fkey + ->expects($this->never()) + ->method('verify'); + $this->model->createAdminAccessToken( + 'adminUser', + Bootstrap::ADMIN_PASSWORD, + 'I identify as JSON' + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers u2fkey + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testGetDataValidRequest() + { + $this->tfa->getProviderByCode(U2fKey::CODE) + ->activate($this->getUserId()); + $userId = $this->getUserId(); + + $data = ['credentialRequestOptions' => ['challenge' => [1, 2, 3]]]; + $this->u2fkey + ->expects($this->once()) + ->method('getAuthenticateData') + ->with( + $this->callback(function ($value) use ($userId) { + return (int)$value->getId() === $userId; + }) + ) + ->willReturn($data); + + $result = $this->model->getAuthenticationData( + 'adminUser', + Bootstrap::ADMIN_PASSWORD + ); + + self::assertInstanceOf(U2fWebAuthnRequestInterface::class, $result); + self::assertSame(json_encode($data), $result->getCredentialRequestOptionsJson()); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers u2fkey + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testVerifyValidRequest() + { + $this->tfa->getProviderByCode(U2fKey::CODE) + ->activate($this->getUserId()); + $userId = $this->getUserId(); + + $this->u2fkey + ->expects($this->once()) + ->method('getAuthenticateData') + ->with( + $this->callback(function ($value) use ($userId) { + return (int)$value->getId() === $userId; + }) + ) + ->willReturn(['credentialRequestOptions' => ['challenge' => [3, 2, 1]]]); + $this->model->getAuthenticationData('adminUser', Bootstrap::ADMIN_PASSWORD); + + $verifyData = ['foo' => 'bar']; + $this->u2fkey + ->expects($this->once()) + ->method('verify') + ->with( + $this->callback(function ($value) use ($userId) { + return (int)$value->getId() === $userId; + }), + $this->callback(function ($data) use ($verifyData) { + return $data->getData('publicKeyCredential') === $verifyData + // Assert the previously issued challenge is used for verification + && $data->getData('originalChallenge') === [3, 2, 1]; + }) + ); + + $token = $this->model->createAdminAccessToken( + 'adminUser', + Bootstrap::ADMIN_PASSWORD, + json_encode($verifyData) + ); + self::assertMatchesRegularExpression('/^[a-z0-9]{32}$/', $token); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers u2fkey + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testVerifyThrowsExceptionRequest() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Something'); + $this->tfa->getProviderByCode(U2fKey::CODE) + ->activate($this->getUserId()); + + $this->u2fkey + ->method('getAuthenticateData') + ->willReturn(['credentialRequestOptions' => ['challenge' => [4, 5, 6]]]); + $this->model->getAuthenticationData('adminUser', Bootstrap::ADMIN_PASSWORD); + + $this->u2fkey + ->method('verify') + ->willThrowException(new \InvalidArgumentException('Something')); + + $result = $this->model->createAdminAccessToken( + 'adminUser', + Bootstrap::ADMIN_PASSWORD, + json_encode(['foo' => 'bar']) + ); + + self::assertEmpty($result); + } + + private function getUserId(): int + { + $user = $this->userFactory->create(); + $user->loadByUsername('adminUser'); + + return (int)$user->getId(); + } +} diff --git a/TwoFactorAuth/Test/Integration/Model/Provider/Engine/U2fKey/ConfigureTest.php b/TwoFactorAuth/Test/Integration/Model/Provider/Engine/U2fKey/ConfigureTest.php new file mode 100644 index 00000000..f9f952a7 --- /dev/null +++ b/TwoFactorAuth/Test/Integration/Model/Provider/Engine/U2fKey/ConfigureTest.php @@ -0,0 +1,264 @@ +userFactory = $objectManager->get(UserFactory::class); + $this->tokenManager = $objectManager->get(UserConfigTokenManagerInterface::class); + $this->tfa = $objectManager->get(TfaInterface::class); + $this->u2fkey = $this->createMock(U2fKey::class); + $this->model = $objectManager->create( + Configure::class, + [ + 'u2fKey' => $this->u2fkey + ] + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers u2fkey + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testGetRegistrationDataInvalidTfat() + { + $this->expectException(\Magento\Framework\Exception\AuthorizationException::class); + $this->expectExceptionMessage('Invalid two-factor authorization token'); + $this->u2fkey + ->expects($this->never()) + ->method('getRegisterData'); + $this->model->getRegistrationData( + 'abc' + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers u2fkey + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testGetRegistrationDataAlreadyConfiguredProvider() + { + $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectExceptionMessage('Provider is already configured.'); + $userId = $this->getUserId(); + $this->tfa->getProviderByCode(U2fKey::CODE) + ->activate($userId); + $this->u2fkey + ->expects($this->never()) + ->method('getRegisterData'); + $this->model->getRegistrationData( + $this->tokenManager->issueFor($userId) + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers duo_security + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testGetRegistrationDataUnavailableProvider() + { + $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectExceptionMessage('Provider is not allowed.'); + $userId = $this->getUserId(); + $this->u2fkey + ->expects($this->never()) + ->method('getRegisterData'); + $this->model->getRegistrationData( + $this->tokenManager->issueFor($userId) + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers u2fkey + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testActivateInvalidTfat() + { + $this->expectException(\Magento\Framework\Exception\AuthorizationException::class); + $this->expectExceptionMessage('Invalid two-factor authorization token'); + $this->u2fkey + ->expects($this->never()) + ->method('registerDevice'); + $this->model->activate( + 'abc', + 'I identify as JSON' + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers u2fkey + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testActivateAlreadyConfiguredProvider() + { + $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectExceptionMessage('Provider is already configured.'); + $userId = $this->getUserId(); + $this->tfa->getProviderByCode(U2fKey::CODE) + ->activate($userId); + $this->u2fkey + ->expects($this->never()) + ->method('registerDevice'); + $this->model->activate( + $this->tokenManager->issueFor($userId), + 'I identify as JSON' + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers duo_security + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testActivateUnavailableProvider() + { + $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectExceptionMessage('Provider is not allowed.'); + $userId = $this->getUserId(); + $this->u2fkey + ->expects($this->never()) + ->method('registerDevice'); + $this->model->activate( + $this->tokenManager->issueFor($userId), + 'I identify as JSON' + ); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers u2fkey + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testGetRegistrationDataValidRequest() + { + $userId = $this->getUserId(); + $data = ['publicKey' => ['challenge' => [1, 2, 3]]]; + + $this->u2fkey + ->method('getRegisterData') + ->with( + $this->callback(function ($value) use ($userId) { + return (int)$value->getId() === $userId; + }) + ) + ->willReturn($data); + + $result = $this->model->getRegistrationData( + $this->tokenManager->issueFor($userId) + ); + + self::assertInstanceOf(U2fWebAuthnRequestInterface::class, $result); + self::assertSame(json_encode($data), $result->getCredentialRequestOptionsJson()); + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers u2fkey + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testActivateValidRequest() + { + $userId = $this->getUserId(); + $tfat = $this->tokenManager->issueFor($userId); + + $this->u2fkey + ->method('getRegisterData') + ->willReturn(['publicKey' => ['challenge' => [3, 2, 1]]]); + $this->model->getRegistrationData($tfat); + + $activateData = ['foo' => 'bar']; + $this->u2fkey + ->method('registerDevice') + ->with( + $this->callback(function ($value) use ($userId) { + return (int)$value->getId() === $userId; + }), + [ + 'publicKeyCredential' => $activateData, + // Asserts the previously issued challenge was used for verification + 'challenge' => [3, 2, 1] + ] + ); + + $this->model->activate($tfat, json_encode($activateData)); + + // Mock registerDevice call above is proof of activation + } + + /** + * @magentoConfigFixture default/twofactorauth/general/force_providers u2fkey + * @magentoDataFixture Magento/User/_files/user_with_role.php + */ + public function testActivateInvalidKeyDataThrowsException() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Something'); + $userId = $this->getUserId(); + $tfat = $this->tokenManager->issueFor($userId); + + $this->u2fkey + ->method('getRegisterData') + ->willReturn(['publicKey' => ['challenge' => [3, 2, 1]]]); + $this->model->getRegistrationData($tfat); + + $this->u2fkey + ->method('registerDevice') + ->willThrowException(new \InvalidArgumentException('Something')); + + $result = $this->model->activate($tfat, json_encode(['foo' => 'bar'])); + + self::assertEmpty($result); + } + + private function getUserId(): int + { + $user = $this->userFactory->create(); + $user->loadByUsername('adminUser'); + + return (int)$user->getId(); + } +} diff --git a/TwoFactorAuth/Test/Integration/Model/Provider/Engine/U2fKey/SessionTest.php b/TwoFactorAuth/Test/Integration/Model/Provider/Engine/U2fKey/SessionTest.php index b58b1ce6..56039e51 100644 --- a/TwoFactorAuth/Test/Integration/Model/Provider/Engine/U2fKey/SessionTest.php +++ b/TwoFactorAuth/Test/Integration/Model/Provider/Engine/U2fKey/SessionTest.php @@ -6,7 +6,7 @@ declare(strict_types=1); -namespace Magento\Model\Provider\Engine\U2fKey; +namespace Magento\TwoFactorAuth\Test\Integration\Model\Provider\Engine\U2fKey; use Magento\Framework\App\ObjectManager; use Magento\TwoFactorAuth\Model\Provider\Engine\U2fKey\Session; @@ -19,7 +19,7 @@ class SessionTest extends TestCase */ private $session; - protected function setUp() + protected function setUp(): void { $this->session = ObjectManager::getInstance()->get(Session::class); } diff --git a/TwoFactorAuth/Test/Integration/Model/TfaSessionTest.php b/TwoFactorAuth/Test/Integration/Model/TfaSessionTest.php index 8b1c20ca..73195242 100644 --- a/TwoFactorAuth/Test/Integration/Model/TfaSessionTest.php +++ b/TwoFactorAuth/Test/Integration/Model/TfaSessionTest.php @@ -6,7 +6,7 @@ declare(strict_types=1); -namespace Magento\Model; +namespace Magento\TwoFactorAuth\Test\Integration\Model; use Magento\Framework\App\ObjectManager; use Magento\TwoFactorAuth\Model\TfaSession; @@ -19,7 +19,7 @@ class TfaSessionTest extends TestCase */ private $session; - protected function setUp() + protected function setUp(): void { $this->session = ObjectManager::getInstance()->get(TfaSession::class); } diff --git a/TwoFactorAuth/Test/Integration/UserConfigManagerTest.php b/TwoFactorAuth/Test/Integration/UserConfigManagerTest.php index 2c0f4dc0..ff947d45 100644 --- a/TwoFactorAuth/Test/Integration/UserConfigManagerTest.php +++ b/TwoFactorAuth/Test/Integration/UserConfigManagerTest.php @@ -33,7 +33,7 @@ class UserConfigManagerTest extends TestCase /** * @inheritDoc */ - protected function setUp() + protected function setUp(): void { $this->userConfigManager = Bootstrap::getObjectManager()->get(UserConfigManagerInterface::class); $this->serializer = Bootstrap::getObjectManager()->get(SerializerInterface::class); diff --git a/TwoFactorAuth/Test/Integration/UserConfigRequestManagerTest.php b/TwoFactorAuth/Test/Integration/UserConfigRequestManagerTest.php index 4e576ac6..bc081fa0 100644 --- a/TwoFactorAuth/Test/Integration/UserConfigRequestManagerTest.php +++ b/TwoFactorAuth/Test/Integration/UserConfigRequestManagerTest.php @@ -1,4 +1,9 @@ create(User::class); @@ -102,13 +107,13 @@ public function testIsRequiredWithConfig(): void * Check that app config request E-mail is NOT sent for a user that does not posses proper rights. * * @return void - * @expectedException \Magento\Framework\Exception\AuthorizationException * @throws \Throwable * @magentoAppArea adminhtml * @magentoAppIsolation enabled */ public function testFailAppConfigRequest(): void { + $this->expectException(\Magento\Framework\Exception\AuthorizationException::class); $this->aclBuilder->getAcl()->deny(null, 'Magento_TwoFactorAuth::config'); $this->manager->sendConfigRequestTo($this->user); } @@ -126,7 +131,7 @@ public function testSendAppConfigRequest(): void $this->assertNotEmpty($message = $this->transportBuilderMock->getSentMessage()); $messageHtml = $message->getBody()->getParts()[0]->getRawContent(); - $this->assertContains( + $this->assertStringContainsString( 'You are required to configure website-wide and personal Two-Factor Authorization in order to login to', $messageHtml ); @@ -156,7 +161,7 @@ public function testSendUserConfigRequest(): void $this->assertNotEmpty($message = $this->transportBuilderMock->getSentMessage()); $messageHtml = $message->getBody()->getParts()[0]->getRawContent(); - $this->assertContains( + $this->assertStringContainsString( 'You are required to configure personal Two-Factor Authorization in order to login to', $messageHtml ); diff --git a/TwoFactorAuth/Test/Integration/UserConfigTokenManagerTest.php b/TwoFactorAuth/Test/Integration/UserConfigTokenManagerTest.php index 901b4594..c98419d6 100644 --- a/TwoFactorAuth/Test/Integration/UserConfigTokenManagerTest.php +++ b/TwoFactorAuth/Test/Integration/UserConfigTokenManagerTest.php @@ -1,4 +1,9 @@ dateTimeMock = $this->getMockBuilder(DateTime::class)->disableOriginalConstructor()->getMock(); $this->userFactory = Bootstrap::getObjectManager()->get(UserFactory::class); diff --git a/TwoFactorAuth/Test/Integration/_files/overrides.xml b/TwoFactorAuth/Test/Integration/_files/overrides.xml new file mode 100644 index 00000000..c6109823 --- /dev/null +++ b/TwoFactorAuth/Test/Integration/_files/overrides.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/TwoFactorAuth/Test/Mftf/ActionGroup/AdminCreateRoleActionGroup.xml b/TwoFactorAuth/Test/Mftf/ActionGroup/AdminCreateRoleActionGroup.xml new file mode 100644 index 00000000..389480a8 --- /dev/null +++ b/TwoFactorAuth/Test/Mftf/ActionGroup/AdminCreateRoleActionGroup.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/TwoFactorAuth/Test/Mftf/ActionGroup/AdminCreateUserRoleWithReportsActionGroup.xml b/TwoFactorAuth/Test/Mftf/ActionGroup/AdminCreateUserRoleWithReportsActionGroup.xml new file mode 100644 index 00000000..905641de --- /dev/null +++ b/TwoFactorAuth/Test/Mftf/ActionGroup/AdminCreateUserRoleWithReportsActionGroup.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/TwoFactorAuth/Test/Mftf/ActionGroup/AdminFillUserRoleRequiredDataActionGroup.xml b/TwoFactorAuth/Test/Mftf/ActionGroup/AdminFillUserRoleRequiredDataActionGroup.xml new file mode 100644 index 00000000..c756a363 --- /dev/null +++ b/TwoFactorAuth/Test/Mftf/ActionGroup/AdminFillUserRoleRequiredDataActionGroup.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/TwoFactorAuth/Test/Mftf/ActionGroup/AdminLoginActionGroup.xml b/TwoFactorAuth/Test/Mftf/ActionGroup/AdminLoginActionGroup.xml new file mode 100644 index 00000000..97bd9001 --- /dev/null +++ b/TwoFactorAuth/Test/Mftf/ActionGroup/AdminLoginActionGroup.xml @@ -0,0 +1,20 @@ + + + + + + {{username}} + + + {{AdminGoogleTfaSection.tfaAuthCode}} + {{AdminGoogleTfaSection.confirm}} + {{AdminLoginMessagesSection.messageByType('error')}} + + + diff --git a/TwoFactorAuth/Test/Mftf/Helper/FillOtp.php b/TwoFactorAuth/Test/Mftf/Helper/FillOtp.php new file mode 100644 index 00000000..3135b8fe --- /dev/null +++ b/TwoFactorAuth/Test/Mftf/Helper/FillOtp.php @@ -0,0 +1,42 @@ +getModule('\\' . MagentoWebDriver::class); + try { + $webDriver->seeElementInDOM($errorMessageSelector); + // Login failed so don't handle 2fa + } catch (\Exception $e) { + $otp = $webDriver->getOTP(); + $webDriver->waitForPageLoad(); + $webDriver->waitForElementVisible($tfaAuthCodeSelector); + $webDriver->fillField($tfaAuthCodeSelector, $otp); + $webDriver->click($confirmSelector); + $webDriver->waitForPageLoad(); + } + } +} diff --git a/TwoFactorAuth/Test/Mftf/Helper/SetSharedSecret.php b/TwoFactorAuth/Test/Mftf/Helper/SetSharedSecret.php new file mode 100644 index 00000000..4d03d013 --- /dev/null +++ b/TwoFactorAuth/Test/Mftf/Helper/SetSharedSecret.php @@ -0,0 +1,43 @@ +getModule('\\' . MagentoWebDriver::class); + $credentialStore = CredentialStore::getInstance(); + if ($username !== getenv('MAGENTO_ADMIN_USERNAME')) { + $sharedSecret = $credentialStore->decryptSecretValue( + $credentialStore->getSecret('magento/tfa/OTP_SHARED_SECRET') + ); + try { + $webDriver->magentoCLI( + 'security:tfa:google:set-secret ' . $username .' ' . $sharedSecret + ); + } catch (\Throwable $exception) { + // Some tests intentionally use bad credentials. + } + } + } +} diff --git a/TwoFactorAuth/Test/Mftf/Section/AdminEditRoleInfoSection.xml b/TwoFactorAuth/Test/Mftf/Section/AdminEditRoleInfoSection.xml new file mode 100644 index 00000000..38c36b9f --- /dev/null +++ b/TwoFactorAuth/Test/Mftf/Section/AdminEditRoleInfoSection.xml @@ -0,0 +1,13 @@ + + + +
+ + +
+
diff --git a/TwoFactorAuth/Test/Mftf/Section/AdminGoogleTfaSection.xml b/TwoFactorAuth/Test/Mftf/Section/AdminGoogleTfaSection.xml new file mode 100644 index 00000000..9297b203 --- /dev/null +++ b/TwoFactorAuth/Test/Mftf/Section/AdminGoogleTfaSection.xml @@ -0,0 +1,15 @@ + + + + +
+ + +
+
diff --git a/TwoFactorAuth/Test/Mftf/Test/AdminBulkOperationsLogIsNotAccessibleForAdminUserWithLimitedAccessTest.xml b/TwoFactorAuth/Test/Mftf/Test/AdminBulkOperationsLogIsNotAccessibleForAdminUserWithLimitedAccessTest.xml new file mode 100644 index 00000000..c5e14532 --- /dev/null +++ b/TwoFactorAuth/Test/Mftf/Test/AdminBulkOperationsLogIsNotAccessibleForAdminUserWithLimitedAccessTest.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/TwoFactorAuth/Test/Mftf/Test/AdminConfigurationPermissionTest.xml b/TwoFactorAuth/Test/Mftf/Test/AdminConfigurationPermissionTest.xml new file mode 100644 index 00000000..87588b48 --- /dev/null +++ b/TwoFactorAuth/Test/Mftf/Test/AdminConfigurationPermissionTest.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/TwoFactorAuth/Test/Mftf/Test/AdminLoginAfterJSMinificationTest.xml b/TwoFactorAuth/Test/Mftf/Test/AdminLoginAfterJSMinificationTest.xml new file mode 100644 index 00000000..883f3b58 --- /dev/null +++ b/TwoFactorAuth/Test/Mftf/Test/AdminLoginAfterJSMinificationTest.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/TwoFactorAuth/Test/Mftf/Test/AdminReviewOrderWithOnlyReportsPermissionTest.xml b/TwoFactorAuth/Test/Mftf/Test/AdminReviewOrderWithOnlyReportsPermissionTest.xml new file mode 100644 index 00000000..394ae495 --- /dev/null +++ b/TwoFactorAuth/Test/Mftf/Test/AdminReviewOrderWithOnlyReportsPermissionTest.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/TwoFactorAuth/Test/Mftf/Test/RestrictedAdminCatalogMassActionPermissionTest.xml b/TwoFactorAuth/Test/Mftf/Test/RestrictedAdminCatalogMassActionPermissionTest.xml new file mode 100644 index 00000000..d6cdcf7c --- /dev/null +++ b/TwoFactorAuth/Test/Mftf/Test/RestrictedAdminCatalogMassActionPermissionTest.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/TwoFactorAuth/Test/Mftf/Test/RestrictedUserRoleForProductRemovalTest.xml b/TwoFactorAuth/Test/Mftf/Test/RestrictedUserRoleForProductRemovalTest.xml new file mode 100644 index 00000000..93211094 --- /dev/null +++ b/TwoFactorAuth/Test/Mftf/Test/RestrictedUserRoleForProductRemovalTest.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/TwoFactorAuth/Test/Mftf/Test/RestrictedUserRoleProductAttributeTest.xml b/TwoFactorAuth/Test/Mftf/Test/RestrictedUserRoleProductAttributeTest.xml new file mode 100644 index 00000000..cd828761 --- /dev/null +++ b/TwoFactorAuth/Test/Mftf/Test/RestrictedUserRoleProductAttributeTest.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/TwoFactorAuth/Test/Mftf/Test/StorefrontGiftWrappingCanBeAppliedOnOrderLevelAndOrderItemForAdditionalWebsiteTest.xml b/TwoFactorAuth/Test/Mftf/Test/StorefrontGiftWrappingCanBeAppliedOnOrderLevelAndOrderItemForAdditionalWebsiteTest.xml new file mode 100644 index 00000000..9141a62e --- /dev/null +++ b/TwoFactorAuth/Test/Mftf/Test/StorefrontGiftWrappingCanBeAppliedOnOrderLevelAndOrderItemForAdditionalWebsiteTest.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/TwoFactorAuth/Test/Mftf/Test/StorefrontInvoiceFilterTest.xml b/TwoFactorAuth/Test/Mftf/Test/StorefrontInvoiceFilterTest.xml new file mode 100644 index 00000000..a3b3e593 --- /dev/null +++ b/TwoFactorAuth/Test/Mftf/Test/StorefrontInvoiceFilterTest.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/TwoFactorAuth/Test/Unit/Model/Config/Backend/Duo/ApiHostnameTest.php b/TwoFactorAuth/Test/Unit/Model/Config/Backend/Duo/ApiHostnameTest.php index df640d54..5a670567 100644 --- a/TwoFactorAuth/Test/Unit/Model/Config/Backend/Duo/ApiHostnameTest.php +++ b/TwoFactorAuth/Test/Unit/Model/Config/Backend/Duo/ApiHostnameTest.php @@ -1,7 +1,11 @@ model = $objectManager->getObject(ApiHostname::class); diff --git a/TwoFactorAuth/Test/Unit/Model/Config/Backend/ForceProvidersTest.php b/TwoFactorAuth/Test/Unit/Model/Config/Backend/ForceProvidersTest.php index 467a6391..248f8a19 100644 --- a/TwoFactorAuth/Test/Unit/Model/Config/Backend/ForceProvidersTest.php +++ b/TwoFactorAuth/Test/Unit/Model/Config/Backend/ForceProvidersTest.php @@ -1,4 +1,9 @@ tfa = $this->createMock(TfaInterface::class); @@ -42,10 +47,10 @@ protected function setUp() * Check that beforeSave validates values. * * @return void - * @expectedException \Magento\Framework\Exception\ValidatorException */ public function testBeforeSaveInvalid(): void { + $this->expectException(\Magento\Framework\Exception\ValidatorException::class); $this->model->setValue(''); $this->model->beforeSave(); } diff --git a/TwoFactorAuth/Test/Unit/Model/Provider/Engine/AuthyTest.php b/TwoFactorAuth/Test/Unit/Model/Provider/Engine/AuthyTest.php index f36e6bf8..f76871e3 100644 --- a/TwoFactorAuth/Test/Unit/Model/Provider/Engine/AuthyTest.php +++ b/TwoFactorAuth/Test/Unit/Model/Provider/Engine/AuthyTest.php @@ -1,4 +1,8 @@ serviceMock = $this->getMockBuilder(Authy\Service::class)->disableOriginalConstructor()->getMock(); @@ -60,7 +64,7 @@ public function getIsEnabledTestDataSet(): array */ public function testIsEnabled(?string $apiKey, bool $expected): void { - $this->serviceMock->method('getApiKey')->willReturn($apiKey); + $this->serviceMock->method('getApiKey')->willReturn((string)$apiKey); $this->assertEquals($expected, $this->model->isEnabled()); } diff --git a/TwoFactorAuth/Test/Unit/Model/Provider/Engine/DuoSecurityTest.php b/TwoFactorAuth/Test/Unit/Model/Provider/Engine/DuoSecurityTest.php index 6eef7bad..cb6d2aa0 100644 --- a/TwoFactorAuth/Test/Unit/Model/Provider/Engine/DuoSecurityTest.php +++ b/TwoFactorAuth/Test/Unit/Model/Provider/Engine/DuoSecurityTest.php @@ -1,4 +1,9 @@ configMock = $this->getMockBuilder(ScopeConfigInterface::class)->disableOriginalConstructor()->getMock(); diff --git a/TwoFactorAuth/Test/Unit/Model/Provider/Engine/GoogleTest.php b/TwoFactorAuth/Test/Unit/Model/Provider/Engine/GoogleTest.php index 9f45e801..50a64d22 100644 --- a/TwoFactorAuth/Test/Unit/Model/Provider/Engine/GoogleTest.php +++ b/TwoFactorAuth/Test/Unit/Model/Provider/Engine/GoogleTest.php @@ -1,9 +1,22 @@ model = $objectManager->getObject(Google::class); + $this->totpFactory = $this->createMock(TotpFactory::class); + $this->totp = $this->createMock(TOTPInterface::class); + $this->user = $this->createMock(UserInterface::class); + $this->scopeConfig = $this->createMock(ScopeConfigInterface::class); + $this->encryptor = $this->createMock(EncryptorInterface::class); + $this->user->method('getId') + ->willReturn('5'); + $this->user->method('getEmail') + ->willReturn('john@example.com'); + $this->configManager = $this->createMock(UserConfigManagerInterface::class); + $this->model = $objectManager->getObject( + Google::class, + [ + 'configManager' => $this->configManager, + 'totpFactory' => $this->totpFactory, + 'scopeConfig' => $this->scopeConfig, + 'encryptor' => $this->encryptor + ] + ); } /** @@ -29,7 +90,110 @@ protected function setUp() * * @return void */ - public function testIsEnabled(): void { + public function testIsEnabled(): void + { $this->assertTrue($this->model->isEnabled()); } + + public function testVerifyWithNoToken() + { + $valid = $this->model->verify($this->user, new DataObject(['tfa_code' => ''])); + + self::assertFalse($valid); + } + + public function testVerifyWithBadToken() + { + $this->configManager->method('getProviderConfig') + ->willReturn(['secret' => 'cba']); + $this->encryptor->method('decrypt') + ->with('cba') + ->willReturn('abc'); + $this->totpFactory->method('create') + ->willReturn($this->totp); + $this->totp->method('verify') + ->with('123456') + ->willReturn(false); + + $valid = $this->model->verify($this->user, new DataObject(['tfa_code' => '123456'])); + + self::assertFalse($valid); + } + + public function testVerifyWithGoodToken() + { + $this->configManager->method('getProviderConfig') + ->willReturn(['secret' => 'cba']); + $this->encryptor->method('decrypt') + ->with('cba') + ->willReturn('abc'); + $this->totpFactory->method('create') + ->with('abc') + ->willReturn($this->totp); + $this->totp->method('verify') + ->with('123456') + ->willReturn(true); + + $valid = $this->model->verify($this->user, new DataObject(['tfa_code' => '123456'])); + + self::assertTrue($valid); + } + + public function testVerifyWithGoodTokenAndWindowFromUserConfig() + { + $this->configManager->method('getProviderConfig') + ->willReturn(['secret' => 'cba', 'window' => 800]); + $this->encryptor->method('decrypt') + ->with('cba') + ->willReturn('abc'); + $this->totpFactory->method('create') + ->willReturn($this->totp); + $this->totp->method('verify') + ->with('123456', null, 800) + ->willReturn(true); + + $valid = $this->model->verify($this->user, new DataObject(['tfa_code' => '123456'])); + + self::assertTrue($valid); + } + + public function testVerifyWithGoodTokenAndWindowFromScopeConfig() + { + $this->scopeConfig->method('getValue') + ->willReturn(800); + $this->configManager->method('getProviderConfig') + ->willReturn(['secret' => 'cba']); + $this->encryptor->method('decrypt') + ->with('cba') + ->willReturn('abc'); + $this->totpFactory->method('create') + ->willReturn($this->totp); + $this->totp->method('verify') + ->with('123456', null, 800) + ->willReturn(true); + + $valid = $this->model->verify($this->user, new DataObject(['tfa_code' => '123456'])); + + self::assertTrue($valid); + } + + public function testVerifyWindowFromUserConfigOverridesScopeConfig() + { + $this->scopeConfig->method('getValue') + ->willReturn(800); + $this->configManager->method('getProviderConfig') + ->willReturn(['secret' => 'cba', 'window' => 500]); + $this->encryptor->method('decrypt') + ->with('cba') + ->willReturn('abc'); + $this->totpFactory->method('create') + ->willReturn($this->totp); + $this->totp->method('verify') + ->with('123456', null, 500) + ->willReturn(true); + + $valid = $this->model->verify($this->user, new DataObject(['tfa_code' => '123456'])); + + self::assertTrue($valid); + } } diff --git a/TwoFactorAuth/Test/Unit/Model/Provider/Engine/U2fKey/ConfigReaderTest.php b/TwoFactorAuth/Test/Unit/Model/Provider/Engine/U2fKey/ConfigReaderTest.php new file mode 100644 index 00000000..198d82d5 --- /dev/null +++ b/TwoFactorAuth/Test/Unit/Model/Provider/Engine/U2fKey/ConfigReaderTest.php @@ -0,0 +1,68 @@ +storeManager = $this->createMock(StoreManagerInterface::class); + $this->reader = $objectManager->getObject( + ConfigReader::class, + [ + 'storeManager' => $this->storeManager + ] + ); + } + + public function testGetValidDomain() + { + $store = $this->createMock(Store::class); + $store->method('getBaseUrl') + ->willReturn('https://domain.com/'); + $this->storeManager + ->method('getStore') + ->with(Store::ADMIN_CODE) + ->willReturn($store); + $result = $this->reader->getDomain(); + self::assertSame('domain.com', $result); + } + + public function testGetInvalidDomain() + { + $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectExceptionMessage('Could not determine domain name.'); + $store = $this->createMock(Store::class); + $store->method('getBaseUrl') + ->willReturn('foo'); + $this->storeManager + ->method('getStore') + ->with(Store::ADMIN_CODE) + ->willReturn($store); + $this->reader->getDomain(); + } +} diff --git a/TwoFactorAuth/Test/Unit/Model/Provider/Engine/U2fKey/WebApiConfigReaderTest.php b/TwoFactorAuth/Test/Unit/Model/Provider/Engine/U2fKey/WebApiConfigReaderTest.php new file mode 100644 index 00000000..a2c13442 --- /dev/null +++ b/TwoFactorAuth/Test/Unit/Model/Provider/Engine/U2fKey/WebApiConfigReaderTest.php @@ -0,0 +1,76 @@ +scopeConfig = $this->createMock(ScopeConfigInterface::class); + $this->defaultConfigReader = $this->createMock(ConfigReader::class); + $this->reader = $objectManager->getObject( + WebApiConfigReader::class, + [ + 'scopeConfig' => $this->scopeConfig, + 'configReader' => $this->defaultConfigReader + ] + ); + } + + public function testDomainFromConfig() + { + $this->defaultConfigReader + ->expects($this->never()) + ->method('getDomain'); + $this->scopeConfig + ->method('getValue') + ->with(U2fKey::XML_PATH_WEBAPI_DOMAIN) + ->willReturn('foo'); + $result = $this->reader->getDomain(); + self::assertSame('foo', $result); + } + + public function testDomainFromDefault() + { + $this->defaultConfigReader + ->method('getDomain') + ->willReturn('foo'); + $this->scopeConfig + ->method('getValue') + ->with(U2fKey::XML_PATH_WEBAPI_DOMAIN) + ->willReturn(null); + $result = $this->reader->getDomain(); + self::assertSame('foo', $result); + } +} diff --git a/TwoFactorAuth/Test/Unit/Model/Provider/Engine/U2fKeyTest.php b/TwoFactorAuth/Test/Unit/Model/Provider/Engine/U2fKeyTest.php index c7e6b18c..c30e3657 100644 --- a/TwoFactorAuth/Test/Unit/Model/Provider/Engine/U2fKeyTest.php +++ b/TwoFactorAuth/Test/Unit/Model/Provider/Engine/U2fKeyTest.php @@ -1,4 +1,9 @@ assertTrue($this->model->isEnabled()); } } diff --git a/TwoFactorAuth/Test/Unit/Model/TfaTest.php b/TwoFactorAuth/Test/Unit/Model/TfaTest.php index 46ae8050..394f6613 100644 --- a/TwoFactorAuth/Test/Unit/Model/TfaTest.php +++ b/TwoFactorAuth/Test/Unit/Model/TfaTest.php @@ -1,4 +1,9 @@ pool = $this->getMockForAbstractClass(ProviderPoolInterface::class); diff --git a/TwoFactorAuth/Test/Unit/Model/UserConfig/HtmlAreaTokenVerifierTest.php b/TwoFactorAuth/Test/Unit/Model/UserConfig/HtmlAreaTokenVerifierTest.php index 501e60a5..bee182f0 100644 --- a/TwoFactorAuth/Test/Unit/Model/UserConfig/HtmlAreaTokenVerifierTest.php +++ b/TwoFactorAuth/Test/Unit/Model/UserConfig/HtmlAreaTokenVerifierTest.php @@ -1,4 +1,9 @@ requestMock = $this->getMockForAbstractClass(RequestInterface::class); diff --git a/TwoFactorAuth/TestFramework/ControllerActionPredispatch.php b/TwoFactorAuth/TestFramework/ControllerActionPredispatch.php index dc4353e4..ebea76b7 100644 --- a/TwoFactorAuth/TestFramework/ControllerActionPredispatch.php +++ b/TwoFactorAuth/TestFramework/ControllerActionPredispatch.php @@ -1,4 +1,9 @@ getEvent()->getData('controller_action'); - if (class_exists('Magento\TestFramework\Request') + if (method_exists($controllerAction, 'getRequest') && $controllerAction->getRequest() instanceof \Magento\TestFramework\Request && !$controllerAction->getRequest()->getParam('tfa_enabled') ) { diff --git a/TwoFactorAuth/TestFramework/TestCase/AbstractBackendController.php b/TwoFactorAuth/TestFramework/TestCase/AbstractBackendController.php index 05a91a43..fc6d4155 100644 --- a/TwoFactorAuth/TestFramework/TestCase/AbstractBackendController.php +++ b/TwoFactorAuth/TestFramework/TestCase/AbstractBackendController.php @@ -1,4 +1,9 @@ + + + + diff --git a/TwoFactorAuth/etc/adminhtml/system.xml b/TwoFactorAuth/etc/adminhtml/system.xml index 94180a24..6144f83c 100644 --- a/TwoFactorAuth/etc/adminhtml/system.xml +++ b/TwoFactorAuth/etc/adminhtml/system.xml @@ -30,8 +30,21 @@ Two-factor authorization providers for admin users to use during login Magento\TwoFactorAuth\Model\Config\Backend\ForceProviders + + + This can be used to override the default email configuration link that is sent when using the Magento Web API's to authenticate. Use the placeholder :tfat to indicate where the token should be injected + + + + + + + This determines how long the one-time-passwords are valid for. + - @@ -64,6 +77,15 @@ + + + + + This domain will be used when issuing and processing WebAuthn challenges via WebApi. The store domain will be used by default. + + diff --git a/TwoFactorAuth/etc/config.xml b/TwoFactorAuth/etc/config.xml index 9dba4e2b..13287ab7 100644 --- a/TwoFactorAuth/etc/config.xml +++ b/TwoFactorAuth/etc/config.xml @@ -20,6 +20,9 @@ + + 30 + diff --git a/TwoFactorAuth/etc/db_schema_whitelist.json b/TwoFactorAuth/etc/db_schema_whitelist.json index c81eca73..b5a55ed9 100644 --- a/TwoFactorAuth/etc/db_schema_whitelist.json +++ b/TwoFactorAuth/etc/db_schema_whitelist.json @@ -12,5 +12,14 @@ "index": { "TFA_COUNTRY_CODES_CODE": true } + }, + "msp_tfa_user_config": { + "constraint": {} + }, + "msp_tfa_trusted": { + "constraint": {} + }, + "tfa_trusted": { + "constraint": {} } } diff --git a/TwoFactorAuth/etc/di.xml b/TwoFactorAuth/etc/di.xml index 3f80d9fa..0b934e87 100644 --- a/TwoFactorAuth/etc/di.xml +++ b/TwoFactorAuth/etc/di.xml @@ -23,13 +23,31 @@ + + + + + + + + + + + + + + + + + Magento\TwoFactorAuth\Command\TfaReset Magento\TwoFactorAuth\Command\TfaProviders + Magento\TwoFactorAuth\Command\GoogleSecret @@ -65,6 +83,10 @@ + + + + Magento\TwoFactorAuth\Model\Provider\Engine\Google diff --git a/TwoFactorAuth/etc/module.xml b/TwoFactorAuth/etc/module.xml index 5cfe6c60..9f24769d 100644 --- a/TwoFactorAuth/etc/module.xml +++ b/TwoFactorAuth/etc/module.xml @@ -10,6 +10,7 @@ + diff --git a/TwoFactorAuth/etc/webapi.xml b/TwoFactorAuth/etc/webapi.xml index 917171ac..2c7cd443 100644 --- a/TwoFactorAuth/etc/webapi.xml +++ b/TwoFactorAuth/etc/webapi.xml @@ -8,59 +8,185 @@ + + + + + + + - + - + - + - + - + - + - + - - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TwoFactorAuth/etc/webapi_rest/di.xml b/TwoFactorAuth/etc/webapi_rest/di.xml new file mode 100644 index 00000000..1f87ae33 --- /dev/null +++ b/TwoFactorAuth/etc/webapi_rest/di.xml @@ -0,0 +1,17 @@ + + + + + + + + Magento\TwoFactorAuth\Model\Config\WebApiUserNotifier + + + diff --git a/TwoFactorAuth/etc/webapi_soap/di.xml b/TwoFactorAuth/etc/webapi_soap/di.xml new file mode 100644 index 00000000..1f87ae33 --- /dev/null +++ b/TwoFactorAuth/etc/webapi_soap/di.xml @@ -0,0 +1,17 @@ + + + + + + + + Magento\TwoFactorAuth\Model\Config\WebApiUserNotifier + + + diff --git a/TwoFactorAuth/view/adminhtml/layout/tfa_authy_auth.xml b/TwoFactorAuth/view/adminhtml/layout/tfa_authy_auth.xml index 54b37fa1..2d8327df 100644 --- a/TwoFactorAuth/view/adminhtml/layout/tfa_authy_auth.xml +++ b/TwoFactorAuth/view/adminhtml/layout/tfa_authy_auth.xml @@ -8,6 +8,7 @@ + diff --git a/TwoFactorAuth/view/adminhtml/layout/tfa_authy_configure.xml b/TwoFactorAuth/view/adminhtml/layout/tfa_authy_configure.xml index 99301065..1a79da42 100644 --- a/TwoFactorAuth/view/adminhtml/layout/tfa_authy_configure.xml +++ b/TwoFactorAuth/view/adminhtml/layout/tfa_authy_configure.xml @@ -8,6 +8,7 @@ + diff --git a/TwoFactorAuth/view/adminhtml/layout/tfa_duo_auth.xml b/TwoFactorAuth/view/adminhtml/layout/tfa_duo_auth.xml index 067c3497..273a7cdf 100644 --- a/TwoFactorAuth/view/adminhtml/layout/tfa_duo_auth.xml +++ b/TwoFactorAuth/view/adminhtml/layout/tfa_duo_auth.xml @@ -8,6 +8,7 @@ + diff --git a/TwoFactorAuth/view/adminhtml/layout/tfa_google_auth.xml b/TwoFactorAuth/view/adminhtml/layout/tfa_google_auth.xml index c361ea70..8288e39e 100644 --- a/TwoFactorAuth/view/adminhtml/layout/tfa_google_auth.xml +++ b/TwoFactorAuth/view/adminhtml/layout/tfa_google_auth.xml @@ -8,6 +8,7 @@ + diff --git a/TwoFactorAuth/view/adminhtml/layout/tfa_google_configure.xml b/TwoFactorAuth/view/adminhtml/layout/tfa_google_configure.xml index cfc8f5a3..a571f628 100644 --- a/TwoFactorAuth/view/adminhtml/layout/tfa_google_configure.xml +++ b/TwoFactorAuth/view/adminhtml/layout/tfa_google_configure.xml @@ -8,6 +8,7 @@ + diff --git a/TwoFactorAuth/view/adminhtml/layout/tfa_screen.xml b/TwoFactorAuth/view/adminhtml/layout/tfa_screen.xml new file mode 100644 index 00000000..419048cc --- /dev/null +++ b/TwoFactorAuth/view/adminhtml/layout/tfa_screen.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + Logout + adminhtml/auth/logout + tfa-logout-link + + + + + diff --git a/TwoFactorAuth/view/adminhtml/layout/tfa_tfa_accessdenied.xml b/TwoFactorAuth/view/adminhtml/layout/tfa_tfa_accessdenied.xml index 1111bafc..956b8550 100644 --- a/TwoFactorAuth/view/adminhtml/layout/tfa_tfa_accessdenied.xml +++ b/TwoFactorAuth/view/adminhtml/layout/tfa_tfa_accessdenied.xml @@ -8,6 +8,7 @@ + diff --git a/TwoFactorAuth/view/adminhtml/layout/tfa_tfa_configure.xml b/TwoFactorAuth/view/adminhtml/layout/tfa_tfa_configure.xml index efe6d105..bb56d4a7 100644 --- a/TwoFactorAuth/view/adminhtml/layout/tfa_tfa_configure.xml +++ b/TwoFactorAuth/view/adminhtml/layout/tfa_tfa_configure.xml @@ -9,6 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + diff --git a/TwoFactorAuth/view/adminhtml/layout/tfa_tfa_requestconfig.xml b/TwoFactorAuth/view/adminhtml/layout/tfa_tfa_requestconfig.xml index d8517a6c..60c099f9 100644 --- a/TwoFactorAuth/view/adminhtml/layout/tfa_tfa_requestconfig.xml +++ b/TwoFactorAuth/view/adminhtml/layout/tfa_tfa_requestconfig.xml @@ -8,6 +8,8 @@ + + diff --git a/TwoFactorAuth/view/adminhtml/layout/tfa_u2f_auth.xml b/TwoFactorAuth/view/adminhtml/layout/tfa_u2f_auth.xml index 375c9f1e..5aa91757 100644 --- a/TwoFactorAuth/view/adminhtml/layout/tfa_u2f_auth.xml +++ b/TwoFactorAuth/view/adminhtml/layout/tfa_u2f_auth.xml @@ -8,6 +8,7 @@ + diff --git a/TwoFactorAuth/view/adminhtml/layout/tfa_u2f_configure.xml b/TwoFactorAuth/view/adminhtml/layout/tfa_u2f_configure.xml index f20ad606..8a7d202d 100644 --- a/TwoFactorAuth/view/adminhtml/layout/tfa_u2f_configure.xml +++ b/TwoFactorAuth/view/adminhtml/layout/tfa_u2f_configure.xml @@ -8,6 +8,7 @@ + diff --git a/TwoFactorAuth/view/adminhtml/templates/tfa/change_provider.phtml b/TwoFactorAuth/view/adminhtml/templates/tfa/change_provider.phtml index d06fbd8e..c9fda998 100644 --- a/TwoFactorAuth/view/adminhtml/templates/tfa/change_provider.phtml +++ b/TwoFactorAuth/view/adminhtml/templates/tfa/change_provider.phtml @@ -11,7 +11,7 @@ diff --git a/TwoFactorAuth/view/adminhtml/templates/tfa/configure.phtml b/TwoFactorAuth/view/adminhtml/templates/tfa/configure.phtml index 29200c3b..2c6fc570 100644 --- a/TwoFactorAuth/view/adminhtml/templates/tfa/configure.phtml +++ b/TwoFactorAuth/view/adminhtml/templates/tfa/configure.phtml @@ -9,15 +9,21 @@ $list = $block->getProvidersList(); ?>

- escapeHtml(__('Please select one or more Two-Factor Authorization providers to be used to authorize admin users')); ?> + escapeHtml(__( + 'Please select one or more Two-Factor Authorization providers to be used to authorize admin users' + )); ?>

- escapeHtml(__('No providers are available to select, please configure required 2FA provider settings on the server via CLI')); ?> + escapeHtml(__( + 'No providers are available to select, please ' + . 'configure required 2FA provider settings on the server via CLI' + )); ?>

-
+
  • diff --git a/TwoFactorAuth/view/adminhtml/templates/tfa/configure_later.phtml b/TwoFactorAuth/view/adminhtml/templates/tfa/configure_later.phtml index e9ea5a46..d46edab2 100644 --- a/TwoFactorAuth/view/adminhtml/templates/tfa/configure_later.phtml +++ b/TwoFactorAuth/view/adminhtml/templates/tfa/configure_later.phtml @@ -9,5 +9,7 @@ /** @var $escaper Magento\Framework\Escaper */ ?> diff --git a/TwoFactorAuth/view/adminhtml/templates/tfa/provider/auth.phtml b/TwoFactorAuth/view/adminhtml/templates/tfa/provider/auth.phtml index 7ddc2c47..71a1d935 100644 --- a/TwoFactorAuth/view/adminhtml/templates/tfa/provider/auth.phtml +++ b/TwoFactorAuth/view/adminhtml/templates/tfa/provider/auth.phtml @@ -8,12 +8,12 @@
-getChildHtml('change-provider') ?> +getChildHtml('change-provider') ?> diff --git a/TwoFactorAuth/view/adminhtml/templates/tfa/provider/configure.phtml b/TwoFactorAuth/view/adminhtml/templates/tfa/provider/configure.phtml index 017cc618..5a891c33 100644 --- a/TwoFactorAuth/view/adminhtml/templates/tfa/provider/configure.phtml +++ b/TwoFactorAuth/view/adminhtml/templates/tfa/provider/configure.phtml @@ -11,7 +11,7 @@ diff --git a/TwoFactorAuth/view/adminhtml/templates/tfa/request_config.phtml b/TwoFactorAuth/view/adminhtml/templates/tfa/request_config.phtml index 7a45baec..e78affe8 100644 --- a/TwoFactorAuth/view/adminhtml/templates/tfa/request_config.phtml +++ b/TwoFactorAuth/view/adminhtml/templates/tfa/request_config.phtml @@ -7,6 +7,10 @@ /** @var \Magento\Backend\Block\Template $block */ ?>
-

escapeHtml(__('You need to configure Two-Factor Authorization in order to proceed to your store\'s admin area')); ?>

-

escapeHtml(__('An E-mail was sent to you with further instructions')); ?>

+

escapeHtml(__( + 'You need to configure Two-Factor Authorization in order to proceed to your store\'s admin area' + )); ?>

+

escapeHtml(__( + 'An E-mail was sent to you with further instructions' + )); ?>

diff --git a/TwoFactorAuth/view/adminhtml/web/css/tfa-screen.css b/TwoFactorAuth/view/adminhtml/web/css/tfa-screen.css new file mode 100644 index 00000000..f2372469 --- /dev/null +++ b/TwoFactorAuth/view/adminhtml/web/css/tfa-screen.css @@ -0,0 +1,13 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +.login-content > li { + list-style: none; +} +.tfa-logout-link { + position: absolute; + top: 1em; + right: 1em; +} diff --git a/TwoFactorAuth/view/adminhtml/web/js/google/auth.js b/TwoFactorAuth/view/adminhtml/web/js/google/auth.js index f97447a6..02f57d3d 100644 --- a/TwoFactorAuth/view/adminhtml/web/js/google/auth.js +++ b/TwoFactorAuth/view/adminhtml/web/js/google/auth.js @@ -7,7 +7,7 @@ define([ 'jquery', 'ko', 'uiComponent', - 'Magento_TwoFactorAuth/js/error', + 'Magento_TwoFactorAuth/js/error' ], function ($, ko, Component, error) { 'use strict'; @@ -43,7 +43,6 @@ define([ /** * Get plain Secret Code * @returns {String} - * @author Konrad Skrzynski */ getSecretCode: function () { return this.secretCode; diff --git a/TwoFactorAuth/view/adminhtml/web/js/u2fkey/auth.js b/TwoFactorAuth/view/adminhtml/web/js/u2fkey/auth.js index 713b488f..6707c2dd 100644 --- a/TwoFactorAuth/view/adminhtml/web/js/u2fkey/auth.js +++ b/TwoFactorAuth/view/adminhtml/web/js/u2fkey/auth.js @@ -32,12 +32,14 @@ define([ */ initConfig: function (config) { this._super(config); + // eslint-disable-next-line no-undef this.authenticateData.credentialRequestOptions.challenge = new Uint8Array( this.authenticateData.credentialRequestOptions.challenge ); this.authenticateData.credentialRequestOptions.allowCredentials = this.authenticateData.credentialRequestOptions.allowCredentials.map(function (credential) { + // eslint-disable-next-line no-undef credential.id = new Uint8Array(credential.id); return credential; @@ -98,6 +100,7 @@ define([ */ _onCredentialSuccess: function (credentialData) { utils.asyncUint8ArrayToUtf8String( + // eslint-disable-next-line no-undef new Uint8Array(credentialData.response.clientDataJSON), function (clientDataJSON) { credentialData.clientDataUtf8JSON = clientDataJSON; diff --git a/TwoFactorAuth/view/adminhtml/web/js/u2fkey/configure.js b/TwoFactorAuth/view/adminhtml/web/js/u2fkey/configure.js index 46498652..c5b7cc1e 100644 --- a/TwoFactorAuth/view/adminhtml/web/js/u2fkey/configure.js +++ b/TwoFactorAuth/view/adminhtml/web/js/u2fkey/configure.js @@ -32,7 +32,9 @@ define([ */ initConfig: function (config) { this._super(config); + // eslint-disable-next-line no-undef this.registerData.publicKey.challenge = new Uint8Array(this.registerData.publicKey.challenge); + // eslint-disable-next-line no-undef this.registerData.publicKey.user.id = new Uint8Array(this.registerData.publicKey.user.id); return this; @@ -92,6 +94,7 @@ define([ */ _onCredentialSuccess: function (credentialData) { utils.asyncUint8ArrayToUtf8String( + // eslint-disable-next-line no-undef new Uint8Array(credentialData.response.clientDataJSON), function (clientDataJSON) { credentialData.clientData = JSON.parse(clientDataJSON); diff --git a/TwoFactorAuth/view/adminhtml/web/js/u2fkey/utils.js b/TwoFactorAuth/view/adminhtml/web/js/u2fkey/utils.js index f09bdd61..0052ca9d 100644 --- a/TwoFactorAuth/view/adminhtml/web/js/u2fkey/utils.js +++ b/TwoFactorAuth/view/adminhtml/web/js/u2fkey/utils.js @@ -26,6 +26,7 @@ define([], function () { */ arrayBufferToBase64: function (buffer) { var binary = '', + // eslint-disable-next-line no-undef bytes = new Uint8Array(buffer), len = bytes.byteLength, i = 0; diff --git a/_metapackage/composer.json b/_metapackage/composer.json index c169aafd..06f99f48 100644 --- a/_metapackage/composer.json +++ b/_metapackage/composer.json @@ -21,6 +21,7 @@ "magento/module-re-captcha-version-2-invisible": "*", "magento/module-re-captcha-version-3-invisible": "*", "magento/module-securitytxt": "*", + "magento/module-two-factor-auth": "*", "google/recaptcha": "^1.2" } }