Completed
Pull Request — develop (#207)
by
unknown
04:00 queued 02:04
created

verifyYubiKeySecondFactorAction()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 53

Duplication

Lines 4
Ratio 7.55 %

Importance

Changes 0
Metric Value
dl 4
loc 53
rs 8.7143
c 0
b 0
f 0
cc 5
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\Form\Type\CancelAuthenticationType;
34
use Surfnet\StepupGateway\GatewayBundle\Form\Type\CancelSecondFactorVerificationType;
35
use Surfnet\StepupGateway\GatewayBundle\Form\Type\ChooseSecondFactorType;
36
use Surfnet\StepupGateway\GatewayBundle\Form\Type\SendSmsChallengeType;
37
use Surfnet\StepupGateway\GatewayBundle\Form\Type\VerifySmsChallengeType;
38
use Surfnet\StepupGateway\GatewayBundle\Form\Type\VerifyYubikeyOtpType;
39
use Surfnet\StepupGateway\GatewayBundle\Saml\ResponseContext;
40
use Surfnet\StepupGateway\U2fVerificationBundle\Value\KeyHandle;
41
use Surfnet\StepupU2fBundle\Dto\SignResponse;
42
use Surfnet\StepupU2fBundle\Form\Type\VerifyDeviceAuthenticationType;
43
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
44
use Symfony\Component\Form\Form;
45
use Symfony\Component\Form\FormError;
46
use Symfony\Component\HttpFoundation\RedirectResponse;
47
use Symfony\Component\HttpFoundation\Request;
48
use Symfony\Component\HttpFoundation\Response;
49
use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBagInterface;
50
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
51
52
/**
53
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
54
 */
55
class SecondFactorController extends Controller
0 ignored issues
show
Deprecated Code introduced by
The class Symfony\Bundle\Framework...e\Controller\Controller has been deprecated with message: since Symfony 4.2, use "Symfony\Bundle\FrameworkBundle\Controller\AbstractController" instead.

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
56
{
57
    public function selectSecondFactorForVerificationAction()
58
    {
59
        $context = $this->getResponseContext();
60
        $originalRequestId = $context->getInResponseTo();
61
62
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
63
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
64
        $logger->notice('Determining which second factor to use...');
65
66
        try {
67
            // Retrieve all requirements to determine the required LoA
68
            $requestedLoa = $context->getRequiredLoa();
69
            $spConfiguredLoas = $context->getServiceProvider()->get('configuredLoas');
70
71
            $normalizedIdpSho = $context->getNormalizedSchacHomeOrganization();
72
            $normalizedUserSho = $this->getStepupService()->getNormalizedUserShoByIdentityNameId($context->getIdentityNameId());
73
74
            $requiredLoa = $this
75
                ->getStepupService()
76
                ->resolveHighestRequiredLoa(
77
                    $requestedLoa,
78
                    $spConfiguredLoas,
79
                    $normalizedIdpSho,
80
                    $normalizedUserSho
81
                );
82
        } catch (LoaCannotBeGivenException $e) {
83
            // Log the message of the domain exception, this contains a meaningful message.
84
            $logger->notice($e->getMessage());
85
            return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendLoaCannotBeGiven');
86
        }
87
88
        $logger->notice(sprintf('Determined that the required Loa is "%s"', $requiredLoa));
89
90
        if ($this->getStepupService()->isIntrinsicLoa($requiredLoa)) {
91
            $this->get('gateway.authentication_logger')->logIntrinsicLoaAuthentication($originalRequestId);
92
93
            return $this->forward($context->getResponseAction());
94
        }
95
96
        $secondFactorCollection = $this
97
            ->getStepupService()
98
            ->determineViableSecondFactors(
99
                $context->getIdentityNameId(),
100
                $requiredLoa,
101
                $this->get('gateway.service.whitelist')
102
            );
103
104
        switch (count($secondFactorCollection)) {
105
            case 0:
106
                $logger->notice('No second factors can give the determined Loa');
107
                return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendLoaCannotBeGiven');
108
                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...
109
110
            case 1:
111
                $secondFactor = $secondFactorCollection->first();
112
                $logger->notice(sprintf(
113
                    'Found "%d" second factors, using second factor of type "%s"',
114
                    count($secondFactorCollection),
115
                    $secondFactor->secondFactorType
116
                ));
117
118
                return $this->selectAndRedirectTo($secondFactor, $context);
119
                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...
120
121
            default:
122
                return $this->forward(
123
                    'SurfnetStepupGatewayGatewayBundle:SecondFactor:chooseSecondFactor',
124
                    ['secondFactors' => $secondFactorCollection]
125
                );
126
                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...
127
        }
128
    }
129
130
    /**
131
     * @Template
132
     * @param Request $request
133
     * @return array|RedirectResponse|Response
134
     * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
135
     */
136
    public function chooseSecondFactorAction(Request $request)
137
    {
138
        $context = $this->getResponseContext();
139
        $originalRequestId = $context->getInResponseTo();
140
141
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
142
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
143
        $logger->notice('Ask the user which one of his suitable second factor tokens to use...');
144
145
        try {
146
            // Retrieve all requirements to determine the required LoA
147
            $requestedLoa = $context->getRequiredLoa();
148
            $spConfiguredLoas = $context->getServiceProvider()->get('configuredLoas');
149
150
            $normalizedIdpSho = $context->getNormalizedSchacHomeOrganization();
151
            $normalizedUserSho = $this->getStepupService()->getNormalizedUserShoByIdentityNameId($context->getIdentityNameId());
152
153
            $requiredLoa = $this
154
                ->getStepupService()
155
                ->resolveHighestRequiredLoa(
156
                    $requestedLoa,
157
                    $spConfiguredLoas,
158
                    $normalizedIdpSho,
159
                    $normalizedUserSho
160
                );
161
        } catch (LoaCannotBeGivenException $e) {
162
            // Log the message of the domain exception, this contains a meaningful message.
163
            $logger->notice($e->getMessage());
164
            return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendLoaCannotBeGiven');
165
        }
166
167
        $logger->notice(sprintf('Determined that the required Loa is "%s"', $requiredLoa));
168
169
        $secondFactors = $this
170
            ->getStepupService()
171
            ->determineViableSecondFactors(
172
                $context->getIdentityNameId(),
173
                $requiredLoa,
174
                $this->get('gateway.service.whitelist')
175
            );
176
177
        $command = new ChooseSecondFactorCommand();
178
        $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...
179
180
        $form = $this
181
            ->createForm(
182
                ChooseSecondFactorType::class,
183
                $command,
184
                ['action' => $this->generateUrl('gateway_verify_second_factor_choose_second_factor')]
185
            )
186
            ->handleRequest($request);
187
        $cancelForm = $this->buildCancelAuthenticationForm()->handleRequest($request);
188
189
        if ($form->isSubmitted() && $form->isValid()) {
190
            $buttonName = $form->getClickedButton()->getName();
191
            $formResults = $request->request->get('gateway_choose_second_factor', false);
192
193
            if (!isset($formResults[$buttonName])) {
194
                throw new InvalidArgumentException(
195
                    sprintf(
196
                        'Second factor type "%s" could not be found in the posted form results.',
197
                        $buttonName
198
                    )
199
                );
200
            }
201
202
            $secondFactorType = $formResults[$buttonName];
203
204
            // Filter the selected second factor from the array collection
205
            $secondFactorFiltered = $secondFactors->filter(
206
                function ($secondFactor) use ($secondFactorType) {
207
                    return $secondFactorType === $secondFactor->secondFactorType;
208
                }
209
            );
210
211
            if ($secondFactorFiltered->isEmpty()) {
212
                throw new InvalidArgumentException(
213
                    sprintf(
214
                        'Second factor type "%s" could not be found in the collection of available second factors.',
215
                        $secondFactorType
216
                    )
217
                );
218
            }
219
220
            $secondFactor = $secondFactorFiltered->first();
221
222
            $logger->notice(sprintf('User chose "%s" to use as second factor', $secondFactorType));
223
224
            // Forward to action to verify possession of second factor
225
            return $this->selectAndRedirectTo($secondFactor, $context);
226
        } else if ($form->isSubmitted() && !$form->isValid()) {
227
            $form->addError(
228
                new FormError(
229
                    $this->get('translator')
230
                      ->trans('gateway.form.gateway_choose_second_factor.unknown_second_factor_type')
231
                )
232
            );
233
        }
234
235
        return [
236
            'form' => $form->createView(),
237
            'cancelForm' => $cancelForm->createView(),
238
            'secondFactors' => $secondFactors,
239
        ];
240
    }
241
242
    public function verifyGssfAction()
243
    {
244
        $context = $this->getResponseContext();
245
        $originalRequestId = $context->getInResponseTo();
246
247
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
248
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
249
        $logger->info('Received request to verify GSSF');
250
251
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
252
253
        $logger->info(sprintf(
254
            'Selected GSSF "%s" for verfication, forwarding to Saml handling',
255
            $selectedSecondFactor
256
        ));
257
258
        /** @var \Surfnet\StepupGateway\GatewayBundle\Service\SecondFactorService $secondFactorService */
259
        $secondFactorService = $this->get('gateway.service.second_factor_service');
260
        /** @var \Surfnet\StepupGateway\GatewayBundle\Entity\SecondFactor $secondFactor */
261
        $secondFactor = $secondFactorService->findByUuid($selectedSecondFactor);
262
        if (!$secondFactor) {
263
            throw new RuntimeException(sprintf(
264
                'Requested verification of GSSF "%s", however that Second Factor no longer exists',
265
                $selectedSecondFactor
266
            ));
267
        }
268
269
        // Also send the response context service id, as later we need to know if this is regular SSO or SFO authn.
270
        $responseContextServiceId = $context->getResponseContextServiceId();
271
272
        return $this->forward(
273
            'SurfnetStepupGatewaySamlStepupProviderBundle:SamlProxy:sendSecondFactorVerificationAuthnRequest',
274
            [
275
                'provider' => $secondFactor->secondFactorType,
276
                'subjectNameId' => $secondFactor->secondFactorIdentifier,
277
                'responseContextServiceId' => $responseContextServiceId,
278
            ]
279
        );
280
    }
281
282
    public function gssfVerifiedAction()
283
    {
284
        $context = $this->getResponseContext();
285
        $originalRequestId = $context->getInResponseTo();
286
287
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
288
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
289
        $logger->info('Attempting to mark GSSF as verified');
290
291
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
292
293
        /** @var \Surfnet\StepupGateway\GatewayBundle\Entity\SecondFactor $secondFactor */
294
        $secondFactor = $this->get('gateway.service.second_factor_service')->findByUuid($selectedSecondFactor);
295
        if (!$secondFactor) {
296
            throw new RuntimeException(
297
                sprintf(
298
                    'Verification of GSSF "%s" succeeded, however that Second Factor no longer exists',
299
                    $selectedSecondFactor
300
                )
301
            );
302
        }
303
304
        $context->markSecondFactorVerified();
305
        $this->getAuthenticationLogger()->logSecondFactorAuthentication($originalRequestId);
306
307
        $logger->info(sprintf(
308
            'Marked GSSF "%s" as verified, forwarding to Gateway controller to respond',
309
            $selectedSecondFactor
310
        ));
311
312
        return $this->forward($context->getResponseAction());
313
    }
314
315
    /**
316
     * @Template
317
     * @param Request $request
318
     * @return array|Response
319
     */
320
    public function verifyYubiKeySecondFactorAction(Request $request)
321
    {
322
        $context = $this->getResponseContext();
323
        $originalRequestId = $context->getInResponseTo();
324
325
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
326
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
327
328
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
329
330
        $logger->notice('Verifying possession of Yubikey second factor');
331
332
        $command = new VerifyYubikeyOtpCommand();
333
        $command->secondFactorId = $selectedSecondFactor;
334
335
        $form = $this->createForm(VerifyYubikeyOtpType::class, $command)->handleRequest($request);
336
        $cancelForm = $this->buildCancelAuthenticationForm()->handleRequest($request);
337
338 View Code Duplication
        if ($form->isSubmitted() && !$form->isValid()) {
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...
339
            // OTP field is rendered empty in the template.
340
            return ['form' => $form->createView(), 'cancelForm' => $cancelForm->createView()];
341
        }
342
343
        $result = $this->getStepupService()->verifyYubikeyOtp($command);
344
345
        if ($result->didOtpVerificationFail()) {
346
            $form->addError(
347
                new FormError($this->get('translator')->trans('gateway.form.verify_yubikey.otp_verification_failed'))
348
            );
349
350
            // OTP field is rendered empty in the template.
351
            return ['form' => $form->createView(), 'cancelForm' => $cancelForm->createView()];
352
        } elseif (!$result->didPublicIdMatch()) {
353
            $form->addError(
354
                new FormError($this->get('translator')->trans('gateway.form.verify_yubikey.public_id_mismatch'))
355
            );
356
357
            // OTP field is rendered empty in the template.
358
            return ['form' => $form->createView(), 'cancelForm' => $cancelForm->createView()];
359
        }
360
361
        $this->getResponseContext()->markSecondFactorVerified();
362
        $this->getAuthenticationLogger()->logSecondFactorAuthentication($originalRequestId);
363
364
        $logger->info(
365
            sprintf(
366
                'Marked Yubikey Second Factor "%s" as verified, forwarding to Saml Proxy to respond',
367
                $selectedSecondFactor
368
            )
369
        );
370
371
        return $this->forward($context->getResponseAction());
372
    }
373
374
    /**
375
     * @Template
376
     * @param Request $request
377
     * @return array|Response
378
     */
379
    public function verifySmsSecondFactorAction(Request $request)
380
    {
381
        $context = $this->getResponseContext();
382
        $originalRequestId = $context->getInResponseTo();
383
384
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
385
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
386
387
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
388
389
        $logger->notice('Verifying possession of SMS second factor, preparing to send');
390
391
        $command = new SendSmsChallengeCommand();
392
        $command->secondFactorId = $selectedSecondFactor;
393
394
        $form = $this->createForm(SendSmsChallengeType::class, $command)->handleRequest($request);
395
        $cancelForm = $this->buildCancelAuthenticationForm()->handleRequest($request);
396
397
        $stepupService = $this->getStepupService();
398
        $phoneNumber = InternationalPhoneNumber::fromStringFormat(
399
            $stepupService->getSecondFactorIdentifier($selectedSecondFactor)
400
        );
401
402
        $otpRequestsRemaining = $stepupService->getSmsOtpRequestsRemainingCount();
403
        $maximumOtpRequests = $stepupService->getSmsMaximumOtpRequestsCount();
404
        $viewVariables = ['otpRequestsRemaining' => $otpRequestsRemaining, 'maximumOtpRequests' => $maximumOtpRequests];
405
406
        if ($form->isSubmitted() && !$form->isValid()) {
407
            return array_merge(
408
                $viewVariables,
409
                ['phoneNumber' => $phoneNumber, 'form' => $form->createView(), 'cancelForm' => $cancelForm->createView()]
410
            );
411
        }
412
413
        $logger->notice('Verifying possession of SMS second factor, sending challenge per SMS');
414
415
        if (!$stepupService->sendSmsChallenge($command)) {
416
            $form->addError(
417
                new FormError($this->get('translator')->trans('gateway.form.send_sms_challenge.sms_sending_failed'))
418
            );
419
420
            return array_merge(
421
                $viewVariables,
422
                ['phoneNumber' => $phoneNumber, 'form' => $form->createView(), 'cancelForm' => $cancelForm->createView()]
423
            );
424
        }
425
426
        return $this->redirect($this->generateUrl('gateway_verify_second_factor_sms_verify_challenge'));
427
    }
428
429
    /**
430
     * @Template
431
     * @param Request $request
432
     * @return array|Response
433
     */
434
    public function verifySmsSecondFactorChallengeAction(Request $request)
435
    {
436
        $context = $this->getResponseContext();
437
        $originalRequestId = $context->getInResponseTo();
438
439
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
440
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
441
442
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
443
444
        $command = new VerifyPossessionOfPhoneCommand();
445
        $form = $this->createForm(VerifySmsChallengeType::class, $command)->handleRequest($request);
446
        $cancelForm = $this->buildCancelAuthenticationForm()->handleRequest($request);
447
448 View Code Duplication
        if ($form->isSubmitted() && !$form->isValid()) {
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...
449
            return ['form' => $form->createView(), 'cancelForm' => $cancelForm->createView()];
450
        }
451
452
        $logger->notice('Verifying input SMS challenge matches');
453
454
        $verification = $this->getStepupService()->verifySmsChallenge($command);
455
456
        if ($verification->wasSuccessful()) {
457
            $this->getStepupService()->clearSmsVerificationState();
458
459
            $this->getResponseContext()->markSecondFactorVerified();
460
            $this->getAuthenticationLogger()->logSecondFactorAuthentication($originalRequestId);
461
462
            $logger->info(
463
                sprintf(
464
                    'Marked Sms Second Factor "%s" as verified, forwarding to Saml Proxy to respond',
465
                    $selectedSecondFactor
466
                )
467
            );
468
469
            return $this->forward($context->getResponseAction());
470
        } elseif ($verification->didOtpExpire()) {
471
            $logger->notice('SMS challenge expired');
472
            $form->addError(
473
                new FormError($this->get('translator')->trans('gateway.form.send_sms_challenge.challenge_expired'))
474
            );
475
        } elseif ($verification->wasAttemptedTooManyTimes()) {
476
            $logger->notice('SMS challenge verification was attempted too many times');
477
            $form->addError(
478
                new FormError($this->get('translator')->trans('gateway.form.send_sms_challenge.too_many_attempts'))
479
            );
480
        } else {
481
            $logger->notice('SMS challenge did not match');
482
            $form->addError(
483
                new FormError(
484
                    $this->get('translator')->trans('gateway.form.send_sms_challenge.sms_challenge_incorrect')
485
                )
486
            );
487
        }
488
489
        return ['form' => $form->createView(), 'cancelForm' => $cancelForm->createView()];
490
    }
491
492
    /**
493
     * @Template
494
     */
495
    public function initiateU2fAuthenticationAction()
496
    {
497
        $context = $this->getResponseContext();
498
        $originalRequestId = $context->getInResponseTo();
499
500
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
501
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
502
503
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
504
        $stepupService = $this->getStepupService();
505
506
        $cancelFormAction = $this->generateUrl('gateway_verify_second_factor_u2f_cancel_authentication');
507
        $cancelForm =
508
            $this->createForm(CancelSecondFactorVerificationType::class, null, ['action' => $cancelFormAction]);
509
510
        $logger->notice('Verifying possession of U2F second factor, looking for registration matching key handle');
511
512
        $service = $this->get('surfnet_stepup_u2f_verification.service.u2f_verification');
513
        $keyHandle = new KeyHandle($stepupService->getSecondFactorIdentifier($selectedSecondFactor));
514
        $registration = $service->findRegistrationByKeyHandle($keyHandle);
515
516
        if ($registration === null) {
517
            $logger->critical(
518
                sprintf('No known registration for key handle of second factor "%s"', $selectedSecondFactor)
519
            );
520
            $this->addFlash('error', 'gateway.u2f.alert.unknown_registration');
521
522
            return ['authenticationFailed' => true, 'cancelForm' => $cancelForm->createView()];
523
        }
524
525
        $logger->notice('Creating sign request');
526
527
        $signRequest = $service->createSignRequest($registration);
528
        $signResponse = new SignResponse();
529
530
        /** @var AttributeBagInterface $session */
531
        $session = $this->get('gateway.session.u2f');
532
        $session->set('request', $signRequest);
533
534
        $formAction = $this->generateUrl('gateway_verify_second_factor_u2f_verify_authentication');
535
        $form = $this->createForm(
536
            VerifyDeviceAuthenticationType::class,
537
            $signResponse,
538
            ['sign_request' => $signRequest, 'action' => $formAction]
539
        );
540
541
        return ['form' => $form->createView(), 'cancelForm' => $cancelForm->createView()];
542
    }
543
544
    /**
545
     * @Template("SurfnetStepupGatewayGatewayBundle:second_factor:initiate_u2f_authentication.html.twig")
546
     *
547
     * @param Request $request
548
     * @return array|Response
549
     */
550
    public function verifyU2fAuthenticationAction(Request $request)
551
    {
552
        $context = $this->getResponseContext();
553
        $originalRequestId = $context->getInResponseTo();
554
555
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
556
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
557
558
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
559
560
        $logger->notice('Received sign response from device');
561
562
        /** @var AttributeBagInterface $session */
563
        $session = $this->get('gateway.session.u2f');
564
        $signRequest = $session->get('request');
565
        $signResponse = new SignResponse();
566
567
        $formAction = $this->generateUrl('gateway_verify_second_factor_u2f_verify_authentication');
568
        $form = $this
569
            ->createForm(
570
                VerifyDeviceAuthenticationType::class,
571
                $signResponse,
572
                ['sign_request' => $signRequest, 'action' => $formAction]
573
            )
574
            ->handleRequest($request);
575
576
        $cancelFormAction = $this->generateUrl('gateway_verify_second_factor_u2f_cancel_authentication');
577
        $cancelForm =
578
            $this->createForm(CancelSecondFactorVerificationType::class, null, ['action' => $cancelFormAction]);
579
580
        if ($form->isSubmitted() && !$form->isValid()) {
581
            $logger->error('U2F authentication verification could not be started because device send illegal data');
582
            $this->addFlash('error', 'gateway.u2f.alert.error');
583
584
            return ['authenticationFailed' => true, 'cancelForm' => $cancelForm->createView()];
585
        }
586
587
        $service = $this->get('surfnet_stepup_u2f_verification.service.u2f_verification');
588
        $result = $service->verifyAuthentication($signRequest, $signResponse);
589
590
        if ($result->wasSuccessful()) {
591
            $context->markSecondFactorVerified();
592
            $this->getAuthenticationLogger()->logSecondFactorAuthentication($originalRequestId);
593
594
            $logger->info(
595
                sprintf(
596
                    'Marked U2F second factor "%s" as verified, forwarding to Saml Proxy to respond',
597
                    $selectedSecondFactor
598
                )
599
            );
600
601
            return $this->forward($context->getResponseAction());
602
        } elseif ($result->didDeviceReportError()) {
603
            $logger->error('U2F device reported error during authentication');
604
            $this->addFlash('error', 'gateway.u2f.alert.device_reported_an_error');
605
        } else {
606
            $logger->error('U2F authentication verification failed');
607
            $this->addFlash('error', 'gateway.u2f.alert.error');
608
        }
609
610
        return ['authenticationFailed' => true, 'cancelForm' => $cancelForm->createView()];
611
    }
612
613
    public function cancelAuthenticationAction()
614
    {
615
        return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendAuthenticationCancelledByUser');
616
    }
617
618
    /**
619
     * @return \Surfnet\StepupGateway\GatewayBundle\Service\StepupAuthenticationService
620
     */
621
    private function getStepupService()
622
    {
623
        return $this->get('gateway.service.stepup_authentication');
624
    }
625
626
    /**
627
     * @return ResponseContext
628
     */
629
    private function getResponseContext()
630
    {
631
        return $this->get($this->get('gateway.proxy.state_handler')->getResponseContextServiceId());
632
    }
633
634
    /**
635
     * @return \Surfnet\StepupGateway\GatewayBundle\Monolog\Logger\AuthenticationLogger
636
     */
637
    private function getAuthenticationLogger()
638
    {
639
        return $this->get('gateway.authentication_logger');
640
    }
641
642
    /**
643
     * @param ResponseContext $context
644
     * @param LoggerInterface $logger
645
     * @return string
646
     */
647
    private function getSelectedSecondFactor(ResponseContext $context, LoggerInterface $logger)
648
    {
649
        $selectedSecondFactor = $context->getSelectedSecondFactor();
650
651
        if (!$selectedSecondFactor) {
652
            $logger->error('Cannot verify possession of an unknown second factor');
653
654
            throw new BadRequestHttpException('Cannot verify possession of an unknown second factor.');
655
        }
656
657
        return $selectedSecondFactor;
658
    }
659
660
    private function selectAndRedirectTo(SecondFactor $secondFactor, ResponseContext $context)
661
    {
662
        $context->saveSelectedSecondFactor($secondFactor);
663
664
        $this->getStepupService()->clearSmsVerificationState();
665
666
        $secondFactorTypeService = $this->get('surfnet_stepup.service.second_factor_type');
667
        $secondFactorType = new SecondFactorType($secondFactor->secondFactorType);
668
669
        $route = 'gateway_verify_second_factor_';
670
        if ($secondFactorTypeService->isGssf($secondFactorType)) {
671
            $route .= 'gssf';
672
        } else {
673
            $route .= strtolower($secondFactor->secondFactorType);
674
        }
675
676
        return $this->redirect($this->generateUrl($route));
677
    }
678
679
    /**
680
     * @return Form
681
     */
682
    private function buildCancelAuthenticationForm()
683
    {
684
        $cancelFormAction = $this->generateUrl('gateway_cancel_authentication');
685
        $cancelForm = $this->createForm(
686
            CancelAuthenticationType::class,
687
            null,
688
            ['action' => $cancelFormAction]
689
        );
690
        return $cancelForm;
691
    }
692
}
693