It’s crucial to ensure the User account’s security when implementing a forgotten password functionality in Magento 2. One aspect of this is preventing users from reusing old passwords. Custom validation needs to be added to the password reset process.
This blog post will guide you through the steps to implement custom validation for the Forgotten Password feature in Magento 2, enhancing the overall security of your online store.
ON THIS PAGE
Steps to Add Custom Validation for Forgotten Password in Magento 2
We have implemented a customer reset password validation feature. When a customer goes to the forgot password page and attempts to enter the same password, we verify whether the customer has previously used this password in the admin panel.
Before implementing the custom validation, it’s important to identify the current password and understand the mechanism used to store and retrieve the user’s password. Magento 2 uses hashing algorithms to store passwords securely.
Create a Custom Validation Module Develop a custom module in Magento 2 to handle the password reset process. This module should include functionality for checking whether the new password matches any of the user’s previous passwords.
Recommended Read: How To Verify the Customer’s Passwords on the Edit Page in Magento 2?
To create a custom validation for Forgotten Password follow the below steps:
Step 1: Create a “registration.php”
app/code/Dckap/ForGotValidate/registration.php
<?php
/**
* @package Dckap_ForGotValidate
* @copyright Copyright (c) 2023 DCKAP Inc (http://www.dckap.com)
* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
*/
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::MODULE,
'Dckap_ForGotValidate',
__DIR__
);
Step 2: Create a “Module.xml” file
The following path has:
app/code/Dckap/EventsObservers/etc/module.xml
<?xml version="1.0"?>
<!-- /**
* @package Dckap_ForGotValidate
* @copyright Copyright (c) 2023 DCKAP Inc (http://www.dckap.com)
* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
*/ -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/Module/etc/module.xsd">
<module name="Dckap_ForGotValidate" setup_version="1.0.0">
<sequence>
<module name="Magento_Customer" />
</sequence>
</module>
</config>
Step 3: Create a “routes.xml” file
The following path has:
app/code/Dckap/ForGotValidate/etc/frontend/routes.xml
<?xml version="1.0"?>
<!-- /**
* @package Dckap_ForGotValidate
* @copyright Copyright (c) 2023 DCKAP Inc (http://www.dckap.com)
* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
*/ -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../lib/internal/Magento/Framework/App/etc/routes.xsd">
<router id="standard">
<route id="forgotvalidate" frontName="forgotvalidate">
<module name="Dckap_ForGotValidate" />
</route>
</router>
</config>
Step 4: Now, create one more file “Customerhash.php”
With that create a file path that has the following:
app/code/Dckap/ForGotValidate/Controller/Index/Customerhash.php
<?php
namespace Dckap\ForGotValidate\Controller\Index;
use Magento\Framework\App\Action\Action;
use Magento\Framework\App\Action\Context;
use Magento\Customer\Api\CustomerRepositoryInterface;
use Magento\Framework\Controller\Result\JsonFactory;
use Magento\Customer\Model\Customer;
use Magento\Framework\Encryption\EncryptorInterface;
use Magento\Customer\Model\CustomerRegistry;
class Customerhash extends Action
{
/**
* @var CustomerRepositoryInterface
*/
protected $customerRepository;
/**
* @var JsonFactory
*/
protected $jsonResultFactory;
/**
* @var Customer
*/
protected $customerModel;
/**
* @var EncryptorInterface
*/
protected $encryptor;
/**
* @var CustomerRegistry
*/
protected $customerRegistry;
/**
* Customerhash constructor.
* @param Context $context
* @param CustomerRepositoryInterface $customerRepository
* @param Customer $customerModel
* @param JsonFactory $jsonResultFactory
* @param EncryptorInterface $encryptor
* @param CustomerRegistry $customerRegistry
*/
public function __construct(
Context $context,
CustomerRepositoryInterface $customerRepository,
Customer $customerModel,
JsonFactory $jsonResultFactory,
EncryptorInterface $encryptor,
CustomerRegistry $customerRegistry,
) {
parent::__construct($context);
$this->customerRepository = $customerRepository;
$this->jsonResultFactory = $jsonResultFactory;
$this->customerModel = $customerModel;
$this->encryptor = $encryptor;
$this->customerRegistry = $customerRegistry;
}
/**
* @return mixed
*/
public function execute()
{
$customerId = $this->getRequest()->getPost('customer_id');
$newPassword = $this->getRequest()->getPost('new_password');
$temp = $this->isPasswordAlreadyUsedForCustomer($customerId,$newPassword);
$customerModel = $this->customerModel->load($customerId);
$customerData = [
'id' => $customerModel->getId(),
'email' => $customerModel->getEmail(),
'password_match' => $temp,
];
$result = $this->jsonResultFactory->create();
return $result->setData($customerData);
}
/**
* @param $customerId
* @param $password
* @return bool
*/
public function isPasswordAlreadyUsedForCustomer($customerId, $password)
{
$customerSecure = $this->customerRegistry->retrieveSecureData($customerId);
$hash = $customerSecure->getPasswordHash();
if ($this->encryptor->validateHash($password, $hash) == true) {
return true;
}
return false;
}
}
Step 5: Create one more file “customer_account_createpassword.xml”
Add a file path that has the following:
app/code/Dckap/ForGotValidate/view/frontend/layout/customer_account_createpassword.xml
<?xml version="1.0"?>
<!--
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="1column" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<head>
<title>Set a New Password</title>
</head>
<body>
<referenceBlock name="root">
<action method="setHeaderTitle">
<argument translate="true" name="title" xsi:type="string">Set a New Password</argument>
</action>
</referenceBlock>
<referenceContainer name="content">
<block class="Magento\Customer\Block\Account\Resetpassword" name="resetPassword" template="Dckap_ForGotValidate::form/resetforgottenpassword.phtml" cacheable="false"/>
</referenceContainer>
</body>
</page>
Step 6: Create one more file “resetforgottenpassword.phtml”
Add a file path that has the following:
We need to override this page from the vendor
app/code/Dckap/ForGotValidate/view/frontend/templates/form/resetforgottenpassword.phtml
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
/** @var \Magento\Customer\Block\Account\Resetpassword $block */
?>
<form action="<?= $block->escapeUrl($block->getUrl('*/*/resetpasswordpost', ['_query' => ['id' =>$block->getRpCustomerId(),'token' => $block->getResetPasswordLinkToken()]])) ?>"
method="post"
<?php if ($block->isAutocompleteDisabled()) : ?> autocomplete="off"<?php endif; ?>
id="form-validate"
class="form password reset"
data-mage-init='{"validation":{}}'>
<div id="form-validate-customerId" value = <?php echo $block->getRpCustomerId();?> >
<input type="hidden" name="customer_id" value="<?php echo $block->getRpCustomerId(); ?>">
</div>
<fieldset class="fieldset" data-hasrequired="<?= $block->escapeHtmlAttr(__('* Required Fields')) ?>">
<div class="field password required" data-mage-init='{"passwordStrengthIndicator": {}}'>
<label class="label" for="password"><span><?= $block->escapeHtml(__('New Password')) ?></span></label>
<div class="control">
<input type="password" class="input-text" name="password" id="password"
data-password-min-length="<?= $block->escapeHtmlAttr($block->getMinimumPasswordLength()) ?>"
data-password-min-character-sets="<?= $block->escapeHtmlAttr($block->getRequiredCharacterClassesNumber()) ?>"
data-validate="{required:true, 'validate-customer-password':true}"
autocomplete="off">
<div id ='password-strength-meter'></div>
<div id="password-strength-meter-container" data-role="password-strength-meter" aria-live="polite">
<div id="password-strength-meter" class="password-strength-meter">
<?= $block->escapeHtml(__('Password Strength')) ?>:
<span id="password-strength-meter-label" data-role="password-strength-meter-label">
<?= $block->escapeHtml(__('No Password')) ?>
</span>
</div>
</div>
</div>
</div>
<div class="field confirmation required">
<label class="label" for="password-confirmation"><span><?= $block->escapeHtml(__('Confirm New Password')) ?></span></label>
<div class="control">
<input type="password" class="input-text" name="password_confirmation" id="password-confirmation" data-validate="{required:true,equalTo:'#password'}" autocomplete="off">
</div>
</div>
</fieldset>
<div class="actions-toolbar">
<div class="primary">
<button type="submit" id ="submit-password" class="action submit primary"><span><?= $block->escapeHtml(__('Set a New Password')) ?></span></button>
</div>
</div>
</form>
<script>
require(['jquery'], function ($) {
$(document).ready(function () {
var customerId = $('input[name="customer_id"]').val();
var newPasswordTimeout;
var response;
$('input[name="password"]').on('input', function () {
clearTimeout(newPasswordTimeout);
newPasswordTimeout = setTimeout(function () {
var newPassword = $('input[name="password"]').val();
console.log(newPassword);
$.ajax({
dataType: 'json',
method: 'POST',
contentType: 'application/x-www-form-urlencoded',
url: '<?php echo $block->getUrl('forgotvalidate/index/customerhash');?>',
showLoader: true,
crossDomain: false,
data: {
customer_id: customerId,
new_password: newPassword
},
success: function (ajaxResponse) {
response = ajaxResponse;
if (response.password_match == true) {
$('#password-strength-meter').html("<div class='message-error-password'>Password already used try for new password.</div>").css('color', 'red');
} else {
$('#password-strength-meter').html('');
}
},
error: function (error) {
console.error('Error:', error);
}
});
}, 1000);
});
$('#submit-password').on('click', function (event) {
event.preventDefault();
if (response && response.password_match == true) {
$('#password-strength-meter').html("<div class='message-error-password'>Password already used try for new password.</div>").css('color', 'red');
} else {
$('#password-strength-meter').html('');
$(this).closest('form').submit();
}
});
});
});
</script>
Read Also: Steps to Create a Custom Price For a Product in Magento 2
Module Usage
Retrieve User’s Password History
we need to Implement a method to retrieve the user’s password history from the database. This history will be used to compare against the new password during the validation process.
Compare New Password with History
During the password reset process, compare the new password provided by the user with their password history. If the new password matches any previous one, reject it and prompt the user to choose a different one.
Display an Error Message
It should be done like the current password is already used for this customer and If the new password is rejected due to being a reused password, display an appropriate error message to inform the user of the requirement to choose a unique password.
Complete Password Reset
Once the user selects a new password that meets the validation criteria, proceed with the password reset process as usual.
Module Output
Implement Custom Validation for the Forgotten Password Feature with Klizer Experts
Users are prevented from reusing old passwords by implementing custom validation for the Forgotten Password functionality in Magento 2, thereby enhancing the security of their accounts.
This extra layer of validation ensures that compromised passwords cannot be reused, reducing the risk of unauthorized access to user accounts. With proper implementation and error handling, the password reset process remains user-friendly while maintaining a high level of security.
Get in touch with us to get further inquiries and explore how our solutions can benefit your business.