Skip to content

Commit c8a78b6

Browse files
committed
OXDEV-10116 Hide change and add reset password on oauth users
1 parent 7d67002 commit c8a78b6

13 files changed

Lines changed: 237 additions & 36 deletions

File tree

metadata.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
'email' => 'info@oxid-esales.com',
3535
'extend' => [
3636
\OxidEsales\Eshop\Application\Controller\NewsletterController::class => \OxidEsales\SecurityModule\Captcha\Shop\NewsletterController::class,
37-
\OxidEsales\Eshop\Application\Controller\ForgotPasswordController::class => \OxidEsales\SecurityModule\Captcha\Shop\ForgotPasswordController::class,
37+
\OxidEsales\Eshop\Application\Controller\ForgotPasswordController::class => \OxidEsales\SecurityModule\Shared\Controller\ForgotPasswordController::class,
3838
\OxidEsales\Eshop\Application\Model\User::class => \OxidEsales\SecurityModule\Shared\Model\User::class,
3939
\OxidEsales\Eshop\Core\InputValidator::class => \OxidEsales\SecurityModule\Shared\Core\InputValidator::class,
4040
\OxidEsales\Eshop\Core\ViewConfig::class => \OxidEsales\SecurityModule\Shared\Core\ViewConfig::class

migration/data/Version20251128093245.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ public function up(Schema $schema): void
1919
$this->addSql('ALTER TABLE `oxuser` ADD column `OESMOTPCODE` VARCHAR(128) default NULL COMMENT "OTP code"');
2020
$this->addSql('ALTER TABLE `oxuser` ADD column `OESMOTPEXPTIME` DATETIME default NULL COMMENT "OTP code expiration time"');
2121
$this->addSql('ALTER TABLE `oxuser` ADD column `OESMOTPATTEMPTS` INT NOT NULL default 0 COMMENT "OTP code attempts"');
22+
$this->addSql('ALTER TABLE `oxuser` ADD column `OESMOTPLASTSENT` DATETIME default NULL COMMENT "Last OTP sent timestamp"');
23+
$this->addSql('ALTER TABLE `oxuser` ADD column `OESMEXTERNALAUTH` TINYINT(1) NOT NULL default 0 COMMENT "User registered via external authentication"');
2224
}
2325

2426
public function down(Schema $schema): void

migration/data/Version20260114104913.php

Lines changed: 0 additions & 25 deletions
This file was deleted.

src/Authentication/OAuth2/Infrastructure/Repository/UserRepository.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,10 @@ public function createUser(OAuth2UserDTOInterface $userDTO): UserDTOInterface
4141
{
4242
$userModel = $this->userFactory->create();
4343
$userModel->assign([
44-
'OXFNAME' => $userDTO->getFirstName(),
45-
'OXLNAME' => $userDTO->getLastName(),
46-
'OXUSERNAME' => $userDTO->getEmail(),
44+
'OXFNAME' => $userDTO->getFirstName(),
45+
'OXLNAME' => $userDTO->getLastName(),
46+
'OXUSERNAME' => $userDTO->getEmail(),
47+
'OESMEXTERNALAUTH' => 1,
4748
]);
4849
$userModel->setPassword($this->passwordGenerator->generatePasswordForOAuthUser());
4950
$userModel->createUser();

src/Captcha/Shop/ForgotPasswordController.php renamed to src/Shared/Controller/ForgotPasswordController.php

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,18 @@
77

88
declare(strict_types=1);
99

10-
namespace OxidEsales\SecurityModule\Captcha\Shop;
10+
namespace OxidEsales\SecurityModule\Shared\Controller;
1111

12+
use OxidEsales\Eshop\Application\Model\User;
1213
use OxidEsales\Eshop\Core\Exception\StandardException;
1314
use OxidEsales\Eshop\Core\Registry;
1415
use OxidEsales\SecurityModule\Captcha\Service\CaptchaServiceInterface;
1516
use OxidEsales\SecurityModule\Captcha\Service\ModuleSettingsServiceInterface;
1617

18+
/**
19+
* @mixin \OxidEsales\Eshop\Application\Controller\ForgotPasswordController
20+
* @eshopExtension
21+
*/
1722
class ForgotPasswordController extends ForgotPasswordController_parent
1823
{
1924
public function forgotPassword(): ?bool
@@ -36,4 +41,20 @@ public function forgotPassword(): ?bool
3641

3742
return parent::forgotPassword();
3843
}
44+
45+
public function updatePassword()
46+
{
47+
$result = parent::updatePassword();
48+
49+
if ($result === 'forgotpwd?success=1') {
50+
$userId = Registry::getSession()->getVariable('usr');
51+
$user = oxNew(User::class);
52+
if ($userId && $user->load($userId) && $user->getFieldData('oesmexternalauth')) {
53+
$user->assign(['OESMEXTERNALAUTH' => 0]);
54+
$user->save();
55+
}
56+
}
57+
58+
return $result;
59+
}
3960
}

