Completed
Push — feature/sf4-update ( f4a4e1...da143b )
by
unknown
02:19
created

verifyYubiKeySecondFactorAction()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 49

Duplication

Lines 4
Ratio 8.16 %

Importance

Changes 0
Metric Value
dl 4
loc 49
rs 8.8016
c 0
b 0
f 0
cc 5
nc 4
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\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(new FormError('gateway.form.gateway_choose_second_factor.unknown_second_factor_type'));
228
        }
229
230
        return [
231
            'form' => $form->createView(),
232
            'cancelForm' => $cancelForm->createView(),
233
            'secondFactors' => $secondFactors,
234
        ];
235
    }
236
237
    public function verifyGssfAction()
238
    {
239
        $context = $this->getResponseContext();
240
        $originalRequestId = $context->getInResponseTo();
241
242
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
243
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
244
        $logger->info('Received request to verify GSSF');
245
246
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
247
248
        $logger->info(sprintf(
249
            'Selected GSSF "%s" for verfication, forwarding to Saml handling',
250
            $selectedSecondFactor
251
        ));
252
253
        /** @var \Surfnet\StepupGateway\GatewayBundle\Service\SecondFactorService $secondFactorService */
254
        $secondFactorService = $this->get('gateway.service.second_factor_service');
255
        /** @var \Surfnet\StepupGateway\GatewayBundle\Entity\SecondFactor $secondFactor */
256
        $secondFactor = $secondFactorService->findByUuid($selectedSecondFactor);
257
        if (!$secondFactor) {
258
            throw new RuntimeException(sprintf(
259
                'Requested verification of GSSF "%s", however that Second Factor no longer exists',
260
                $selectedSecondFactor
261
            ));
262
        }
263
264
        // Also send the response context service id, as later we need to know if this is regular SSO or SFO authn.
265
        $responseContextServiceId = $context->getResponseContextServiceId();
266
267
        return $this->forward(
268
            'SurfnetStepupGatewaySamlStepupProviderBundle:SamlProxy:sendSecondFactorVerificationAuthnRequest',
269
            [
270
                'provider' => $secondFactor->secondFactorType,
271
                'subjectNameId' => $secondFactor->secondFactorIdentifier,
272
                'responseContextServiceId' => $responseContextServiceId,
273
            ]
274
        );
275
    }
276
277
    public function gssfVerifiedAction()
278
    {
279
        $context = $this->getResponseContext();
280
        $originalRequestId = $context->getInResponseTo();
281
282
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
283
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
284
        $logger->info('Attempting to mark GSSF as verified');
285
286
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
287
288
        /** @var \Surfnet\StepupGateway\GatewayBundle\Entity\SecondFactor $secondFactor */
289
        $secondFactor = $this->get('gateway.service.second_factor_service')->findByUuid($selectedSecondFactor);
290
        if (!$secondFactor) {
291
            throw new RuntimeException(
292
                sprintf(
293
                    'Verification of GSSF "%s" succeeded, however that Second Factor no longer exists',
294
                    $selectedSecondFactor
295
                )
296
            );
297
        }
298
299
        $context->markSecondFactorVerified();
300
        $this->getAuthenticationLogger()->logSecondFactorAuthentication($originalRequestId);
301
302
        $logger->info(sprintf(
303
            'Marked GSSF "%s" as verified, forwarding to Gateway controller to respond',
304
            $selectedSecondFactor
305
        ));
306
307
        return $this->forward($context->getResponseAction());
308
    }
309
310
    /**
311
     * @Template
312
     * @param Request $request
313
     * @return array|Response
314
     */
315
    public function verifyYubiKeySecondFactorAction(Request $request)
316
    {
317
        $context = $this->getResponseContext();
318
        $originalRequestId = $context->getInResponseTo();
319
320
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
321
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
322
323
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
324
325
        $logger->notice('Verifying possession of Yubikey second factor');
326
327
        $command = new VerifyYubikeyOtpCommand();
328
        $command->secondFactorId = $selectedSecondFactor;
329
330
        $form = $this->createForm(VerifyYubikeyOtpType::class, $command)->handleRequest($request);
331
        $cancelForm = $this->buildCancelAuthenticationForm()->handleRequest($request);
332
333 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...
334
            // OTP field is rendered empty in the template.
335
            return ['form' => $form->createView(), 'cancelForm' => $cancelForm->createView()];
336
        }
