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

StepUpAuthenticationService   C

Complexity

Total Complexity 27

Size/Duplication

Total Lines 310
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 24

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 27
lcom 1
cbo 24
dl 0
loc 310
c 2
b 0
f 0
rs 5.238

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 19 1
B determineViableSecondFactors() 0 35 4
C resolveHighestRequiredLoa() 0 75 11
A isIntrinsicLoa() 0 4 1
B verifyYubikeyOtp() 0 30 3
A getSecondFactorIdentifier() 0 7 1
A getSmsOtpRequestsRemainingCount() 0 4 1
A getSmsMaximumOtpRequestsCount() 0 4 1
A sendSmsChallenge() 0 15 1
A verifySmsChallenge() 0 4 1
A clearSmsVerificationState() 0 4 1
A determineInstitutionsByIdentityNameId() 0 4 1
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