Skip to content

Commit 8d0d2e8

Browse files
committed
OXDEV-9078 Reimplement the login flow service and use new code in the flow process
1 parent 4158079 commit 8d0d2e8

10 files changed

Lines changed: 290 additions & 130 deletions

File tree

src/Authentication/TwoFactorAuth/Controller/TwoFactorAuthController.php

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@
1111

1212
use OxidEsales\Eshop\Application\Controller\FrontendController;
1313
use OxidEsales\Eshop\Core\UtilsView;
14-
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Exception\OTPValidationException;
14+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Exception\InvalidCodeException;
1515
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Service\AuthorizeServiceInterface;
16-
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Service\UserServiceInterface;
16+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Service\TwoFAServiceInterface;
17+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Service\TwoFAUserServiceInterface;
1718
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Transput\AuthCodeRequestInterface;
1819
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Transput\JsonResponseInterface;
1920

@@ -28,8 +29,9 @@ class TwoFactorAuthController extends FrontendController
2829
protected $_sThisTemplate = '@oe_security_module/templates/two_factor_auth';
2930

3031
public function __construct(
32+
private readonly TwoFAServiceInterface $twoFAService,
33+
private readonly TwoFAUserServiceInterface $twoFAUserService,
3134
private readonly AuthorizeServiceInterface $authService,
32-
private readonly UserServiceInterface $userService,
3335
private readonly AuthCodeRequestInterface $authCodeRequest,
3436
private readonly UtilsView $utilsView,
3537
private readonly JsonResponseInterface $jsonResponse,
@@ -39,13 +41,12 @@ public function __construct(
3941

4042
public function handleOTP(): ?string
4143
{
42-
try {
43-
$this->authService->validate(
44-
$this->authCodeRequest->getCode()
45-
);
44+
$userId = $this->twoFAUserService->getPendingUserId();
4645

47-
$this->userService->finalizeLogin();
48-
} catch (OTPValidationException $e) {
46+
try {
47+
$this->twoFAService->verify($userId, $this->authCodeRequest->getCode());
48+
$this->twoFAUserService->loginUser($userId);
49+
} catch (InvalidCodeException $e) {
4950
$this->utilsView->addErrorToDisplay($e);
5051
}
5152

src/Authentication/TwoFactorAuth/OTP/Service/OtpCodeValidatorServiceInterface.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,13 @@
99

1010
namespace OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\Service;
1111

12+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Exception\InvalidCodeException;
13+
1214
interface OtpCodeValidatorServiceInterface
1315
{
16+
/**
17+
* @throws InvalidCodeException
18+
*/
1419
public function validateCode(
1520
string $userId,
1621
#[\SensitiveParameter] string $inputCode

src/Authentication/TwoFactorAuth/Service/TwoFAServiceInterface.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
namespace OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Service;
99

10+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Exception\InvalidCodeException;
11+
1012
interface TwoFAServiceInterface
1113
{
1214
public function isVerified(string $userId): bool;
@@ -15,6 +17,9 @@ public function triggerChallenge(string $userId): void;
1517

1618
public function invalidateChallenge(string $userId): void;
1719

20+
/**
21+
* @throws InvalidCodeException
22+
*/
1823
public function verify(string $userId, #[\SensitiveParameter] string $code): void;
1924

2025
public function resend(string $userId): void;
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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\Authentication\TwoFactorAuth\Service;
11+
12+
use OxidEsales\Eshop\Core\Utils;
13+
use OxidEsales\EshopCommunity\Internal\Framework\Session\SessionInterface;
14+
use OxidEsales\SecurityModule\Authentication\Service\InternalRedirectServiceInterface;
15+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Infrastructure\Factory\UserModelFactoryInterface;
16+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Settings\TwoFASettingsInterface;
17+
18+
class TwoFAUserService implements TwoFAUserServiceInterface
19+
{
20+
public const USER_SESSION_KEY = 'pending_authorized_user';
21+
22+
public function __construct(
23+
private TwoFAServiceInterface $twoFAService,
24+
private TwoFASettingsInterface $settings,
25+
private Utils $utils,
26+
private SessionInterface $session,
27+
private UserModelFactoryInterface $userFactory,
28+
private InternalRedirectServiceInterface $redirectService,
29+
) {
30+
}
31+
32+
public function startChallengeForUser(string $userId): void
33+
{
34+
$this->session->set(self::USER_SESSION_KEY, $userId);
35+
$this->twoFAService->triggerChallenge($userId);
36+
$this->utils->redirect($this->settings->getVerificationUrl());
37+
}
38+
39+
public function getPendingUserId(): string
40+
{
41+
return $this->session->get(self::USER_SESSION_KEY);
42+
}
43+
44+
public function loginUser(string $userId): void
45+
{
46+
$this->session->remove(self::USER_SESSION_KEY);
47+
48+
$user = $this->userFactory->create();
49+
$user->load($userId);
50+
51+
/** @phpstan-ignore argument.type (password is null because user already authenticated to come here) */
52+
$user->login($user->getFieldData('oxusername'), null, false);
53+
54+
$this->utils->redirect($this->redirectService->getRedirectUrl(), false);
55+
}
56+
57+
public function isChallengeVerified(string $userId): bool
58+
{
59+
return $this->twoFAService->isVerified($userId);
60+
}
61+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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\Authentication\TwoFactorAuth\Service;
11+
12+
interface TwoFAUserServiceInterface
13+
{
14+
public function startChallengeForUser(string $userId): void;
15+
16+
public function getPendingUserId(): string;
17+
18+
public function loginUser(string $userId): void;
19+
20+
public function isChallengeVerified(string $userId): bool;
21+
}

src/Authentication/TwoFactorAuth/Service/services.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,7 @@ services:
3535
OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Service\ResendOTPServiceInterface:
3636
class: OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Service\ResendOTPService
3737
public: true
38+
39+
OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Service\TwoFAUserServiceInterface:
40+
class: OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Service\TwoFAUserService
41+
public: true

src/Shared/Model/User.php

Lines changed: 7 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@
1212
use OxidEsales\Eshop\Core\Exception\InputException;
1313
use OxidEsales\Eshop\Core\Exception\UserException;
1414
use OxidEsales\Eshop\Core\Registry;
15-
// use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Service\TwoFAServiceInterface;
16-
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Service\UserServiceInterface;
15+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Service\TwoFAUserServiceInterface;
1716
use OxidEsales\SecurityModule\Captcha\Captcha\Image\Exception\CaptchaValidateException as ImageCaptchaException;
1817
use OxidEsales\SecurityModule\Captcha\Captcha\HoneyPot\Exception\CaptchaValidateException as HoneyPotCaptchaException;
1918
use OxidEsales\SecurityModule\Captcha\Service\CaptchaServiceInterface;
@@ -91,31 +90,14 @@ protected function onLogin($userName, #[\SensitiveParameter] $password)
9190
{
9291
parent::onLogin($userName, $password);
9392

94-
if (!$this->isOTPEnabled() || $this->isAdmin()) {
95-
return;
96-
}
97-
9893
$userId = $this->getId();
99-
if (!$userId) {
100-
return;
101-
}
102-
103-
$userService = $this->getService(UserServiceInterface::class);
104-
$userSessionKey = Registry::getSession()->getVariable('OTP_PASS');
105-
if ($userSessionKey && $userSessionKey === $userId) {
106-
$userService->clearOTPSessionVariables();
107-
return;
94+
$settingsService = $this->getService(TwoFASettingsServiceInterface::class);
95+
if ($userId && $settingsService->isTwoFactorAuthEnabled() && !$this->isAdmin()) {
96+
$twoFAUserService = $this->getService(TwoFAUserServiceInterface::class);
97+
if (!$twoFAUserService->isChallengeVerified($userId)) {
98+
$twoFAUserService->startChallengeForUser($userId);
99+
}
108100
}
109-
110-
$userService->handleLogin($userId);
111-
112-
// $settingsService = $this->getService(TwoFASettingsServiceInterface::class);
113-
// if ($settingsService->isTwoFactorAuthEnabled() && !$this->isAdmin()) {
114-
// $authentication = $this->getService(TwoFAServiceInterface::class);
115-
// if (!$authentication->isVerified($userId)) {
116-
// $authentication->triggerChallenge($userId);
117-
// }
118-
// }
119101
}
120102

121103
private function isCaptchaEnabled(): bool
@@ -124,12 +106,6 @@ private function isCaptchaEnabled(): bool
124106
return $settingsService->isCaptchaEnabled() || $settingsService->isHoneyPotCaptchaEnabled();
125107
}
126108

127-
private function isOTPEnabled(): bool
128-
{
129-
$settingsService = $this->getService(TwoFASettingsServiceInterface::class);
130-
return $settingsService->isTwoFactorAuthEnabled();
131-
}
132-
133109
protected function shouldValidateCaptcha(): bool
134110
{
135111
return !$this->getUser();

tests/Integration/Shared/Model/UserTest.php

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,15 @@
1717
use OxidEsales\Eshop\Core\Utils;
1818
use OxidEsales\EshopCommunity\Core\Di\ContainerFacade;
1919
use OxidEsales\EshopCommunity\Internal\Framework\Module\Facade\ModuleSettingServiceInterface;
20+
use DateTimeImmutable;
21+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\Infrastructure\Repository\OtpChallengeStateRepositoryInterface;
2022
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Service\AuthorizeService;
2123
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Service\ModuleSettingsService;
2224
use OxidEsales\SecurityModule\Captcha\Service\ModuleSettingsServiceInterface as CaptchaSettingsServiceInterface;
2325
use OxidEsales\SecurityModule\Core\Module;
2426
use OxidEsales\SecurityModule\Tests\Integration\IntegrationTestCase;
2527

28+
// todo-critical: rework the 2FA part of the test. it changes the real configs on the fly and causing side effects
2629
class UserTest extends IntegrationTestCase
2730
{
2831
private const OTP_USER_NAME = 'user@oxid-esales.com';
@@ -207,31 +210,24 @@ public function testLoginWithOTPEnabledStoresUserIdInSession(): void
207210
$this->assertEquals($subject->getId(), $sessionUserId);
208211
}
209212

210-
public function testLoginWithOTPPassSessionVariableSkipsOTPRedirect(): void
213+
public function testLoginWithVerifiedChallengeStateSkipsOTPRedirect(): void
211214
{
212215
$this->disableCaptcha();
213216
$this->enableTwoFactorAuth();
214217

215-
$subject = oxNew(User::class);
216-
$subject->load($this->getOTPUserId());
218+
$userId = $this->getOTPUserId();
217219

218-
Registry::getSession()->setVariable('OTP_PASS', $subject->getId());
220+
$stateRepo = $this->get(OtpChallengeStateRepositoryInterface::class);
221+
$stateRepo->createChallengeState($userId, 'hash', new DateTimeImmutable('+5 minutes'));
222+
$stateRepo->markVerified($userId);
219223

220224
$utilsMock = $this->createMock(Utils::class);
221225
$utilsMock->expects($this->never())->method('redirect');
222226
Registry::set(Utils::class, $utilsMock);
223227

224-
$result = $subject->login(self::OTP_USER_NAME, self::OTP_USER_PASSWORD);
228+
$result = oxNew(User::class)->login(self::OTP_USER_NAME, self::OTP_USER_PASSWORD);
225229

226230
$this->assertTrue($result);
227-
$this->assertNull(
228-
Registry::getSession()->getVariable('OTP_PASS'),
229-
'OTP_PASS session variable should be cleared after successful login'
230-
);
231-
$this->assertNull(
232-
Registry::getSession()->getVariable(AuthorizeService::USER_SESSION_KEY),
233-
'USER_SESSION_KEY should not be set when OTP was already validated'
234-
);
235231
}
236232

237233
public function testLoginWithOTPPassSessionVariableMismatchTriggersOTPFlow(): void

0 commit comments

Comments
 (0)