Completed
Push — feature/authentication-restyle ( 13c021 )
by
unknown
02:26
created

verifyU2fAuthenticationAction()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 62
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 62
rs 8.9167
c 0
b 0
f 0
cc 4
eloc 40
nc 4
nop 1

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Controller;
20
21
use Psr\Log\LoggerInterface;
22
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
23
use Surfnet\StepupBundle\Command\VerifyPossessionOfPhoneCommand;
24
use Surfnet\StepupBundle\Value\PhoneNumber\InternationalPhoneNumber;
25
use Surfnet\StepupBundle\Value\SecondFactorType;
26
use Surfnet\StepupGateway\GatewayBundle\Command\ChooseSecondFactorCommand;
27
use Surfnet\StepupGateway\GatewayBundle\Command\SendSmsChallengeCommand;
28
use Surfnet\StepupGateway\GatewayBundle\Command\VerifyYubikeyOtpCommand;
29
use Surfnet\StepupGateway\GatewayBundle\Entity\SecondFactor;
30
use Surfnet\StepupGateway\GatewayBundle\Exception\InvalidArgumentException;
31
use Surfnet\StepupGateway\GatewayBundle\Exception\LoaCannotBeGivenException;
32
use Surfnet\StepupGateway\GatewayBundle\Exception\RuntimeException;
33
use Surfnet\StepupGateway\GatewayBundle\Saml\ResponseContext;
34
use Surfnet\StepupGateway\U2fVerificationBundle\Value\KeyHandle;
35
use Surfnet\StepupU2fBundle\Dto\SignResponse;
36
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
37
use Symfony\Component\Form\FormError;
38
use Symfony\Component\HttpFoundation\RedirectResponse;
39
use Symfony\Component\HttpFoundation\Request;
40
use Symfony\Component\HttpFoundation\Response;
41
use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBagInterface;
42
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
43
44
/**
45
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
46
 */