337
338
        $result = $this->getStepupService()->verifyYubikeyOtp($command);
339
340
        if ($result->didOtpVerificationFail()) {
341
            $form->addError(new FormError('gateway.form.verify_yubikey.otp_verification_failed'));
342
343
            // OTP field is rendered empty in the template.
344
            return ['form' => $form->createView(), 'cancelForm' => $cancelForm->createView()];
345
        } elseif (!$result->didPublicIdMatch()) {
346
            $form->addError(new FormError('gateway.form.verify_yubikey.public_id_mismatch'));
347
348
            // OTP field is rendered empty in the template.
349
            return ['form' => $form->createView(), 'cancelForm' => $cancelForm->createView()];
350
        }
351
352
        $this->getResponseContext()->markSecondFactorVerified();
353
        $this->getAuthenticationLogger()->logSecondFactorAuthentication($originalRequestId);
354
355
        $logger->info(
356
            sprintf(
357
                'Marked Yubikey Second Factor "%s" as verified, forwarding to Saml Proxy to respond',
358
                $selectedSecondFactor
359
            )
360
        );
361
362
        return $this->forward($context->getResponseAction());
363
    }
364
365
    /**
366
     * @Template
367
     * @param Request $request
368
     * @return array|Response
369
     */
370
    public function verifySmsSecondFactorAction(Request $request)
371
    {
372
        $context = $this->getResponseContext();
373
        $originalRequestId = $context->getInResponseTo();
374
375
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
376
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
377
378
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
379
380
        $logger->notice('Verifying possession of SMS second factor, preparing to send');
381
382
        $command = new SendSmsChallengeCommand();
383
        $command->secondFactorId = $selectedSecondFactor;
384
385
        $form = $this->createForm(SendSmsChallengeType::class, $command)->handleRequest($request);
386
        $cancelForm = $this->buildCancelAuthenticationForm()->handleRequest($request);
387
388
        $stepupService = $this->getStepupService();
389
        $phoneNumber = InternationalPhoneNumber::fromStringFormat(
390
            $stepupService->getSecondFactorIdentifier($selectedSecondFactor)
391
        );
392
393
        $otpRequestsRemaining = $stepupService->getSmsOtpRequestsRemainingCount();
394
        $maximumOtpRequests = $stepupService->getSmsMaximumOtpRequestsCount();
395
        $viewVariables = ['otpRequestsRemaining' => $otpRequestsRemaining, 'maximumOtpRequests' => $maximumOtpRequests];
396
397
        if ($form->isSubmitted() && !$form->isValid()) {
398
            return array_merge(
399
                $viewVariables,
400
                ['phoneNumber' => $phoneNumber, 'form' => $form->createView(), 'cancelForm' => $cancelForm->createView()]
401
            );
402
        }
403
404
        $logger->notice('Verifying possession of SMS second factor, sending challenge per SMS');
405
406
        if (!$stepupService->sendSmsChallenge($command)) {
407
            $form->addError(new FormError('gateway.form.send_sms_challenge.sms_sending_failed'));
408
409
            return array_merge(
410
                $viewVariables,
411
                ['phoneNumber' => $phoneNumber, 'form' => $form->createView(), 'cancelForm' => $cancelForm->createView()]
412
            );
413
        }
414
415
        return $this->redirect($this->generateUrl('gateway_verify_second_factor_sms_verify_challenge'));
416
    }
417
418
    /**
419
     * @Template
420
     * @param Request $request
421
     * @return array|Response
422
     */
423
    public function verifySmsSecondFactorChallengeAction(Request $request)
424
    {
425
        $context = $this->getResponseContext();
426
        $originalRequestId = $context->getInResponseTo();
427
428
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
429
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
430
431
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
432
433
        $command = new VerifyPossessionOfPhoneCommand();
434
        $form = $this->createForm(VerifySmsChallengeType::class, $command)->handleRequest($request);
435
        $cancelForm = $this->buildCancelAuthenticationForm()->handleRequest($request);
436
437 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...
438
            return ['form' => $form->createView(), 'cancelForm' => $cancelForm->createView()];
439
        }
440
441
        $logger->notice('Verifying input SMS challenge matches');
442
443
        $verification = $this->getStepupService()->verifySmsChallenge($command);
