Completed
Push — master ( 525d8b...576186 )
by
unknown
02:20
created

determineInstitutionsByIdentityNameId()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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