Completed
Push — feature/multiple-second-factor... ( 036b5b )
by
unknown
17:57
created

StepUpAuthenticationService::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 19
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 19
rs 9.4285
cc 1
eloc 17
nc 1
nop 8

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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
     * @param string           $requestedLoa
162
     * @param string           $identityNameId
163
     * @param ServiceProvider  $serviceProvider
164
     * @return null|Loa
165
     *
166
     * @SuppressWarnings(PHPMD.CyclomaticComplexity) see https://www.pivotaltracker.com/story/show/96065350
167
     * @SuppressWarnings(PHPMD.NPathComplexity)      see https://www.pivotaltracker.com/story/show/96065350
168
     */
169
    public function resolveHighestRequiredLoa(
170
        $requestedLoa,
171
        $identityNameId,
172
        ServiceProvider $serviceProvider
173
    ) {
174
        $loaCandidates = new ArrayCollection();
175
176
        if ($requestedLoa) {
177
            $loaCandidates->add($requestedLoa);
178
            $this->logger->info(sprintf('Added requested Loa "%s" as candidate', $requestedLoa));
179
        }
180
181
        $spConfiguredLoas = $serviceProvider->get('configuredLoas');
182
183
        if (!$loaCandidates->contains($spConfiguredLoas['__default__'])) {
184
            $loaCandidates->add($spConfiguredLoas['__default__']);
185
        }
186
187
        $this->logger->info(sprintf('Added SP\'s default Loa "%s" as candidate', $spConfiguredLoas['__default__']));
188
189
        $institutions = $this->determineInstitutionsByIdentityNameId($identityNameId);
190
        $this->logger->info(sprintf('Loaded institution(s) for "%s"', $identityNameId));
191
192
        $matchingInstitutions = $this->institutionMatchingHelper->findMatches(
193
            array_keys($spConfiguredLoas),
194
            $institutions
195
        );
196
197
        if (count($matchingInstitutions) > 0) {
198
            $this->logger->info('Found matching SP configured LoA\'s');
199
            foreach ($matchingInstitutions as $matchingInstitution) {
200
                $loaCandidates->add($spConfiguredLoas[$matchingInstitution]);
201
                $this->logger->info(sprintf(
202
                    'Added SP\'s Loa "%s" as candidate',
203
                    $spConfiguredLoas[$matchingInstitution]
204
                ));
205
            }
206
        }
207
208
        if (!count($loaCandidates)) {
209
            throw new RuntimeException('No Loa can be found, at least one Loa (SP default) should be found');
210
        }
211
212
        $actualLoas = new ArrayCollection();
213
        foreach ($loaCandidates as $loaDefinition) {
214
            $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...
215
            if ($loa) {
216
                $actualLoas->add($loa);
217
            }
218
        }
219
220
        if (!count($actualLoas)) {
221
            $this->logger->info(sprintf(
222
                'Out of "%d" candidates, no existing Loa could be found, no authentication is possible.',
223
                count($loaCandidates)
224
            ));
225
226
            return null;
227
        }
228
229
        /** @var \Surfnet\StepupBundle\Value\Loa $highestLoa */
230
        $highestLoa = $actualLoas->first();
231
        foreach ($actualLoas as $loa) {
232
            // if the current highest Loa cannot satisfy the next Loa, that must be of a higher level...
233
            if (!$highestLoa->canSatisfyLoa($loa)) {
234
                $highestLoa = $loa;
235
            }
236
        }
237
238
        $this->logger->info(
239
            sprintf('Out of %d candidate Loa\'s, Loa "%s" is the highest', count($loaCandidates), $highestLoa)
240
        );
241
242
        return $highestLoa;
243
    }
244
245
    /**
246
     * Returns whether the given Loa identifier identifies the minimum Loa, intrinsic to being authenticated via an IdP.
247
     *
248
     * @param Loa $loa
249
     * @return bool
250
     */
251
    public function isIntrinsicLoa(Loa $loa)
252
    {
253
        return $loa->levelIsLowerOrEqualTo(Loa::LOA_1);
254
    }
