Completed
Push — master ( 1e88b2...6b6b05 )
by Boy
03:57
created

SecondFactorController   C

Complexity

Total Complexity 36

Size/Duplication

Total Lines 481
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 22

Importance

Changes 8
Bugs 0 Features 2
Metric Value
wmc 36
c 8
b 0
f 2
lcom 1
cbo 22
dl 0
loc 481
rs 5.0946

13 Methods

Rating   Name   Duplication   Size   Complexity  
B verifyGssfAction() 0 37 2
B gssfVerifiedAction() 0 32 2
B verifyYubiKeySecondFactorAction() 0 52 5
A initiateU2fAuthenticationAction() 0 48 2
B verifyU2fAuthenticationAction() 0 62 4
A cancelU2fAuthenticationAction() 0 4 1
A getStepupService() 0 4 1
A getResponseContext() 0 4 1
A getAuthenticationLogger() 0 4 1
A getSelectedSecondFactor() 0 12 2
B verifySmsSecondFactorAction() 0 44 4
B verifySmsSecondFactorChallengeAction() 0 52 6
B selectSecondFactorForVerificationAction() 0 73 5
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\StepupGateway\GatewayBundle\Command\SendSmsChallengeCommand;
26
use Surfnet\StepupGateway\GatewayBundle\Command\VerifyYubikeyOtpCommand;
27
use Surfnet\StepupGateway\GatewayBundle\Exception\RuntimeException;
28
use Surfnet\StepupGateway\GatewayBundle\Saml\ResponseContext;
29
use Surfnet\StepupGateway\U2fVerificationBundle\Value\KeyHandle;
30
use Surfnet\StepupU2fBundle\Dto\SignResponse;
31
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
32
use Symfony\Component\Form\FormError;
33
use Symfony\Component\HttpFoundation\Request;
34
use Symfony\Component\HttpFoundation\Response;
35
use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBagInterface;
36
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
37
38
/**
39
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
40
 */
