Skip to content

Commit 5c22fb2

Browse files
committed
OXDEV-9078 Start implementing the OTP facade - challenge validation related
* Rename the OTPService to OtpFacade to better represent the idea * isVerified challenge have completion state * verify triggers the verification service * invalidate challenge triggers invalidation service
1 parent 95d5693 commit 5c22fb2

4 files changed

Lines changed: 180 additions & 36 deletions

File tree

src/Authentication/TwoFactorAuth/OTP/OTPService.php

Lines changed: 0 additions & 35 deletions
This file was deleted.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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\OTP;
11+
12+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\Service\OtpChallengeStateServiceInterface;
13+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\Service\OtpCodeValidatorServiceInterface;
14+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Service\TwoFAServiceInterface;
15+
16+
class OtpFacade implements TwoFAServiceInterface
17+
{
18+
public function __construct(
19+
private OtpChallengeStateServiceInterface $stateService,
20+
private OtpCodeValidatorServiceInterface $codeValidator,
21+
) {
22+
}
23+
24+
public function isVerified(string $userId): bool
25+
{
26+
$state = $this->stateService->getChallengeState($userId);
27+
28+
if ($state === null || $state->getVerifiedAt() === null) {
29+
return false;
30+
}
31+
32+
return $state->getExpiresAt() > new \DateTimeImmutable();
33+
}
34+
35+
public function triggerChallenge(string $userId): void
36+
{
37+
// todo-critical: implement
38+
}
39+
40+
public function invalidateChallenge(string $userId): void
41+
{
42+
$this->stateService->deleteChallengeState($userId);
43+
}
44+
45+
public function verify(string $userId, #[\SensitiveParameter] string $code): void
46+
{
47+
$this->codeValidator->validateCode($userId, $code);
48+
}
49+
50+
public function resend(string $userId): void
51+
{
52+
// todo-critical: implement
53+
}
54+
}

src/Authentication/TwoFactorAuth/OTP/services.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ services:
66
autowire: true
77
public: false
88

9-
OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\OTPService: ~
9+
OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\OtpFacade: ~
1010

1111
OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\Infrastructure\Repository\OtpChallengeStateRepositoryInterface:
1212
class: OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\Infrastructure\Repository\OtpChallengeStateRepository
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
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\OTP;
11+
12+
use DateTimeImmutable;
13+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\OtpFacade;
14+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\Service\OtpChallengeStateServiceInterface;
15+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\Service\OtpCodeValidatorServiceInterface;
16+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\DTO\OtpChallengeStateInterface;
17+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Service\TwoFAServiceInterface;
18+
use PHPUnit\Framework\Attributes\Test;
19+
use PHPUnit\Framework\TestCase;
20+
21+
class OTPServiceTest extends TestCase
22+
{
23+
#[Test]
24+
public function isVerifiedReturnsFalseWhenNoChallengeState(): void
25+
{
26+
$stateServiceMock = $this->createMock(OtpChallengeStateServiceInterface::class);
27+
$stateServiceMock->expects($this->once())
28+
->method('getChallengeState')
29+
->with($userId = uniqid())
30+
->willReturn(null);
31+
32+
$sut = $this->getSut(stateService: $stateServiceMock);
33+
34+
$this->assertFalse($sut->isVerified(userId: $userId));
35+
}
36+
37+
#[Test]
38+
public function isVerifiedReturnsFalseWhenVerifiedAtIsNull(): void
39+
{
40+
$stateStub = $this->createStub(OtpChallengeStateInterface::class);
41+
$stateStub->method('getVerifiedAt')->willReturn(null);
42+
43+
$stateServiceMock = $this->createMock(OtpChallengeStateServiceInterface::class);
44+
$stateServiceMock->expects($this->once())
45+
->method('getChallengeState')
46+
->with($userId = uniqid())
47+
->willReturn($stateStub);
48+
49+
$sut = $this->getSut(stateService: $stateServiceMock);
50+
51+
$this->assertFalse($sut->isVerified(userId: $userId));
52+
}
53+
54+
#[Test]
55+
public function isVerifiedReturnsFalseWhenExpired(): void
56+
{
57+
$stateStub = $this->createStub(OtpChallengeStateInterface::class);
58+
$stateStub->method('getVerifiedAt')->willReturn(new DateTimeImmutable());
59+
$stateStub->method('getExpiresAt')->willReturn(new DateTimeImmutable('-1 second'));
60+
61+
$stateServiceMock = $this->createMock(OtpChallengeStateServiceInterface::class);
62+
$stateServiceMock->expects($this->once())
63+
->method('getChallengeState')
64+
->with($userId = uniqid())
65+
->willReturn($stateStub);
66+
67+
$sut = $this->getSut(stateService: $stateServiceMock);
68+
69+
$this->assertFalse($sut->isVerified(userId: $userId));
70+
}
71+
72+
#[Test]
73+
public function isVerifiedReturnsTrueWhenVerifiedAtIsSet(): void
74+
{
75+
$stateStub = $this->createStub(OtpChallengeStateInterface::class);
76+
$stateStub->method('getVerifiedAt')->willReturn(new DateTimeImmutable());
77+
$stateStub->method('getExpiresAt')->willReturn(new DateTimeImmutable('+5 minutes'));
78+
79+
$stateServiceMock = $this->createMock(OtpChallengeStateServiceInterface::class);
80+
$stateServiceMock->expects($this->once())
81+
->method('getChallengeState')
82+
->with($userId = uniqid())
83+
->willReturn($stateStub);
84+
85+
$sut = $this->getSut(stateService: $stateServiceMock);
86+
87+
$this->assertTrue($sut->isVerified(userId: $userId));
88+
}
89+
90+
#[Test]
91+
public function invalidateChallengeDeletesChallengeState(): void
92+
{
93+
$stateServiceSpy = $this->createMock(OtpChallengeStateServiceInterface::class);
94+
$stateServiceSpy->expects($this->once())
95+
->method('deleteChallengeState')
96+
->with($userId = uniqid());
97+
98+
$sut = $this->getSut(stateService: $stateServiceSpy);
99+
100+
$sut->invalidateChallenge(userId: $userId);
101+
}
102+
103+
#[Test]
104+
public function verifyTriggersCodeValidator(): void
105+
{
106+
$codeValidatorSpy = $this->createMock(OtpCodeValidatorServiceInterface::class);
107+
$codeValidatorSpy->expects($this->once())
108+
->method('validateCode')
109+
->with($userId = uniqid(), $code = uniqid());
110+
111+
$sut = $this->getSut(codeValidator: $codeValidatorSpy);
112+
113+
$sut->verify(userId: $userId, code: $code);
114+
}
115+
116+
private function getSut(
117+
OtpChallengeStateServiceInterface $stateService = null,
118+
OtpCodeValidatorServiceInterface $codeValidator = null,
119+
): OtpFacade {
120+
return new OtpFacade(
121+
stateService: $stateService ?? $this->createStub(OtpChallengeStateServiceInterface::class),
122+
codeValidator: $codeValidator ?? $this->createStub(OtpCodeValidatorServiceInterface::class),
123+
);
124+
}
125+
}

0 commit comments

Comments
 (0)