Completed
Push — feature/multiple-second-factor... ( c50fa2...2ed4f1 )
by
unknown
01:53
created

SecondFactorService::getSecondFactorsForIdentity()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 28
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 28
rs 8.8571
cc 2
eloc 20
nc 2
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\StepupMiddlewareClient\Identity\Dto\UnverifiedSecondFactorSearchQuery;
22
use Surfnet\StepupMiddlewareClient\Identity\Dto\VerifiedSecondFactorSearchQuery;
23
use Surfnet\StepupMiddlewareClient\Identity\Dto\VettedSecondFactorSearchQuery;
24
use Surfnet\StepupMiddlewareClientBundle\Dto\CollectionDto;
25
use Surfnet\StepupMiddlewareClientBundle\Identity\Command\RevokeOwnSecondFactorCommand;
26
use Surfnet\StepupMiddlewareClientBundle\Identity\Command\VerifyEmailCommand;
27
use Surfnet\StepupMiddlewareClientBundle\Identity\Dto\UnverifiedSecondFactor;
28
use Surfnet\StepupMiddlewareClientBundle\Identity\Dto\UnverifiedSecondFactorCollection;
29
use Surfnet\StepupMiddlewareClientBundle\Identity\Dto\VerifiedSecondFactor;
30
use Surfnet\StepupMiddlewareClientBundle\Identity\Dto\VerifiedSecondFactorCollection;
31
use Surfnet\StepupMiddlewareClientBundle\Identity\Dto\VettedSecondFactor;
32
use Surfnet\StepupMiddlewareClientBundle\Identity\Dto\VettedSecondFactorCollection;
33
use Surfnet\StepupMiddlewareClientBundle\Identity\Service\SecondFactorService as MiddlewareSecondFactorService;
34
use Surfnet\StepupSelfService\SelfServiceBundle\Command\RevokeCommand;
35
use Surfnet\StepupSelfService\SelfServiceBundle\Exception\LogicException;
36
37
/**
38
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
39
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
40
 */