47
class SecondFactorController extends Controller
48
{
49
    public function selectSecondFactorForVerificationAction()
50
    {
51
        $context = $this->getResponseContext();
52
        $originalRequestId = $context->getInResponseTo();
53
54
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
55
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
56
        $logger->notice('Determining which second factor to use...');
57
58
        try {
59
            // Retrieve all requirements to determine the required LoA
60
            $requestedLoa = $context->getRequiredLoa();
61
            $spConfiguredLoas = $context->getServiceProvider()->get('configuredLoas');
62
63
            $normalizedIdpSho = $context->getNormalizedSchacHomeOrganization();
64
            $normalizedUserSho = $this->getStepupService()->getNormalizedUserShoByIdentityNameId($context->getIdentityNameId());
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 128 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
65
66
            $requiredLoa = $this
67
                ->getStepupService()
68
                ->resolveHighestRequiredLoa(
69
                    $requestedLoa,
70
                    $spConfiguredLoas,
71
                    $normalizedIdpSho,
72
                    $normalizedUserSho
73
                );
74
        } catch (LoaCannotBeGivenException $e) {
75
            // Log the message of the domain exception, this contains a meaningful message.
76
            $logger->notice($e->getMessage());
77
            return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendLoaCannotBeGiven');
78
        }
79
80
        $logger->notice(sprintf('Determined that the required Loa is "%s"', $requiredLoa));
81
82
        if ($this->getStepupService()->isIntrinsicLoa($requiredLoa)) {
83
            $this->get('gateway.authentication_logger')->logIntrinsicLoaAuthentication($originalRequestId);
84
85
            return $this->forward($context->getResponseAction());
86
        }
87
88
        $secondFactorCollection = $this
89
            ->getStepupService()
90
            ->determineViableSecondFactors(
91
                $context->getIdentityNameId(),
92
                $requiredLoa,
93
                $this->get('gateway.service.whitelist')
94
            );
95
96
        switch (count($secondFactorCollection)) {
97
            case 0:
98
                $logger->notice('No second factors can give the determined Loa');
99
                return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendLoaCannotBeGiven');
100
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
101
102
            case 1:
103
                $secondFactor = $secondFactorCollection->first();
104
                $logger->notice(sprintf(
105
                    'Found "%d" second factors, using second factor of type "%s"',
106
                    count($secondFactorCollection),
107
                    $secondFactor->secondFactorType
108
                ));
109
110
                return $this->selectAndRedirectTo($secondFactor, $context);
111
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
112
113
            default:
114
                return $this->forward(
115
                    'SurfnetStepupGatewayGatewayBundle:SecondFactor:chooseSecondFactor',
116
                    ['secondFactors' => $secondFactorCollection]
117
                );
118
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
119
        }
120
    }
121
122
    /**
123
     * @Template
124
     * @param Request $request
125
     * @return array|RedirectResponse|Response
126
     */
127
    public function chooseSecondFactorAction(Request $request)
128
    {
129
        $context = $this->getResponseContext();
130
        $originalRequestId = $context->getInResponseTo();
131
132
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
133
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
134
        $logger->notice('Ask the user which one of his suitable second factor tokens to use...');
135
136
        try {
137
            // Retrieve all requirements to determine the required LoA
138
            $requestedLoa = $context->getRequiredLoa();
139
            $spConfiguredLoas = $context->getServiceProvider()->get('configuredLoas');
140
141
            $normalizedIdpSho = $context->getNormalizedSchacHomeOrganization();
142
            $normalizedUserSho = $this->getStepupService()->getNormalizedUserShoByIdentityNameId($context->getIdentityNameId());
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 128 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
143
144
            $requiredLoa = $this
145
                ->getStepupService()
146
                ->resolveHighestRequiredLoa(
147
                    $requestedLoa,
148
                    $spConfiguredLoas,
149
                    $normalizedIdpSho,
150
                    $normalizedUserSho
151
                );
152
        } catch (LoaCannotBeGivenException $e) {
153
            // Log the message of the domain exception, this contains a meaningful message.
154
            $logger->notice($e->getMessage());
155
            return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendLoaCannotBeGiven');
156
        }
157
158
        $logger->notice(sprintf('Determined that the required Loa is "%s"', $requiredLoa));
159
160
        $secondFactors = $this
161
            ->getStepupService()
162
            ->determineViableSecondFactors(
163
                $context->getIdentityNameId(),
164
                $requiredLoa,
165
                $this->get('gateway.service.whitelist')
166
            );
167
168
        $command = new ChooseSecondFactorCommand();
169
        $command->secondFactors = $secondFactors;
0 ignored issues
show
Documentation Bug introduced by
It seems like $secondFactors of type object<Doctrine\Common\Collections\Collection> is incompatible with the declared type array<integer,object<Sur...e\Entity\SecondFactor>> of property $secondFactors.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
170
171
        $form = $this
172
            ->createForm(
173
                'gateway_choose_second_factor',
174
                $command,
175
                ['action' => $this->generateUrl('gateway_verify_second_factor_choose_second_factor')]
176
            )
177
            ->handleRequest($request);
178
179
        if ($form->isSubmitted() && $form->isValid()) {
180
            $buttonName = $form->getClickedButton()->getName();
181
            $formResults = $request->request->get('gateway_choose_second_factor', false);
182
183
            if (!isset($formResults[$buttonName])) {
184
                throw new InvalidArgumentException(
185
                    sprintf(
186
                        'Second factor type "%s" could not be found in the posted form results.',
187
                        $buttonName
188
                    )
189
                );
190
            }
191
192
            $secondFactorType = $formResults[$buttonName];
193
194
            // Filter the selected second factor from the array collection
195
            $secondFactorFiltered = $secondFactors->filter(
196
                function ($secondFactor) use ($secondFactorType) {
197
                    return $secondFactorType === $secondFactor->secondFactorType;
198
                }
199
            );
200
201
            if ($secondFactorFiltered->isEmpty()) {
202
                throw new InvalidArgumentException(
203
                    sprintf(
204
                        'Second factor type "%s" could not be found in the collection of available second factors.',
205
                        $secondFactorType
206
                    )
207
                );
208
            }
209
210
            $secondFactor = $secondFactorFiltered->first();
211
212
            $logger->notice(sprintf('User chose "%s" to use as second factor', $secondFactorType));
213
214
            // Forward to action to verify possession of second factor
215
            return $this->selectAndRedirectTo($secondFactor, $context);
216
        } else if ($form->isSubmitted() && !$form->isValid()) {
217
            $form->addError(new FormError('gateway.form.gateway_choose_second_factor.unknown_second_factor_type'));
218
        }
219
220
        return [
221
            'form' => $form->createView(),
222
            'secondFactors' => $secondFactors,
223
        ];
224
    }
225
226
    public function verifyGssfAction()
227
    {
228
        $context = $this->getResponseContext();
229
        $originalRequestId = $context->getInResponseTo();
230
231
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
232
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
233
        $logger->info('Received request to verify GSSF');
234
235
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
236
237
        $logger->info(sprintf(
238
            'Selected GSSF "%s" for verfication, forwarding to Saml handling',
239
            $selectedSecondFactor
240
        ));
241
242
        /** @var \Surfnet\StepupGateway\GatewayBundle\Service\SecondFactorService $secondFactorService */
243
        $secondFactorService = $this->get('gateway.service.second_factor_service');
244
        /** @var \Surfnet\StepupGateway\GatewayBundle\Entity\SecondFactor $secondFactor */
245
        $secondFactor = $secondFactorService->findByUuid($selectedSecondFactor);
246
        if (!$secondFactor) {
247
            throw new RuntimeException(sprintf(
248
                'Requested verification of GSSF "%s", however that Second Factor no longer exists',
249
                $selectedSecondFactor
250
            ));
251
        }
252
253
        return $this->forward(
254
            'SurfnetStepupGatewaySamlStepupProviderBundle:SamlProxy:sendSecondFactorVerificationAuthnRequest',
255
            [
256
                'provider' => $secondFactor->secondFactorType,
257
                'subjectNameId' => $secondFactor->secondFactorIdentifier
258
            ]
259
        );
260
    }
261
262
    public function gssfVerifiedAction()
263
    {
264
        $context = $this->getResponseContext();
265
        $originalRequestId = $context->getInResponseTo();
266
267
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
268
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
269
        $logger->info('Attempting to mark GSSF as verified');
270
271
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
272
273
        /** @var \Surfnet\StepupGateway\GatewayBundle\Entity\SecondFactor $secondFactor */
274
        $secondFactor = $this->get('gateway.service.second_factor_service')->findByUuid($selectedSecondFactor);
275
        if (!$secondFactor) {
276
            throw new RuntimeException(
277
                sprintf(
278
                    'Verification of GSSF "%s" succeeded, however that Second Factor no longer exists',
279
                    $selectedSecondFactor
280
                )
281
            );
282
        }
283
284
        $context->markSecondFactorVerified();
285
        $this->getAuthenticationLogger()->logSecondFactorAuthentication($originalRequestId);
286
287
        $logger->info(sprintf(
288
            'Marked GSSF "%s" as verified, forwarding to Gateway controller to respond',
289
            $selectedSecondFactor
290
        ));
291
292
        return $this->forward($context->getResponseAction());
293
    }
294
295
    /**
296
     * @Template
297
     * @param Request $request
298
     * @return array|Response
299
     */
300
    public function verifyYubiKeySecondFactorAction(Request $request)
301
    {
302
        $context = $this->getResponseContext();
303
        $originalRequestId = $context->getInResponseTo();
304
305
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
306
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
307
308
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
309
310
        $logger->notice('Verifying possession of Yubikey second factor');
311
312
        $command = new VerifyYubikeyOtpCommand();
313
        $command->secondFactorId = $selectedSecondFactor;
314
315
        $form = $this->createForm('gateway_verify_yubikey_otp', $command)->handleRequest($request);
316
317
        if ($form->get('cancel')->isClicked()) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Symfony\Component\Form\FormInterface as the method isClicked() does only exist in the following implementations of said interface: Symfony\Component\Form\SubmitButton.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
318
            return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendAuthenticationCancelledByUser');
319
        }
320
321
        if (!$form->isValid()) {
322
            // OTP field is rendered empty in the template.
323
            return ['form' => $form->createView()];
324
        }
325
326
        $result = $this->getStepupService()->verifyYubikeyOtp($command);
327
328
        if ($result->didOtpVerificationFail()) {
329
            $form->addError(new FormError('gateway.form.verify_yubikey.otp_verification_failed'));
330
331
            // OTP field is rendered empty in the template.
332
            return ['form' => $form->createView()];
333
        } elseif (!$result->didPublicIdMatch()) {
334
            $form->addError(new FormError('gateway.form.verify_yubikey.public_id_mismatch'));
335
336
            // OTP field is rendered empty in the template.
337
            return ['form' => $form->createView()];
338
        }
339
340
        $this->getResponseContext()->markSecondFactorVerified();
341
        $this->getAuthenticationLogger()->logSecondFactorAuthentication($originalRequestId);
342
343
        $logger->info(
344
            sprintf(
345
                'Marked Yubikey Second Factor "%s" as verified, forwarding to Saml Proxy to respond',
346
                $selectedSecondFactor
347
            )
348
        );
349
350
        return $this->forward($context->getResponseAction());
351
    }
