Skip to content

Commit 4083813

Browse files
committed
refactor: convert device data to a DTO
Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>
1 parent e9babd6 commit 4083813

File tree

9 files changed

+115
-55
lines changed

9 files changed

+115
-55
lines changed

lib/Db/PublicKeyCredentialEntity.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ public static function fromPublicKeyCrendentialSource(string $name,
121121
$publicKeyCredentialEntity->setUserHandle($publicKeyCredentialSource->getUserHandle());
122122
$publicKeyCredentialEntity->setCounter($publicKeyCredentialSource->getCounter());
123123
$publicKeyCredentialEntity->setCreatedAt($ctime);
124+
$publicKeyCredentialEntity->setActive(true);
124125

125126
return $publicKeyCredentialEntity;
126127
}

lib/Listener/StateChangeRegistryUpdater.php

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
namespace OCA\TwoFactorWebauthn\Listener;
1111

1212
use OCA\TwoFactorWebauthn\Event\StateChanged;
13+
use OCA\TwoFactorWebauthn\Model\Device;
1314
use OCA\TwoFactorWebauthn\Provider\WebAuthnProvider;
1415
use OCA\TwoFactorWebauthn\Service\WebAuthnManager;
1516
use OCP\Authentication\TwoFactorAuth\IRegistry;
@@ -38,13 +39,15 @@ public function __construct(IRegistry $providerRegistry, WebAuthnManager $manage
3839

3940
public function handle(Event $event): void {
4041
if ($event instanceof StateChanged) {
41-
$devices = array_filter($this->manager->getDevices($event->getUser()), function ($device) {
42-
return $device['active'] === true;
43-
});
44-
if ($event->isEnabled() && count($devices) > 0) {
42+
$devices = array_filter(
43+
$this->manager->getDevices($event->getUser()),
44+
static fn (Device $device) => $device->isActive(),
45+
);
46+
$hasDevices = !empty($devices);
47+
if ($hasDevices && $event->isEnabled()) {
4548
// The first device was enabled -> enable provider for this user
4649
$this->providerRegistry->enableProviderFor($this->provider, $event->getUser());
47-
} elseif (!$event->isEnabled() && empty($devices)) {
50+
} elseif (!$hasDevices && !$event->isEnabled()) {
4851
// The last device was removed -> disable provider for this user
4952
$this->providerRegistry->disableProviderFor($this->provider, $event->getUser());
5053
}

lib/Model/Device.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\TwoFactorWebauthn\Model;
11+
12+
use OCA\TwoFactorWebauthn\Db\PublicKeyCredentialEntity;
13+
14+
final class Device implements \JsonSerializable {
15+
public function __construct(
16+
private readonly int $entityId,
17+
private readonly string $id,
18+
private readonly string $name,
19+
private readonly ?int $createdAt,
20+
private readonly bool $active,
21+
) {
22+
}
23+
24+
public static function fromPublicKeyCredentialEntity(PublicKeyCredentialEntity $entity): self {
25+
return new self(
26+
$entity->getId(),
27+
$entity->getPublicKeyCredentialId(),
28+
$entity->getName(),
29+
$entity->getCreatedAt(),
30+
$entity->isActive() === true,
31+
);
32+
}
33+
34+
public function getEntityId(): int {
35+
return $this->entityId;
36+
}
37+
38+
public function getId(): string {
39+
return $this->id;
40+
}
41+
42+
public function getName(): string {
43+
return $this->name;
44+
}
45+
46+
public function getCreatedAt(): ?int {
47+
return $this->createdAt;
48+
}
49+
50+
public function isActive(): bool {
51+
return $this->active;
52+
}
53+
54+
#[\ReturnTypeWillChange]
55+
public function jsonSerialize() {
56+
return [
57+
'entityId' => $this->getEntityId(),
58+
'id' => $this->getId(),
59+
'name' => $this->getName(),
60+
'createdAt' => $this->getCreatedAt(),
61+
'active' => $this->isActive(),
62+
];
63+
}
64+
}

lib/Provider/WebAuthnProvider.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
namespace OCA\TwoFactorWebauthn\Provider;
1111

1212
use OCA\TwoFactorWebauthn\AppInfo\Application;
13+
use OCA\TwoFactorWebauthn\Model\Device;
1314
use OCA\TwoFactorWebauthn\Service\WebAuthnManager;
1415
use OCA\TwoFactorWebauthn\Settings\Personal;
1516
use OCP\AppFramework\Services\IInitialState;
@@ -104,9 +105,10 @@ public function verifyChallenge(IUser $user, string $challenge): bool {
104105
* Decides whether 2FA is enabled for the given user
105106
*/
106107
public function isTwoFactorAuthEnabledForUser(IUser $user): bool {
107-
$devices = array_filter($this->manager->getDevices($user), function ($device) {
108-
return $device['active'] === true;
109-
});
108+
$devices = array_filter(
109+
$this->manager->getDevices($user),
110+
static fn (Device $device) => $device->isActive(),
111+
);
110112
return count($devices) > 0;
111113
}
112114

lib/Service/WebAuthnManager.php

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
use OCA\TwoFactorWebauthn\Db\PublicKeyCredentialEntity;
1919
use OCA\TwoFactorWebauthn\Db\PublicKeyCredentialEntityMapper;
2020
use OCA\TwoFactorWebauthn\Event\StateChanged;
21+
use OCA\TwoFactorWebauthn\Model\Device;
2122
use OCA\TwoFactorWebauthn\Repository\WebauthnPublicKeyCredentialSourceRepository;
23+
use OCP\AppFramework\Utility\ITimeFactory;
2224
use OCP\EventDispatcher\IEventDispatcher;
2325
use OCP\IRequest;
2426
use OCP\ISession;
@@ -80,6 +82,7 @@ public function __construct(
8082
LoggerInterface $logger,
8183
private readonly IRequest $request,
8284
private readonly ISecureRandom $random,
85+
private readonly ITimeFactory $time,
8386
) {
8487
$this->session = $session;
8588
$this->repository = $repository;
@@ -171,7 +174,7 @@ public function buildPublicKeyCredentialLoader(AttestationStatementSupportManage
171174
return $publicKeyCredentialLoader;
172175
}
173176

174-
public function finishRegister(IUser $user, string $name, $data): array {
177+
public function finishRegister(IUser $user, string $name, $data): Device {
175178
if (!$this->session->exists(self::TWOFACTORAUTH_WEBAUTHN_REGISTRATION)) {
176179
throw new Exception('Twofactor Webauthn registration process was not properly initialized');
177180
}
@@ -212,30 +215,23 @@ public function finishRegister(IUser $user, string $name, $data): array {
212215
$this->stripPort($this->request->getServerHost()),
213216
);
214217

215-
$this->repository->saveCredentialSource($publicKeyCredentialSource, $name);
216-
$entity = $this->mapper->findPublicKeyCredential(base64_encode($publicKeyCredentialSource->getPublicKeyCredentialId()));
218+
$entity = PublicKeyCredentialEntity::fromPublicKeyCrendentialSource(
219+
$name,
220+
$publicKeyCredentialSource,
221+
$this->time->getTime(),
222+
);
223+
$entity = $this->mapper->insert($entity);
217224
$this->eventDispatcher->dispatch(StateChanged::class, new StateChanged($user, true));
218-
219-
return [
220-
'entityId' => $entity->getId(),
221-
'id' => base64_encode($publicKeyCredentialSource->getPublicKeyCredentialId()),
222-
'name' => $name,
223-
'createdAt' => $entity->getCreatedAt(),
224-
'active' => true,
225-
];
225+
return Device::fromPublicKeyCredentialEntity($entity);
226226
}
227227

228+
/**
229+
* @param IUser $user
230+
* @return Device[]
231+
*/
228232
public function getDevices(IUser $user): array {
229233
$credentials = $this->mapper->findPublicKeyCredentials($user->getUID());
230-
return array_map(function (PublicKeyCredentialEntity $credential) {
231-
return [
232-
'entityId' => $credential->getId(),
233-
'id' => $credential->getPublicKeyCredentialId(),
234-
'name' => $credential->getName(),
235-
'createdAt' => $credential->getCreatedAt(),
236-
'active' => ($credential->isActive() === true)
237-
];
238-
}, $credentials);
234+
return array_map(Device::fromPublicKeyCredentialEntity(...), $credentials);
239235
}
240236

241237
private function stripPort(string $serverHost): string {

tests/Unit/Controller/SettingsControllerTest.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
use ChristophWurst\Nextcloud\Testing\TestCase;
1212
use OCA\TwoFactorWebauthn\Controller\SettingsController;
13+
use OCA\TwoFactorWebauthn\Model\Device;
1314
use OCA\TwoFactorWebauthn\Service\WebAuthnManager;
1415
use OCP\AppFramework\Http\JSONResponse;
1516
use OCP\IRequest;
@@ -71,16 +72,17 @@ public function testFinishRegister(): void {
7172
->willReturn($user);
7273
$data = 'some data';
7374

75+
$device = new Device(1, 'key-1', 'my key', null, true);
7476
$this->webauthnManager->expects(self::once())
7577
->method('finishRegister')
7678
->with(
7779
self::equalTo($user),
7880
self::equalTo('my key'),
7981
self::equalTo($data))
80-
->willReturn([]);
82+
->willReturn($device);
8183

8284
$resp = $this->controller->finishRegister('my key', $data);
8385

84-
self::assertEquals(new JSONResponse([]), $resp);
86+
self::assertEquals(new JSONResponse($device), $resp);
8587
}
8688
}

tests/Unit/Listener/StateChangeRegistryUpdaterTest.php

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

1212
use OCA\TwoFactorWebauthn\Event\StateChanged;
1313
use OCA\TwoFactorWebauthn\Listener\StateChangeRegistryUpdater;
14+
use OCA\TwoFactorWebauthn\Model\Device;
1415
use OCA\TwoFactorWebauthn\Provider\WebAuthnProvider;
1516
use OCA\TwoFactorWebauthn\Service\WebAuthnManager;
1617
use OCP\Authentication\TwoFactorAuth\IRegistry;
@@ -59,11 +60,7 @@ public function testHandleEnableFirstDevice(): void {
5960
$this->manager->expects(self::once())
6061
->method('getDevices')
6162
->willReturn([
62-
[
63-
'id' => 1,
64-
'name' => 'utf1',
65-
'active' => true,
66-
],
63+
new Device(1, 'credential-id-1', 'utf2', null, true),
6764
]);
6865
$this->providerRegistry->expects(self::once())
6966
->method('enableProviderFor')
@@ -97,11 +94,7 @@ public function testHandleDisableWithRemainingDevices(): void {
9794
$this->manager->expects(self::once())
9895
->method('getDevices')
9996
->willReturn([
100-
[
101-
'id' => 2,
102-
'name' => 'utf2',
103-
'active' => true,
104-
],
97+
new Device(2, 'credential-id-2', 'utf2', null, true),
10598
]);
10699
$this->providerRegistry->expects(self::never())
107100
->method('disableProviderFor')

tests/Unit/Provider/WebAuthnProviderTest.php

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
namespace OCA\TwoFactorWebauthn\Tests\Unit\Provider;
1111

12+
use OCA\TwoFactorWebauthn\Model\Device;
1213
use OCA\TwoFactorWebauthn\Provider\WebAuthnLoginProvider;
1314
use OCA\TwoFactorWebauthn\Provider\WebAuthnProvider;
1415
use OCA\TwoFactorWebauthn\Service\WebAuthnManager;
@@ -129,11 +130,7 @@ public function testIsTwoFactorAuthEnabledForUser(): void {
129130
$this->manager->expects(self::once())
130131
->method('getDevices')
131132
->willReturn([
132-
[
133-
'id' => 'k1',
134-
'name' => 'n1',
135-
'active' => true,
136-
],
133+
new Device(1, 'k1', 'n1', null, true),
137134
]);
138135

139136
self::assertTrue($this->provider->isTwoFactorAuthEnabledForUser($user));
@@ -144,11 +141,7 @@ public function testIsTwoFactorAuthDisabledForUserBecauseDisabledDevice(): void
144141
$this->manager->expects(self::once())
145142
->method('getDevices')
146143
->willReturn([
147-
[
148-
'id' => 'k1',
149-
'name' => 'n1',
150-
'active' => false,
151-
],
144+
new Device(1, 'k1', 'n1', null, false),
152145
]);
153146

154147
self::assertFalse($this->provider->isTwoFactorAuthEnabledForUser($user));

tests/Unit/Service/WebAuthnManagerTest.php

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use OCA\TwoFactorWebauthn\Event\StateChanged;
1515
use OCA\TwoFactorWebauthn\Repository\WebauthnPublicKeyCredentialSourceRepository;
1616
use OCA\TwoFactorWebauthn\Service\WebAuthnManager;
17+
use OCP\AppFramework\Utility\ITimeFactory;
1718
use OCP\EventDispatcher\IEventDispatcher;
1819
use OCP\IRequest;
1920
use OCP\ISession;
@@ -45,6 +46,7 @@ class WebAuthnManagerTest extends TestCase {
4546

4647
private IRequest&MockObject $request;
4748
private ISecureRandom&MockObject $random;
49+
private ITimeFactory&MockObject $time;
4850

4951
protected function setUp(): void {
5052
parent::setUp();
@@ -56,6 +58,7 @@ protected function setUp(): void {
5658
$this->logger = $this->createMock(LoggerInterface::class);
5759
$this->request = $this->createMock(IRequest::class);
5860
$this->random = $this->createMock(ISecureRandom::class);
61+
$this->time = $this->createMock(ITimeFactory::class);
5962

6063
$this->manager = new WebAuthnManager(
6164
$this->session,
@@ -65,17 +68,20 @@ protected function setUp(): void {
6568
$this->logger,
6669
$this->request,
6770
$this->random,
71+
$this->time,
6872
);
6973
}
7074

71-
/**
72-
* @param IUser $user
73-
* @param int $nr
74-
*/
75-
private function mockRegistrations(IUser $user, $nr): void {
75+
private function mockRegistrations(IUser $user, int $nr): void {
7676
$regs = [];
7777
for ($i = 0; $i < $nr; $i++) {
7878
$reg = new PublicKeyCredentialEntity();
79+
$reg->setId($i);
80+
$reg->setPublicKeyCredentialId("credential-id-$i");
81+
$reg->setName("key-$i");
82+
$reg->setUserHandle($user->getUID());
83+
$reg->setActive(true);
84+
$reg->setCreatedAt(null);
7985
$regs[] = $reg;
8086
}
8187
$this->mapper->expects(self::once())

0 commit comments

Comments
 (0)