Skip to content

Commit 43d7c8c

Browse files
committed
OXDEV-9078 Use resend policy and implement resend in OTP service
1 parent f63822d commit 43d7c8c

7 files changed

Lines changed: 116 additions & 1 deletion

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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\Exception;
11+
12+
class ResendCooldownException extends TwoFAException
13+
{
14+
public function __construct()
15+
{
16+
parent::__construct('ERROR_RESEND_COOLDOWN');
17+
}
18+
}

src/Authentication/TwoFactorAuth/OTP/OtpFacade.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@
99

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

12+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Exception\ResendCooldownException;
1213
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\Notifier\Factory\OtpNotifierFactoryInterface;
1314
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\Service\OtpChallengeStateServiceInterface;
1415
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\Service\OtpCodeGeneratorServiceInterface;
1516
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\Service\OtpCodeValidatorServiceInterface;
17+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\Service\OtpSendPolicyServiceInterface;
1618
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Service\TwoFAServiceInterface;
1719

1820
class OtpFacade implements TwoFAServiceInterface
@@ -22,6 +24,7 @@ public function __construct(
2224
private OtpCodeValidatorServiceInterface $codeValidator,
2325
private OtpCodeGeneratorServiceInterface $codeGenerator,
2426
private OtpNotifierFactoryInterface $notifierFactory,
27+
private OtpSendPolicyServiceInterface $sendPolicy,
2528
) {
2629
}
2730

@@ -59,6 +62,12 @@ public function verify(string $userId, #[\SensitiveParameter] string $code): voi
5962

6063
public function resend(string $userId): void
6164
{
62-
// todo-critical: implement
65+
if (!$this->sendPolicy->canSend($userId)) {
66+
throw new ResendCooldownException();
67+
}
68+
69+
$code = $this->codeGenerator->generateCode();
70+
$this->stateService->refreshChallengeState($userId, $code);
71+
$this->notifierFactory->create($userId)->notify($userId, $code);
6372
}
6473
}

src/Authentication/TwoFactorAuth/OTP/Service/services.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,6 @@ services:
1414

1515
OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\Service\OtpCodeHasherServiceInterface:
1616
class: OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\Service\OtpCodeHasherService
17+
18+
OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\Service\OtpSendPolicyServiceInterface:
19+
class: OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\Service\OtpSendPolicyService

src/Authentication/TwoFactorAuth/Service/TwoFAServiceInterface.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
namespace OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Service;
99

1010
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Exception\CodeValidationException;
11+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Exception\ResendCooldownException;
1112

1213
interface TwoFAServiceInterface
1314
{
@@ -22,5 +23,7 @@ public function invalidateChallenge(string $userId): void;
2223
*/
2324
public function verify(string $userId, #[\SensitiveParameter] string $code): void;
2425

26+
// todo-low: consider extracting resend to a separate ResendableInterface, not all 2FA methods support it (e.g. TOTP)
27+
/** @throws ResendCooldownException */
2528
public function resend(string $userId): void;
2629
}

tests/Integration/ServiceAvailabilityTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,14 @@ public static function serviceAvailabilityDataProvider(): array
4545
[\OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Service\TwoFAServiceInterface::class],
4646

4747
[\OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\Infrastructure\Repository\OtpChallengeStateRepositoryInterface::class],
48+
[\OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\Service\OtpChallengeStateServiceInterface::class],
49+
[\OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\Service\OtpCodeValidatorServiceInterface::class],
4850
[\OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\Service\OtpCodeGeneratorServiceInterface::class],
4951
[\OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\Service\OtpCodeHasherServiceInterface::class],
5052
[\OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Settings\TwoFASettingsInterface::class],
5153
[\OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\Notifier\OtpNotifierInterface::class],
5254
[\OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\Notifier\Factory\OtpNotifierFactoryInterface::class],
55+
[\OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\Service\OtpSendPolicyServiceInterface::class],
5356
];
5457
// phpcs:enable
5558
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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\Exception;
11+
12+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Exception\ResendCooldownException;
13+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Exception\TwoFAException;
14+
use PHPUnit\Framework\TestCase;
15+
16+
class ResendCooldownExceptionTest extends TestCase
17+
{
18+
public function testException(): void
19+
{
20+
$exception = new ResendCooldownException();
21+
22+
$this->assertInstanceOf(TwoFAException::class, $exception);
23+
$this->assertSame('ERROR_RESEND_COOLDOWN', $exception->getMessage());
24+
}
25+
}