352
353
    /**
354
     * @Template
355
     * @param Request $request
356
     * @return array|Response
357
     */
358
    public function verifySmsSecondFactorAction(Request $request)
359
    {
360
        $context = $this->getResponseContext();
361
        $originalRequestId = $context->getInResponseTo();
362
363
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
364
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
365
366
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
367
368
        $logger->notice('Verifying possession of SMS second factor, preparing to send');
369
370
        $command = new SendSmsChallengeCommand();
371
        $command->secondFactorId = $selectedSecondFactor;
372
373
        $form = $this->createForm('gateway_send_sms_challenge', $command)->handleRequest($request);
374
375
        $cancelFormAction = $this->generateUrl('gateway_cancel_authentication');
376
        $cancelForm = $this->createForm(
377
            'gateway_cancel_authentication',
378
            null,
379
            ['action' => $cancelFormAction]
380
        )->handleRequest($request);
381
382
        $stepupService = $this->getStepupService();
383
        $phoneNumber = InternationalPhoneNumber::fromStringFormat(
384
            $stepupService->getSecondFactorIdentifier($selectedSecondFactor)
385
        );
386
387
        $otpRequestsRemaining = $stepupService->getSmsOtpRequestsRemainingCount();
388
        $maximumOtpRequests = $stepupService->getSmsMaximumOtpRequestsCount();
389
        $viewVariables = ['otpRequestsRemaining' => $otpRequestsRemaining, 'maximumOtpRequests' => $maximumOtpRequests];
390
391
        if (!$form->isValid()) {
392
            return array_merge(
393
                $viewVariables,
394
                ['phoneNumber' => $phoneNumber, 'form' => $form->createView(), 'cancelForm' => $cancelForm->createView()]
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 121 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
395
            );
396
        }
397
398
        $logger->notice('Verifying possession of SMS second factor, sending challenge per SMS');
399
400
        if (!$stepupService->sendSmsChallenge($command)) {
401
            $form->addError(new FormError('gateway.form.send_sms_challenge.sms_sending_failed'));
402
403
            return array_merge(
404
                $viewVariables,
405
                ['phoneNumber' => $phoneNumber, 'form' => $form->createView(), 'cancelForm' => $cancelForm->createView()]
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 121 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
406
            );
407
        }
408
409
        return $this->redirect($this->generateUrl('gateway_verify_second_factor_sms_verify_challenge'));
410
    }
