Skip to content

Commit f9df490

Browse files
committed
OXDEV-9078 Add a service to work with challenge state
1 parent 30140c1 commit f9df490

3 files changed

Lines changed: 223 additions & 0 deletions

File tree

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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\Service;
11+
12+
use DateTimeImmutable;
13+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\{
14+
DTO\OtpChallengeStateInterface,
15+
Infrastructure\Repository\OtpChallengeStateRepositoryInterface,
16+
};
17+
18+
class OtpChallengeStateService implements OtpChallengeStateServiceInterface
19+
{
20+
public function __construct(
21+
private OtpChallengeStateRepositoryInterface $challengeStateRepository,
22+
private OtpCodeHasherServiceInterface $codeHasher,
23+
) {
24+
}
25+
26+
public function getChallengeState(string $userId): ?OtpChallengeStateInterface
27+
{
28+
return $this->challengeStateRepository->findByUserId($userId);
29+
}
30+
31+
public function createChallengeState(string $userId, #[\SensitiveParameter] string $code): void
32+
{
33+
// todo-medium: get expiration from settings, test should be improved also to check the value given to repo
34+
$expiresAt = new DateTimeImmutable('+5 minutes');
35+
$codeHash = $this->codeHasher->hash($code);
36+
37+
$this->challengeStateRepository->createChallengeState($userId, $codeHash, $expiresAt);
38+
}
39+
40+
// todo-high: challenge if we want this second method to exist.
41+
public function refreshChallengeState(string $userId, #[\SensitiveParameter] string $code): void
42+
{
43+
// todo-medium: get expiration from settings, test should be improved also to check the value given to repo
44+
$expiresAt = new DateTimeImmutable('+5 minutes');
45+
$codeHash = $this->codeHasher->hash($code);
46+
47+
$this->challengeStateRepository->refreshChallengeState($userId, $codeHash, $expiresAt);
48+
}
49+
50+
public function markVerified(string $userId): void
51+
{
52+
$this->challengeStateRepository->markVerified($userId);
53+
}
54+
55+
public function incrementAttempts(string $userId): void
56+
{
57+
$this->challengeStateRepository->incrementAttempts($userId);
58+
}
59+
60+
public function deleteChallengeState(string $userId): void
61+
{
62+
$this->challengeStateRepository->deleteChallengeState($userId);
63+
}
64+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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\Service;
11+
12+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\DTO\OtpChallengeStateInterface;
13+
14+
interface OtpChallengeStateServiceInterface
15+
{
16+
public function getChallengeState(string $userId): ?OtpChallengeStateInterface;
17+
18+
public function createChallengeState(string $userId, #[\SensitiveParameter] string $code): void;
19+
20+
public function refreshChallengeState(string $userId, #[\SensitiveParameter] string $code): void;
21+
22+
public function markVerified(string $userId): void;
23+
24+
public function incrementAttempts(string $userId): void;
25+
26+
public function deleteChallengeState(string $userId): void;
27+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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\Service;
11+
12+
use DateTimeImmutable;
13+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\DTO\OtpChallengeStateInterface;
14+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\Infrastructure\Repository\OtpChallengeStateRepositoryInterface;
15+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\Service\OtpChallengeStateService;
16+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\Service\OtpCodeHasherServiceInterface;
17+
use PHPUnit\Framework\Attributes\Test;
18+
use PHPUnit\Framework\TestCase;
19+
20+
class OtpChallengeStateServiceTest extends TestCase
21+
{
22+
#[Test]
23+
public function getChallengeStateProxiesToRepository(): void
24+
{
25+
$stateStub = $this->createStub(OtpChallengeStateInterface::class);
26+
27+
$repositoryMock = $this->createMock(OtpChallengeStateRepositoryInterface::class);
28+
$repositoryMock->expects($this->once())
29+
->method('findByUserId')
30+
->with($userId = uniqid())
31+
->willReturn($stateStub);
32+
33+
$sut = $this->getSut(repository: $repositoryMock);
34+
35+
$this->assertSame($stateStub, $sut->getChallengeState(userId: $userId));
36+
}
37+
38+
#[Test]
39+
public function createChallengeStateHashesCodeAndPassesToRepository(): void
40+
{
41+
$hasherMock = $this->createMock(OtpCodeHasherServiceInterface::class);
42+
$hasherMock->expects($this->once())
43+
->method('hash')
44+
->with($code = uniqid())
45+
->willReturn($codeHash = uniqid());
46+
47+
$repositorySpy = $this->createMock(OtpChallengeStateRepositoryInterface::class);
48+
$repositorySpy->expects($this->once())
49+
->method('createChallengeState')
50+
->with(
51+
$userId = uniqid(),
52+
$codeHash,
53+
$this->callback(fn(DateTimeImmutable $expiresAt) => $expiresAt > new DateTimeImmutable())
54+
);
55+
56+
$sut = $this->getSut(repository: $repositorySpy, hasher: $hasherMock);
57+
58+
$sut->createChallengeState(userId: $userId, code: $code);
59+
}
60+
61+
#[Test]
62+
public function refreshChallengeStateHashesCodeAndPassesToRepository(): void
63+
{
64+
$hasherMock = $this->createMock(OtpCodeHasherServiceInterface::class);
65+
$hasherMock->expects($this->once())
66+
->method('hash')
67+
->with($code = uniqid())
68+
->willReturn($codeHash = uniqid());
69+
70+
$repositorySpy = $this->createMock(OtpChallengeStateRepositoryInterface::class);
71+
$repositorySpy->expects($this->once())
72+
->method('refreshChallengeState')
73+
->with(
74+
$userId = uniqid(),
75+
$codeHash,
76+
$this->callback(fn(DateTimeImmutable $expiresAt) => $expiresAt > new DateTimeImmutable())
77+
);
78+
79+
$sut = $this->getSut(repository: $repositorySpy, hasher: $hasherMock);
80+
81+
$sut->refreshChallengeState(userId: $userId, code: $code);
82+
}
83+
84+
#[Test]
85+
public function markVerifiedProxiesToRepository(): void
86+
{
87+
$repositorySpy = $this->createMock(OtpChallengeStateRepositoryInterface::class);
88+
$repositorySpy->expects($this->once())
89+
->method('markVerified')
90+
->with($userId = uniqid());
91+
92+
$sut = $this->getSut(repository: $repositorySpy);
93+
94+
$sut->markVerified(userId: $userId);
95+
}
96+
97+
#[Test]
98+
public function incrementAttemptsProxiesToRepository(): void
99+
{
100+
$repositorySpy = $this->createMock(OtpChallengeStateRepositoryInterface::class);
101+
$repositorySpy->expects($this->once())
102+
->method('incrementAttempts')
103+
->with($userId = uniqid());
104+
105+
$sut = $this->getSut(repository: $repositorySpy);
106+
107+
$sut->incrementAttempts(userId: $userId);
108+
}
109+
110+
#[Test]
111+
public function deleteChallengeStateProxiesToRepository(): void
112+
{
113+
$repositorySpy = $this->createMock(OtpChallengeStateRepositoryInterface::class);
114+
$repositorySpy->expects($this->once())
115+
->method('deleteChallengeState')
116+
->with($userId = uniqid());
117+
118+
$sut = $this->getSut(repository: $repositorySpy);
119+
120+
$sut->deleteChallengeState(userId: $userId);
121+
}
122+
123+
private function getSut(
124+
OtpChallengeStateRepositoryInterface $repository = null,
125+
OtpCodeHasherServiceInterface $hasher = null,
126+
): OtpChallengeStateService {
127+
return new OtpChallengeStateService(
128+
challengeStateRepository: $repository ?? $this->createStub(OtpChallengeStateRepositoryInterface::class),
129+
codeHasher: $hasher ?? $this->createStub(OtpCodeHasherServiceInterface::class),
130+
);
131+
}
132+
}

0 commit comments

Comments
 (0)