444
445
        if ($verification->wasSuccessful()) {
446
            $this->getStepupService()->clearSmsVerificationState();
447
448
            $this->getResponseContext()->markSecondFactorVerified();
449
            $this->getAuthenticationLogger()->logSecondFactorAuthentication($originalRequestId);
450
451
            $logger->info(
452
                sprintf(
453
                    'Marked Sms Second Factor "%s" as verified, forwarding to Saml Proxy to respond',
454
                    $selectedSecondFactor
455
                )
456
            );
457
458
            return $this->forward($context->getResponseAction());
459
        } elseif ($verification->didOtpExpire()) {
460
            $logger->notice('SMS challenge expired');
461
            $form->addError(new FormError('gateway.form.send_sms_challenge.challenge_expired'));
462
        } elseif ($verification->wasAttemptedTooManyTimes()) {
463
            $logger->notice('SMS challenge verification was attempted too many times');
464
            $form->addError(new FormError('gateway.form.send_sms_challenge.too_many_attempts'));
465
        } else {
466
            $logger->notice('SMS challenge did not match');
467
            $form->addError(new FormError('gateway.form.send_sms_challenge.sms_challenge_incorrect'));
468
        }
469
470
        return ['form' => $form->createView(), 'cancelForm' => $cancelForm->createView()];
471
    }
472
473
    /**
474
     * @Template
475
     */
476
    public function initiateU2fAuthenticationAction()
477
    {
478
        $context = $this->getResponseContext();
479
        $originalRequestId = $context->getInResponseTo();
480
481
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
482
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
483
484
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
485
        $stepupService = $this->getStepupService();
486
487
        $cancelFormAction = $this->generateUrl('gateway_verify_second_factor_u2f_cancel_authentication');
488
        $cancelForm =
489
            $this->createForm(CancelSecondFactorVerificationType::class, null, ['action' => $cancelFormAction]);
490
491
        $logger->notice('Verifying possession of U2F second factor, looking for registration matching key handle');
492
493
        $service = $this->get('surfnet_stepup_u2f_verification.service.u2f_verification');
494
        $keyHandle = new KeyHandle($stepupService->getSecondFactorIdentifier($selectedSecondFactor));
495
        $registration = $service->findRegistrationByKeyHandle($keyHandle);
496
497
        if ($registration === null) {
498
            $logger->critical(
499
                sprintf('No known registration for key handle of second factor "%s"', $selectedSecondFactor)
500
            );
501
            $this->addFlash('error', 'gateway.u2f.alert.unknown_registration');
502
503
            return ['authenticationFailed' => true, 'cancelForm' => $cancelForm->createView()];
504
        }
505
506
        $logger->notice('Creating sign request');
507
508
        $signRequest = $service->createSignRequest($registration);
509
        $signResponse = new SignResponse();
510
511
        /** @var AttributeBagInterface $session */
512
        $session = $this->get('gateway.session.u2f');
513
        $session->set('request', $signRequest);
514
515
        $formAction = $this->generateUrl('gateway_verify_second_factor_u2f_verify_authentication');
516
        $form = $this->createForm(
517
            VerifyDeviceAuthenticationType::class,
518
            $signResponse,
519
            ['sign_request' => $signRequest, 'action' => $formAction]
520
        );
521
522
        return ['form' => $form->createView(), 'cancelForm' => $cancelForm->createView()];
523
    }
524
525
    /**
526
     * @Template("SurfnetStepupGatewayGatewayBundle:SecondFactor:initiateU2fAuthentication.html.twig")
527
     *
528
     * @param Request $request
529
     * @return array|Response
530
     */
531
    public function verifyU2fAuthenticationAction(Request $request)
