Completed
Pull Request — master (#102)
by Boy
18:16
created

StepUpAuthenticationService::sendSmsChallenge()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 15
rs 9.4285
cc 1
eloc 9
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\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
    /**
71
     * @var \Symfony\Component\Translation\TranslatorInterface
72
     */
73
    private $translator;
74
75
    /**
76
     * @var \Psr\Log\LoggerInterface
77
     */
78
    private $logger;
79
80
    /**
81
     * @param LoaResolutionService   $loaResolutionService
82
     * @param SecondFactorRepository $secondFactorRepository
83
     * @param YubikeyService         $yubikeyService
84
     * @param SmsSecondFactorService $smsService
85
     * @param TranslatorInterface    $translator
86
     * @param LoggerInterface        $logger
87
     */
88
    public function __construct(
89
        LoaResolutionService $loaResolutionService,
90
        SecondFactorRepository $secondFactorRepository,
91
        YubikeyService $yubikeyService,
92
        SmsSecondFactorService $smsService,
93
        TranslatorInterface $translator,
94
        LoggerInterface $logger
95
    ) {
96
        $this->loaResolutionService = $loaResolutionService;
97
        $this->secondFactorRepository = $secondFactorRepository;
98
        $this->yubikeyService = $yubikeyService;
99
        $this->smsService = $smsService;
100
        $this->translator = $translator;
101
        $this->logger = $logger;
102
    }
103
104
    /**
105
     * @param string          $identityNameId
106
     * @param Loa             $requiredLoa
107
     * @return \Doctrine\Common\Collections\ArrayCollection
108
     */
109
    public function determineViableSecondFactors(
110
        $identityNameId,
111
        Loa $requiredLoa
112
    ) {
113
        $candidateSecondFactors = $this->secondFactorRepository->getAllMatchingFor($requiredLoa, $identityNameId);
114
        $this->logger->info(
115
            sprintf('Loaded %d matching candidate second factors', count($candidateSecondFactors))
116
        );
117
118
        return $candidateSecondFactors;
119
    }
120
121
    /**
122
     * @param string           $requestedLoa
123
     * @param ServiceProvider  $serviceProvider
124
     * @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...
125
     * @return null|Loa
126
     *
127
     * @SuppressWarnings(PHPMD.CyclomaticComplexity) see https://www.pivotaltracker.com/story/show/96065350
128
     * @SuppressWarnings(PHPMD.NPathComplexity)      see https://www.pivotaltracker.com/story/show/96065350
129
     */
130
    public function resolveHighestRequiredLoa(
131
        $requestedLoa,
132
        ServiceProvider $serviceProvider,
133
        IdentityProvider $authenticatingIdp = null
134
    ) {
135
        $loaCandidates = new ArrayCollection();
136
137
        if ($requestedLoa) {
138
            $loaCandidates->add($requestedLoa);
139
            $this->logger->info(sprintf('Added requested Loa "%s" as candidate', $requestedLoa));
140
        }
141
142
        $spConfiguredLoas = $serviceProvider->get('configuredLoas');
143
        $loaCandidates->add($spConfiguredLoas['__default__']);
144
        $this->logger->info(sprintf('Added SP\'s default Loa "%s" as candidate', $spConfiguredLoas['__default__']));
145
146
        if ($authenticatingIdp) {
147 View Code Duplication
            if (array_key_exists($authenticatingIdp->getEntityId(), $spConfiguredLoas)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
148
                $loaCandidates->add($spConfiguredLoas[$authenticatingIdp->getEntityId()]);
149
                $this->logger->info(sprintf(
150
                    'Added SP\'s Loa "%s" for this IdP as candidate',
151
                    $spConfiguredLoas[$authenticatingIdp->getEntityId()]
152
                ));
153
            }
154
155
            $idpConfiguredLoas = $authenticatingIdp->get('configuredLoas');
156
            $loaCandidates->add($idpConfiguredLoas['__default__']);
157
            $this->logger->info(
158
                sprintf('Added authenticating IdP\'s default Loa "%s" as candidate', $spConfiguredLoas['__default__'])
159
            );
160
161 View Code Duplication
            if (array_key_exists($serviceProvider->getEntityId(), $idpConfiguredLoas)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

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