41
class SecondFactorController extends Controller
42
{
43
    public function selectSecondFactorForVerificationAction()
44
    {
45
        $context = $this->getResponseContext();
46
        $originalRequestId = $context->getInResponseTo();
47
48
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
49
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
50
        $logger->notice('Determining which second factor to use...');
51
52
        $requiredLoa = $this
53
            ->getStepupService()
54
            ->resolveHighestRequiredLoa(
55
                $context->getRequiredLoa(),
56
                $context->getServiceProvider(),
57
                $context->getAuthenticatingIdp()
58
            );
59
60
        if ($requiredLoa === null) {
61
            $logger->notice(
62
                'No valid required Loa can be determined, no authentication is possible, Loa cannot be given'
63
            );
64
65
            return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendLoaCannotBeGiven');
66
        } else {
67
            $logger->notice(sprintf('Determined that the required Loa is "%s"', $requiredLoa));
68
        }
69
70
        if ($this->getStepupService()->isIntrinsicLoa($requiredLoa)) {
71
            $this->get('gateway.authentication_logger')->logIntrinsicLoaAuthentication($originalRequestId);
72
73
            return $this->forward($context->getResponseAction());
74
        }
75
76
        $secondFactorCollection = $this
77
            ->getStepupService()
78
            ->determineViableSecondFactors($context->getIdentityNameId(), $requiredLoa);
79
80
        if (count($secondFactorCollection) === 0) {
81
            $logger->notice('No second factors can give the determined Loa');
82
83
            return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendLoaCannotBeGiven');
84
        }
85
86
        // will be replaced by a second factor selection screen once we support multiple
87
        /** @var \Surfnet\StepupGateway\GatewayBundle\Entity\SecondFactor $secondFactor */
88
        $secondFactor = $secondFactorCollection->first();
89
        // when multiple second factors are supported this should be moved into the
90
        // StepUpAuthenticationService::determineViableSecondFactors and handled in a performant way
91
        // currently keeping this here for visibility
92
        if (!$this->get('gateway.service.whitelist')->contains($secondFactor->institution)) {
93
            $logger->notice(sprintf(
94
                'Second factor "%s" is listed for institution "%s" which is not on the whitelist, sending Loa '
95
                . 'cannot be given response',
96
                $secondFactor->secondFactorId,
97
                $secondFactor->institution
98
            ));
99
100
            return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendLoaCannotBeGiven');
101
        }
102
103
        $logger->notice(sprintf(
104
            'Found "%d" second factors, using second factor of type "%s"',
105
            count($secondFactorCollection),
106
            $secondFactor->secondFactorType
107
        ));
108
109
        $context->saveSelectedSecondFactor($secondFactor->secondFactorId);
110
111
        $this->getStepupService()->clearSmsVerificationState();
112
113
        $route = 'gateway_verify_second_factor_' . strtolower($secondFactor->secondFactorType);
114
        return $this->redirect($this->generateUrl($route));
115
    }
116
117
    public function verifyGssfAction()
118
    {
119
        $context = $this->getResponseContext();
120
        $originalRequestId = $context->getInResponseTo();
121
122
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
123
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
124
        $logger->info('Received request to verify GSSF');
125
126
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
127
128
        $logger->info(sprintf(
129
            'Selected GSSF "%s" for verfication, forwarding to Saml handling',
130
            $selectedSecondFactor
131
        ));
132
133
        /** @var \Surfnet\StepupGateway\GatewayBundle\Service\SecondFactorService $secondFactorService */
134
        $secondFactorService = $this->get('gateway.service.second_factor_service');
135
        /** @var \Surfnet\StepupGateway\GatewayBundle\Entity\SecondFactor $secondFactor */
136
        $secondFactor = $secondFactorService->findByUuid($selectedSecondFactor);
137
        if (!$secondFactor) {
138
            $logger->critical(sprintf(
139
                'Requested verification of GSSF "%s", however that Second Factor no longer exists',
140
                $selectedSecondFactor
141
            ));
142
143
            throw new RuntimeException('Verification of selected second factor that no longer exists');
144
        }
145
146
        return $this->forward(
147
            'SurfnetStepupGatewaySamlStepupProviderBundle:SamlProxy:sendSecondFactorVerificationAuthnRequest',
148
            [
149
                'provider' => $secondFactor->secondFactorType,
150
                'subjectNameId' => $secondFactor->secondFactorIdentifier
151
            ]
152
        );
153
    }
154
155
    public function gssfVerifiedAction()
156
    {
157
        $context = $this->getResponseContext();
158
        $originalRequestId = $context->getInResponseTo();
159
160
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
161
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
162
        $logger->info('Attempting to mark GSSF as verified');
163
164
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
165
166
        /** @var \Surfnet\StepupGateway\GatewayBundle\Entity\SecondFactor $secondFactor */
167
        $secondFactor = $this->get('gateway.service.second_factor_service')->findByUuid($selectedSecondFactor);
168
        if (!$secondFactor) {
169
            $logger->critical(sprintf(
170
                'Verification of GSSF "%s" succeeded, however that Second Factor no longer exists',
171
                $selectedSecondFactor
172
            ));
173
174
            throw new RuntimeException('Verification of selected second factor that no longer exists');
175
        }
176
177
        $context->markSecondFactorVerified();
178
        $this->getAuthenticationLogger()->logSecondFactorAuthentication($originalRequestId);
179
180
        $logger->info(sprintf(
181
            'Marked GSSF "%s" as verified, forwarding to Gateway controller to respond',
182
            $selectedSecondFactor
183
        ));
184
185
        return $this->forward($context->getResponseAction());
186
    }
187
188
    /**
189
     * @Template
190
     * @param Request $request
191
     * @return array|Response
192
     */
193
    public function verifyYubiKeySecondFactorAction(Request $request)
194
    {
195
        $context = $this->getResponseContext();
196
        $originalRequestId = $context->getInResponseTo();
197
198
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
199
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
200
201
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
202
203
        $logger->notice('Verifying possession of Yubikey second factor');
204
205
        $command = new VerifyYubikeyOtpCommand();
206
        $command->secondFactorId = $selectedSecondFactor;
207
208
        $form = $this->createForm('gateway_verify_yubikey_otp', $command)->handleRequest($request);
209
210
        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...
211
            return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendAuthenticationCancelledByUser');
212
        }
213
214
        if (!$form->isValid()) {
215
            // OTP field is rendered empty in the template.
216
            return ['form' => $form->createView()];
217
        }
218
219
        $result = $this->getStepupService()->verifyYubikeyOtp($command);
220
221
        if ($result->didOtpVerificationFail()) {
222
            $form->addError(new FormError('gateway.form.verify_yubikey.otp_verification_failed'));
223
224
            // OTP field is rendered empty in the template.
225
            return ['form' => $form->createView()];
226
        } elseif (!$result->didPublicIdMatch()) {
227
            $form->addError(new FormError('gateway.form.verify_yubikey.public_id_mismatch'));
228
229
            // OTP field is rendered empty in the template.
230
            return ['form' => $form->createView()];
231
        }
232
233
        $this->getResponseContext()->markSecondFactorVerified();
234
        $this->getAuthenticationLogger()->logSecondFactorAuthentication($originalRequestId);
235
236
        $logger->info(
237
            sprintf(
238
                'Marked Yubikey Second Factor "%s" as verified, forwarding to Saml Proxy to respond',
239
                $selectedSecondFactor
240
            )
241
        );
242
243
        return $this->forward($context->getResponseAction());
244
    }