532
    {
533
        $context = $this->getResponseContext();
534
        $originalRequestId = $context->getInResponseTo();
535
536
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
537
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
538
539
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
540
541
        $logger->notice('Received sign response from device');
542
543
        /** @var AttributeBagInterface $session */
544
        $session = $this->get('gateway.session.u2f');
545
        $signRequest = $session->get('request');
546
        $signResponse = new SignResponse();
547
548
        $formAction = $this->generateUrl('gateway_verify_second_factor_u2f_verify_authentication');
549
        $form = $this
550
            ->createForm(
551
                VerifyDeviceAuthenticationType::class,
552
                $signResponse,
553
                ['sign_request' => $signRequest, 'action' => $formAction]
554
            )
555
            ->handleRequest($request);
556
557
        $cancelFormAction = $this->generateUrl('gateway_verify_second_factor_u2f_cancel_authentication');
558
        $cancelForm =
559
            $this->createForm(CancelSecondFactorVerificationType::class, null, ['action' => $cancelFormAction]);
560
561
        if ($form->isSubmitted() && !$form->isValid()) {
562
            $logger->error('U2F authentication verification could not be started because device send illegal data');
563
            $this->addFlash('error', 'gateway.u2f.alert.error');
564
565
            return ['authenticationFailed' => true, 'cancelForm' => $cancelForm->createView()];
566
        }
567
568
        $service = $this->get('surfnet_stepup_u2f_verification.service.u2f_verification');
569
        $result = $service->verifyAuthentication($signRequest, $signResponse);
570
571
        if ($result->wasSuccessful()) {
572
            $context->markSecondFactorVerified();
573
            $this->getAuthenticationLogger()->logSecondFactorAuthentication($originalRequestId);
574
575
            $logger->info(
576
                sprintf(
577
                    'Marked U2F second factor "%s" as verified, forwarding to Saml Proxy to respond',
578
                    $selectedSecondFactor
579
                )
580
            );
581
582
            return $this->forward($context->getResponseAction());
583
        } elseif ($result->didDeviceReportError()) {
584
            $logger->error('U2F device reported error during authentication');
585
            $this->addFlash('error', 'gateway.u2f.alert.device_reported_an_error');
586
        } else {
587
            $logger->error('U2F authentication verification failed');
588
            $this->addFlash('error', 'gateway.u2f.alert.error');
589
        }
590
591
        return ['authenticationFailed' => true, 'cancelForm' => $cancelForm->createView()];
592
    }
593
594
    public function cancelAuthenticationAction()
595
    {
596
        return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendAuthenticationCancelledByUser');
597
    }
598
599
    /**
600
     * @return \Surfnet\StepupGateway\GatewayBundle\Service\StepupAuthenticationService
601
     */
602
    private function getStepupService()
603
    {
604
        return $this->get('gateway.service.stepup_authentication');
605
    }
606
607
    /**
608
     * @return ResponseContext
609
     */
610
    private function getResponseContext()
611
    {
612
        return $this->get($this->get('gateway.proxy.state_handler')->getResponseContextServiceId());
613
    }
614
615
    /**
616
     * @return \Surfnet\StepupGateway\GatewayBundle\Monolog\Logger\AuthenticationLogger
617
     */
618
    private function getAuthenticationLogger()
619
    {
620
        return $this->get('gateway.authentication_logger');
621
    }
622
623
    /**
624
     * @param ResponseContext $context
625
     * @param LoggerInterface $logger
626
     * @return string
627
     */
628
    private function getSelectedSecondFactor(ResponseContext $context, LoggerInterface $logger)
629
    {
630
        $selectedSecondFactor = $context->getSelectedSecondFactor();
631
632
        if (!$selectedSecondFactor) {
633
            $logger->error('Cannot verify possession of an unknown second factor');
634
635
            throw new BadRequestHttpException('Cannot verify possession of an unknown second factor.');
636
        }
637
638
        return $selectedSecondFactor;
639
    }
640
641
    private function selectAndRedirectTo(SecondFactor $secondFactor, ResponseContext $context)
642
    {
643
        $context->saveSelectedSecondFactor($secondFactor);
644
645
        $this->getStepupService()->clearSmsVerificationState();
646
647
        $secondFactorTypeService = $this->get('surfnet_stepup.service.second_factor_type');
648
        $secondFactorType = new SecondFactorType($secondFactor->secondFactorType);
649
650
        $route = 'gateway_verify_second_factor_';
651
        if ($secondFactorTypeService->isGssf($secondFactorType)) {
652
            $route .= 'gssf';
653
        } else {
654
            $route .= strtolower($secondFactor->secondFactorType);
655
        }
656
657
        return $this->redirect($this->generateUrl($route));
658
    }
659
660
    /**
661
     * @return Form
662
     */
663
    private function buildCancelAuthenticationForm()
664
    {
665
        $cancelFormAction = $this->generateUrl('gateway_cancel_authentication');
666
        $cancelForm = $this->createForm(
667
            CancelAuthenticationType::class,
668
            null,
669
            ['action' => $cancelFormAction]
670
        );
671
        return $cancelForm;
672
    }
673
}
674