411
412
    /**
413
     * @Template
414
     * @param Request $request
415
     * @return array|Response
416
     */
417
    public function verifySmsSecondFactorChallengeAction(Request $request)
418
    {
419
        $context = $this->getResponseContext();
420
        $originalRequestId = $context->getInResponseTo();
421
422
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
423
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
424
425
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
426
427
        $command = new VerifyPossessionOfPhoneCommand();
428
        $form = $this->createForm('gateway_verify_sms_challenge', $command)->handleRequest($request);
429
430
        if ($form->get('cancel')->isClicked()) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Symfony\Component\Form\FormInterface as the method isClicked() does only exist in the following implementations of said interface: Symfony\Component\Form\SubmitButton.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
431
            return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendAuthenticationCancelledByUser');
432
        }
433
434
        if (!$form->isValid()) {
435
            return ['form' => $form->createView()];
436
        }
437
438
        $logger->notice('Verifying input SMS challenge matches');
439
440
        $verification = $this->getStepupService()->verifySmsChallenge($command);
441
442
        if ($verification->wasSuccessful()) {
443
            $this->getStepupService()->clearSmsVerificationState();
444
445
            $this->getResponseContext()->markSecondFactorVerified();
446
            $this->getAuthenticationLogger()->logSecondFactorAuthentication($originalRequestId);
447
448
            $logger->info(
449
                sprintf(
450
                    'Marked Sms Second Factor "%s" as verified, forwarding to Saml Proxy to respond',
451
                    $selectedSecondFactor
452
                )
453
            );
454
455
            return $this->forward($context->getResponseAction());
456
        } elseif ($verification->didOtpExpire()) {
457
            $logger->notice('SMS challenge expired');
458
            $form->addError(new FormError('gateway.form.send_sms_challenge.challenge_expired'));
459
        } elseif ($verification->wasAttemptedTooManyTimes()) {
460
            $logger->notice('SMS challenge verification was attempted too many times');
461
            $form->addError(new FormError('gateway.form.send_sms_challenge.too_many_attempts'));
462
        } else {
463
            $logger->notice('SMS challenge did not match');
464
            $form->addError(new FormError('gateway.form.send_sms_challenge.sms_challenge_incorrect'));
465
        }