245
246
    /**
247
     * @Template
248
     * @param Request $request
249
     * @return array|Response
250
     */
251
    public function verifySmsSecondFactorAction(Request $request)
252
    {
253
        $context = $this->getResponseContext();
254
        $originalRequestId = $context->getInResponseTo();
255
256
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
257
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
258
259
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
260
261
        $logger->notice('Verifying possession of SMS second factor, preparing to send');
262
263
        $command = new SendSmsChallengeCommand();
264
        $command->secondFactorId = $selectedSecondFactor;
265
266
        $form = $this->createForm('gateway_send_sms_challenge', $command)->handleRequest($request);
267
268
        $stepupService = $this->getStepupService();
269
        $phoneNumber = InternationalPhoneNumber::fromStringFormat(
270
            $stepupService->getSecondFactorIdentifier($selectedSecondFactor)
271
        );
272
273
        $otpRequestsRemaining = $stepupService->getSmsOtpRequestsRemainingCount();
274
        $maximumOtpRequests = $stepupService->getSmsMaximumOtpRequestsCount();
275
        $viewVariables = ['otpRequestsRemaining' => $otpRequestsRemaining, 'maximumOtpRequests' => $maximumOtpRequests];
276
277
        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...
278
            return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendAuthenticationCancelledByUser');
279
        }
280
281
        if (!$form->isValid()) {
282
            return array_merge($viewVariables, ['phoneNumber' => $phoneNumber, 'form' => $form->createView()]);
283
        }
284
285
        $logger->notice('Verifying possession of SMS second factor, sending challenge per SMS');
286
287
        if (!$stepupService->sendSmsChallenge($command)) {
288
            $form->addError(new FormError('gateway.form.send_sms_challenge.sms_sending_failed'));
289
290
            return array_merge($viewVariables, ['phoneNumber' => $phoneNumber, 'form' => $form->createView()]);
291
        }
292
293
        return $this->redirect($this->generateUrl('gateway_verify_second_factor_sms_verify_challenge'));
294
    }
295
296
    /**
297
     * @Template
298
     * @param Request $request
299
     * @return array|Response
300
     */
301
    public function verifySmsSecondFactorChallengeAction(Request $request)
302
    {
303
        $context = $this->getResponseContext();
304
        $originalRequestId = $context->getInResponseTo();
305
306
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
307
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
308
309
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
310
311
        $command = new VerifyPossessionOfPhoneCommand();
312
        $form = $this->createForm('gateway_verify_sms_challenge', $command)->handleRequest($request);
313
314
        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...
315
            return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendAuthenticationCancelledByUser');
316
        }