src/Shared/Core/ViewConfig.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,11 @@ public function getRemainingAttempts(): int
6464
{
6565
return $this->getService(AuthorizeServiceInterface::class)->getRemainingAttempts();
6666
}
67+
68+
public function isExternalAuthUser(): bool
69+
{
70+
$user = $this->getUser();
71+
72+
return $user && (bool) $user->getFieldData('oesmexternalauth');
73+
}
6774
}

tests/Integration/Captcha/Shop/ForgotPasswordControllerTest.php renamed to tests/Integration/Shared/Controller/ForgotPasswordControllerTest.php

Lines changed: 111 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@
77

88
declare(strict_types=1);
99

10-
namespace OxidEsales\SecurityModule\Tests\Integration\Captcha\Shop;
10+
namespace OxidEsales\SecurityModule\Tests\Integration\Shared\Controller;
1111

1212
use OxidEsales\Eshop\Application\Controller\ForgotPasswordController;
13+
use OxidEsales\Eshop\Application\Model\User;
1314
use OxidEsales\Eshop\Core\Registry;
1415
use OxidEsales\Eshop\Core\Request;
1516
use OxidEsales\Eshop\Core\UtilsView;
1617
use OxidEsales\EshopCommunity\Core\Di\ContainerFacade;
17-
use OxidEsales\EshopCommunity\Tests\Integration\IntegrationTestCase;
18+
use OxidEsales\SecurityModule\Tests\Integration\IntegrationTestCase;
1819
use OxidEsales\SecurityModule\Captcha\Service\ModuleSettingsServiceInterface;
1920

2021
class ForgotPasswordControllerTest extends IntegrationTestCase
@@ -121,4 +122,112 @@ public function testForgotPasswordWithEmptyCaptcha()
121122
$subject = oxNew(ForgotPasswordController::class);
122123
$subject->forgotPassword();
123124
}
125+
126+
public function testUpdatePasswordClearsExternalAuthFlagOnSuccess(): void
127+
{
128+
$userId = uniqid();
129+
$user = $this->createTestUser($userId);
130+
131+
$this->assertEquals(1, (int) $user->getFieldData('oesmexternalauth'));
132+
133+
$updateKey = $user->getFieldData('oxupdatekey');
134+
$shopId = $user->getFieldData('oxshopid');
135+
$uid = md5($user->getId() . $shopId . $updateKey);
136+
137+
$password = uniqid();
138+
139+
$requestMock = $this->getMockBuilder(Request::class)
140+
->disableOriginalConstructor()
141+
->onlyMethods(['getRequestParameter', 'getRequestEscapedParameter'])
142+
->getMock();
143+
144+
$requestMock->method('getRequestParameter')
145+
->willReturnCallback(function ($param) use ($password) {
146+
return match ($param) {
147+
'password_new', 'password_new_confirm' => $password,
148+
default => null,
149+
};
150+
});
151+
152+
$requestMock->method('getRequestEscapedParameter')
153+
->willReturnCallback(function ($param) use ($uid) {
154+
return match ($param) {
155+
'uid' => $uid,
156+
default => null,
157+
};
158+
});
159+
160+
Registry::set(Request::class, $requestMock);
161+
162+
$subject = oxNew(ForgotPasswordController::class);
163+
$result = $subject->updatePassword();
164+
165+
$this->assertSame('forgotpwd?success=1', $result);
166+
167+
$updatedUser = oxNew(User::class);
168+
$updatedUser->load($userId);
169+
$this->assertEquals(0, (int) $updatedUser->getFieldData('oesmexternalauth'));
170+
}
171+
172+
public function testUpdatePasswordKeepsExternalAuthFlagOnFailure(): void
173+
{
174+
$userId = uniqid();
175+
$this->createTestUser($userId);
176+
$password = uniqid();
177+
178+
$requestMock = $this->getMockBuilder(Request::class)
179+
->disableOriginalConstructor()
180+
->onlyMethods(['getRequestParameter', 'getRequestEscapedParameter'])
181+
->getMock();
182+
183+
$requestMock->method('getRequestParameter')
184+
->willReturnCallback(function ($param) use ($password) {
185+
return match ($param) {
186+
'password_new', 'password_new_confirm' => $password,
187+
default => null,
188+
};
189+
});
190+
191+
$requestMock->method('getRequestEscapedParameter')
192+
->willReturnCallback(function ($param) {
193+
return match ($param) {
194+
'uid' => 'invalid_uid',
195+
default => null,
196+
};
197+
});
198+
199+
Registry::set(Request::class, $requestMock);
200+
201+
$subject = oxNew(ForgotPasswordController::class);
202+
$result = $subject->updatePassword();
203+
204+
$this->assertNotSame('forgotpwd?success=1', $result);
205+
206+
$unchangedUser = oxNew(User::class);
207+
$unchangedUser->load($userId);
208+
$this->assertEquals(1, (int) $unchangedUser->getFieldData('oesmexternalauth'));
209+
}
210+
211+
/**
212+
* Helper method to create the test user.
213+
*
214+
* @return \OxidEsales\Eshop\Application\Model\User
215+
*/
216+
protected function createTestUser(string $userId)
217+
{
218+
$user = oxNew(User::class);
219+
$user->setId($userId);
220+
$user->assign([
221+
'oxactive' => 1,
222+
'b2bparentid' => '',
223+
'b2brightdirectorder' => 1,
224+
'oxpassword' => uniqid(),
225+
'oesmexternalauth' => 1,
226+
]);
227+
228+
$user->save();
229+
$user->setUpdateKey();
230+
231+
return $user;
232+
}
124233
}

