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

determineInstitutionsByIdentityNameId()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 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
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