Skip to content

Commit b550107

Browse files
ENGCOM-8461: Extend password reset token validity on password change page load #25279
- Merge Pull Request #25279 from fredden/magento2:password-reset/race-form-fill - Merged commits: 1. c7fc6d5 2. ede673f 3. c19a1a1 4. 8cd951a 5. 9c15c95 6. 35a8976 7. defadd0 8. 5c43558 9. c3f3a57 10. 03c65e9 11. e82531d 12. 0a4176e 13. eba286f 14. 42a3eb7 15. 1518f8c 16. 7aef712 17. 35b5ad8 18. dfe2370 19. c0cbe80
2 parents 8c95f07 + c0cbe80 commit b550107

File tree

4 files changed

+157
-18
lines changed

4 files changed

+157
-18
lines changed

app/code/Magento/Customer/Controller/Account/CreatePassword.php

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,22 @@
99

1010
use Magento\Customer\Api\AccountManagementInterface;
1111
use Magento\Customer\Model\ForgotPasswordToken\ConfirmCustomerByToken;
12+
use Magento\Customer\Model\ForgotPasswordToken\GetCustomerByToken;
1213
use Magento\Customer\Model\Session;
13-
use Magento\Framework\App\Action\HttpGetActionInterface;
14-
use Magento\Framework\View\Result\PageFactory;
1514
use Magento\Framework\App\Action\Context;
15+
use Magento\Framework\App\Action\HttpGetActionInterface;
1616
use Magento\Framework\App\ObjectManager;
17+
use Magento\Framework\Controller\Result\Redirect;
18+
use Magento\Framework\View\Result\Page;
19+
use Magento\Framework\View\Result\PageFactory;
1720

