Skip to content

Commit 748e93e

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

13 files changed

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

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)