Completed
Push — bugfix/consider-sp-specific-lo... ( a05dff )
by
unknown
16:53
created

StepUpAuthenticationService::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 19
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
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\IdentityProvider;
24
use Surfnet\SamlBundle\Entity\ServiceProvider;
25
use Surfnet\StepupBundle\Command\SendSmsChallengeCommand as StepupSendSmsChallengeCommand;
26
use Surfnet\StepupBundle\Command\VerifyPossessionOfPhoneCommand;
27
use Surfnet\StepupBundle\Service\LoaResolutionService;
28
use Surfnet\StepupBundle\Service\SecondFactorTypeService;
29
use Surfnet\StepupBundle\Service\SmsSecondFactor\OtpVerification;
30
use Surfnet\StepupBundle\Service\SmsSecondFactorService;
31
use Surfnet\StepupBundle\Value\Loa;
32
use Surfnet\StepupBundle\Value\PhoneNumber\InternationalPhoneNumber;
33
use Surfnet\StepupBundle\Value\YubikeyOtp;
34
use Surfnet\StepupBundle\Value\YubikeyPublicId;
35
use Surfnet\StepupGateway\ApiBundle\Dto\Otp as ApiOtp;
36
use Surfnet\StepupGateway\ApiBundle\Dto\Requester;
37
use Surfnet\StepupGateway\ApiBundle\Service\YubikeyService;
38
use Surfnet\StepupGateway\GatewayBundle\Command\SendSmsChallengeCommand;
39
use Surfnet\StepupGateway\GatewayBundle\Command\VerifyYubikeyOtpCommand;
40
use Surfnet\StepupGateway\GatewayBundle\Entity\SecondFactor;
41
use Surfnet\StepupGateway\GatewayBundle\Entity\SecondFactorRepository;
42
use Surfnet\StepupGateway\GatewayBundle\Exception\RuntimeException;
43
use Surfnet\StepupGateway\GatewayBundle\Service\StepUp\YubikeyOtpVerificationResult;
44
use Symfony\Component\Translation\TranslatorInterface;
45
46
/**
47
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
48
 */
49
class StepUpAuthenticationService
50
{
51
    /**
52
     * @var \Surfnet\StepupBundle\Service\LoaResolutionService
53
     */
54
    private $loaResolutionService;
55
56
    /**
57
     * @var \Surfnet\StepupGateway\GatewayBundle\Entity\SecondFactorRepository
58
     */
59
    private $secondFactorRepository;
60
61
    /**
62
     * @var \Surfnet\StepupGateway\ApiBundle\Service\YubikeyService
63
     */
64
    private $yubikeyService;
65
66
    /**
67
     * @var \Surfnet\StepupBundle\Service\SmsSecondFactorService
68
     */
69
    private $smsService;
70
71
    /** @var InstitutionMatchingHelper */
72
    private $institutionMatchingHelper;
73
74
    /**
75
     * @var \Symfony\Component\Translation\TranslatorInterface
76
     */
77
    private $translator;
78
79
    /**
80
     * @var \Psr\Log\LoggerInterface
81
     */
82
    private $logger;
83
84
    /**
85
     * @var SecondFactorTypeService
86
     */
87
    private $secondFactorTypeService;
88
89
    /**
90
     * @param LoaResolutionService   $loaResolutionService
91
     * @param SecondFactorRepository $secondFactorRepository
92
     * @param YubikeyService         $yubikeyService
93
     * @param SmsSecondFactorService $smsService
94
     * @param InstitutionMatchingHelper $institutionMatchingHelper
95
     * @param TranslatorInterface    $translator
96
     * @param LoggerInterface        $logger
97
     * @param SecondFactorTypeService $secondFactorTypeService
98
     */
99
    public function __construct(
100
        LoaResolutionService $loaResolutionService,
101
        SecondFactorRepository $secondFactorRepository,
102
        YubikeyService $yubikeyService,
103
        SmsSecondFactorService $smsService,
104
        InstitutionMatchingHelper $institutionMatchingHelper,
105
        TranslatorInterface $translator,
106
        LoggerInterface $logger,
107
        SecondFactorTypeService $secondFactorTypeService
108
    ) {
109
        $this->loaResolutionService = $loaResolutionService;
110
        $this->secondFactorRepository = $secondFactorRepository;
111
        $this->yubikeyService = $yubikeyService;
112
        $this->smsService = $smsService;
113
        $this->institutionMatchingHelper = $institutionMatchingHelper;
114
        $this->translator = $translator;
115
        $this->logger = $logger;
116
        $this->secondFactorTypeService = $secondFactorTypeService;
117
    }
118
119
    /**
120
     * @param string          $identityNameId
121
     * @param Loa             $requiredLoa
122
     * @return \Doctrine\Common\Collections\ArrayCollection
123
     */
124
    public function determineViableSecondFactors(
125
        $identityNameId,
126
        Loa $requiredLoa
127
    ) {
128
129
        $candidateSecondFactors = $this->secondFactorRepository->getAllMatchingFor(
130
            $requiredLoa,
131
            $identityNameId,
132
            $this->secondFactorTypeService
133
        );
134
        $this->logger->info(
135
            sprintf('Loaded %d matching candidate second factors', count($candidateSecondFactors))
136
        );
137
138
        return $candidateSecondFactors;
139
    }
140
141
    /**
142
     * @param string           $requestedLoa
143
     * @param string           $identityNameId
144
     * @param ServiceProvider  $serviceProvider
145
     * @param IdentityProvider $authenticatingIdp
0 ignored issues
show
Documentation introduced by
Should the type for parameter $authenticatingIdp not be null|IdentityProvider?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

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