tests/Unit/Authentication/TwoFactorAuth/OTP/OtpFacadeTest.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\Service\OtpCodeGeneratorServiceInterface;
1818
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\Service\OtpCodeValidatorServiceInterface;
1919
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\DTO\OtpChallengeStateInterface;
20+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\Service\OtpSendPolicyServiceInterface;
21+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Exception\ResendCooldownException;
2022
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Exception\InvalidCodeException;
2123
use PHPUnit\Framework\Attributes\Test;
2224
use PHPUnit\Framework\TestCase;
@@ -177,17 +179,69 @@ public function triggerChallengeGeneratesCodeCreatesStateAndNotifies(): void
177179
$sut->triggerChallenge(userId: $userId);
178180
}
179181

182+
#[Test]
183+
public function resendThrowsCooldownExceptionWhenSendNotAllowed(): void
184+
{
185+
$sendPolicyStub = $this->createStub(OtpSendPolicyServiceInterface::class);
186+
$sendPolicyStub->method('canSend')
187+
->with($userId = uniqid())
188+
->willReturn(false);
189+
190+
$sut = $this->getSut(sendPolicy: $sendPolicyStub);
191+
192+
$this->expectException(ResendCooldownException::class);
193+
194+
$sut->resend(userId: $userId);
195+
}
196+
197+
#[Test]
198+
public function resendRefreshesStateAndNotifiesWhenAllowed(): void
199+
{
200+
$sendPolicyStub = $this->createStub(OtpSendPolicyServiceInterface::class);
201+
$sendPolicyStub->method('canSend')
202+
->with($userId = uniqid())
203+
->willReturn(true);
204+
205+
$codeGeneratorStub = $this->createStub(OtpCodeGeneratorServiceInterface::class);
206+
$codeGeneratorStub->method('generateCode')
207+
->willReturn($code = uniqid());
208+
209+
$stateServiceSpy = $this->createMock(OtpChallengeStateServiceInterface::class);
210+
$stateServiceSpy->expects($this->once())
211+
->method('refreshChallengeState')
212+
->with($userId, $code);
213+
214+
$notifierSpy = $this->createMock(OtpNotifierInterface::class);
215+
$notifierSpy->expects($this->once())
216+
->method('notify')
217+
->with($userId, $code);
218+
219+
$notifierFactoryStub = $this->createStub(OtpNotifierFactoryInterface::class);
220+
$notifierFactoryStub->method('create')->willReturn($notifierSpy);
221+
222+
$sut = $this->getSut(
223+
stateService: $stateServiceSpy,
224+
codeGenerator: $codeGeneratorStub,
225+
notifierFactory: $notifierFactoryStub,
226+
sendPolicy: $sendPolicyStub,
227+
);
228+
229+
$sut->resend(userId: $userId);
230+
}
231+
180232
private function getSut(
181233
OtpChallengeStateServiceInterface $stateService = null,
182234
OtpCodeValidatorServiceInterface $codeValidator = null,
183235
OtpCodeGeneratorServiceInterface $codeGenerator = null,
184236
OtpNotifierFactoryInterface $notifierFactory = null,
237+
OtpSendPolicyServiceInterface $sendPolicy = null,
185238
): OtpFacade {
186239
return new OtpFacade(
187240
stateService: $stateService ?? $this->createStub(OtpChallengeStateServiceInterface::class),
188241
codeValidator: $codeValidator ?? $this->createStub(OtpCodeValidatorServiceInterface::class),
189242
codeGenerator: $codeGenerator ?? $this->createStub(OtpCodeGeneratorServiceInterface::class),
190243
notifierFactory: $notifierFactory ?? $this->createStub(OtpNotifierFactoryInterface::class),
244+
sendPolicy: $sendPolicy ?? $this->createStub(OtpSendPolicyServiceInterface::class),
191245
);
192246
}
193247
}

0 commit comments

Comments
 (0)