Completed
Push — master ( 7e1619...49fbee )
by
unknown
02:24
created

VettingService::vet()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 23
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 23
rs 9.0856
cc 2
eloc 16
nc 2
nop 1
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\StepupRa\RaBundle\Service;
20
21
use RuntimeException;
22
use Surfnet\StepupBundle\Command\SendSmsChallengeCommand;
23
use Surfnet\StepupBundle\Command\VerifyPossessionOfPhoneCommand;
24
use Surfnet\StepupBundle\Service\SecondFactorTypeService;
25
use Surfnet\StepupBundle\Service\SmsSecondFactor\OtpVerification;
26
use Surfnet\StepupBundle\Service\SmsSecondFactorService;
27
use Surfnet\StepupBundle\Value\PhoneNumber\InternationalPhoneNumber;
28
use Surfnet\StepupBundle\Value\SecondFactorType;
29
use Surfnet\StepupMiddlewareClientBundle\Identity\Command\VetSecondFactorCommand;
30
use Surfnet\StepupRa\RaBundle\Command\CreateU2fSignRequestCommand;
31
use Surfnet\StepupRa\RaBundle\Command\StartVettingProcedureCommand;
32
use Surfnet\StepupRa\RaBundle\Command\VerifyIdentityCommand;
33
use Surfnet\StepupRa\RaBundle\Command\VerifyU2fAuthenticationCommand;
34
use Surfnet\StepupRa\RaBundle\Command\VerifyYubikeyPublicIdCommand;
35
use Surfnet\StepupRa\RaBundle\Exception\DomainException;
36
use Surfnet\StepupRa\RaBundle\Exception\InvalidArgumentException;
37
use Surfnet\StepupRa\RaBundle\Exception\LoaTooLowException;
38
use Surfnet\StepupRa\RaBundle\Exception\RegistrationCodeExpiredException;
39
use Surfnet\StepupRa\RaBundle\Exception\UnknownVettingProcedureException;
40
use Surfnet\StepupRa\RaBundle\Repository\VettingProcedureRepository;
41
use Surfnet\StepupRa\RaBundle\Service\Gssf\VerificationResult as GssfVerificationResult;
42
use Surfnet\StepupRa\RaBundle\Service\U2f\AuthenticationVerificationResult;
43
use Surfnet\StepupRa\RaBundle\Service\U2f\SignRequestCreationResult;
44
use Surfnet\StepupRa\RaBundle\Value\DateTime;
45
use Surfnet\StepupRa\RaBundle\VettingProcedure;
46
use Surfnet\StepupU2fBundle\Dto\SignRequest;
47
use Surfnet\StepupU2fBundle\Dto\SignResponse;
48
use Symfony\Component\Translation\TranslatorInterface;
49
50
/**
51
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
52
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
53
 */