255
256
    /**
257
     * @param VerifyYubikeyOtpCommand $command
258
     * @return YubikeyOtpVerificationResult
259
     */
260
    public function verifyYubikeyOtp(VerifyYubikeyOtpCommand $command)
261
    {
262
        /** @var SecondFactor $secondFactor */
263
        $secondFactor = $this->secondFactorRepository->findOneBySecondFactorId($command->secondFactorId);
264
265
        $requester = new Requester();
266
        $requester->identity = $secondFactor->identityId;
267
        $requester->institution = $secondFactor->institution;
268
269
        $otp = new ApiOtp();
270
        $otp->value = $command->otp;
271
272
        $result = $this->yubikeyService->verify($otp, $requester);
273
274
        if (!$result->isSuccessful()) {
275
            return new YubikeyOtpVerificationResult(YubikeyOtpVerificationResult::RESULT_OTP_VERIFICATION_FAILED, null);
276
        }
277
278
        $otp = YubikeyOtp::fromString($command->otp);
279
        $publicId = YubikeyPublicId::fromOtp($otp);
280
281
        if (!$publicId->equals(new YubikeyPublicId($secondFactor->secondFactorIdentifier))) {
282
            return new YubikeyOtpVerificationResult(
283
                YubikeyOtpVerificationResult::RESULT_PUBLIC_ID_DID_NOT_MATCH,
284
                $publicId
285
            );
286
        }
287
288
        return new YubikeyOtpVerificationResult(YubikeyOtpVerificationResult::RESULT_PUBLIC_ID_MATCHED, $publicId);
289
    }
290
291
    /**
292
     * @param string $secondFactorId
293
     * @return string
294
     */
295
    public function getSecondFactorIdentifier($secondFactorId)
296
    {
297
        /** @var SecondFactor $secondFactor */
298
        $secondFactor = $this->secondFactorRepository->findOneBySecondFactorId($secondFactorId);
299
300
        return $secondFactor->secondFactorIdentifier;
301
    }
302
303
    /**
304
     * @return int
305
     */
306
    public function getSmsOtpRequestsRemainingCount()
307
    {
308
        return $this->smsService->getOtpRequestsRemainingCount();
309
    }
310
311
    /**
312
     * @return int
313
     */
314
    public function getSmsMaximumOtpRequestsCount()
315
    {
316
        return $this->smsService->getMaximumOtpRequestsCount();
317
    }
318
319
    /**
320
     * @param SendSmsChallengeCommand $command
321
     * @return bool
322
     */
323
    public function sendSmsChallenge(SendSmsChallengeCommand $command)
324
    {
325
        /** @var SecondFactor $secondFactor */
326
        $secondFactor = $this->secondFactorRepository->findOneBySecondFactorId($command->secondFactorId);
327
328
        $phoneNumber = InternationalPhoneNumber::fromStringFormat($secondFactor->secondFactorIdentifier);
329
330
        $stepupCommand = new StepupSendSmsChallengeCommand();
331
        $stepupCommand->phoneNumber = $phoneNumber;
332
        $stepupCommand->body = $this->translator->trans('gateway.second_factor.sms.challenge_body');
333
        $stepupCommand->identity = $secondFactor->identityId;
334
        $stepupCommand->institution = $secondFactor->institution;
335
336
        return $this->smsService->sendChallenge($stepupCommand);
337
    }
338
339
    /**
340
     * @param VerifyPossessionOfPhoneCommand $command
341
     * @return OtpVerification
342
     */
343
    public function verifySmsChallenge(VerifyPossessionOfPhoneCommand $command)
344
    {
345
        return $this->smsService->verifyPossession($command);
346
    }
347
348
    public function clearSmsVerificationState()
349
    {
350
        $this->smsService->clearSmsVerificationState();
351
    }
352
353
    private function determineInstitutionsByIdentityNameId($identityNameId)
354
    {
355
        return $this->secondFactorRepository->getAllInstitutions($identityNameId);
356
    }
357
}
358