317
318
        if (!$form->isValid()) {
319
            return ['form' => $form->createView()];
320
        }
321
322
        $logger->notice('Verifying input SMS challenge matches');
323
324
        $verification = $this->getStepupService()->verifySmsChallenge($command);
325
326
        if ($verification->wasSuccessful()) {
327
            $this->getStepupService()->clearSmsVerificationState();
328
329
            $this->getResponseContext()->markSecondFactorVerified();
330
            $this->getAuthenticationLogger()->logSecondFactorAuthentication($originalRequestId);
331
332
            $logger->info(
333
                sprintf(
334
                    'Marked Sms Second Factor "%s" as verified, forwarding to Saml Proxy to respond',
335
                    $selectedSecondFactor
336
                )
337
            );
338
339
            return $this->forward($context->getResponseAction());
340
        } elseif ($verification->didOtpExpire()) {
341
            $logger->notice('SMS challenge expired');
342
            $form->addError(new FormError('gateway.form.send_sms_challenge.challenge_expired'));
343
        } elseif ($verification->wasAttemptedTooManyTimes()) {
344
            $logger->notice('SMS challenge verification was attempted too many times');
345
            $form->addError(new FormError('gateway.form.send_sms_challenge.too_many_attempts'));
346
        } else {
347
            $logger->notice('SMS challenge did not match');
348
            $form->addError(new FormError('gateway.form.send_sms_challenge.sms_challenge_incorrect'));
349
        }
350
351
        return ['form' => $form->createView()];
352
    }
353
354
    /**
355
     * @Template
356
     */
357
    public function initiateU2fAuthenticationAction()
358
    {
359
        $context = $this->getResponseContext();
360
        $originalRequestId = $context->getInResponseTo();
361
362
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
363
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
364
365
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
366
        $stepupService = $this->getStepupService();
367
368
        $cancelFormAction = $this->generateUrl('gateway_verify_second_factor_u2f_cancel_authentication');
369
        $cancelForm =
370
            $this->createForm('gateway_cancel_second_factor_verification', null, ['action' => $cancelFormAction]);
371
372
        $logger->notice('Verifying possession of U2F second factor, looking for registration matching key handle');
373
374
        $service = $this->get('surfnet_stepup_u2f_verification.service.u2f_verification');
375
        $keyHandle = new KeyHandle($stepupService->getSecondFactorIdentifier($selectedSecondFactor));
376
        $registration = $service->findRegistrationByKeyHandle($keyHandle);
377
378
        if ($registration === null) {
379
            $logger->critical(
380
                sprintf('No known registration for key handle of second factor "%s"', $selectedSecondFactor)
381
            );
382
            $this->addFlash('error', 'gateway.u2f.alert.unknown_registration');
383
384
            return ['authenticationFailed' => true, 'cancelForm' => $cancelForm->createView()];
385
        }
386
387
        $logger->notice('Creating sign request');
388
389
        $signRequest = $service->createSignRequest($registration);
390
        $signResponse = new SignResponse();
391
392
        /** @var AttributeBagInterface $session */
393
        $session = $this->get('gateway.session.u2f');
394
        $session->set('request', $signRequest);
395
396
        $formAction = $this->generateUrl('gateway_verify_second_factor_u2f_verify_authentication');
397
        $form = $this->createForm(
398
            'surfnet_stepup_u2f_verify_device_authentication',
399
            $signResponse,
400
            ['sign_request' => $signRequest, 'action' => $formAction]
401
        );
402
403
        return ['form' => $form->createView(), 'cancelForm' => $cancelForm->createView()];
404
    }
405
406
    /**
407
     * @Template("SurfnetStepupGatewayGatewayBundle:SecondFactor:initiateU2fAuthentication.html.twig")
408
     *
409
     * @param Request $request
410
     * @return array|Response
411
     */
