Completed
Push — feature/single-token-test-butt... ( 1fda2e )
by
unknown
03:12
created

SecondFactorService::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 11
rs 9.4285
cc 1
eloc 9
nc 1
nop 4
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)
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...
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)
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...
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
0 ignored issues
show
Documentation introduced by
Should the return type not be UnverifiedSecondFactorCollection|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
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
0 ignored issues
show
Documentation introduced by
Should the return type not be VerifiedSecondFactorCollection|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
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
0 ignored issues
show
Documentation introduced by
Should the return type not be VettedSecondFactorCollection|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
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)
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...
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)
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...
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);
0 ignored issues
show
Bug introduced by
It seems like $unverified defined by $this->findUnverifiedByIdentity($identity->id) on line 372 can be null; however, Surfnet\StepupSelfServic...e::determineAvailable() 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...
Bug introduced by
It seems like $verified defined by $this->findVerifiedByIdentity($identity->id) on line 373 can be null; however, Surfnet\StepupSelfServic...e::determineAvailable() 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...
Bug introduced by
It seems like $vetted defined by $this->findVettedByIdentity($identity->id) on line 374 can be null; however, Surfnet\StepupSelfServic...e::determineAvailable() 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...
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