466
467
        return ['form' => $form->createView()];
468
    }
469
470
    /**
471
     * @Template
472
     */
473
    public function initiateU2fAuthenticationAction()
474
    {
475
        $context = $this->getResponseContext();
476
        $originalRequestId = $context->getInResponseTo();
477
478
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
479
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
480
481
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
482
        $stepupService = $this->getStepupService();
483
484
        $cancelFormAction = $this->generateUrl('gateway_verify_second_factor_u2f_cancel_authentication');
485
        $cancelForm =
486
            $this->createForm('gateway_cancel_second_factor_verification', null, ['action' => $cancelFormAction]);
487
488
        $logger->notice('Verifying possession of U2F second factor, looking for registration matching key handle');
489
490
        $service = $this->get('surfnet_stepup_u2f_verification.service.u2f_verification');
491
        $keyHandle = new KeyHandle($stepupService->getSecondFactorIdentifier($selectedSecondFactor));
492
        $registration = $service->findRegistrationByKeyHandle($keyHandle);
493
494
        if ($registration === null) {
495
            $logger->critical(
496
                sprintf('No known registration for key handle of second factor "%s"', $selectedSecondFactor)
497
            );
498
            $this->addFlash('error', 'gateway.u2f.alert.unknown_registration');
499
500
            return ['authenticationFailed' => true, 'cancelForm' => $cancelForm->createView()];
501
        }
502
503
        $logger->notice('Creating sign request');
504
505
        $signRequest = $service->createSignRequest($registration);
506
        $signResponse = new SignResponse();
507
508
        /** @var AttributeBagInterface $session */
509
        $session = $this->get('gateway.session.u2f');
510
        $session->set('request', $signRequest);
511
512
        $formAction = $this->generateUrl('gateway_verify_second_factor_u2f_verify_authentication');
513
        $form = $this->createForm(
514
            'surfnet_stepup_u2f_verify_device_authentication',
515
            $signResponse,
516
            ['sign_request' => $signRequest, 'action' => $formAction]
517
        );
518
519
        return ['form' => $form->createView(), 'cancelForm' => $cancelForm->createView()];
520
    }
521
522
    /**
523
     * @Template("SurfnetStepupGatewayGatewayBundle:SecondFactor:initiateU2fAuthentication.html.twig")
524
     *
525
     * @param Request $request
526
     * @return array|Response
527
     */
528
    public function verifyU2fAuthenticationAction(Request $request)