412
    public function verifyU2fAuthenticationAction(Request $request)
413
    {
414
        $context = $this->getResponseContext();
415
        $originalRequestId = $context->getInResponseTo();
416
417
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
418
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
419
420
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
421
422
        $logger->notice('Received sign response from device');
423
424
        /** @var AttributeBagInterface $session */
425
        $session = $this->get('gateway.session.u2f');
426
        $signRequest = $session->get('request');
427
        $signResponse = new SignResponse();
428
429
        $formAction = $this->generateUrl('gateway_verify_second_factor_u2f_verify_authentication');
430
        $form = $this
431
            ->createForm(
432
                'surfnet_stepup_u2f_verify_device_authentication',
433
                $signResponse,
434
                ['sign_request' => $signRequest, 'action' => $formAction]
435
            )
436
            ->handleRequest($request);
437
438
        $cancelFormAction = $this->generateUrl('gateway_verify_second_factor_u2f_cancel_authentication');
439
        $cancelForm =
440
            $this->createForm('gateway_cancel_second_factor_verification', null, ['action' => $cancelFormAction]);
441
442
        if (!$form->isValid()) {
443
            $logger->error('U2F authentication verification could not be started because device send illegal data');
444
            $this->addFlash('error', 'gateway.u2f.alert.error');
445
446
            return ['authenticationFailed' => true, 'cancelForm' => $cancelForm->createView()];
447
        }
448
449
        $service = $this->get('surfnet_stepup_u2f_verification.service.u2f_verification');
450
        $result = $service->verifyAuthentication($signRequest, $signResponse);
451
452
        if ($result->wasSuccessful()) {
453
            $context->markSecondFactorVerified();
454
            $this->getAuthenticationLogger()->logSecondFactorAuthentication($originalRequestId);
455
456
            $logger->info(
457
                sprintf(
458
                    'Marked U2F second factor "%s" as verified, forwarding to Saml Proxy to respond',
459
                    $selectedSecondFactor
460
                )
461
            );
462
463
            return $this->forward($context->getResponseAction());
464
        } elseif ($result->didDeviceReportError()) {
465
            $logger->error('U2F device reported error during authentication');
466
            $this->addFlash('error', 'gateway.u2f.alert.device_reported_an_error');
467
        } else {
468
            $logger->error('U2F authentication verification failed');
469
            $this->addFlash('error', 'gateway.u2f.alert.error');
470
        }
471
472
        return ['authenticationFailed' => true, 'cancelForm' => $cancelForm->createView()];
473
    }
474
475
    public function cancelU2fAuthenticationAction()
476
    {
477
        return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendAuthenticationCancelledByUser');
478
    }
479
480
    /**
481
     * @return \Surfnet\StepupGateway\GatewayBundle\Service\StepupAuthenticationService
482
     */
483
    private function getStepupService()
484
    {
485
        return $this->get('gateway.service.stepup_authentication');
486
    }
487
488
    /**
489
     * @return ResponseContext
490
     */
491
    private function getResponseContext()
492
    {
493
        return $this->get($this->get('gateway.proxy.state_handler')->getResponseContextServiceId());
494
    }
495
496
    /**
497
     * @return \Surfnet\StepupGateway\GatewayBundle\Monolog\Logger\AuthenticationLogger
498
     */
499
    private function getAuthenticationLogger()
500
    {
501
        return $this->get('gateway.authentication_logger');
502
    }
503
504
    /**
505
     * @param ResponseContext $context
506
     * @param LoggerInterface $logger
507
     * @return string
508
     */
509
    private function getSelectedSecondFactor(ResponseContext $context, LoggerInterface $logger)
510
    {
511
        $selectedSecondFactor = $context->getSelectedSecondFactor();
512
513
        if (!$selectedSecondFactor) {
514
            $logger->error('Cannot verify possession of an unknown second factor');
515
516
            throw new BadRequestHttpException('Cannot verify possession of an unknown second factor.');
517
        }
518
519
        return $selectedSecondFactor;
520
    }
521
}
522