├── Api
└── RequestLoginRepositoryInterface.php
├── Block
└── Adminhtml
│ └── System
│ └── Config
│ └── Button.php
├── Controller
├── Account
│ ├── CreatePost.php
│ ├── Edit.php
│ ├── ForgotPassword.php
│ └── ForgotPasswordPost.php
├── Adminhtml
│ └── Pwl
│ │ ├── ProcessLogin.php
│ │ └── RequestLogin.php
└── Pwl
│ ├── ProcessLogin.php
│ └── RequestLogin.php
├── Enum
└── Config.php
├── Exception
└── RequestException.php
├── Helper
└── JwtHelper.php
├── LICENCE
├── Model
├── Admin
│ ├── User.php
│ └── User
│ │ └── Save.php
├── LoginRequest.php
├── LoginRequestRepository.php
├── ResourceModel
│ ├── Admin
│ │ └── User.php
│ ├── LoginRequest.php
│ └── LoginRequest
│ │ └── Collection.php
└── Template
│ └── Manager.php
├── Plugin
├── AdobeImsReAuth
│ ├── Block
│ │ ├── System
│ │ │ └── Account
│ │ │ │ └── Edit
│ │ │ │ └── RemoveReAuthVerification.php
│ │ └── User
│ │ │ ├── Edit
│ │ │ └── Tab
│ │ │ │ └── RemoveReAuthVerification.php
│ │ │ └── Role
│ │ │ └── Tab
│ │ │ └── RemoveReAuthVerification.php
│ └── PerformIdentityCheckMessagePlugin.php
├── Model
│ └── AccountManagement.php
└── Webapi
│ └── Controller
│ └── Rest
│ └── DisableApi.php
├── Processor
└── EmailProcessor.php
├── README.md
├── Service
├── Admin
│ ├── Login.php
│ └── PasswordVerification.php
├── Customer
│ └── Login.php
├── JwtManager.php
├── Password.php
├── Queue.php
├── Request.php
└── Validation.php
├── Test
└── Unit
│ ├── Plugin
│ ├── Model
│ │ └── AccountManagementTest.php
│ └── Webapi
│ │ └── Controller
│ │ └── Rest
│ │ └── DisableApiTest.php
│ ├── Processor
│ └── EmailProcessorTest.php
│ ├── Service
│ ├── Customer
│ │ └── LoginTest.php
│ ├── JwtManagerTest.php
│ ├── PasswordTest.php
│ ├── QueueTest.php
│ ├── RequestTest.php
│ └── ValidationTest.php
│ └── ViewModel
│ └── LoginTest.php
├── ViewModel
└── Request
│ └── Login.php
├── composer.json
├── etc
├── adminhtml
│ ├── di.xml
│ ├── routes.xml
│ └── system.xml
├── config.xml
├── db_schema.xml
├── db_schema_whitelist.json
├── di.xml
├── email_templates.xml
├── frontend
│ ├── di.xml
│ └── routes.xml
└── module.xml
├── registration.php
└── view
├── adminhtml
├── email
│ └── login.html
├── layout
│ └── adminhtml_auth_login.xml
├── templates
│ └── admin
│ │ └── login.phtml
└── web
│ └── css
│ └── source
│ └── _module.less
└── frontend
├── email
└── login.html
├── layout
├── customer_account_create.xml
├── customer_account_edit.xml
├── customer_account_index.xml
└── customer_account_login.xml
├── templates
├── account
│ └── dashboard
│ │ └── info.phtml
└── form
│ ├── edit.phtml
│ ├── login.phtml
│ └── register.phtml
└── web
└── css
└── source
└── _module.less
/Api/RequestLoginRepositoryInterface.php:
--------------------------------------------------------------------------------
1 | _scopeConfig->getValue(Config::XML_PATH_HOODOOR_SECRET_KEY->value);
20 | $element->setType('password')->setValue($value)->setReadonly(true);
21 | $html = parent::_getElementHtml($element);
22 | return $html . $this->getButtonHtml() . $this->getJs($element);
23 | }
24 |
25 | private function getButtonHtml(): string
26 | {
27 | $button = $this->getLayout()->createBlock(WidgetButton::class)
28 | ->setData([
29 | 'id' => 'generate_secret_key',
30 | 'label' => __('Generate Secret Key'),
31 | 'class' => 'generate-secret-key-button'
32 | ]);
33 |
34 | return $button->toHtml();
35 | }
36 |
37 | private function getJs(AbstractElement $element): string
38 | {
39 | return '
40 |
47 | ';
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Controller/Account/CreatePost.php:
--------------------------------------------------------------------------------
1 | password) {
95 | $this->password = $this->passwordService->generate();
96 | }
97 | $this->getRequest()->setParams([
98 | 'password' => $this->password,
99 | 'password_confirmation' => $this->password
100 | ]);
101 | return parent::execute();
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/Controller/Account/Edit.php:
--------------------------------------------------------------------------------
1 | getRequest()->getParam('changepass')) {
18 | return $this->_redirect('customer/account');
19 | }
20 | return parent::execute();
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Controller/Account/ForgotPassword.php:
--------------------------------------------------------------------------------
1 | messageManager->addErrorMessage(__('Access denied.'));
17 | return $this->_redirect('customer/account');
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Controller/Account/ForgotPasswordPost.php:
--------------------------------------------------------------------------------
1 | messageManager->addErrorMessage(__('Access denied.'));
17 | return $this->_redirect('customer/account');
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Controller/Adminhtml/Pwl/ProcessLogin.php:
--------------------------------------------------------------------------------
1 | redirectFactory->create();
37 | $params = $this->request->getParams();
38 | if ($params) {
39 | try {
40 | if (isset($params['request'])) {
41 | $decryptedData = $this->jwtManager->validateToken($params['request']);
42 | if ($decryptedData) {
43 | $params = [
44 | 'email' => $decryptedData->email,
45 | 'token' => $decryptedData->token
46 | ];
47 | }
48 | if (isset($params['email']) && isset($params['token'])) {
49 | try {
50 | $loginRequest = $this->loginRequestRepository->get($params['email']);
51 | if ($loginRequest->getToken() === $params['token']) {
52 | if ($loginRequest->hasBeenUsed() || $loginRequest->hasExpired()) {
53 | $this->messageManager->addErrorMessage(
54 | __('Unable to execute the request. Please try again.')
55 | );
56 | return $redirect->setPath('*');
57 | }
58 | $this->loginRequestRepository->lock($loginRequest);
59 | $this->request->setParams(['email' => $params['email']]);
60 | $this->adminLoginService->perform($this->request);
61 | $this->loginRequestRepository->delete($loginRequest);
62 | }
63 | } catch (NoSuchEntityException) {
64 | throw new RequestException(_('Invalid request. Please try again.'));
65 | }
66 | } else {
67 | throw new RequestException(
68 | _('Invalid request. Please try again.')
69 | );
70 | }
71 | } else {
72 | throw new RequestException(
73 | _('Invalid request. Please try again.')
74 | );
75 | }
76 | } catch (\Exception $e) {
77 | $this->messageManager->addErrorMessage($e->getMessage());
78 | }
79 | }
80 | return $redirect->setPath('*');
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/Controller/Adminhtml/Pwl/RequestLogin.php:
--------------------------------------------------------------------------------
1 | redirectFactory->create();
39 |
40 | $params = $this->request->getParams();
41 | if ($params) {
42 | $isFormKey = $this->formKey->isEmpty();
43 | if ($isFormKey) {
44 | $this->messageManager->addErrorMessage(__('Invalid Form Key. Please refresh the page.'));
45 | return $redirect->setPath('*/*');
46 | }
47 | if (!isset($params['login']['username'])) {
48 | $this->messageManager->addErrorMessage(
49 | __('You must enter a valid email address.')
50 | );
51 | return $redirect->setPath('*/*');
52 | } else {
53 | try {
54 | $this->queueService->add($params, 'admin');
55 | $this->messageManager->addSuccessMessage(
56 | __('If an account exists, you will receive an email to proceed with your request.')
57 | );
58 | } catch (\Exception $e) {
59 | $this->messageManager->addErrorMessage($e->getMessage());
60 | return $redirect->setPath('*/*');
61 | }
62 | }
63 | }
64 |
65 | return $redirect->setPath('*/*');
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Controller/Pwl/ProcessLogin.php:
--------------------------------------------------------------------------------
1 | redirectFactory->create();
37 | $params = $this->request->getParams();
38 | if ($params) {
39 | try {
40 | if (isset($params['request'])) {
41 | $decryptedData = $this->jwtManager->validateToken($params['request']);
42 | if ($decryptedData) {
43 | $params = [
44 | 'email' => $decryptedData->email,
45 | 'token' => $decryptedData->token
46 | ];
47 | }
48 | if (isset($params['email']) && isset($params['token'])) {
49 | try {
50 | $request = $this->loginRequestRepository->get($params['email']);
51 | if ($request->getToken() === $params['token']) {
52 | if ($request->hasBeenUsed() || $request->hasExpired()) {
53 | $this->messageManager->addErrorMessage(
54 | __('Unable to execute request. Please try again.')
55 | );
56 | return $redirect->setPath('*/account/login');
57 | }
58 | $this->loginRequestRepository->lock($request);
59 | $this->loginService->perform($params);
60 | $this->loginRequestRepository->delete($request);
61 | }
62 | } catch (NoSuchEntityException) {
63 | throw new RequestException(_('Invalid request. Please try again.'));
64 | }
65 | } else {
66 | throw new RequestException(_('Invalid request. Please try again.'));
67 | }
68 | } else {
69 | throw new RequestException(_('Invalid request. Please try again.'));
70 | }
71 | } catch (RequestException $e) {
72 | $this->messageManager->addErrorMessage($e->getMessage());
73 | }
74 | }
75 | return $redirect->setPath('*/account');
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/Controller/Pwl/RequestLogin.php:
--------------------------------------------------------------------------------
1 | redirectFactory->create();
36 |
37 | $params = $this->request->getParams();
38 | if ($params) {
39 | $isFormKey = $this->formKey->isPresent();
40 | if (!$isFormKey) {
41 | $this->messageManager->addErrorMessage(
42 | __('Invalid Form Key. Please refresh the page.')
43 | );
44 | return $redirect->setPath('*/*/login');
45 | }
46 | if (!isset($params['login']['username'])) {
47 | $this->messageManager->addErrorMessage(
48 | __('You must enter a valid email address.')
49 | );
50 | return $redirect->setPath('*/*/login');
51 | } else {
52 | try {
53 | $this->queueService->add($params, 'customer');
54 | $this->messageManager->addSuccessMessage(
55 | __('If a customer account exists, you will receive an email to proceed with your request.')
56 | );
57 | } catch (\Exception $e) {
58 | $this->messageManager->addErrorMessage($e->getMessage());
59 | return $redirect->setPath('*/*/login');
60 | }
61 | }
62 | }
63 |
64 | return $redirect->setPath('customer/account');
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Enum/Config.php:
--------------------------------------------------------------------------------
1 | resourceModel->loadByEmail($email);
62 | if ($data !== false) {
63 | $this->setData($data);
64 | $this->setOrigData();
65 | }
66 | return $this;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Model/Admin/User/Save.php:
--------------------------------------------------------------------------------
1 | isEnabled()) {
36 | parent::execute();
37 | return;
38 | }
39 |
40 | $userId = (int)$this->getRequest()->getParam('user_id');
41 | $data = $this->getRequest()->getPostValue();
42 | if (array_key_exists('form_key', $data)) {
43 | unset($data['form_key']);
44 | }
45 | if (!$data) {
46 | $this->_redirect('adminhtml/*/');
47 | return;
48 | }
49 |
50 | $model = $this->_userFactory->create()->load($userId);
51 | if ($userId && $model->isObjectNew()) {
52 | $this->messageManager->addError(__('This user no longer exists.'));
53 | $this->_redirect('adminhtml/*/');
54 | return;
55 | }
56 | $model->setData($this->_getAdminUserData($data));
57 | $userRoles = $this->getRequest()->getParam('roles', []);
58 | if (count($userRoles)) {
59 | $model->setRoleId($userRoles[0]);
60 | }
61 |
62 | /** @var $currentUser User */
63 | $currentUser = $this->_objectManager->get(\Magento\Backend\Model\Auth\Session::class)->getUser();
64 | if ($userId == $currentUser->getId()
65 | && $this->_objectManager->get(\Magento\Framework\Validator\Locale::class)
66 | ->isValid($data['interface_locale'])
67 | ) {
68 | $this->_objectManager->get(
69 | \Magento\Backend\Model\Locale\Manager::class
70 | )->switchBackendInterfaceLocale(
71 | $data['interface_locale']
72 | );
73 | }
74 |
75 | try {
76 | $model->save();
77 | $this->messageManager->addSuccess(__('You saved the user.'));
78 | $this->_getSession()->setUserData(false);
79 | $this->_redirect('adminhtml/*/');
80 |
81 | $model->sendNotificationEmailsIfRequired();
82 | } catch (UserLockedException $e) {
83 | $this->_auth->logout();
84 | $this->getSecurityCookie()->setLogoutReasonCookie(
85 | \Magento\Security\Model\AdminSessionsManager::LOGOUT_REASON_USER_LOCKED
86 | );
87 | $this->_redirect('*');
88 | } catch (NotificationExceptionInterface $exception) {
89 | $this->messageManager->addErrorMessage($exception->getMessage());
90 | } catch (\Magento\Framework\Validator\Exception $e) {
91 | $messages = $e->getMessages();
92 | $this->messageManager->addMessages($messages);
93 | $this->redirectToEdit($model, $data);
94 | } catch (\Magento\Framework\Exception\LocalizedException $e) {
95 | if ($e->getMessage()) {
96 | $this->messageManager->addError($e->getMessage());
97 | }
98 | $this->redirectToEdit($model, $data);
99 | }
100 | }
101 |
102 | private function getSecurityCookie()
103 | {
104 | if (!($this->securityCookie instanceof SecurityCookie)) {
105 | return \Magento\Framework\App\ObjectManager::getInstance()->get(SecurityCookie::class);
106 | } else {
107 | return $this->securityCookie;
108 | }
109 | }
110 |
111 | private function isEnabled(): bool
112 | {
113 | return $this->scopeConfig->isSetFlag(self::XML_PATH_ENABLE_ADMIN);
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/Model/LoginRequest.php:
--------------------------------------------------------------------------------
1 | _init(ResourceModel\LoginRequest::class);
17 | }
18 |
19 | public function setEmail(string $email): LoginRequest
20 | {
21 | return $this->setData('email', $email);
22 | }
23 |
24 | public function setType(string $type): LoginRequest
25 | {
26 | return $this->setData('type', $type);
27 | }
28 |
29 | public function setToken(string $token): LoginRequest
30 | {
31 | return $this->setData('token', $token);
32 | }
33 |
34 | public function setIsUsed(int $isUsed): LoginRequest
35 | {
36 | return $this->setData('is_used', $isUsed);
37 | }
38 |
39 | public function setExpiresAt(\DateTime $expiresAt): LoginRequest
40 | {
41 | return $this->setData('expires_at', $expiresAt);
42 | }
43 |
44 | public function getEmail(): ?string
45 | {
46 | return $this->getData('email');
47 | }
48 |
49 | public function getType(): ?string
50 | {
51 | return $this->getData('type');
52 | }
53 |
54 | public function getToken(): ?string
55 | {
56 | return $this->getData('token');
57 | }
58 |
59 | public function getIsUsed(): ?string
60 | {
61 | return $this->getData('is_used');
62 | }
63 |
64 | public function getExpiresAt(): ?string
65 | {
66 | return $this->getData('expires_at');
67 | }
68 |
69 | public function hasExpired(): bool
70 | {
71 | $now = new \DateTime;
72 | return $now->getTimestamp() > $this->getExpiresAt();
73 | }
74 |
75 | public function hasBeenUsed(): bool
76 | {
77 | return $this->getIsUsed() !== "0";
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/Model/LoginRequestRepository.php:
--------------------------------------------------------------------------------
1 | loginRequestFactory->create();
27 | $this->loginRequestResource->load($model, $email, 'email');
28 | if (!$model->getId()) {
29 | throw new NoSuchEntityException(__('Request with email "%1" does not exist.', $email));
30 | }
31 | return $model;
32 | }
33 |
34 | public function getById(string $id): LoginRequest
35 | {
36 | $model = $this->loginRequestFactory->create();
37 | $this->loginRequestResource->load($model, $id);
38 | if (!$model->getId()) {
39 | throw new NoSuchEntityException(__('Request with id "%1" does not exist.', $id));
40 | }
41 | return $model;
42 | }
43 |
44 | public function save(LoginRequest $model): bool
45 | {
46 | try {
47 | $this->loginRequestResource->save($model);
48 | } catch (\Exception $e) {
49 | throw new CouldNotSaveException(__(
50 | 'Could not save the request: %1',
51 | $e->getMessage()
52 | ));
53 | }
54 | return true;
55 | }
56 |
57 | public function delete(LoginRequest $model): bool
58 | {
59 | try {
60 | $this->loginRequestResource->delete($model);
61 | } catch (\Exception $e) {
62 | throw new CouldNotDeleteException(__(
63 | 'Could not delete the request: %1',
64 | $e->getMessage()
65 | ));
66 | }
67 | return true;
68 | }
69 |
70 | public function lock(LoginRequest $model): bool
71 | {
72 | try {
73 | $model->setIsUsed(1);
74 | $this->save($model);
75 | } catch (\Exception $e) {
76 | throw new (__(
77 | 'Could not lock the request: %1',
78 | $e->getMessage()
79 | ));
80 | }
81 | return true;
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/Model/ResourceModel/Admin/User.php:
--------------------------------------------------------------------------------
1 | getConnection();
15 |
16 | $select = $connection->select()->from($this->getMainTable())->where('email=:email');
17 |
18 | $binds = ['email' => $email];
19 |
20 | return $connection->fetchRow($select, $binds);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Model/ResourceModel/LoginRequest.php:
--------------------------------------------------------------------------------
1 | _init('login_request_queue', 'entity_id');
18 | }
19 |
20 | public function load(AbstractModel $object, $value, $field = null): LoginRequest|static
21 | {
22 | if ($field === 'email') {
23 | $object->beforeLoad($value, $field);
24 | $connection = $this->getConnection();
25 | if ($connection && $value !== null) {
26 | $select = $connection->select()
27 | ->from($this->getMainTable())
28 | ->where('email = (?)', $value)
29 | ->order('entity_id DESC');
30 | $data = $connection->fetchRow($select);
31 | if ($data) {
32 | $object->setData($data);
33 | }
34 | }
35 |
36 | $this->unserializeFields($object);
37 | $this->_afterLoad($object);
38 | $object->afterLoad();
39 | $object->setOrigData();
40 | $object->setHasDataChanges(false);
41 |
42 | return $this;
43 | }
44 |
45 | return parent::load($object, $value, $field);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Model/ResourceModel/LoginRequest/Collection.php:
--------------------------------------------------------------------------------
1 | _init(
19 | \Opengento\Hoodoor\Model\LoginRequest::class,
20 | \Opengento\Hoodoor\Model\ResourceModel\LoginRequest::class
21 | );
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Model/Template/Manager.php:
--------------------------------------------------------------------------------
1 | scopeConfig->isSetFlag(Config::XML_PATH_HOODOOR_ENABLE_FRONTEND->value);
23 | if ($type === 'admin') {
24 | $enable = $this->scopeConfig->isSetFlag(Config::XML_PATH_HOODOOR_ENABLE_ADMIN->value);
25 | }
26 | return $enable ? $override : $default;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Plugin/AdobeImsReAuth/Block/System/Account/Edit/RemoveReAuthVerification.php:
--------------------------------------------------------------------------------
1 | passwordVerification->remove($subject);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Plugin/AdobeImsReAuth/Block/User/Edit/Tab/RemoveReAuthVerification.php:
--------------------------------------------------------------------------------
1 | passwordVerification->remove($subject);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Plugin/AdobeImsReAuth/Block/User/Role/Tab/RemoveReAuthVerification.php:
--------------------------------------------------------------------------------
1 | passwordVerification->remove($subject);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Plugin/AdobeImsReAuth/PerformIdentityCheckMessagePlugin.php:
--------------------------------------------------------------------------------
1 | scopeConfig->isSetFlag(self::XML_PATH_HOODOOR_ENABLE_ADMIN);
32 | if ($hoodoorAdminIsEnable) {
33 | return true;
34 | }
35 |
36 | if ($this->adminImsConfig->enabled() === false) {
37 | return $proceed($passwordString);
38 | }
39 |
40 | try {
41 | return $proceed($passwordString);
42 | } catch (AuthenticationException $exception) {
43 | throw new AuthenticationException(
44 | __('Please perform the AdobeIms reAuth and try again.')
45 | );
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Plugin/Model/AccountManagement.php:
--------------------------------------------------------------------------------
1 | getPathInfo();
25 |
26 | foreach ($blockedRoutes as $route) {
27 | if (str_contains($currentPath, $route)) {
28 | throw new AuthorizationException(__('Access to this API is disabled.'));
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Processor/EmailProcessor.php:
--------------------------------------------------------------------------------
1 | accountData = null;
37 | }
38 |
39 | public function sendMail(string $to, string $type): void
40 | {
41 | try {
42 | $templateId = $this->scopeConfig->getValue(Config::XML_PATH_HOODOOR_TEMPLATE_ID->value);
43 | $fromEmail = $this->scopeConfig->getValue(Config::XML_PATH_HOODOOR_SENDER_EMAIL->value);
44 | $fromName = $this->scopeConfig->getValue(Config::XML_PATH_HOODOOR_SENDER_NAME->value);
45 |
46 | $accountData = $this->getAccountDataByEmail($to);
47 | $requestEmail = $accountData->getEmail();
48 | $requestToken = $accountData->getToken();
49 |
50 | $templateVars = [
51 | 'type' => $type,
52 | 'request' => $this->jwtManager->generateToken([
53 | 'email' => $requestEmail,
54 | 'token' => $requestToken
55 | ], 900)
56 | ];
57 |
58 | $this->inlineTranslation->suspend();
59 |
60 | $storeScope = ScopeInterface::SCOPE_STORE;
61 |
62 | $transport = $this->transportBuilder
63 | ->setTemplateIdentifier($templateId)
64 | ->setTemplateModel($type === 'admin' ? BackendTemplate::class : Template::class)
65 | ->setTemplateOptions([
66 | 'area' => $type === 'admin' ? Area::AREA_ADMINHTML : Area::AREA_FRONTEND,
67 | 'store' => Store::DEFAULT_STORE_ID
68 | ])
69 | ->setTemplateVars($templateVars)
70 | ->setFromByScope(
71 | [
72 | 'email' => $fromEmail,
73 | 'name' => $fromName
74 | ],
75 | $storeScope
76 | )
77 | ->addTo($to)
78 | ->getTransport();
79 |
80 | $transport->sendMessage();
81 |
82 | $this->inlineTranslation->resume();
83 |
84 | } catch (\Exception $e) {
85 | $this->logger->debug($e->getMessage());
86 | }
87 | }
88 |
89 | private function getAccountDataByEmail(string $email): LoginRequest
90 | {
91 | if (!$this->accountData) {
92 | $this->accountData = $this->loginRequestRepository->get($email);
93 | }
94 | return $this->accountData;
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Module for Magento 2
2 |
3 | [](https://packagist.org/packages/opengento/module-hoodoor)
4 | [](./LICENSE)
5 | [](https://packagist.org/packages/opengento/module-hoodoor/stats)
6 | [](https://packagist.org/packages/opengento/module-hoodoor/stats)
7 |
8 | This module provides a top-notch security for your customers' accounts by adopting a passwordless approach, effectively removing the vulnerability of weak passwords from your database. This instills a sense of confidence and reliability in your platform among your customers.
9 |
10 | - [Setup](#setup)
11 | - [Composer installation](#composer-installation)
12 | - [Setup the module](#setup-the-module)
13 | - [Settings](#settings)
14 | - [Documentation](#documentation)
15 | - [Support](#support)
16 | - [Authors](#authors)
17 | - [License](#license)
18 |
19 | ## Setup
20 |
21 | Magento 2 Open Source or Commerce edition is required.
22 |
23 | ### Composer installation
24 |
25 | Run the following composer command:
26 |
27 | ```
28 | composer require opengento/module-hoodoor
29 | ```
30 |
31 | ### Setup the module
32 |
33 | Run the following magento command:
34 |
35 | ```
36 | bin/magento setup:upgrade
37 | ```
38 |
39 | **If you are in production mode, do not forget to recompile and redeploy the static resources.**
40 |
41 | ## Settings
42 |
43 | The configuration for this module is available in `Stores > Configuration > OpenGento > Hoodoor`.
44 |
45 | Make sure you have generated a secret key.
46 |
47 | ## Documentation
48 |
49 | ### Compatibility and Activation:
50 |
51 | This module is compatible with Magento 2 version 2.4.6-p4.
52 |
53 | You have the flexibility to enable its functionality either on the Magento frontend or backend. To activate either option, adjust the corresponding values in the config settings.
54 |
55 | ### Token Expiration and Customization:
56 |
57 | By default, the authentication token remains valid for 15 minutes after the email is sent. However, you have the option to customize this duration according to your requirements. Refer to the PHP documentation on how to modify the datetime value.
58 |
59 | ### Enhanced Security Measures:
60 |
61 | We have implemented a robust security layer to ensure a high level of protection for the data transmitted via the HTTP protocol.
62 |
63 | ### Private Key Generation:
64 |
65 | To process requests securely, it is essential to generate a private key in the settings. This private key serves as a crucial component for decrypting and authenticating requests. Failure to provide this key may hinder the ability to decipher and establish connections effectively.
66 |
67 | ## Support
68 |
69 | Raise a new [request](https://github.com/opengento/magento2-hoodoor-login/issues) to the issue tracker.
70 |
71 | ## Authors
72 |
73 | - **Opengento Community** - *Lead* - [](https://twitter.com/opengento)
74 | - **Ronan Guérin** - *Maintainer* - [](https://github.com/ronangr1)
75 | - **Contributors** - *Contributor* - [](https://github.com/opengento/magento2-store-path-url/graphs/contributors)
76 |
77 | ## License
78 |
79 | This project is licensed under the MIT License - see the [LICENSE](./LICENSE) details.
80 |
81 | ***That's all folks!***
82 |
--------------------------------------------------------------------------------
/Service/Admin/Login.php:
--------------------------------------------------------------------------------
1 | getParams();
39 | $backendUser = $this->getBackendUser($params);
40 | if ($backendUser) {
41 | $password = $this->random->getUniqueHash();
42 | // Set new password each time you need to login
43 | $backendUser->setPassword($password);
44 | $backendUser->save();
45 | $request = $request->setPostValue('login', [
46 | 'username' => $backendUser->getUserName(),
47 | 'password' => $password
48 | ]);
49 | // Now login
50 | $this->processNotLoggedInUser($request);
51 | }
52 | }
53 |
54 | private function getBackendUser(array $data): ?User
55 | {
56 | try {
57 | $user = $this->user->loadByEmail($data['email']);
58 | if ($user->getId()) {
59 | return $user;
60 | }
61 | } catch (\Exception $e) {
62 | $this->logger->debug($e->getMessage());
63 | }
64 | return null;
65 | }
66 |
67 | private function processNotLoggedInUser(RequestInterface $request): void
68 | {
69 | $isRedirectNeeded = false;
70 | if ($request->getPost('login')) {
71 | if ($this->performLogin($request)) {
72 | $isRedirectNeeded = $this->redirectIfNeededAfterLogin($request);
73 | }
74 | }
75 | if (!$isRedirectNeeded && !$request->isForwarded()) {
76 | if ($request->getParam('isIframe')) {
77 | $request->setForwarded(true)
78 | ->setRouteName('adminhtml')
79 | ->setControllerName('auth')
80 | ->setActionName('deniedIframe')
81 | ->setDispatched(false);
82 | } elseif ($request->getParam('isAjax')) {
83 | $request->setForwarded(true)
84 | ->setRouteName('adminhtml')
85 | ->setControllerName('auth')
86 | ->setActionName('deniedJson')
87 | ->setDispatched(false);
88 | } else {
89 | $request->setForwarded(true)
90 | ->setRouteName('adminhtml')
91 | ->setControllerName('auth')
92 | ->setActionName('login')
93 | ->setDispatched(false);
94 | }
95 | }
96 | }
97 |
98 | private function performLogin(RequestInterface $request): bool
99 | {
100 | $outputValue = true;
101 | $postLogin = $request->getPost('login');
102 | $username = $postLogin['username'] ?? '';
103 | $password = $postLogin['password'] ?? '';
104 | $request->setPostValue('login', null);
105 |
106 | try {
107 | $this->auth->login($username, $password);
108 | } catch (AuthenticationException $e) {
109 | if (!$request->getParam('messageSent')) {
110 | $this->messageManager->addErrorMessage($e->getMessage());
111 | $request->setParam('messageSent', true);
112 | $outputValue = false;
113 | }
114 | }
115 | return $outputValue;
116 | }
117 |
118 | private function redirectIfNeededAfterLogin(RequestInterface $request): bool
119 | {
120 | $requestUri = null;
121 |
122 | // Checks, whether secret key is required for admin access or request uri is explicitly set
123 | if ($this->url->useSecretKey()) {
124 | // The requested URL has an invalid secret key and therefore redirecting to this URL
125 | // will cause a security vulnerability.
126 | $requestUri = $this->url->getUrl($this->url->getStartupPageUrl());
127 | } elseif ($request) {
128 | $requestUri = $request->getRequestUri();
129 | }
130 |
131 | if (!$requestUri) {
132 | return false;
133 | }
134 |
135 | $this->response->setRedirect($requestUri);
136 | $this->actionFlag->set('', ActionInterface::FLAG_NO_DISPATCH, true);
137 | return true;
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/Service/Admin/PasswordVerification.php:
--------------------------------------------------------------------------------
1 | isEnabled()) {
31 | return;
32 | }
33 |
34 | $form = $subject->getForm();
35 | if (!$form instanceof DataForm) {
36 | return;
37 | }
38 |
39 | $this->removePasswordFields($form);
40 | $this->removeVerificationFieldset($form);
41 |
42 | $subject->setForm($form);
43 | }
44 |
45 | private function isEnabled(): bool
46 | {
47 | return $this->scopeConfig->isSetFlag(self::XML_PATH_ENABLE_ADMIN);
48 | }
49 |
50 | private function removePasswordFields(DataForm $form): void
51 | {
52 | /** @var Fieldset|null $baseFs */
53 | $baseFs = $form->getElement('base_fieldset');
54 | if (!$baseFs instanceof Fieldset) {
55 | return;
56 | }
57 | $this->unsetByIds(
58 | $baseFs->getElements(),
59 | ['password', 'confirmation']
60 | );
61 | }
62 |
63 | private function removeVerificationFieldset(DataForm $form): void
64 | {
65 | $elements = $form->getElements();
66 | $this->unsetByIds(
67 | $elements,
68 | ['current_user_verification_fieldset']
69 | );
70 | $form->setElements($elements);
71 | }
72 |
73 | private function unsetByIds(ElementCollection $collection, array $ids): void
74 | {
75 | foreach ($collection as $key => $elem) {
76 | if (in_array((string)$elem->getId(), $ids, true)) {
77 | unset($collection[$key]);
78 | }
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/Service/Customer/Login.php:
--------------------------------------------------------------------------------
1 | customerRepository->get($data['email']);
45 | $this->credentialsValidator->checkPasswordDifferentFromEmail($data['email'], $data['token']);
46 | $customerSecure = $this->customerRegistry->retrieveSecureData($customer->getId());
47 | $customerSecure->setPasswordHash($this->createPasswordHash($data['token']));
48 | $this->sessionCleaner->clearFor((int)$customer->getId());
49 | $this->customerRepository->save($customer);
50 | // Now login
51 | $customer = $this->customerAccountManagement->authenticate($data['email'], $data['token']);
52 | $this->session->setCustomerDataAsLoggedIn($customer);
53 | if ($this->getCookieManager()->getCookie('mage-cache-sessid')) {
54 | $metadata = $this->getCookieMetadataFactory()->createCookieMetadata();
55 | $metadata->setPath('/');
56 | $this->getCookieManager()->deleteCookie('mage-cache-sessid', $metadata);
57 | }
58 | return true;
59 | } catch (InputException|InputMismatchException|LocalizedException $e) {
60 | $this->logger->debug($e->getMessage());
61 | }
62 | return false;
63 | }
64 |
65 | private function createPasswordHash(string $password): string
66 | {
67 | return $this->encryptor->getHash($password, true);
68 | }
69 |
70 | private function getCookieManager(): PhpCookieManager
71 | {
72 | return $this->cookieMetadataManager;
73 | }
74 |
75 | private function getCookieMetadataFactory(): CookieMetadataFactory
76 | {
77 | return $this->cookieMetadataFactory;
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/Service/JwtManager.php:
--------------------------------------------------------------------------------
1 | secretKey = $this->scopeConfig->getValue(Config::XML_PATH_HOODOOR_SECRET_KEY->value);
25 | }
26 |
27 | public function generateToken(array $payload, int $expirationInSeconds): string
28 | {
29 | $issuedAt = time();
30 | $expire = $issuedAt + $expirationInSeconds;
31 |
32 | $tokenPayload = array_merge($payload, [
33 | 'iat' => $issuedAt,
34 | 'exp' => $expire,
35 | ]);
36 |
37 | return $this->jwtHelper->encode($tokenPayload, $this->secretKey, 'HS256');
38 | }
39 |
40 | public function validateToken(string $token): bool|\stdClass
41 | {
42 | try {
43 | return $this->jwtHelper->decode($token, new Key($this->secretKey, 'HS256'));
44 | } catch (\Exception $e) {
45 | $this->logger->critical($e->getMessage());
46 | }
47 | return false;
48 | }
49 | }
50 |
51 |
--------------------------------------------------------------------------------
/Service/Password.php:
--------------------------------------------------------------------------------
1 | random->getUniqueHash();
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Service/Queue.php:
--------------------------------------------------------------------------------
1 | requestService->create($params['login']['username'], $type);
23 | if ($success) {
24 | $this->emailProcessor->sendMail($params['login']['username'], $type);
25 | return true;
26 | }
27 | return false;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Service/Request.php:
--------------------------------------------------------------------------------
1 | validationService->validate($email, $type);
33 | if ($isValid) {
34 | $dateTime = new \DateTime();
35 | $maxTimeExpiration = $this->scopeConfig
36 | ->getValue(Config::XML_PATH_HOODOOR_MAX_TIME_EXPIRATION->value);
37 | $loginRequest = $this->loginRequestFactory->create();
38 | $loginRequest->setEmail($email)
39 | ->setType($type)
40 | ->setToken($this->random->getUniqueHash())
41 | ->setExpiresAt($dateTime->modify($maxTimeExpiration));
42 | $this->loginRequestRepository->save($loginRequest);
43 | return true;
44 | }
45 | } catch (\Exception $e) {
46 | $this->logger->debug($e->getMessage());
47 | }
48 | return false;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Service/Validation.php:
--------------------------------------------------------------------------------
1 | user->loadByEmail($email);
29 | return $user && $user->getIsActive();
30 | }
31 | return $this->customerRepository->get($email)->getId() !== null;
32 | } catch (NoSuchEntityException $e) {
33 | $this->logger->debug($e->getMessage());
34 | }
35 | return false;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Test/Unit/Plugin/Model/AccountManagementTest.php:
--------------------------------------------------------------------------------
1 | plugin = new AccountManagement();
21 | }
22 |
23 | public function testBeforeChangePasswordThrowsException()
24 | {
25 | $this->expectException(\Exception::class);
26 | $this->expectExceptionMessage('Access denied.');
27 |
28 | $this->plugin->beforeChangePassword(
29 | $this->createMock(SubjectAccountManagement::class),
30 | 'customer@example.com',
31 | 'current_password',
32 | 'new_password'
33 | );
34 | }
35 |
36 | public function testBeforeChangePasswordByIdThrowsException()
37 | {
38 | $this->expectException(\Exception::class);
39 | $this->expectExceptionMessage('Access denied.');
40 |
41 | $this->plugin->beforeChangePasswordById(
42 | $this->createMock(SubjectAccountManagement::class),
43 | 123,
44 | 'current_password',
45 | 'new_password'
46 | );
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Test/Unit/Plugin/Webapi/Controller/Rest/DisableApiTest.php:
--------------------------------------------------------------------------------
1 | plugin = new DisableApi();
21 | $this->restControllerMock = $this->createMock(Rest::class);
22 | }
23 |
24 | public function testBeforeDispatchThrowsAuthorizationExceptionForBlockedRoutes()
25 | {
26 | $blockedRoutes = [
27 | 'https://magento.test/rest/V1/customers',
28 | 'https://magento.test/rest/all/V1/customers',
29 | 'https://magento.test/rest/default/V1/customers'
30 | ];
31 |
32 | foreach ($blockedRoutes as $blockedRoute) {
33 | $requestMock = $this->getMockBuilder(HttpRequest::class)
34 | ->disableOriginalConstructor()
35 | ->onlyMethods(['getPathInfo'])
36 | ->getMock();
37 |
38 | $requestMock->expects($this->once())
39 | ->method('getPathInfo')
40 | ->willReturn($blockedRoute);
41 |
42 | $this->expectException(AuthorizationException::class);
43 | $this->expectExceptionMessage('Access to this API is disabled.');
44 |
45 | $this->plugin->beforeDispatch($this->restControllerMock, $requestMock);
46 | }
47 | }
48 |
49 | public function testBeforeDispatchDoesNotThrowExceptionForAllowedRoutes()
50 | {
51 | $allowedRoutes = [
52 | 'https://magento.test/one/allowed/route',
53 | 'https://magento.test/any/allowed/route',
54 | 'https://magento.test/another/allowed/route'
55 | ];
56 |
57 | foreach ($allowedRoutes as $allowedRoute) {
58 | $requestMock = $this->getMockBuilder(HttpRequest::class)
59 | ->disableOriginalConstructor()
60 | ->onlyMethods(['getPathInfo'])
61 | ->getMock();
62 |
63 | $requestMock->expects($this->once())
64 | ->method('getPathInfo')
65 | ->willReturn($allowedRoute);
66 |
67 | $this->plugin->beforeDispatch($this->restControllerMock, $requestMock);
68 | $this->addToAssertionCount(1);
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Test/Unit/Processor/EmailProcessorTest.php:
--------------------------------------------------------------------------------
1 | loginRequestRepositoryMock = $this->createMock(RequestLoginRepositoryInterface::class);
46 | $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class);
47 | $this->transportBuilderMock = $this->createMock(TransportBuilder::class);
48 | $this->inlineTranslationMock = $this->createMock(StateInterface::class);
49 | $this->loggerMock = $this->createMock(LoggerInterface::class);
50 | $this->jwtManagerMock = $this->createMock(JwtManager::class);
51 |
52 | $this->emailProcessor = new EmailProcessor(
53 | $this->loginRequestRepositoryMock,
54 | $this->scopeConfigMock,
55 | $this->transportBuilderMock,
56 | $this->inlineTranslationMock,
57 | $this->loggerMock,
58 | $this->jwtManagerMock
59 | );
60 | }
61 |
62 | public function testSendMailSuccessfully()
63 | {
64 | $to = 'customer@magento.test';
65 | $type = 'frontend';
66 |
67 | $loginRequestMock = $this->createMock(LoginRequest::class);
68 | $loginRequestMock->expects($this->once())->method('getEmail')->willReturn($to);
69 | $loginRequestMock->expects($this->once())->method('getToken')->willReturn('test_token');
70 |
71 | $this->loginRequestRepositoryMock->expects($this->once())
72 | ->method('get')
73 | ->with($to)
74 | ->willReturn($loginRequestMock);
75 |
76 | $this->scopeConfigMock->method('getValue')
77 | ->willReturnMap([
78 | [Config::XML_PATH_HOODOOR_TEMPLATE_ID->value, ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null, 'template_id'],
79 | [Config::XML_PATH_HOODOOR_SENDER_EMAIL->value, ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null, 'sender@magento.test'],
80 | [Config::XML_PATH_HOODOOR_SENDER_NAME->value, ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null, 'Sender Name']
81 | ]);
82 |
83 | $this->jwtManagerMock->expects($this->once())
84 | ->method('generateToken')
85 | ->with(['email' => $to, 'token' => 'test_token'], 900)
86 | ->willReturn('jwt_token');
87 |
88 | $this->inlineTranslationMock->expects($this->once())->method('suspend');
89 |
90 | $this->transportBuilderMock->expects($this->once())
91 | ->method('setTemplateIdentifier')
92 | ->with('template_id')
93 | ->willReturnSelf();
94 |
95 | $this->transportBuilderMock->expects($this->once())
96 | ->method('setTemplateModel')
97 | ->with(Template::class)
98 | ->willReturnSelf();
99 |
100 | $this->transportBuilderMock->expects($this->once())
101 | ->method('setTemplateOptions')
102 | ->with([
103 | 'area' => Area::AREA_FRONTEND,
104 | 'store' => Store::DEFAULT_STORE_ID
105 | ])
106 | ->willReturnSelf();
107 |
108 | $this->transportBuilderMock->expects($this->once())
109 | ->method('setTemplateVars')
110 | ->with(['type' => 'frontend', 'request' => 'jwt_token'])
111 | ->willReturnSelf();
112 |
113 | $this->transportBuilderMock->expects($this->once())
114 | ->method('setFromByScope')
115 | ->with(['email' => 'sender@magento.test', 'name' => 'Sender Name'], ScopeInterface::SCOPE_STORE)
116 | ->willReturnSelf();
117 |
118 | $this->transportBuilderMock->expects($this->once())
119 | ->method('addTo')
120 | ->with($to)
121 | ->willReturnSelf();
122 |
123 | $transportMock = $this->createMock(TransportInterface::class);
124 | $this->transportBuilderMock->expects($this->once())
125 | ->method('getTransport')
126 | ->willReturn($transportMock);
127 |
128 | $transportMock->expects($this->once())->method('sendMessage');
129 |
130 | $this->inlineTranslationMock->expects($this->once())->method('resume');
131 |
132 | $this->emailProcessor->sendMail($to, $type);
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/Test/Unit/Service/Customer/LoginTest.php:
--------------------------------------------------------------------------------
1 | customerRepositoryMock = $this->createMock(CustomerRepositoryInterface::class);
54 | $this->customerAccountManagementMock = $this->createMock(AccountManagementInterface::class);
55 | $this->credentialsValidatorMock = $this->createMock(CredentialsValidator::class);
56 | $this->customerRegistryMock = $this->createMock(CustomerRegistry::class);
57 | $this->encryptorMock = $this->createMock(Encryptor::class);
58 | $this->sessionCleanerMock = $this->createMock(SessionCleanerInterface::class);
59 | $this->sessionMock = $this->createMock(Session::class);
60 | $this->cookieMetadataFactoryMock = $this->createMock(CookieMetadataFactory::class);
61 | $this->cookieMetadataManagerMock = $this->createMock(PhpCookieManager::class);
62 | $this->loggerMock = $this->createMock(LoggerInterface::class);
63 |
64 | $this->loginService = new Login(
65 | $this->customerRepositoryMock,
66 | $this->customerAccountManagementMock,
67 | $this->credentialsValidatorMock,
68 | $this->customerRegistryMock,
69 | $this->encryptorMock,
70 | $this->sessionCleanerMock,
71 | $this->sessionMock,
72 | $this->cookieMetadataFactoryMock,
73 | $this->cookieMetadataManagerMock,
74 | $this->loggerMock
75 | );
76 | }
77 |
78 | public function testPerformReturnsTrueOnSuccessfulLogin()
79 | {
80 | $data = ['email' => 'customer@magento.test', 'token' => 'secure_token'];
81 |
82 | $customerMock = $this->createMock(CustomerInterface::class);
83 | $customerMock->method('getId')->willReturn(123);
84 |
85 | $customerSecureMock = $this->getMockBuilder(CustomerSecure::class)
86 | ->disableOriginalConstructor()
87 | ->addMethods(['setPasswordHash'])
88 | ->getMock();
89 |
90 | $this->customerRepositoryMock->expects($this->once())
91 | ->method('get')
92 | ->with($data['email'])
93 | ->willReturn($customerMock);
94 |
95 | $this->credentialsValidatorMock->expects($this->once())
96 | ->method('checkPasswordDifferentFromEmail')
97 | ->with($data['email'], $data['token']);
98 |
99 | $this->customerRegistryMock->expects($this->once())
100 | ->method('retrieveSecureData')
101 | ->with(123)
102 | ->willReturn($customerSecureMock);
103 |
104 | $this->encryptorMock->expects($this->once())
105 | ->method('getHash')
106 | ->willReturn('hashed_password');
107 |
108 | $customerSecureMock->expects($this->once())
109 | ->method('setPasswordHash')
110 | ->with('hashed_password');
111 |
112 | $this->sessionCleanerMock->expects($this->once())
113 | ->method('clearFor')
114 | ->with(123);
115 |
116 | $this->customerRepositoryMock->expects($this->once())
117 | ->method('save')
118 | ->with($customerMock);
119 |
120 | $this->customerAccountManagementMock->expects($this->once())
121 | ->method('authenticate')
122 | ->with($data['email'], $data['token'])
123 | ->willReturn($customerMock);
124 |
125 | $this->sessionMock->expects($this->once())
126 | ->method('setCustomerDataAsLoggedIn')
127 | ->with($customerMock);
128 |
129 | $this->assertTrue($this->loginService->perform($data));
130 | }
131 |
132 | public function testPerformReturnsFalseOnException()
133 | {
134 | $data = ['email' => 'invalid@magento.test', 'token' => 'secure_token'];
135 |
136 | $this->customerRepositoryMock->expects($this->once())
137 | ->method('get')
138 | ->with($data['email'])
139 | ->willThrowException(new NoSuchEntityException(__('Invalid request')));
140 |
141 | $this->loggerMock->expects($this->once())
142 | ->method('debug')
143 | ->with('Invalid request');
144 |
145 | $this->assertFalse($this->loginService->perform($data));
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/Test/Unit/Service/JwtManagerTest.php:
--------------------------------------------------------------------------------
1 | jwtHelperMock = $this->createMock(JwtHelper::class);
34 | $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class);
35 | $this->loggerMock = $this->createMock(LoggerInterface::class);
36 |
37 | $this->scopeConfigMock->method('getValue')
38 | ->with(Config::XML_PATH_HOODOOR_SECRET_KEY->value)
39 | ->willReturn($this->secretKey);
40 |
41 | $this->jwtManager = new JwtManager(
42 | $this->jwtHelperMock,
43 | $this->scopeConfigMock,
44 | $this->loggerMock
45 | );
46 | }
47 |
48 | public function testGenerateToken()
49 | {
50 | $payload = ['user_id' => 1];
51 | $expirationInSeconds = 3600;
52 | $issuedAt = time();
53 | $expectedExpire = $issuedAt + $expirationInSeconds;
54 |
55 | $expectedTokenPayload = array_merge($payload, [
56 | 'iat' => $issuedAt,
57 | 'exp' => $expectedExpire,
58 | ]);
59 |
60 | $this->jwtHelperMock->expects($this->once())
61 | ->method('encode')
62 | ->with($expectedTokenPayload, $this->secretKey, 'HS256')
63 | ->willReturn('generated_token');
64 |
65 | $generatedToken = $this->jwtManager->generateToken($payload, $expirationInSeconds);
66 |
67 | $this->assertEquals('generated_token', $generatedToken);
68 | }
69 |
70 | public function testValidateTokenReturnsDecodedPayload()
71 | {
72 | $token = 'valid_token';
73 | $decodedPayload = (object) ['user_id' => 1, 'iat' => time(), 'exp' => time() + 3600];
74 |
75 | $this->jwtHelperMock->expects($this->once())
76 | ->method('decode')
77 | ->with($token, new Key($this->secretKey, 'HS256'))
78 | ->willReturn($decodedPayload);
79 |
80 | $this->assertEquals($decodedPayload, $this->jwtManager->validateToken($token));
81 | }
82 |
83 | public function testValidateTokenReturnsFalseOnInvalidToken()
84 | {
85 | $token = 'invalid_token';
86 |
87 | $this->jwtHelperMock->expects($this->once())
88 | ->method('decode')
89 | ->with($token, new Key($this->secretKey, 'HS256'))
90 | ->willThrowException(new \Exception('Invalid token'));
91 |
92 | $this->loggerMock->expects($this->once())
93 | ->method('critical')
94 | ->with('Invalid token');
95 |
96 | $this->assertFalse($this->jwtManager->validateToken($token));
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Test/Unit/Service/PasswordTest.php:
--------------------------------------------------------------------------------
1 | randomMock = $this->createMock(Random::class);
24 |
25 | $this->passwordService = new Password($this->randomMock);
26 | }
27 |
28 | public function testGenerateReturnsUniqueHash()
29 | {
30 | $this->randomMock
31 | ->expects($this->once())
32 | ->method('getUniqueHash')
33 | ->willReturn('uniqueRandomHash');
34 |
35 | $this->assertEquals('uniqueRandomHash', $this->passwordService->generate());
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Test/Unit/Service/QueueTest.php:
--------------------------------------------------------------------------------
1 | requestServiceMock = $this->createMock(Request::class);
27 | $this->emailProcessorMock = $this->createMock(EmailProcessor::class);
28 |
29 | $this->queueService = new Queue(
30 | $this->requestServiceMock,
31 | $this->emailProcessorMock
32 | );
33 | }
34 |
35 | public function testAddReturnsTrueWhenRequestCreationSucceeds()
36 | {
37 | $params = [
38 | 'login' => [
39 | 'username' => 'test@magento.test'
40 | ]
41 | ];
42 |
43 | $type = 'login';
44 |
45 | $this->requestServiceMock->expects($this->once())
46 | ->method('create')
47 | ->with('test@magento.test', $type)
48 | ->willReturn(true);
49 |
50 | $this->emailProcessorMock->expects($this->once())
51 | ->method('sendMail')
52 | ->with('test@magento.test', $type);
53 |
54 | $this->assertTrue($this->queueService->add($params, $type));
55 | }
56 |
57 | public function testAddReturnsFalseWhenRequestCreationFails()
58 | {
59 | $params = [
60 | 'login' => [
61 | 'username' => 'test@magento.test'
62 | ]
63 | ];
64 | $type = 'login';
65 |
66 | $this->requestServiceMock->expects($this->once())
67 | ->method('create')
68 | ->with('test@magento.test', $type)
69 | ->willReturn(false);
70 |
71 | $this->emailProcessorMock->expects($this->never())
72 | ->method('sendMail');
73 |
74 | $this->assertFalse($this->queueService->add($params, $type));
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/Test/Unit/Service/RequestTest.php:
--------------------------------------------------------------------------------
1 | scopeConfigMock = $this->createMock(ScopeConfigInterface::class);
43 | $this->loginRequestFactoryMock = $this->createMock(LoginRequestFactory::class);
44 | $this->loginRequestRepositoryMock = $this->createMock(LoginRequestRepository::class);
45 | $this->validationServiceMock = $this->createMock(Validation::class);
46 | $this->randomMock = $this->createMock(Random::class);
47 | $this->loggerMock = $this->createMock(LoggerInterface::class);
48 |
49 | $this->requestService = new Request(
50 | $this->scopeConfigMock,
51 | $this->loginRequestFactoryMock,
52 | $this->loginRequestRepositoryMock,
53 | $this->validationServiceMock,
54 | $this->randomMock,
55 | $this->loggerMock
56 | );
57 | }
58 |
59 | public function testCreateReturnsTrueWhenRequestIsValid()
60 | {
61 | $email = 'customer@magento.test';
62 | $type = 'login';
63 | $uniqueHash = 'uniqueHash';
64 | $maxTimeExpiration = '+1 hour';
65 |
66 | $this->validationServiceMock->expects($this->once())
67 | ->method('validate')
68 | ->with($email, $type)
69 | ->willReturn(true);
70 |
71 | $this->scopeConfigMock->expects($this->once())
72 | ->method('getValue')
73 | ->with(Config::XML_PATH_HOODOOR_MAX_TIME_EXPIRATION->value)
74 | ->willReturn($maxTimeExpiration);
75 |
76 | $this->randomMock->expects($this->once())
77 | ->method('getUniqueHash')
78 | ->willReturn($uniqueHash);
79 |
80 | $loginRequestMock = $this->createMock(LoginRequest::class);
81 |
82 | $this->loginRequestFactoryMock->expects($this->once())
83 | ->method('create')
84 | ->willReturn($loginRequestMock);
85 |
86 | $loginRequestMock->expects($this->once())->method('setEmail')->with($email)->willReturnSelf();
87 | $loginRequestMock->expects($this->once())->method('setType')->with($type)->willReturnSelf();
88 | $loginRequestMock->expects($this->once())->method('setToken')->with($uniqueHash)->willReturnSelf();
89 | $loginRequestMock->expects($this->once())->method('setExpiresAt')->with($this->isInstanceOf(DateTime::class))->willReturnSelf();
90 |
91 | $this->loginRequestRepositoryMock->expects($this->once())
92 | ->method('save')
93 | ->with($loginRequestMock);
94 |
95 | $this->assertTrue($this->requestService->create($email, $type));
96 | }
97 |
98 | public function testCreateReturnsFalseWhenValidationFails()
99 | {
100 | $email = 'invalid@magento.test';
101 | $type = 'login';
102 |
103 | $this->validationServiceMock->expects($this->once())
104 | ->method('validate')
105 | ->with($email, $type)
106 | ->willReturn(false);
107 |
108 | $this->scopeConfigMock->expects($this->never())->method('getValue');
109 | $this->loginRequestFactoryMock->expects($this->never())->method('create');
110 | $this->loginRequestRepositoryMock->expects($this->never())->method('save');
111 |
112 | $this->assertFalse($this->requestService->create($email, $type));
113 | }
114 |
115 | public function testCreateReturnsFalseAndLogsErrorWhenExceptionIsThrown()
116 | {
117 | $email = 'customer@magento.test';
118 | $type = 'login';
119 | $exceptionMessage = 'Something went wrong while processing your request.';
120 |
121 | $this->validationServiceMock->expects($this->once())
122 | ->method('validate')
123 | ->with($email, $type)
124 | ->willReturn(true);
125 |
126 | $this->scopeConfigMock->expects($this->once())
127 | ->method('getValue')
128 | ->willThrowException(new RequestException($exceptionMessage));
129 |
130 | $this->loggerMock->expects($this->once())
131 | ->method('debug')
132 | ->with($exceptionMessage);
133 |
134 | $this->assertFalse($this->requestService->create($email, $type));
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/Test/Unit/Service/ValidationTest.php:
--------------------------------------------------------------------------------
1 | customerRepositoryMock = $this->createMock(CustomerRepositoryInterface::class);
32 | $this->loggerMock = $this->createMock(LoggerInterface::class);
33 | $this->userMock = $this->createMock(User::class);
34 |
35 | $this->validationService = new Validation(
36 | $this->customerRepositoryMock,
37 | $this->loggerMock,
38 | $this->userMock
39 | );
40 | }
41 |
42 | public function testValidateReturnsTrueForActiveAdmin()
43 | {
44 | $email = 'admin@magento.test';
45 | $type = 'admin';
46 |
47 | $this->userMock->expects($this->once())
48 | ->method('loadByEmail')
49 | ->with($email)
50 | ->willReturnSelf();
51 |
52 | $this->userMock->expects($this->once())
53 | ->method('getIsActive')
54 | ->willReturn(true);
55 |
56 | $this->assertTrue($this->validationService->validate($email, $type));
57 | }
58 |
59 | public function testValidateReturnsFalseForInactiveAdmin()
60 | {
61 | $email = 'inactive_admin@magento.test';
62 | $type = 'admin';
63 |
64 | $this->userMock->expects($this->once())
65 | ->method('loadByEmail')
66 | ->with($email)
67 | ->willReturnSelf();
68 |
69 | $this->userMock->expects($this->once())
70 | ->method('getIsActive')
71 | ->willReturn(false);
72 |
73 | $this->assertFalse($this->validationService->validate($email, $type));
74 | }
75 |
76 | public function testValidateReturnsTrueForExistingCustomer()
77 | {
78 | $email = 'customer@magento.test';
79 | $type = 'customer';
80 |
81 | $customerMock = $this->createMock(CustomerInterface::class);
82 | $customerMock->method('getId')->willReturn(123);
83 |
84 | $this->customerRepositoryMock->expects($this->once())
85 | ->method('get')
86 | ->with($email)
87 | ->willReturn($customerMock);
88 |
89 | $this->assertTrue($this->validationService->validate($email, $type));
90 | }
91 |
92 | public function testValidateReturnsFalseForNonExistingCustomer()
93 | {
94 | $email = 'nonexisting@magento.test';
95 | $type = 'customer';
96 |
97 | $this->customerRepositoryMock->expects($this->once())
98 | ->method('get')
99 | ->with($email)
100 | ->willThrowException(new NoSuchEntityException(__('No such entity')));
101 |
102 | $this->loggerMock->expects($this->once())
103 | ->method('debug')
104 | ->with('No such entity');
105 |
106 | $this->assertFalse($this->validationService->validate($email, $type));
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/Test/Unit/ViewModel/LoginTest.php:
--------------------------------------------------------------------------------
1 | urlBackendMock = $this->createMock(BackendUrlInterface::class);
31 | $this->urlFrontendMock = $this->createMock(FrontendUrlInterface::class);
32 | $this->stateMock = $this->createMock(State::class);
33 |
34 | $this->loginViewModel = new Login(
35 | $this->urlFrontendMock,
36 | $this->urlBackendMock,
37 | $this->stateMock
38 | );
39 | }
40 |
41 | public function testGetPostActionUrlReturnsAdminUrlIfAdminArea()
42 | {
43 | $this->stateMock
44 | ->expects($this->once())
45 | ->method('getAreaCode')
46 | ->willReturn(Area::AREA_ADMINHTML);
47 |
48 | $this->urlBackendMock
49 | ->expects($this->once())
50 | ->method('getUrl')
51 | ->with('admin/pwl/requestlogin')
52 | ->willReturn('https://magento.test/admin/pwl/requestlogin');
53 |
54 | $this->assertEquals(
55 | 'https://magento.test/admin/pwl/requestlogin',
56 | $this->loginViewModel->getPostActionUrl()
57 | );
58 | }
59 |
60 | public function testGetPostActionUrlReturnsFrontendUrlIfFrontendArea()
61 | {
62 | $this->stateMock
63 | ->expects($this->once())
64 | ->method('getAreaCode')
65 | ->willReturn(Area::AREA_FRONTEND);
66 |
67 | $this->urlFrontendMock
68 | ->expects($this->once())
69 | ->method('getUrl')
70 | ->with('customer/pwl/requestlogin')
71 | ->willReturn('https://magento.test/customer/pwl/requestlogin');
72 |
73 | $this->assertEquals(
74 | 'https://magento.test/customer/pwl/requestlogin',
75 | $this->loginViewModel->getPostActionUrl()
76 | );
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/ViewModel/Request/Login.php:
--------------------------------------------------------------------------------
1 | state->getAreaCode();
28 |
29 | if ($areaCode === Area::AREA_ADMINHTML) {
30 | return $this->urlBackend->getUrl('admin/pwl/requestlogin');
31 | }
32 |
33 | return $this->urlFrontend->getUrl('customer/pwl/requestlogin');
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "opengento/module-hoodoor",
3 | "description": "This module enables you to log in without a password and without relying on a third-party service.",
4 | "keywords": [
5 | "php",
6 | "magento",
7 | "magento2",
8 | "module",
9 | "extension",
10 | "free"
11 | ],
12 | "require": {
13 | "php": "^8.1",
14 | "psr/log": "*",
15 | "magento/framework": "*",
16 | "firebase/php-jwt": "^6.10"
17 | },
18 | "require-dev": {
19 | "magento/magento-coding-standard": "^33",
20 | "roave/security-advisories": "dev-latest"
21 | },
22 | "type": "magento2-module",
23 | "license": [
24 | "MIT"
25 | ],
26 | "homepage": "https://github.com/opengento/magento2-hoodoor",
27 | "authors": [
28 | {
29 | "name": "Opengento Team",
30 | "email": "opengento@gmail.com",
31 | "homepage": "https://opengento.fr/",
32 | "role": "lead"
33 | }
34 | ],
35 | "support": {
36 | "source": "https://github.com/opengento/magento2-hoodoor",
37 | "issues": "https://github.com/opengento/magento2-hoodoor/issues"
38 | },
39 | "autoload": {
40 | "files": [
41 | "registration.php"
42 | ],
43 | "psr-4": {
44 | "Opengento\\Hoodoor\\": ""
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/etc/adminhtml/di.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
11 |
23 |
{{trans "To authenticate, please click on the link below:"}}
14 | 15 |16 | 17 | {{var this.getUrl($store,'admin/pwl/processlogin',[_query:[request:$request],_nosid:1])}} 18 | 19 |
20 | 21 |{{trans "If you are not the author of this request, please ignore this message."}}
22 | -------------------------------------------------------------------------------- /view/adminhtml/layout/adminhtml_auth_login.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 |{{trans "To authenticate, please click on the link below:"}}
16 | 17 | 22 | 23 |{{trans "If you are not the author of this request, please ignore this message."}}
24 | 25 | {{template config_path="design/email/footer_template"}} 26 | -------------------------------------------------------------------------------- /view/frontend/layout/customer_account_create.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 |
18 | = $escaper->escapeHtml($block->getName()) ?>
19 | = $escaper->escapeHtml($block->getCustomer()->getEmail()) ?>
20 |