529
    {
530
        $context = $this->getResponseContext();
531
        $originalRequestId = $context->getInResponseTo();
532
533
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
534
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
535
536
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
537
538
        $logger->notice('Received sign response from device');
539
540
        /** @var AttributeBagInterface $session */
541
        $session = $this->get('gateway.session.u2f');
542
        $signRequest = $session->get('request');
543
        $signResponse = new SignResponse();
544
545
        $formAction = $this->generateUrl('gateway_verify_second_factor_u2f_verify_authentication');
546
        $form = $this
547
            ->createForm(
548
                'surfnet_stepup_u2f_verify_device_authentication',
549
                $signResponse,
550
                ['sign_request' => $signRequest, 'action' => $formAction]
551
            )
552
            ->handleRequest($request);
553
554
        $cancelFormAction = $this->generateUrl('gateway_verify_second_factor_u2f_cancel_authentication');
555
        $cancelForm =
556
            $this->createForm('gateway_cancel_second_factor_verification', null, ['action' => $cancelFormAction]);
557
558
        if (!$form->isValid()) {
559
            $logger->error('U2F authentication verification could not be started because device send illegal data');
560
            $this->addFlash('error', 'gateway.u2f.alert.error');
561
562
            return ['authenticationFailed' => true, 'cancelForm' => $cancelForm->createView()];
563
        }
564
565
        $service = $this->get('surfnet_stepup_u2f_verification.service.u2f_verification');
566
        $result = $service->verifyAuthentication($signRequest, $signResponse);
567
568
        if ($result->wasSuccessful()) {
569
            $context->markSecondFactorVerified();
570
            $this->getAuthenticationLogger()->logSecondFactorAuthentication($originalRequestId);
571
572
            $logger->info(
573
                sprintf(
574
                    'Marked U2F second factor "%s" as verified, forwarding to Saml Proxy to respond',
575
                    $selectedSecondFactor
576
                )
577
            );
578
579
            return $this->forward($context->getResponseAction());
580
        } elseif ($result->didDeviceReportError()) {
581
            $logger->error('U2F device reported error during authentication');
582
            $this->addFlash('error', 'gateway.u2f.alert.device_reported_an_error');
583
        } else {
584
            $logger->error('U2F authentication verification failed');
585
            $this->addFlash('error', 'gateway.u2f.alert.error');
586
        }
587
588
        return ['authenticationFailed' => true, 'cancelForm' => $cancelForm->createView()];
589
    }
590
591
    public function cancelAuthenticationAction()
592
    {
593
        return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendAuthenticationCancelledByUser');
594
    }
595
596
    /**
597
     * @return \Surfnet\StepupGateway\GatewayBundle\Service\StepupAuthenticationService
598
     */
599
    private function getStepupService()
600
    {
601
        return $this->get('gateway.service.stepup_authentication');
602
    }
603
604
    /**
605
     * @return ResponseContext
606
     */
607
    private function getResponseContext()
608
    {
609
        return $this->get($this->get('gateway.proxy.state_handler')->getResponseContextServiceId());
610
    }
611
612
    /**
613
     * @return \Surfnet\StepupGateway\GatewayBundle\Monolog\Logger\AuthenticationLogger
614
     */
615
    private function getAuthenticationLogger()
616
    {
617
        return $this->get('gateway.authentication_logger');
618
    }
619
620
    /**
621
     * @param ResponseContext $context
622
     * @param LoggerInterface $logger
623
     * @return string
624
     */
625
    private function getSelectedSecondFactor(ResponseContext $context, LoggerInterface $logger)
626
    {
627
        $selectedSecondFactor = $context->getSelectedSecondFactor();
628
629
        if (!$selectedSecondFactor) {
630
            $logger->error('Cannot verify possession of an unknown second factor');
631
632
            throw new BadRequestHttpException('Cannot verify possession of an unknown second factor.');
633
        }
634
635
        return $selectedSecondFactor;
636
    }
637
638
    private function selectAndRedirectTo(SecondFactor $secondFactor, ResponseContext $context)
639
    {
640
        $context->saveSelectedSecondFactor($secondFactor);
641
642
        $this->getStepupService()->clearSmsVerificationState();
643
644
        $secondFactorTypeService = $this->get('surfnet_stepup.service.second_factor_type');
645
        $secondFactorType = new SecondFactorType($secondFactor->secondFactorType);
646
647
        $route = 'gateway_verify_second_factor_';
648
        if ($secondFactorTypeService->isGssf($secondFactorType)) {
649
            $route .= 'gssf';
650
        } else {
651
            $route .= strtolower($secondFactor->secondFactorType);
652
        }
653
654
        return $this->redirect($this->generateUrl($route));
655
    }
656
}
657