Completed
Push — bugfix/consider-sp-specific-lo... ( a05dff...32587a )
by
unknown
02:10
created

StepUpAuthenticationService   C

Complexity

Total Complexity 23

Size/Duplication

Total Lines 288
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 22

Importance

Changes 0
Metric Value
wmc 23
lcom 1
cbo 22
dl 0
loc 288
rs 5.7894
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 19 1
A determineViableSecondFactors() 0 16 1
C resolveHighestRequiredLoa() 0 72 10
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\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
0 ignored issues
show
Unused Code introduced by
The parameter $authenticatingIdp is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
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 (!count($loaCandidates)) {
188
            throw new RuntimeException('No Loa can be found, at least one Loa (SP default) should be found');
189
        }
190
191
        $actualLoas = new ArrayCollection();
192
        foreach ($loaCandidates as $loaDefinition) {
193
            $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...
194
            if ($loa) {
195
                $actualLoas->add($loa);
196
            }
197
        }
198
199
        if (!count($actualLoas)) {
200
            $this->logger->info(sprintf(
201
                'Out of "%d" candidates, no existing Loa could be found, no authentication is possible.',
202
                count($loaCandidates)
203
            ));
204
205
            return null;
206
        }
207
208
        /** @var \Surfnet\StepupBundle\Value\Loa $highestLoa */
209
        $highestLoa = $actualLoas->first();
210
        foreach ($actualLoas as $loa) {
211
            // if the current highest Loa cannot satisfy the next Loa, that must be of a higher level...
212
            if (!$highestLoa->canSatisfyLoa($loa)) {
213
                $highestLoa = $loa;
214
            }
215
        }
216
217
        $this->logger->info(
218
            sprintf('Out of %d candidate Loa\'s, Loa "%s" is the highest', count($loaCandidates), $highestLoa)
219
        );
220
221
        return $highestLoa;
222
    }
223
224
    /**
225
     * Returns whether the given Loa identifier identifies the minimum Loa, intrinsic to being authenticated via an IdP.
226
     *
227
     * @param Loa $loa
228
     * @return bool
229
     */
230
    public function isIntrinsicLoa(Loa $loa)
231
    {
232
        return $loa->levelIsLowerOrEqualTo(Loa::LOA_1);
233
    }
234
235
    /**
236
     * @param VerifyYubikeyOtpCommand $command
237
     * @return YubikeyOtpVerificationResult
238
     */
239
    public function verifyYubikeyOtp(VerifyYubikeyOtpCommand $command)
240
    {
241
        /** @var SecondFactor $secondFactor */
242
        $secondFactor = $this->secondFactorRepository->findOneBySecondFactorId($command->secondFactorId);
243
244
        $requester = new Requester();
245
        $requester->identity = $secondFactor->identityId;
246
        $requester->institution = $secondFactor->institution;
247
248
        $otp = new ApiOtp();
249
        $otp->value = $command->otp;
250
251
        $result = $this->yubikeyService->verify($otp, $requester);
252
253
        if (!$result->isSuccessful()) {
254
            return new YubikeyOtpVerificationResult(YubikeyOtpVerificationResult::RESULT_OTP_VERIFICATION_FAILED, null);
255
        }
256
257
        $otp = YubikeyOtp::fromString($command->otp);
258
        $publicId = YubikeyPublicId::fromOtp($otp);
259
260
        if (!$publicId->equals(new YubikeyPublicId($secondFactor->secondFactorIdentifier))) {
261
            return new YubikeyOtpVerificationResult(
262
                YubikeyOtpVerificationResult::RESULT_PUBLIC_ID_DID_NOT_MATCH,
263
                $publicId
264
            );
265
        }
266
267
        return new YubikeyOtpVerificationResult(YubikeyOtpVerificationResult::RESULT_PUBLIC_ID_MATCHED, $publicId);
268
    }
269
270
    /**
271
     * @param string $secondFactorId
272
     * @return string
273
     */
274
    public function getSecondFactorIdentifier($secondFactorId)
275
    {
276
        /** @var SecondFactor $secondFactor */
277
        $secondFactor = $this->secondFactorRepository->findOneBySecondFactorId($secondFactorId);
278
279
        return $secondFactor->secondFactorIdentifier;
280
    }
281
282
    /**
283
     * @return int
284
     */
285
    public function getSmsOtpRequestsRemainingCount()
286
    {
287
        return $this->smsService->getOtpRequestsRemainingCount();
288
    }
289
290
    /**
291
     * @return int
292
     */
293
    public function getSmsMaximumOtpRequestsCount()
294
    {
295
        return $this->smsService->getMaximumOtpRequestsCount();
296
    }
297
298
    /**
299
     * @param SendSmsChallengeCommand $command
300
     * @return bool
301
     */
302
    public function sendSmsChallenge(SendSmsChallengeCommand $command)
303
    {
304
        /** @var SecondFactor $secondFactor */
305
        $secondFactor = $this->secondFactorRepository->findOneBySecondFactorId($command->secondFactorId);
306
307
        $phoneNumber = InternationalPhoneNumber::fromStringFormat($secondFactor->secondFactorIdentifier);
308
309
        $stepupCommand = new StepupSendSmsChallengeCommand();
310
        $stepupCommand->phoneNumber = $phoneNumber;
311
        $stepupCommand->body = $this->translator->trans('gateway.second_factor.sms.challenge_body');
312
        $stepupCommand->identity = $secondFactor->identityId;
313
        $stepupCommand->institution = $secondFactor->institution;
314
315
        return $this->smsService->sendChallenge($stepupCommand);
316
    }
317
318
    /**
319
     * @param VerifyPossessionOfPhoneCommand $command
320
     * @return OtpVerification
321
     */
322
    public function verifySmsChallenge(VerifyPossessionOfPhoneCommand $command)
323
    {
324
        return $this->smsService->verifyPossession($command);
325
    }
326
327
    public function clearSmsVerificationState()
328
    {
329
        $this->smsService->clearSmsVerificationState();
330
    }
331
332
    private function determineInstitutionsByIdentityNameId($identityNameId)
333
    {
334
        return $this->secondFactorRepository->getAllInstitutions($identityNameId);
335
    }
336
}
337