41
class SecondFactorService
42
{
43
    /**
44
     * @var \Surfnet\StepupMiddlewareClientBundle\Identity\Service\SecondFactorService
45
     */
46
    private $secondFactors;
47
48
    /**
49
     * @var \Surfnet\StepupSelfService\SelfServiceBundle\Service\CommandService
50
     */
51
    private $commandService;
52
53
    /**
54
     * @var \Surfnet\StepupSelfService\SelfServiceBundle\Service\U2fSecondFactorService
55
     */
56
    private $u2fSecondFactorService;
57
58
    /**
59
     * @param MiddlewareSecondFactorService $secondFactors
60
     * @param CommandService                $commandService
61
     * @param U2fSecondFactorService        $u2fSecondFactorService
62
     */
63
    public function __construct(
64
        MiddlewareSecondFactorService $secondFactors,
65
        CommandService $commandService,
66
        U2fSecondFactorService $u2fSecondFactorService
67
    ) {
68
        $this->secondFactors = $secondFactors;
69
        $this->commandService = $commandService;
70
        $this->u2fSecondFactorService = $u2fSecondFactorService;
71
    }
72
73
    /**
74
     * @param string $identityId
75
     * @param string $nonce
76
     * @return bool
77
     */
78
    public function verifyEmail($identityId, $nonce)
79
    {
80
        $command                    = new VerifyEmailCommand();
81
        $command->identityId        = $identityId;
82
        $command->verificationNonce = $nonce;
83
84
        $result = $this->commandService->execute($command);
85
86
        return $result->isSuccessful();
87
    }
88
89
    /**
90
     * @param RevokeCommand $command
91
     * @return bool
92
     */
93
    public function revoke(RevokeCommand $command)
94
    {
95
        /** @var UnverifiedSecondFactor|VerifiedSecondFactor|VettedSecondFactor $secondFactor */
96
        $secondFactor = $command->secondFactor;
97
98
        $apiCommand = new RevokeOwnSecondFactorCommand();
99
        $apiCommand->identityId = $command->identity->id;
100
        $apiCommand->secondFactorId = $secondFactor->id;
101
102
        $result = $this->commandService->execute($apiCommand);
103
104
        if ($secondFactor->type === 'u2f') {
105
            $this->u2fSecondFactorService->revokeRegistration(
106
                $command->identity,
107
                $secondFactor->secondFactorIdentifier
108
            );
109
        }
110
111
        return $result->isSuccessful();
112
    }
113
114
    /**
115
     * Returns whether the given registrant has registered second factors with Step-up. The state of the second factor
116
     * is irrelevant.
117
     *
118
     * @param string $identityId
119
     * @return bool
120
     */
121
    public function doSecondFactorsExistForIdentity($identityId)
122
    {
123
        $unverifiedSecondFactors = $this->findUnverifiedByIdentity($identityId);
124
        $verifiedSecondFactors = $this->findVerifiedByIdentity($identityId);
125
        $vettedSecondFactors = $this->findVettedByIdentity($identityId);
126
127
        return $unverifiedSecondFactors->getTotalItems() +
128
               $verifiedSecondFactors->getTotalItems() +
129
               $vettedSecondFactors->getTotalItems() > 0;
130
    }
131
132
    public function identityHasSecondFactorOfStateWithId($identityId, $state, $secondFactorId)
133
    {
134
        switch ($state) {
135
            case 'unverified':
136
                $secondFactors = $this->findUnverifiedByIdentity($identityId);
137
                break;
138
            case 'verified':
139
                $secondFactors = $this->findVerifiedByIdentity($identityId);
140
                break;
141
            case 'vetted':
142
                $secondFactors = $this->findVettedByIdentity($identityId);
143
                break;
144
            default:
145
                throw new LogicException(sprintf('Invalid second factor state "%s" given.', $state));
146
        }
147
148
        if (count($secondFactors->getElements()) === 0) {
149
            return false;
150
        }
151
152
        foreach ($secondFactors->getElements() as $secondFactor) {
153
            if ($secondFactor->id === $secondFactorId) {
154
                return true;
155
            }
156
        }
157
158
        return false;
159
    }
160
161
    /**
162
     * Returns the given registrant's unverified second factors.
163
     *
164
     * @param string $identityId
165
     * @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...
166
     */
167
    public function findUnverifiedByIdentity($identityId)
168
    {
169
        return $this->secondFactors->searchUnverified(
170
            (new UnverifiedSecondFactorSearchQuery())->setIdentityId($identityId)
171
        );
172
    }
173
174
    /**
175
     * Returns the given registrant's verified second factors.
176
     *
177
     * @param string $identityId
178
     * @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...
179
     */
180
    public function findVerifiedByIdentity($identityId)
181
    {
182
        return $this->secondFactors->searchVerified(
183
            (new VerifiedSecondFactorSearchQuery())->setIdentityId($identityId)
184
        );
185
    }
186
187
    /**
188
     * Returns the given registrant's verified second factors.
189
     *
190
     * @param string $identityId
191
     * @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...
192
     */
193
    public function findVettedByIdentity($identityId)
194
    {
195
        return $this->secondFactors->searchVetted(
196
            (new VettedSecondFactorSearchQuery())->setIdentityId($identityId)
197
        );
198
    }
199
200
    /**
201
     * @param string $secondFactorId
202
     * @return null|UnverifiedSecondFactor
203
     */
204
    public function findOneUnverified($secondFactorId)
205
    {
206
        return $this->secondFactors->getUnverified($secondFactorId);
207
    }
208
209
    /**
210
     * @param string $secondFactorId
211
     * @return null|VerifiedSecondFactor
212
     */
213
    public function findOneVerified($secondFactorId)
214
    {
215
        return $this->secondFactors->getVerified($secondFactorId);
216
    }
217
218
    /**
219
     * @param string $secondFactorId
220
     * @return null|VettedSecondFactor
221
     */
222
    public function findOneVetted($secondFactorId)
223
    {
224
        return $this->secondFactors->getVetted($secondFactorId);
225
    }
226
227
    /**
228
     * @param string $identityId
229
     * @param string $verificationNonce
230
     * @return UnverifiedSecondFactor|null
231
     */
232 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...
233
    {
234
        $secondFactors = $this->secondFactors->searchUnverified(
235
            (new UnverifiedSecondFactorSearchQuery())
236
                ->setIdentityId($identityId)
237
                ->setVerificationNonce($verificationNonce)
238
        );
239
240
        $elements = $secondFactors->getElements();
241
242
        switch (count($elements)) {
243
            case 0:
244
                return null;
245
            case 1:
246
                return reset($elements);
247
            default:
248
                throw new LogicException('There cannot be more than one unverified second factor with the same nonce');
249
        }
250
    }
251
252
    /**
253
     * @param string $secondFactorId
254
     * @param string $identityId
255
     * @return null|string
256
     */
257 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...
258
    {
259
        $query = (new VerifiedSecondFactorSearchQuery())
260
            ->setIdentityId($identityId)
261
            ->setSecondFactorId($secondFactorId);
262
263
        /** @var VerifiedSecondFactor[] $verifiedSecondFactors */
264
        $verifiedSecondFactors = $this->secondFactors->searchVerified($query)->getElements();
265
266
        switch (count($verifiedSecondFactors)) {
267
            case 0:
268
                return null;
269
            case 1:
270
                return reset($verifiedSecondFactors)->registrationCode;
271
            default:
272
                throw new LogicException('Searching by second factor ID cannot result in multiple results.');
273
        }
274
    }
275
276
    /**
277
     * @param array $allSecondFactors
278
     * @param UnverifiedSecondFactorCollection $unverifiedCollection
279
     * @param VerifiedSecondFactorCollection $verifiedCollection
280
     * @param VettedSecondFactorCollection $vettedCollection
281
     * @return array
282
     */
283
    private function determineAvailable(
284
        array $allSecondFactors,
285
        UnverifiedSecondFactorCollection $unverifiedCollection,
286
        VerifiedSecondFactorCollection $verifiedCollection,
287
        VettedSecondFactorCollection $vettedCollection
288
    ) {
289
        $allSecondFactors = $this->filterAvailableSecondFactors($allSecondFactors, $unverifiedCollection);
290
        $allSecondFactors = $this->filterAvailableSecondFactors($allSecondFactors, $verifiedCollection);
291
        $allSecondFactors = $this->filterAvailableSecondFactors($allSecondFactors, $vettedCollection);
292
        return $allSecondFactors;
293
    }
294
295
    /**
296
     * @param array $allSecondFactors
297
     * @param CollectionDto $collection
298
     * @return array
299
     */
300
    private function filterAvailableSecondFactors(array $allSecondFactors, CollectionDto $collection)
301
    {
302
        foreach ($collection->getElements() as $secondFactor) {
303
            $keyFound = array_search($secondFactor->type, $allSecondFactors);
304
            if (is_numeric($keyFound)) {
305
                unset($allSecondFactors[$keyFound]);
306
            }
307
        }
308
        return $allSecondFactors;
309
    }
310
311
    /**
312
     * @param $identity
313
     * @param $allSecondFactors
314
     * @param $allowedSecondFactors
315
     * @param $maximumNumberOfRegistrations
316
     * @return SecondFactorTypeCollection
317
     */
318
    public function getSecondFactorsForIdentity(
319
        $identity,
320
        $allSecondFactors,
321
        $allowedSecondFactors,
322
        $maximumNumberOfRegistrations
323
    ) {
324
        $unverified = $this->findUnverifiedByIdentity($identity->id);
325
        $verified = $this->findVerifiedByIdentity($identity->id);
326
        $vetted = $this->findVettedByIdentity($identity->id);
327
        // Determine which Second Factors are still available for registration.
328
        $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 324 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 325 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 326 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...
329
330
        if (!empty($allowedSecondFactors)) {
331
            $available = array_intersect(
332
                $available,
333
                $allowedSecondFactors
334
            );
335
        }
336
337
        $collection = new SecondFactorTypeCollection();
338
        $collection->unverified = $unverified;
339
        $collection->verified   = $verified;
340
        $collection->vetted     = $vetted;
341
        $collection->available  = array_combine($available, $available);
342
        $collection->maxNumberOfRegistrations = $maximumNumberOfRegistrations;
343
344
        return $collection;
345
    }
346
}
347