1821
/**
19-
* Class CreatePassword
20-
*
21-
* @package Magento\Customer\Controller\Account
22+
* Controller for front-end customer password reset form
2223
*/
2324
class CreatePassword extends \Magento\Customer\Controller\AbstractAccount implements HttpGetActionInterface
2425
{
2526
/**
26-
* @var \Magento\Customer\Api\AccountManagementInterface
27+
* @var AccountManagementInterface
2728
*/
2829
protected $accountManagement;
2930

@@ -38,37 +39,46 @@ class CreatePassword extends \Magento\Customer\Controller\AbstractAccount implem
3839
protected $resultPageFactory;
3940

4041
/**
41-
* @var \Magento\Customer\Model\ForgotPasswordToken\ConfirmCustomerByToken
42+
* @var ConfirmCustomerByToken
4243
*/
4344
private $confirmByToken;
4445

4546
/**
46-
* @param \Magento\Framework\App\Action\Context $context
47-
* @param \Magento\Customer\Model\Session $customerSession
48-
* @param \Magento\Framework\View\Result\PageFactory $resultPageFactory
49-
* @param \Magento\Customer\Api\AccountManagementInterface $accountManagement
50-
* @param \Magento\Customer\Model\ForgotPasswordToken\ConfirmCustomerByToken $confirmByToken
47+
* @var GetCustomerByToken
48+
*/
49+
private $getByToken;
50+
51+
/**
52+
* @param Context $context
53+
* @param Session $customerSession
54+
* @param PageFactory $resultPageFactory
55+
* @param AccountManagementInterface $accountManagement
56+
* @param ConfirmCustomerByToken|null $confirmByToken
57+
* @param GetCustomerByToken|null $getByToken
5158
*/
5259
public function __construct(
5360
Context $context,
5461
Session $customerSession,
5562
PageFactory $resultPageFactory,
5663
AccountManagementInterface $accountManagement,
57-
ConfirmCustomerByToken $confirmByToken = null
64+
ConfirmCustomerByToken $confirmByToken = null,
65+
GetCustomerByToken $getByToken = null
5866
) {
5967
$this->session = $customerSession;
6068
$this->resultPageFactory = $resultPageFactory;
6169
$this->accountManagement = $accountManagement;
6270
$this->confirmByToken = $confirmByToken
6371
?? ObjectManager::getInstance()->get(ConfirmCustomerByToken::class);
72+
$this->getByToken = $getByToken
73+
?? ObjectManager::getInstance()->get(GetCustomerByToken::class);
6474

6575
parent::__construct($context);
6676
}
6777

6878
/**
6979
* Resetting password handler
7080
*
71-
* @return \Magento\Framework\Controller\Result\Redirect|\Magento\Framework\View\Result\Page
81+
* @return Redirect|Page
7282
*/
7383
public function execute()
7484
{
@@ -83,14 +93,19 @@ public function execute()
8393

8494
$this->confirmByToken->execute($resetPasswordToken);
8595

96+
// Extend token validity to avoid expiration while this form is
97+
// being completed by the user.
98+
$customer = $this->getByToken->execute($resetPasswordToken);
99+
$this->accountManagement->changeResetPasswordLinkToken($customer, $resetPasswordToken);
100+
86101
if ($isDirectLink) {
87102
$this->session->setRpToken($resetPasswordToken);
88103
$resultRedirect = $this->resultRedirectFactory->create();
89104
$resultRedirect->setPath('*/*/createpassword');
90105

91106
return $resultRedirect;
92107
} else {
93-
/** @var \Magento\Framework\View\Result\Page $resultPage */
108+
/** @var Page $resultPage */
94109
$resultPage = $this->resultPageFactory->create();
95110
$resultPage->getLayout()
96111
->getBlock('resetPassword')
@@ -100,7 +115,7 @@ public function execute()
100115
}
101116
} catch (\Exception $exception) {
102117
$this->messageManager->addErrorMessage(__('Your password reset link has expired.'));
103-
/** @var \Magento\Framework\Controller\Result\Redirect $resultRedirect */
118+
/** @var Redirect $resultRedirect */
104119
$resultRedirect = $this->resultRedirectFactory->create();
105120
$resultRedirect->setPath('*/*/forgotpassword');
106121

app/code/Magento/User/Controller/Adminhtml/Auth/ResetPassword.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
<?php
22
/**
3-
*
43
* Copyright © Magento, Inc. All rights reserved.
54
* See COPYING.txt for license details.
65
*/
76
namespace Magento\User\Controller\Adminhtml\Auth;
87

9-
class ResetPassword extends \Magento\User\Controller\Adminhtml\Auth
8+
use Magento\Framework\App\Action\HttpGetActionInterface;
9+
10+
/**
11+
* Controller for admin user password reset form
12+
*/
13+
class ResetPassword extends \Magento\User\Controller\Adminhtml\Auth implements HttpGetActionInterface
1014
{
1115
/**
1216
* Display reset forgotten password form
@@ -22,6 +26,12 @@ public function execute()
2226
try {
2327
$this->_validateResetPasswordLinkToken($userId, $passwordResetToken);
2428

29+
// Extend token validity to avoid expiration while this form is
30+
// being completed by the user.
31+
$user = $this->_userFactory->create()->load($userId);
32+
$user->changeResetPasswordLinkToken($passwordResetToken);
33+
$user->save();
34+
2535
$this->_view->loadLayout();
2636

2737
$content = $this->_view->getLayout()->getBlock('content');

dev/tests/integration/testsuite/Magento/Customer/Controller/CreatePasswordTest.php

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@
1010
use Magento\Customer\Model\CustomerRegistry;
1111
use Magento\Customer\Model\ResourceModel\Customer as CustomerResource;
1212
use Magento\Customer\Model\Session;
13+
use Magento\Framework\Intl\DateTimeFactory;
1314
use Magento\Framework\Math\Random;
15+
use Magento\Framework\Message\MessageInterface;
1416
use Magento\Framework\ObjectManagerInterface;
17+
use Magento\Framework\Stdlib\DateTime;
1518
use Magento\Framework\View\LayoutInterface;
1619
use Magento\Store\Api\WebsiteRepositoryInterface;
1720
use Magento\TestFramework\Helper\Bootstrap;
@@ -42,6 +45,9 @@ class CreatePasswordTest extends AbstractController
4245
/** @var CustomerRegistry */
4346
private $customerRegistry;
4447

48+
/** @var DateTimeFactory */
49+
private $dateTimeFactory;
50+
4551
/** @var WebsiteRepositoryInterface */
4652
private $websiteRepository;
4753

@@ -61,6 +67,7 @@ protected function setUp(): void
6167
$this->random = $this->objectManager->get(Random::class);
6268
$this->customerResource = $this->objectManager->get(CustomerResource::class);
6369
$this->customerRegistry = $this->objectManager->get(CustomerRegistry::class);
70+
$this->dateTimeFactory = $this->objectManager->get(DateTimeFactory::class);
6471
$this->websiteRepository = $this->objectManager->get(WebsiteRepositoryInterface::class);
6572
}
6673

@@ -94,4 +101,69 @@ public function testCreatePassword(): void
94101
$block = $this->layout->getBlock('resetPassword');
95102
$this->assertEquals($token, $block->getResetPasswordLinkToken());
96103
}
104+
105+
/**
106+
* @magentoDataFixture Magento/Customer/_files/customer_with_website.php
107+
*
108+
* @return void
109+
*/
110+
public function testTokenHasExpired(): void
111+
{
112+
$defaultWebsite = $this->websiteRepository->get('base')->getId();
113+
$customer = $this->customerRegistry->retrieveByEmail('john.doe@magento.com', $defaultWebsite);
114+
$this->customerId = $customer->getId();
115+
$token = $this->random->getUniqueHash();
116+
$tooLongAgo = $this->dateTimeFactory->create()
117+
->sub(\DateInterval::createFromDateString('1 month'))
118+
->format(DateTime::DATETIME_PHP_FORMAT);
119+
120+
$customer->changeResetPasswordLinkToken($token);
121+
$customer->setData('confirmation', 'confirmation');
122+
$customerSecure = $this->customerRegistry->retrieveSecureData($this->customerId);
123+
$customerSecure->setRpTokenCreatedAt($tooLongAgo);
124+
$this->customerResource->save($customer);
125+
126+
$this->session->setRpToken($token);
127+
$this->session->setRpCustomerId($this->customerId);
128+
129+
$this->dispatch('customer/account/createPassword');
130+
131+
$this->assertRedirect($this->stringContains('customer/account/forgotpassword'));
132+
$this->assertSessionMessages(
133+
$this->equalTo(['Your password reset link has expired.']),
134+
MessageInterface::TYPE_ERROR
135+
);
136+
}
137+
138+
/**
139+
* @magentoDataFixture Magento/Customer/_files/customer_with_website.php
140+
*
141+
* @return void
142+
*/
143+
public function testTokenExtendedOnPageLoad(): void
144+
{
145+
$defaultWebsite = $this->websiteRepository->get('base')->getId();
146+
$customer = $this->customerRegistry->retrieveByEmail('john.doe@magento.com', $defaultWebsite);
147+
$this->customerId = $customer->getId();
148+
$token = $this->random->getUniqueHash();
149+
$anHourAgo = $this->dateTimeFactory->create()
150+
->sub(\DateInterval::createFromDateString('1 hour'))
151+
->format(DateTime::DATETIME_PHP_FORMAT);
152+
153+
$customer->changeResetPasswordLinkToken($token);
154+
$customer->setData('confirmation', 'confirmation');
155+
$customerSecure = $this->customerRegistry->retrieveSecureData($this->customerId);
156+
$customerSecure->setRpTokenCreatedAt($anHourAgo);
157+
$this->customerResource->save($customer);
158+
159+
$this->session->setRpToken($token);
160+
$this->session->setRpCustomerId($this->customerId);
161+
162+
$this->dispatch('customer/account/createPassword');
163+
$block = $this->layout->getBlock('resetPassword');
164+
$this->assertEquals($token, $block->getResetPasswordLinkToken());
165+
166+
$customerSecure = $this->customerRegistry->retrieveSecureData($this->customerId);
167+
$this->assertNotEquals($anHourAgo, $customerSecure->getRpTokenCreatedAt());
168+
}
97169
}

dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/AuthTest.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@
55
*/
66
namespace Magento\User\Controller\Adminhtml;
77

8+
use Magento\Framework\Intl\DateTimeFactory;
9+
use Magento\Framework\Stdlib\DateTime;
810
use Magento\TestFramework\Mail\Template\TransportBuilderMock;
911
use Magento\TestFramework\Helper\Bootstrap;
1012

1113
/**
1214
* Test class for \Magento\User\Controller\Adminhtml\Auth
1315
*
1416
* @magentoAppArea adminhtml
17+
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
1518
*/
1619
class AuthTest extends \Magento\TestFramework\TestCase\AbstractBackendController
1720
{
@@ -106,6 +109,45 @@ public function testResetPasswordAction()
106109
$this->assertTrue((bool)strpos($this->getResponse()->getBody(), $resetPasswordToken));
107110
}
108111

112+
/**
113+
* Test reset password action extends expiry of token
114+
*
115+
* @covers \Magento\User\Controller\Adminhtml\Auth\ResetPassword::execute
116+
* @covers \Magento\User\Controller\Adminhtml\Auth\ResetPassword::_validateResetPasswordLinkToken
117+
* @magentoDataFixture Magento/User/_files/dummy_user.php
118+
*/
119+
public function testResetPasswordActionWithTokenNearExpiry()
120+
{
121+
/** @var $user \Magento\User\Model\User */
122+
$user = Bootstrap::getObjectManager()->create(
123+
\Magento\User\Model\User::class
124+
)->loadByUsername(
125+
'dummy_username'
126+
);
127+
$this->assertNotEmpty($user->getId(), 'Broken fixture');
128+
$resetPasswordToken = Bootstrap::getObjectManager()->get(
129+
\Magento\User\Helper\Data::class
130+
)->generateResetPasswordLinkToken();
131+
$user->changeResetPasswordLinkToken($resetPasswordToken);
132+
133+
$anHourAgo = Bootstrap::getObjectManager()->create(DateTimeFactory::class)
134+
->create()
135+
->sub(\DateInterval::createFromDateString('1 hour'))
136+
->format(DateTime::DATETIME_PHP_FORMAT);
137+
$user->setRpTokenCreatedAt($anHourAgo);
138+
$user->save();
139+
140+
$this->getRequest()->setQueryValue('token', $resetPasswordToken)->setQueryValue('id', $user->getId());
141+
$this->dispatch('backend/admin/auth/resetpassword');
142+
143+
$this->assertEquals('adminhtml', $this->getRequest()->getRouteName());
144+
$this->assertEquals('auth', $this->getRequest()->getControllerName());
145+
$this->assertEquals('resetpassword', $this->getRequest()->getActionName());
146+
$this->assertTrue((bool)strpos($this->getResponse()->getBody(), $resetPasswordToken));
147+
148+
$this->assertNotEquals($anHourAgo, $user->reload()->getRpTokenCreatedAt());
149+
}
150+
109151
/**
110152
* @covers \Magento\User\Controller\Adminhtml\Auth\ResetPassword::execute
111153
* @covers \Magento\User\Controller\Adminhtml\Auth\ResetPassword::_validateResetPasswordLinkToken

0 commit comments

Comments
 (0)