tests/PhpStan/phpstan-bootstrap.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,5 @@ class_alias(
2929

3030
class_alias(
3131
\OxidEsales\Eshop\Application\Controller\ForgotPasswordController::class,
32-
\OxidEsales\SecurityModule\Captcha\Shop\ForgotPasswordController_parent::class
32+
\OxidEsales\SecurityModule\Shared\Controller\ForgotPasswordController_parent::class
3333
);

tests/Unit/Authentication/OAuth2/Infrastructure/Repository/UserRepositoryTest.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,10 @@ public function testCreateUser(): void
106106

107107
$userModel = $this->createMock(UserModel::class);
108108
$userModel->method('assign')->with([
109-
'OXFNAME' => $firstName,
110-
'OXLNAME' => $lastName,
111-
'OXUSERNAME' => $username,
109+
'OXFNAME' => $firstName,
110+
'OXLNAME' => $lastName,
111+
'OXUSERNAME' => $username,
112+
'OESMEXTERNALAUTH' => 1,
112113
]);
113114
$userModel->method('setPassword')->with($password);
114115

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
/**
4+
* Copyright © OXID eSales AG. All rights reserved.
5+
* See LICENSE file for license details.
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace OxidEsales\SecurityModule\Tests\Unit\Authentication\TwoFactorAuth\Transput;
11+
12+
use OxidEsales\Eshop\Core\Utils;
13+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Transput\JsonResponse;
14+
use PHPUnit\Framework\TestCase;
15+
16+
class JsonResponseTest extends TestCase
17+
{
18+
public function testSendSetsCorrectHeaders(): void
19+
{
20+
$utilsMock = $this->createMock(Utils::class);
21+
22+
$headers = [];
23+
$utilsMock->method('setHeader')->willReturnCallback(function ($value) use (&$headers) {
24+
$headers[] = $value;
25+
});
26+
$utilsMock->method('showMessageAndExit');
27+
28+
$sut = new JsonResponse($utilsMock);
29+
$sut->send(['key' => 'value']);
30+
31+
$this->assertContains('HTTP/1.1 200', $headers);
32+
$this->assertContains('Content-Type: application/json', $headers);
33+
}
34+
35+
public function testSendOutputsJsonEncodedData(): void
36+
{
37+
$data = ['status' => 'ok', 'code' => 123];
38+
39+
$utilsMock = $this->createMock(Utils::class);
40+
$utilsMock->method('setHeader');
41+
$utilsMock->expects($this->once())
42+
->method('showMessageAndExit')
43+
->with(json_encode($data));
44+
45+
$sut = new JsonResponse($utilsMock);
46+
$sut->send($data);
47+
}
48+
49+
public function testSendWithCustomStatusCode(): void
50+
{
51+
$utilsMock = $this->createMock(Utils::class);
52+
53+
$headers = [];
54+
$utilsMock->method('setHeader')->willReturnCallback(function ($value) use (&$headers) {
55+
$headers[] = $value;
56+
});
57+
$utilsMock->method('showMessageAndExit');
58+
59+
$sut = new JsonResponse($utilsMock);
60+
$sut->setStatusCode(429);
61+
$sut->send([]);
62+
63+
$this->assertContains('HTTP/1.1 429', $headers);
64+
}
65+
}

0 commit comments

Comments
 (0)