54
class VettingService
55
{
56
    const REGISTRATION_CODE_EXPIRED_ERROR =
57
        'Surfnet\Stepup\Exception\DomainException: Cannot vet second factor, the registration window is closed.';
58
59
    /**
60
     * @var \Surfnet\StepupBundle\Service\SmsSecondFactorService
61
     */
62
    private $smsSecondFactorService;
63
64
    /**
65
     * @var \Surfnet\StepupRa\RaBundle\Service\YubikeySecondFactorService
66
     */
67
    private $yubikeySecondFactorService;
68
69
    /**
70
     * @var \Surfnet\StepupRa\RaBundle\Service\GssfService
71
     */
72
    private $gssfService;
73
74
    /**
75
     * @var \Surfnet\StepupRa\RaBundle\Service\U2fService
76
     */
77
    private $u2fService;
78
79
    /**
80
     * @var \Surfnet\StepupRa\RaBundle\Service\CommandService
81
     */
82
    private $commandService;
83
84
    /**
85
     * @var \Surfnet\StepupRa\RaBundle\Repository\VettingProcedureRepository
86
     */
87
    private $vettingProcedureRepository;
88
89
    /**
90
     * @var \Symfony\Component\Translation\TranslatorInterface
91
     */
92
    private $translator;
93
94
    /**
95
     * @var \Surfnet\StepupRa\RaBundle\Service\IdentityService
96
     */
97
    private $identityService;
98
99
    /**
100
     * @var \Surfnet\StepupBundle\Service\SecondFactorTypeService
101
     */
102
    private $secondFactorTypeService;
103
104
    public function __construct(
105
        SmsSecondFactorService $smsSecondFactorService,
106
        YubikeySecondFactorService $yubikeySecondFactorService,
107
        GssfService $gssfService,
108
        U2fService $u2fService,
109
        CommandService $commandService,
110
        VettingProcedureRepository $vettingProcedureRepository,
111
        TranslatorInterface $translator,
112
        IdentityService $identityService,
113
        SecondFactorTypeService $secondFactorTypeService
114
    ) {
115
        $this->smsSecondFactorService = $smsSecondFactorService;
116
        $this->yubikeySecondFactorService = $yubikeySecondFactorService;
117
        $this->gssfService = $gssfService;
118
        $this->u2fService = $u2fService;
119
        $this->commandService = $commandService;
120
        $this->vettingProcedureRepository = $vettingProcedureRepository;
121
        $this->translator = $translator;
122
        $this->identityService = $identityService;
123
        $this->secondFactorTypeService = $secondFactorTypeService;
124
    }
125
126
    /**
127
     * @param StartVettingProcedureCommand $command
128
     * @return bool
129
     */
130
    public function isLoaSufficientToStartProcedure(StartVettingProcedureCommand $command)
131
    {
132
        $secondFactorType = new SecondFactorType($command->secondFactor->type);
133
134
        return $this->secondFactorTypeService->isSatisfiedBy($secondFactorType, $command->authorityLoa);
135
    }
136
137
    /**
138
     * @param StartVettingProcedureCommand $command
139
     * @return bool
140
     */
141
    public function isExpiredRegistrationCode(StartVettingProcedureCommand $command)
142
    {
143
        return DateTime::now()->comesAfter(
144
            new DateTime(
145
                $command->secondFactor->registrationRequestedAt
146
                    ->add(new \DateInterval('P14D'))
147
                    ->setTime(23, 59, 59)
148
            )
149
        );
150
    }
151
152
    /**
153
     * @param StartVettingProcedureCommand $command
154
     * @return string The procedure ID.
155
     */
156
    public function startProcedure(StartVettingProcedureCommand $command)
157
    {
158
        $this->smsSecondFactorService->clearSmsVerificationState();
159
160
        if (!$this->isLoaSufficientToStartProcedure($command)) {
161
            throw new LoaTooLowException(
162
                sprintf(
163
                    "Registration authority has LoA '%s', which is not enough to allow vetting of a '%s' second factor",
164
                    $command->authorityLoa,
165
                    $command->secondFactor->type
166
                )
167
            );
168
        }
169
170
        $procedure = VettingProcedure::start(
171
            $command->secondFactor->id,
172
            $command->authorityId,
173
            $command->registrationCode,
174
            $command->secondFactor
175
        );
176
177
        $this->vettingProcedureRepository->store($procedure);
178
179
        return $procedure->getId();
180
    }
181
182
    /**
183
     * @param string $procedureId
184
     * @throws UnknownVettingProcedureException
185
     */
186 View Code Duplication
    public function cancelProcedure($procedureId)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
187
    {
188
        if (!is_string($procedureId)) {
189
            throw InvalidArgumentException::invalidType('string', 'procedureId', $procedureId);
190
        }
191
192
        $procedure = $this->vettingProcedureRepository->retrieve($procedureId);
193
194
        if (!$procedure) {
195
            throw new UnknownVettingProcedureException(
196
                sprintf("No vetting procedure with id '%s' is known.", $procedureId)
197
            );
198
        }
199
200
        $this->vettingProcedureRepository->remove($procedureId);
201
    }
202
203
    /**
204
     * @return int
205
     */
206
    public function getSmsOtpRequestsRemainingCount()
207
    {
208
        return $this->smsSecondFactorService->getOtpRequestsRemainingCount();
209
    }
210
211
    /**
212
     * @return int
213
     */
214
    public function getSmsMaximumOtpRequestsCount()
215
    {
216
        return $this->smsSecondFactorService->getMaximumOtpRequestsCount();
217
    }
218
219
    /**
220
     * @param string $procedureId
221
     * @param SendSmsChallengeCommand $command
222
     * @return bool
223
     * @throws UnknownVettingProcedureException
224
     * @throws RuntimeException
225
     */
226
    public function sendSmsChallenge($procedureId, SendSmsChallengeCommand $command)
227
    {
228
        $procedure = $this->getProcedure($procedureId);
229
230
        $phoneNumber = InternationalPhoneNumber::fromStringFormat(
231
            $procedure->getSecondFactor()->secondFactorIdentifier
232
        );
233
234
        $identity = $this->identityService->findById($procedure->getSecondFactor()->identityId);
235
236
        if (!$identity) {
237
            throw new RuntimeException("Second factor is coupled to an identity that doesn't exist");
238
        }
239
240
        $command->phoneNumber = $phoneNumber;
241
        $command->body        = $this->translator->trans('ra.vetting.sms.challenge_body', [], 'messages', $identity->preferredLocale);
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 134 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
242
        $command->identity    = $procedure->getSecondFactor()->identityId;
243
        $command->institution = $procedure->getSecondFactor()->institution;
244
245
        return $this->smsSecondFactorService->sendChallenge($command);
246
    }
247
248
    /**
249
     * @param string                   $procedureId
250
     * @param VerifyPossessionOfPhoneCommand $command
251
     * @return OtpVerification
252
     * @throws UnknownVettingProcedureException
253
     * @throws DomainException
254
     */
255
    public function verifyPhoneNumber($procedureId, VerifyPossessionOfPhoneCommand $command)
256
    {
257
        $procedure = $this->getProcedure($procedureId);
258
259
        $verification = $this->smsSecondFactorService->verifyPossession($command);
260
261
        if (!$verification->wasSuccessful()) {
262
            return $verification;
263
        }
264
265
        $procedure->verifySecondFactorIdentifier($verification->getPhoneNumber());
266
        $this->vettingProcedureRepository->store($procedure);
0 ignored issues
show
Bug introduced by
It seems like $procedure defined by $this->getProcedure($procedureId) on line 257 can be null; however, Surfnet\StepupRa\RaBundl...dureRepository::store() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
267
268
        return $verification;
269
    }
270
271
    /**
272
     * @param string                       $procedureId
273
     * @param VerifyYubikeyPublicIdCommand $command
274
     * @return YubikeySecondFactor\VerificationResult
275
     */
276
    public function verifyYubikeyPublicId($procedureId, VerifyYubikeyPublicIdCommand $command)
277
    {
278
        $procedure = $this->getProcedure($procedureId);
279
280
        $command->expectedPublicId = $procedure->getSecondFactor()->secondFactorIdentifier;
281
        $command->identityId = $procedure->getSecondFactor()->identityId;
282
        $command->institution = $procedure->getSecondFactor()->institution;
283
284
        $result = $this->yubikeySecondFactorService->verifyYubikeyPublicId($command);
285
286
        if ($result->didPublicIdMatch()) {
287
            $procedure->verifySecondFactorIdentifier($result->getPublicId()->getYubikeyPublicId());
288
289
            $this->vettingProcedureRepository->store($procedure);
0 ignored issues
show
Bug introduced by
It seems like $procedure defined by $this->getProcedure($procedureId) on line 278 can be null; however, Surfnet\StepupRa\RaBundl...dureRepository::store() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
290
        }
291
292
        return $result;
293
    }
294
295
    /**
296
     * @param string $procedureId
297
     */
298
    public function startGssfVerification($procedureId)
299
    {
300
        $procedure = $this->getProcedure($procedureId);
301
302
        $this->gssfService->startVerification($procedure->getSecondFactor()->secondFactorIdentifier, $procedureId);
303
    }
304
305
    /**
306
     * @param string $gssfId
307
     * @return GssfVerificationResult
308
     */
309
    public function verifyGssfId($gssfId)
310
    {
311
        $result = $this->gssfService->verify($gssfId);
312
313
        if (!$result->isSuccess()) {
314
            return $result;
315
        }
316
317
        $procedure = $this->getProcedure($result->getProcedureId());
318
        $procedure->verifySecondFactorIdentifier($gssfId);
319
320
        $this->vettingProcedureRepository->store($procedure);
0 ignored issues
show
Bug introduced by
It seems like $procedure defined by $this->getProcedure($result->getProcedureId()) on line 317 can be null; however, Surfnet\StepupRa\RaBundl...dureRepository::store() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
321
322
        return $result;
323
    }
324
325
    /**
326
     * @param string $procedureId
327
     * @return SignRequestCreationResult
328
     */
329
    public function createU2fSignRequest($procedureId)
330
    {
331
        $procedure = $this->getProcedure($procedureId);
332
333
        $command = new CreateU2fSignRequestCommand();
334
        $command->keyHandle = $procedure->getSecondFactor()->secondFactorIdentifier;
335
        $command->identityId = $procedure->getSecondFactor()->identityId;
336
        $command->institution = $procedure->getSecondFactor()->institution;
337
338
        return $this->u2fService->createSignRequest($command);
339
    }
340
341
    /**
342
     * @param string       $procedureId
343
     * @param SignRequest  $signRequest
344
     * @param SignResponse $signResponse
345
     * @return AuthenticationVerificationResult
346
     */
347
    public function verifyU2fAuthentication($procedureId, SignRequest $signRequest, SignResponse $signResponse)
348
    {
349
        $procedure = $this->getProcedure($procedureId);
350
351
        $command = new VerifyU2fAuthenticationCommand();
352
        $command->identityId = $procedure->getSecondFactor()->identityId;
353
        $command->institution = $procedure->getSecondFactor()->institution;
354
        $command->signRequest = $signRequest;
355
        $command->signResponse = $signResponse;
356
357
        $result = $this->u2fService->verifyAuthentication($command);
358
359
        if ($result->wasSuccessful()) {
360
            $procedure->verifySecondFactorIdentifier($signResponse->keyHandle);
361
            $this->vettingProcedureRepository->store($procedure);
0 ignored issues
show
Bug introduced by
It seems like $procedure defined by $this->getProcedure($procedureId) on line 349 can be null; however, Surfnet\StepupRa\RaBundl...dureRepository::store() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
362
        }
363
364
        return $result;
365
    }
366
367
    /**
368
     * @param string $procedureId
369
     * @param VerifyIdentityCommand $command
370
     * @return void
371
     * @throws UnknownVettingProcedureException
372
     * @throws DomainException
373
     */
374
    public function verifyIdentity($procedureId, VerifyIdentityCommand $command)
375
    {
376
        $procedure = $this->getProcedure($procedureId);
377
        $procedure->verifyIdentity($command->documentNumber, $command->identityVerified);
378
379
        $this->vettingProcedureRepository->store($procedure);
0 ignored issues
show
Bug introduced by
It seems like $procedure defined by $this->getProcedure($procedureId) on line 376 can be null; however, Surfnet\StepupRa\RaBundl...dureRepository::store() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
380
    }
381
382
    /**
383
     * @param string $procedureId
384
     * @return \Surfnet\StepupMiddlewareClient\Service\ExecutionResult
385
     * @throws UnknownVettingProcedureException
386
     * @throws DomainException
387
     */
388
    public function vet($procedureId)
389
    {
390
        $procedure = $this->getProcedure($procedureId);
391
        $procedure->vet();
392
393
        $command = new VetSecondFactorCommand();
394
        $command->authorityId = $procedure->getAuthorityId();
395
        $command->identityId = $procedure->getSecondFactor()->identityId;
396
        $command->secondFactorId = $procedure->getSecondFactor()->id;
397
        $command->registrationCode = $procedure->getRegistrationCode();
398
        $command->secondFactorType = $procedure->getSecondFactor()->type;
399
        $command->secondFactorIdentifier = $procedure->getInputSecondFactorIdentifier();
400
        $command->documentNumber = $procedure->getDocumentNumber();
401
        $command->identityVerified = $procedure->isIdentityVerified();
402
403
        $result = $this->commandService->execute($command);
404
405
        if ($result->isSuccessful()) {
406
            $this->vettingProcedureRepository->remove($procedureId);
407
        }
408
409
        return $result;
410
    }
411
412
    /**
413
     * @param string $procedureId
414
     * @return string
415
     * @throws UnknownVettingProcedureException
416
     */
417
    public function getIdentityCommonName($procedureId)
418
    {
419
        return $this->getProcedure($procedureId)->getSecondFactor()->commonName;
420
    }
421
422
    /**
423
     * @param $procedureId
424
     * @return string
425
     * @throws UnknownVettingProcedureException
426
     */
427
    public function getSecondFactorIdentifier($procedureId)
428
    {
429
        return $this->getProcedure($procedureId)->getSecondFactor()->secondFactorIdentifier;
430
    }
431
432
    /**
433
     * @param string $procedureId
434
     * @return null|VettingProcedure
435
     * @throws UnknownVettingProcedureException
436
     */
437 View Code Duplication
    private function getProcedure($procedureId)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
438
    {
439
        if (!is_string($procedureId)) {
440
            throw InvalidArgumentException::invalidType('string', 'procedureId', $procedureId);
441
        }
442
443
        $procedure = $this->vettingProcedureRepository->retrieve($procedureId);
444
445
        if (!$procedure) {
446
            throw new UnknownVettingProcedureException(
447
                sprintf("No vetting procedure with id '%s' is known.", $procedureId)
448
            );
449
        }
450
451
        return $procedure;
452
    }
453
454
    /**
455
     * @param string $procedureId
456
     * @return bool
457
     */
458
    public function hasProcedure($procedureId)
459
    {
460
        if (!is_string($procedureId)) {
461
            throw InvalidArgumentException::invalidType('string', 'procedureId', $procedureId);
462
        }
463
464
        return $this->vettingProcedureRepository->retrieve($procedureId) !== null;
465
    }
466
}
467