Completed
Push — develop ( 004627...2e1a86 )
by
unknown
16s
created

resolveHighestRequiredLoa()   D

Complexity

Conditions 15
Paths 212

Size

Total Lines 96
Code Lines 54

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 96
rs 4.3892
cc 15
eloc 54
nc 212
nop 4

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\StepupGateway\GatewayBundle\Service;
20
21
use Doctrine\Common\Collections\ArrayCollection;
22
use Psr\Log\LoggerInterface;
23
use Surfnet\SamlBundle\Entity\ServiceProvider;
24
use Surfnet\StepupBundle\Command\SendSmsChallengeCommand as StepupSendSmsChallengeCommand;
25
use Surfnet\StepupBundle\Command\VerifyPossessionOfPhoneCommand;
26
use Surfnet\StepupBundle\Service\LoaResolutionService;
27
use Surfnet\StepupBundle\Service\SecondFactorTypeService;
28
use Surfnet\StepupBundle\Service\SmsSecondFactor\OtpVerification;
29
use Surfnet\StepupBundle\Service\SmsSecondFactorService;
30
use Surfnet\StepupBundle\Value\Loa;
31
use Surfnet\StepupBundle\Value\PhoneNumber\InternationalPhoneNumber;
32
use Surfnet\StepupBundle\Value\YubikeyOtp;
33
use Surfnet\StepupBundle\Value\YubikeyPublicId;
34
use Surfnet\StepupGateway\ApiBundle\Dto\Otp as ApiOtp;
35
use Surfnet\StepupGateway\ApiBundle\Dto\Requester;
36
use Surfnet\StepupGateway\ApiBundle\Service\YubikeyService;
37
use Surfnet\StepupGateway\GatewayBundle\Command\SendSmsChallengeCommand;
38
use Surfnet\StepupGateway\GatewayBundle\Command\VerifyYubikeyOtpCommand;
39
use Surfnet\StepupGateway\GatewayBundle\Entity\SecondFactor;
40
use Surfnet\StepupGateway\GatewayBundle\Entity\SecondFactorRepository;
41
use Surfnet\StepupGateway\GatewayBundle\Exception\RuntimeException;
42
use Surfnet\StepupGateway\GatewayBundle\Service\StepUp\YubikeyOtpVerificationResult;
43
use Symfony\Component\Translation\TranslatorInterface;
44
45
/**
46
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
47
 */
