1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* Copyright 2014 SURFnet bv |
5
|
|
|
* |
6
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
7
|
|
|
* you may not use this file except in compliance with the License. |
8
|
|
|
* You may obtain a copy of the License at |
9
|
|
|
* |
10
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0 |
11
|
|
|
* |
12
|
|
|
* Unless required by applicable law or agreed to in writing, software |
13
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS, |
14
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
15
|
|
|
* See the License for the specific language governing permissions and |
16
|
|
|
* limitations under the License. |
17
|
|
|
*/ |
18
|
|
|
|
19
|
|
|
namespace Surfnet\StepupSelfService\SelfServiceBundle\Service; |
20
|
|
|
|
21
|
|
|
use Surfnet\StepupBundle\Service\SecondFactorTypeService; |
22
|
|
|
use Surfnet\StepupBundle\Value\Loa; |
23
|
|
|
use Surfnet\StepupBundle\Value\SecondFactorType; |
24
|
|
|
use Surfnet\StepupMiddlewareClient\Identity\Dto\UnverifiedSecondFactorSearchQuery; |
25
|
|
|
use Surfnet\StepupMiddlewareClient\Identity\Dto\VerifiedSecondFactorSearchQuery; |
26
|
|
|
use Surfnet\StepupMiddlewareClient\Identity\Dto\VettedSecondFactorSearchQuery; |
27
|
|
|
use Surfnet\StepupMiddlewareClientBundle\Dto\CollectionDto; |
28
|
|
|
use Surfnet\StepupMiddlewareClientBundle\Identity\Command\RevokeOwnSecondFactorCommand; |
29
|
|
|
use Surfnet\StepupMiddlewareClientBundle\Identity\Command\VerifyEmailCommand; |
30
|
|
|
use Surfnet\StepupMiddlewareClientBundle\Identity\Dto\UnverifiedSecondFactor; |
31
|
|
|
use Surfnet\StepupMiddlewareClientBundle\Identity\Dto\UnverifiedSecondFactorCollection; |
32
|
|
|
use Surfnet\StepupMiddlewareClientBundle\Identity\Dto\VerifiedSecondFactor; |
33
|
|
|
use Surfnet\StepupMiddlewareClientBundle\Identity\Dto\VerifiedSecondFactorCollection; |
34
|
|
|
use Surfnet\StepupMiddlewareClientBundle\Identity\Dto\VettedSecondFactor; |
35
|
|
|
use Surfnet\StepupMiddlewareClientBundle\Identity\Dto\VettedSecondFactorCollection; |
36
|
|
|
use Surfnet\StepupMiddlewareClientBundle\Identity\Service\SecondFactorService as MiddlewareSecondFactorService; |
37
|
|
|
use Surfnet\StepupSelfService\SelfServiceBundle\Command\RevokeCommand; |
38
|
|
|
use Surfnet\StepupSelfService\SelfServiceBundle\Exception\LogicException; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* @SuppressWarnings(PHPMD.TooManyPublicMethods) |
42
|
|
|
* @SuppressWarnings(PHPMD.CouplingBetweenObjects) |
43
|
|
|
*/ |
44
|
|
|
class SecondFactorService |
45
|
|
|
{ |
46
|
|
|
/** |
47
|
|
|
* @var \Surfnet\StepupMiddlewareClientBundle\Identity\Service\SecondFactorService |
48
|
|
|
*/ |
49
|
|
|
private $secondFactors; |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* @var \Surfnet\StepupSelfService\SelfServiceBundle\Service\CommandService |
53
|
|
|
*/ |
54
|
|
|
private $commandService; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* @var \Surfnet\StepupSelfService\SelfServiceBundle\Service\U2fSecondFactorService |
58
|
|
|
*/ |
59
|
|
|
private $u2fSecondFactorService; |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* @var SecondFactorTypeService |
63
|
|
|
*/ |
64
|
|
|
private $secondFactorTypeService; |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* @param MiddlewareSecondFactorService $secondFactors |
68
|
|
|
* @param CommandService $commandService |
69
|
|
|
* @param U2fSecondFactorService $u2fSecondFactorService |
70
|
|
|
* @param SecondFactorTypeService $secondFactorTypeService |
71
|
|
|
*/ |
72
|
|
|
public function __construct( |
73
|
|
|
MiddlewareSecondFactorService $secondFactors, |
74
|
|
|
CommandService $commandService, |
75
|
|
|
U2fSecondFactorService $u2fSecondFactorService, |
76
|
|
|
SecondFactorTypeService $secondFactorTypeService |
77
|
|
|
) { |
78
|
|
|
$this->secondFactors = $secondFactors; |
79
|
|
|
$this->commandService = $commandService; |
80
|
|
|
$this->u2fSecondFactorService = $u2fSecondFactorService; |
81
|
|
|
$this->secondFactorTypeService = $secondFactorTypeService; |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* @param string $identityId |
86
|
|
|
* @param string $nonce |
87
|
|
|
* @return bool |
88
|
|
|
*/ |
89
|
|
|
public function verifyEmail($identityId, $nonce) |
90
|
|
|
{ |
91
|
|
|
$command = new VerifyEmailCommand(); |
92
|
|
|
$command->identityId = $identityId; |
93
|
|
|
$command->verificationNonce = $nonce; |
94
|
|
|
|
95
|
|
|
$result = $this->commandService->execute($command); |
96
|
|
|
|
97
|
|
|
return $result->isSuccessful(); |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
/** |
101
|
|
|
* @param RevokeCommand $command |
102
|
|
|
* @return bool |
103
|
|
|
*/ |
104
|
|
|
public function revoke(RevokeCommand $command) |
105
|
|
|
{ |
106
|
|
|
/** @var UnverifiedSecondFactor|VerifiedSecondFactor|VettedSecondFactor $secondFactor */ |
107
|
|
|
$secondFactor = $command->secondFactor; |
108
|
|
|
|
109
|
|
|
$apiCommand = new RevokeOwnSecondFactorCommand(); |
110
|
|
|
$apiCommand->identityId = $command->identity->id; |
111
|
|
|
$apiCommand->secondFactorId = $secondFactor->id; |
112
|
|
|
|
113
|
|
|
$result = $this->commandService->execute($apiCommand); |
114
|
|
|
|
115
|
|
|
if ($secondFactor->type === 'u2f') { |
116
|
|
|
$this->u2fSecondFactorService->revokeRegistration( |
117
|
|
|
$command->identity, |
118
|
|
|
$secondFactor->secondFactorIdentifier |
119
|
|
|
); |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
return $result->isSuccessful(); |
123
|
|
|
} |
124
|
|
|
|
125
|
|
|
/** |
126
|
|
|
* Returns whether the given registrant has registered second factors with Step-up. The state of the second factor |
127
|
|
|
* is irrelevant. |
128
|
|
|
* |
129
|
|
|
* @param string $identityId |
130
|
|
|
* @return bool |
131
|
|
|
*/ |
132
|
|
|
public function doSecondFactorsExistForIdentity($identityId) |
133
|
|
|
{ |
134
|
|
|
$unverifiedSecondFactors = $this->findUnverifiedByIdentity($identityId); |
135
|
|
|
$verifiedSecondFactors = $this->findVerifiedByIdentity($identityId); |
136
|
|
|
$vettedSecondFactors = $this->findVettedByIdentity($identityId); |
137
|
|
|
|
138
|
|
|
return $unverifiedSecondFactors->getTotalItems() + |
139
|
|
|
$verifiedSecondFactors->getTotalItems() + |
140
|
|
|
$vettedSecondFactors->getTotalItems() > 0; |
141
|
|
|
} |
142
|
|
|
|
143
|
|
View Code Duplication |
public function identityHasSecondFactorOfStateWithId($identityId, $state, $secondFactorId) |
|
|
|
|
144
|
|
|
{ |
145
|
|
|
switch ($state) { |
146
|
|
|
case 'unverified': |
147
|
|
|
$secondFactors = $this->findUnverifiedByIdentity($identityId); |
148
|
|
|
break; |
149
|
|
|
case 'verified': |
150
|
|
|
$secondFactors = $this->findVerifiedByIdentity($identityId); |
151
|
|
|
break; |
152
|
|
|
case 'vetted': |
153
|
|
|
$secondFactors = $this->findVettedByIdentity($identityId); |
154
|
|
|
break; |
155
|
|
|
default: |
156
|
|
|
throw new LogicException(sprintf('Invalid second factor state "%s" given.', $state)); |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
if (count($secondFactors->getElements()) === 0) { |
160
|
|
|
return false; |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
foreach ($secondFactors->getElements() as $secondFactor) { |
164
|
|
|
if ($secondFactor->id === $secondFactorId) { |
165
|
|
|
return true; |
166
|
|
|
} |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
return false; |
170
|
|
|
} |
171
|
|
|
|
172
|
|
|
/** |
173
|
|
|
* Tests if the identity owns a second factor token for a given state, with a satisfiable LoA |
174
|
|
|
* |
175
|
|
|
* @param $identityId |
176
|
|
|
* @param $state |
177
|
|
|
* @param Loa $loa |
178
|
|
|
* @return bool |
179
|
|
|
*/ |
180
|
|
View Code Duplication |
public function identityHasSecondFactorOfStateWithMinimalLoa($identityId, $state, Loa $loa) |
|
|
|
|
181
|
|
|
{ |
182
|
|
|
switch ($state) { |
183
|
|
|
case 'unverified': |
184
|
|
|
$secondFactors = $this->findUnverifiedByIdentity($identityId); |
185
|
|
|
break; |
186
|
|
|
case 'verified': |
187
|
|
|
$secondFactors = $this->findVerifiedByIdentity($identityId); |
188
|
|
|
break; |
189
|
|
|
case 'vetted': |
190
|
|
|
$secondFactors = $this->findVettedByIdentity($identityId); |
191
|
|
|
break; |
192
|
|
|
default: |
193
|
|
|
throw new LogicException(sprintf('Invalid second factor state "%s" given.', $state)); |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
if (count($secondFactors->getElements()) === 0) { |
197
|
|
|
return false; |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
foreach ($secondFactors->getElements() as $secondFactor) { |
201
|
|
|
if ($this->secondFactorTypeService->canSatisfy(new SecondFactorType($secondFactor->type), $loa)) { |
202
|
|
|
return true; |
203
|
|
|
} |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
return false; |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
/** |
210
|
|
|
* Returns the given registrant's unverified second factors. |
211
|
|
|
* |
212
|
|
|
* @param string $identityId |
213
|
|
|
* @return UnverifiedSecondFactorCollection |
|
|
|
|
214
|
|
|
*/ |
215
|
|
|
public function findUnverifiedByIdentity($identityId) |
216
|
|
|
{ |
217
|
|
|
return $this->secondFactors->searchUnverified( |
218
|
|
|
(new UnverifiedSecondFactorSearchQuery())->setIdentityId($identityId) |
219
|
|
|
); |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
/** |
223
|
|
|
* Returns the given registrant's verified second factors. |
224
|
|
|
* |
225
|
|
|
* @param string $identityId |
226
|
|
|
* @return VerifiedSecondFactorCollection |
|
|
|
|
227
|
|
|
*/ |
228
|
|
|
public function findVerifiedByIdentity($identityId) |
229
|
|
|
{ |
230
|
|
|
return $this->secondFactors->searchVerified( |
231
|
|
|
(new VerifiedSecondFactorSearchQuery())->setIdentityId($identityId) |
232
|
|
|
); |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
/** |
236
|
|
|
* Returns the given registrant's verified second factors. |
237
|
|
|
* |
238
|
|
|
* @param string $identityId |
239
|
|
|
* @return VettedSecondFactorCollection |
|
|
|
|
240
|
|
|
*/ |
241
|
|
|
public function findVettedByIdentity($identityId) |
242
|
|
|
{ |
243
|
|
|
return $this->secondFactors->searchVetted( |
244
|
|
|
(new VettedSecondFactorSearchQuery())->setIdentityId($identityId) |
245
|
|
|
); |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
/** |
249
|
|
|
* @param string $secondFactorId |
250
|
|
|
* @return null|UnverifiedSecondFactor |
251
|
|
|
*/ |
252
|
|
|
public function findOneUnverified($secondFactorId) |
253
|
|
|
{ |
254
|
|
|
return $this->secondFactors->getUnverified($secondFactorId); |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
/** |
258
|
|
|
* @param string $secondFactorId |
259
|
|
|
* @return null|VerifiedSecondFactor |
260
|
|
|
*/ |
261
|
|
|
public function findOneVerified($secondFactorId) |
262
|
|
|
{ |
263
|
|
|
return $this->secondFactors->getVerified($secondFactorId); |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
/** |
267
|
|
|
* @param string $secondFactorId |
268
|
|
|
* @return null|VettedSecondFactor |
269
|
|
|
*/ |
270
|
|
|
public function findOneVetted($secondFactorId) |
271
|
|
|
{ |
272
|
|
|
return $this->secondFactors->getVetted($secondFactorId); |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
/** |
276
|
|
|
* @param string $identityId |
277
|
|
|
* @param string $verificationNonce |
278
|
|
|
* @return UnverifiedSecondFactor|null |
279
|
|
|
*/ |
280
|
|
View Code Duplication |
public function findUnverifiedByVerificationNonce($identityId, $verificationNonce) |
|
|
|
|
281
|
|
|
{ |
282
|
|
|
$secondFactors = $this->secondFactors->searchUnverified( |
283
|
|
|
(new UnverifiedSecondFactorSearchQuery()) |
284
|
|
|
->setIdentityId($identityId) |
285
|
|
|
->setVerificationNonce($verificationNonce) |
286
|
|
|
); |
287
|
|
|
|
288
|
|
|
$elements = $secondFactors->getElements(); |
289
|
|
|
|
290
|
|
|
switch (count($elements)) { |
291
|
|
|
case 0: |
292
|
|
|
return null; |
293
|
|
|
case 1: |
294
|
|
|
return reset($elements); |
295
|
|
|
default: |
296
|
|
|
throw new LogicException('There cannot be more than one unverified second factor with the same nonce'); |
297
|
|
|
} |
298
|
|
|
} |
299
|
|
|
|
300
|
|
|
/** |
301
|
|
|
* @param string $secondFactorId |
302
|
|
|
* @param string $identityId |
303
|
|
|
* @return null|string |
304
|
|
|
*/ |
305
|
|
View Code Duplication |
public function getRegistrationCode($secondFactorId, $identityId) |
|
|
|
|
306
|
|
|
{ |
307
|
|
|
$query = (new VerifiedSecondFactorSearchQuery()) |
308
|
|
|
->setIdentityId($identityId) |
309
|
|
|
->setSecondFactorId($secondFactorId); |
310
|
|
|
|
311
|
|
|
/** @var VerifiedSecondFactor[] $verifiedSecondFactors */ |
312
|
|
|
$verifiedSecondFactors = $this->secondFactors->searchVerified($query)->getElements(); |
313
|
|
|
|
314
|
|
|
switch (count($verifiedSecondFactors)) { |
315
|
|
|
case 0: |
316
|
|
|
return null; |
317
|
|
|
case 1: |
318
|
|
|
return reset($verifiedSecondFactors)->registrationCode; |
319
|
|
|
default: |
320
|
|
|
throw new LogicException('Searching by second factor ID cannot result in multiple results.'); |
321
|
|
|
} |
322
|
|
|
} |
323
|
|
|
|
324
|
|
|
/** |
325
|
|
|
* @param array $allSecondFactors |
326
|
|
|
* @param UnverifiedSecondFactorCollection $unverifiedCollection |
327
|
|
|
* @param VerifiedSecondFactorCollection $verifiedCollection |
328
|
|
|
* @param VettedSecondFactorCollection $vettedCollection |
329
|
|
|
* @return array |
330
|
|
|
*/ |
331
|
|
|
private function determineAvailable( |
332
|
|
|
array $allSecondFactors, |
333
|
|
|
UnverifiedSecondFactorCollection $unverifiedCollection, |
334
|
|
|
VerifiedSecondFactorCollection $verifiedCollection, |
335
|
|
|
VettedSecondFactorCollection $vettedCollection |
336
|
|
|
) { |
337
|
|
|
$allSecondFactors = $this->filterAvailableSecondFactors($allSecondFactors, $unverifiedCollection); |
338
|
|
|
$allSecondFactors = $this->filterAvailableSecondFactors($allSecondFactors, $verifiedCollection); |
339
|
|
|
$allSecondFactors = $this->filterAvailableSecondFactors($allSecondFactors, $vettedCollection); |
340
|
|
|
return $allSecondFactors; |
341
|
|
|
} |
342
|
|
|
|
343
|
|
|
/** |
344
|
|
|
* @param array $allSecondFactors |
345
|
|
|
* @param CollectionDto $collection |
346
|
|
|
* @return array |
347
|
|
|
*/ |
348
|
|
|
private function filterAvailableSecondFactors(array $allSecondFactors, CollectionDto $collection) |
349
|
|
|
{ |
350
|
|
|
foreach ($collection->getElements() as $secondFactor) { |
351
|
|
|
$keyFound = array_search($secondFactor->type, $allSecondFactors); |
352
|
|
|
if (is_numeric($keyFound)) { |
353
|
|
|
unset($allSecondFactors[$keyFound]); |
354
|
|
|
} |
355
|
|
|
} |
356
|
|
|
return $allSecondFactors; |
357
|
|
|
} |
358
|
|
|
|
359
|
|
|
/** |
360
|
|
|
* @param $identity |
361
|
|
|
* @param $allSecondFactors |
362
|
|
|
* @param $allowedSecondFactors |
363
|
|
|
* @param $maximumNumberOfRegistrations |
364
|
|
|
* @return SecondFactorTypeCollection |
365
|
|
|
*/ |
366
|
|
|
public function getSecondFactorsForIdentity( |
367
|
|
|
$identity, |
368
|
|
|
$allSecondFactors, |
369
|
|
|
$allowedSecondFactors, |
370
|
|
|
$maximumNumberOfRegistrations |
371
|
|
|
) { |
372
|
|
|
$unverified = $this->findUnverifiedByIdentity($identity->id); |
373
|
|
|
$verified = $this->findVerifiedByIdentity($identity->id); |
374
|
|
|
$vetted = $this->findVettedByIdentity($identity->id); |
375
|
|
|
// Determine which Second Factors are still available for registration. |
376
|
|
|
$available = $this->determineAvailable($allSecondFactors, $unverified, $verified, $vetted); |
|
|
|
|
377
|
|
|
|
378
|
|
|
if (!empty($allowedSecondFactors)) { |
379
|
|
|
$available = array_intersect( |
380
|
|
|
$available, |
381
|
|
|
$allowedSecondFactors |
382
|
|
|
); |
383
|
|
|
} |
384
|
|
|
|
385
|
|
|
$collection = new SecondFactorTypeCollection(); |
386
|
|
|
$collection->unverified = $unverified; |
387
|
|
|
$collection->verified = $verified; |
388
|
|
|
$collection->vetted = $vetted; |
389
|
|
|
$collection->available = array_combine($available, $available); |
390
|
|
|
$collection->maxNumberOfRegistrations = $maximumNumberOfRegistrations; |
391
|
|
|
|
392
|
|
|
return $collection; |
393
|
|
|
} |
394
|
|
|
} |
395
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.