48
class StepUpAuthenticationService
49
{
50
    /**
51
     * @var \Surfnet\StepupBundle\Service\LoaResolutionService
52
     */
53
    private $loaResolutionService;
54
55
    /**
56
     * @var \Surfnet\StepupGateway\GatewayBundle\Entity\SecondFactorRepository
57
     */
58
    private $secondFactorRepository;
59
60
    /**
61
     * @var \Surfnet\StepupGateway\ApiBundle\Service\YubikeyService
62
     */
63
    private $yubikeyService;
64
65
    /**
66
     * @var \Surfnet\StepupBundle\Service\SmsSecondFactorService
67
     */
68
    private $smsService;
69
70
    /** @var InstitutionMatchingHelper */
71
    private $institutionMatchingHelper;
72
73
    /**
74
     * @var \Symfony\Component\Translation\TranslatorInterface
75
     */
76
    private $translator;
77
78
    /**
79
     * @var \Psr\Log\LoggerInterface
80
     */
81
    private $logger;
82
83
    /**
84
     * @var SecondFactorTypeService
85
     */
86
    private $secondFactorTypeService;
87
88
    /**
89
     * @param LoaResolutionService   $loaResolutionService
90
     * @param SecondFactorRepository $secondFactorRepository
91
     * @param YubikeyService         $yubikeyService
92
     * @param SmsSecondFactorService $smsService
93
     * @param InstitutionMatchingHelper $institutionMatchingHelper
94
     * @param TranslatorInterface    $translator
95
     * @param LoggerInterface        $logger
96
     * @param SecondFactorTypeService $secondFactorTypeService
97
     */
98
    public function __construct(
99
        LoaResolutionService $loaResolutionService,
100
        SecondFactorRepository $secondFactorRepository,
101
        YubikeyService $yubikeyService,
102
        SmsSecondFactorService $smsService,
103
        InstitutionMatchingHelper $institutionMatchingHelper,
104
        TranslatorInterface $translator,
105
        LoggerInterface $logger,
106
        SecondFactorTypeService $secondFactorTypeService
107
    ) {
108
        $this->loaResolutionService = $loaResolutionService;
109
        $this->secondFactorRepository = $secondFactorRepository;
110
        $this->yubikeyService = $yubikeyService;
111
        $this->smsService = $smsService;
112
        $this->institutionMatchingHelper = $institutionMatchingHelper;
113
        $this->translator = $translator;
114
        $this->logger = $logger;
115
        $this->secondFactorTypeService = $secondFactorTypeService;
116
    }
117
118
    /**
119
     * @param string $identityNameId
120
     * @param Loa $requiredLoa
121
     * @param WhitelistService $whitelistService
122
     * @return \Doctrine\Common\Collections\Collection
123
     */
124
    public function determineViableSecondFactors(
125
        $identityNameId,
126
        Loa $requiredLoa,
127
        WhitelistService $whitelistService
128
    ) {
129
130
        $candidateSecondFactors = $this->secondFactorRepository->getAllMatchingFor(
131
            $requiredLoa,
132
            $identityNameId,
133
            $this->secondFactorTypeService
134
        );
135
        $this->logger->info(
136
            sprintf('Loaded %d matching candidate second factors', count($candidateSecondFactors))
137
        );
138
139
        foreach ($candidateSecondFactors as $key => $secondFactor) {
140
            if (!$whitelistService->contains($secondFactor->institution)) {
141
                $this->logger->notice(
142
                    sprintf(
143
                        'Second factor "%s" is listed for institution "%s" which is not on the whitelist',
144
                        $secondFactor->secondFactorId,
145
                        $secondFactor->institution
146
                    )
147
                );
148
149
                $candidateSecondFactors->remove($key);
150
            }
151
        }
152
153
        if ($candidateSecondFactors->isEmpty()) {
154
            $this->logger->alert('No suitable candidate second factors found, sending Loa cannot be given response');
155
        }
156
157
        return $candidateSecondFactors;
158
    }
159
160
    /**
161
     * Retrieves the required LoA for the authenticating user
162
     *
163
     * The required LoA is based on several variables. These are:
164
     *
165
     *  1. SP Requested LoA.
166
     *  2. The optional SP/institution specific LoA configuration
167
     *  3. The identity of the authenticating user (used to test if the user can provide a token for the institution
168
     *     he is authenticating for). Only used when the SP/institution specific LoA configuration is in play.
169
     *  4. The institution of the authenticating user, based on the schacHomeOrganization of the user. This is used
170
     *     to validate the registered tokens are actually vetted by the correct institution. Only used when the
171
     *     SP/institution specific LoA configuration is in play.
172
     *
173
     * These four variables determine the required LoA for the authenticating user. The possible outcomes are covered
174
     * by unit tests. These tests can be found in the Test folder of this bundle.
175
     *
176
     * @see: StepUpAuthenticationServiceTest::test_resolve_highest_required_loa_conbinations
177
     *
178
     * @param string $requestedLoa The SP requested LoA
179
     * @param $identityNameId
180
     * @param string $identityInstitution
181
     * @param ServiceProvider $serviceProvider
182
     * @return null|Loa
183
     * @SuppressWarnings(PHPMD.CyclomaticComplexity) see https://www.pivotaltracker.com/story/show/96065350
184
     * @SuppressWarnings(PHPMD.NPathComplexity)      see https://www.pivotaltracker.com/story/show/96065350
185
     */
186
    public function resolveHighestRequiredLoa(
187
        $requestedLoa,
188
        $identityNameId,
189
        $identityInstitution,
190
        ServiceProvider $serviceProvider
191
    ) {
192
        $loaCandidates = new ArrayCollection();
193
194
        if ($requestedLoa) {
195
            $loaCandidates->add($requestedLoa);
196
            $this->logger->info(sprintf('Added requested Loa "%s" as candidate', $requestedLoa));
197
        }
198
199
        // Load the SP/institution specific LoA configuration
200
        $spConfiguredLoas = $serviceProvider->get('configuredLoas');
201
202
        if (array_key_exists('__default__', $spConfiguredLoas) &&
203
            !$loaCandidates->contains($spConfiguredLoas['__default__'])
204
        ) {
205
            $loaCandidates->add($spConfiguredLoas['__default__']);
206
            $this->logger->info(sprintf('Added SP\'s default Loa "%s" as candidate', $spConfiguredLoas['__default__']));
207
        }
208
209
        if (count($spConfiguredLoas) > 1 && is_null($identityInstitution)) {
210
            throw new RuntimeException(
211
                'SP configured LOA\'s are applicable but the authenticating user has no ' .
212
                'schacHomeOrganization in the assertion.'
213
            );
214
        }
215
216
        // Load the authenticating users institutions based on its vetted tokens.
217
        $institutionsBasedOnVettedTokens = [];
0 ignored issues
show
Comprehensibility Naming introduced by
The variable name $institutionsBasedOnVettedTokens exceeds the maximum configured length of 30.

Very long variable names usually make code harder to read. It is therefore recommended not to make variable names too verbose.

Loading history...
218
        // But only do so if there are SP/institution specific LoA restrictions
219
        if (!$this->hasDefaultSpConfig($spConfiguredLoas)) {
220
            $institutionsBasedOnVettedTokens = $this->determineInstitutionsByIdentityNameId(
221
                $identityNameId,
222
                $identityInstitution,
223
                $spConfiguredLoas
224
            );
225
        }
226
227
        $this->logger->info(sprintf('Loaded institution(s) for "%s"', $identityNameId));
228
229
        // Match the users institutions LoA's against the SP configured institutions
230
        $matchingInstitutions = $this->institutionMatchingHelper->findMatches(
231
            array_keys($spConfiguredLoas),
232
            $institutionsBasedOnVettedTokens
233
        );
234
235
        if (count($matchingInstitutions) > 0) {
236
            $this->logger->info('Found matching SP configured LoA\'s');
237
            foreach ($matchingInstitutions as $matchingInstitution) {
238
                $loaCandidates->add($spConfiguredLoas[$matchingInstitution]);
239
                $this->logger->info(sprintf(
240
                    'Added SP\'s Loa "%s" as candidate',
241
                    $spConfiguredLoas[$matchingInstitution]
242
                ));
243
            }
244
        }
245
246
        if (!count($loaCandidates)) {
247
            throw new RuntimeException('No Loa can be found, at least one Loa (SP default) should be found');
248
        }
249
250
        $actualLoas = new ArrayCollection();
251
        foreach ($loaCandidates as $loaDefinition) {
252
            $loa = $this->loaResolutionService->getLoa($loaDefinition);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $loa is correct as $this->loaResolutionServ...>getLoa($loaDefinition) (which targets Surfnet\StepupBundle\Ser...lutionService::getLoa()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
253
            if ($loa) {
254
                $actualLoas->add($loa);
255
            }
256
        }
257
258
        if (!count($actualLoas)) {
259
            $this->logger->info(sprintf(
260
                'Out of "%d" candidates, no existing Loa could be found, no authentication is possible.',
261
                count($loaCandidates)
262
            ));
263
264
            return null;
265
        }
266
267
        /** @var \Surfnet\StepupBundle\Value\Loa $highestLoa */
268
        $highestLoa = $actualLoas->first();
269
        foreach ($actualLoas as $loa) {
270
            // if the current highest Loa cannot satisfy the next Loa, that must be of a higher level...
271
            if (!$highestLoa->canSatisfyLoa($loa)) {
272
                $highestLoa = $loa;
273
            }
274
        }
275
276
        $this->logger->info(
277
            sprintf('Out of %d candidate Loa\'s, Loa "%s" is the highest', count($loaCandidates), $highestLoa)
278
        );
279
280
        return $highestLoa;
281
    }
282
283
    /**
284
     * Returns whether the given Loa identifier identifies the minimum Loa, intrinsic to being authenticated via an IdP.
285
     *
286
     * @param Loa $loa
287
     * @return bool
288
     */
289
    public function isIntrinsicLoa(Loa $loa)
290
    {
291
        return $loa->levelIsLowerOrEqualTo(Loa::LOA_1);
292
    }
293
294
    /**
295
     * @param VerifyYubikeyOtpCommand $command
296
     * @return YubikeyOtpVerificationResult
297
     */
298
    public function verifyYubikeyOtp(VerifyYubikeyOtpCommand $command)
299
    {
300
        /** @var SecondFactor $secondFactor */
301
        $secondFactor = $this->secondFactorRepository->findOneBySecondFactorId($command->secondFactorId);
302
303
        $requester = new Requester();
304
        $requester->identity = $secondFactor->identityId;
305
        $requester->institution = $secondFactor->institution;
306
307
        $otp = new ApiOtp();
308
        $otp->value = $command->otp;
309
310
        $result = $this->yubikeyService->verify($otp, $requester);
311
312
        if (!$result->isSuccessful()) {
313
            return new YubikeyOtpVerificationResult(YubikeyOtpVerificationResult::RESULT_OTP_VERIFICATION_FAILED, null);
314
        }
315
316
        $otp = YubikeyOtp::fromString($command->otp);
317
        $publicId = YubikeyPublicId::fromOtp($otp);
318
319
        if (!$publicId->equals(new YubikeyPublicId($secondFactor->secondFactorIdentifier))) {
320
            return new YubikeyOtpVerificationResult(
321
                YubikeyOtpVerificationResult::RESULT_PUBLIC_ID_DID_NOT_MATCH,
322
                $publicId
323
            );
324
        }
325
326
        return new YubikeyOtpVerificationResult(YubikeyOtpVerificationResult::RESULT_PUBLIC_ID_MATCHED, $publicId);
327
    }
328
329
    /**
330
     * @param string $secondFactorId
331
     * @return string
332
     */
333
    public function getSecondFactorIdentifier($secondFactorId)
334
    {
335
        /** @var SecondFactor $secondFactor */
336
        $secondFactor = $this->secondFactorRepository->findOneBySecondFactorId($secondFactorId);
337
338
        return $secondFactor->secondFactorIdentifier;
339
    }
340
341
    /**
342
     * @return int
343
     */
344
    public function getSmsOtpRequestsRemainingCount()
345
    {
346
        return $this->smsService->getOtpRequestsRemainingCount();
347
    }
348
349
    /**
350
     * @return int
351
     */
352
    public function getSmsMaximumOtpRequestsCount()
353
    {
354
        return $this->smsService->getMaximumOtpRequestsCount();
355
    }
356
357
    /**
358
     * @param SendSmsChallengeCommand $command
359
     * @return bool
360
     */
361
    public function sendSmsChallenge(SendSmsChallengeCommand $command)
362
    {
363
        /** @var SecondFactor $secondFactor */
364
        $secondFactor = $this->secondFactorRepository->findOneBySecondFactorId($command->secondFactorId);
365
366
        $phoneNumber = InternationalPhoneNumber::fromStringFormat($secondFactor->secondFactorIdentifier);
367
368
        $stepupCommand = new StepupSendSmsChallengeCommand();
369
        $stepupCommand->phoneNumber = $phoneNumber;
370
        $stepupCommand->body = $this->translator->trans('gateway.second_factor.sms.challenge_body');
371
        $stepupCommand->identity = $secondFactor->identityId;
372
        $stepupCommand->institution = $secondFactor->institution;
373
374
        return $this->smsService->sendChallenge($stepupCommand);
375
    }
376
377
    /**
378
     * @param VerifyPossessionOfPhoneCommand $command
379
     * @return OtpVerification
380
     */
381
    public function verifySmsChallenge(VerifyPossessionOfPhoneCommand $command)
382
    {
383
        return $this->smsService->verifyPossession($command);
384
    }
385
386
    public function clearSmsVerificationState()
387
    {
388
        $this->smsService->clearSmsVerificationState();
389
    }
390
391
    /**
392
     * Tests if the authenticating user has any vetted tokens for the institution he is authenticating for.
393
     *
394
     * The user needs to have a SHO and one or more vetted tokens for this method to return any institutions.
395
     *
396
     * @param string $identityNameId Used to load vetted tokens
397
     * @param string $identityInstitution Used to match against the institutions of vetted tokens
398
     * @param array $spConfiguredLoas Used for validation (are users tokens applicable for any of the configured
399
     * SP/institution configured institutions?)
400
     * @return array
401
     */
402
    private function determineInstitutionsByIdentityNameId($identityNameId, $identityInstitution, $spConfiguredLoas)
403
    {
404
        // Load the institutions based on the nameId of the authenticating user. This information is extracted from
405
        // the second factors projection in the Gateway. So the institutions are based on the vetted tokens of the user.
406
        $institutions = $this->secondFactorRepository->getAllInstitutions($identityNameId);
407
408
        // Validations are performed on the institutions
409
        if (empty($institutions) && array_key_exists($identityInstitution, $spConfiguredLoas)) {
410
            throw new RuntimeException(
411
                'The authenticating user cannot provide a token for the institution it is authenticating for.'
412
            );
413
        }
414
415
        if (empty($institutions)) {
416
            throw new RuntimeException(
417
                'The authenticating user does not have any vetted tokens.'
418
            );
419
        }
420
421
        // The user has vetted tokens and it's SHO was loaded from the assertion
422
        if (!is_null($identityInstitution) && !empty($institutions)) {
423
424
            $institutionMatches = $this->institutionMatchingHelper->findMatches(
425
                $institutions,
426
                [$identityInstitution]
427
            );
428
429
            if (empty($institutionMatches)) {
430
                throw new RuntimeException(
431
                    'None of the authenticating users tokens are registered at an institution the user is currently ' .
432
                    'authenticating from.'
433
                );
434
            }
435
            // Add the SHO of the authenticating user to the list of institutions based on vetted tokens. This ensures
436
            // the correct LOA can be based on the organisation of the user.
437
            $institutions[] = $identityInstitution;
438
        }
439
440
441
        return $institutions;
442
    }
443
444
    private function hasDefaultSpConfig($spConfiguredLoas)
445
    {
446
        if (array_key_exists('__default__', $spConfiguredLoas) && count($spConfiguredLoas) === 1) {
0 ignored issues
show
Unused Code introduced by
This if statement, and the following return statement can be replaced with return array_key_exists(...pConfiguredLoas) === 1;.
Loading history...
447
            return true;
448
        }
449
        return false;
450
    }
451
}
452