Completed
Push — master ( 24e2c8...aa2c21 )
by
unknown
02:53
created

selectSecondFactorForVerificationAction()   C

Complexity

Conditions 7
Paths 7

Size

Total Lines 90
Code Lines 53

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 90
rs 6.5083
cc 7
eloc 53
nc 7
nop 0

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\SendSmsChallengeCommand;
27
use Surfnet\StepupGateway\GatewayBundle\Command\VerifyYubikeyOtpCommand;
28
use Surfnet\StepupGateway\GatewayBundle\Exception\LoaCannotBeGivenException;
29
use Surfnet\StepupGateway\GatewayBundle\Exception\RuntimeException;
30
use Surfnet\StepupGateway\GatewayBundle\Saml\ResponseContext;
31
use Surfnet\StepupGateway\U2fVerificationBundle\Value\KeyHandle;
32
use Surfnet\StepupU2fBundle\Dto\SignResponse;
33
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
34
use Symfony\Component\Form\FormError;
35
use Symfony\Component\HttpFoundation\Request;
36
use Symfony\Component\HttpFoundation\Response;
37
use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBagInterface;
38
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
39
40
/**
41
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
42
 */
43
class SecondFactorController extends Controller
44
{
45
    public function selectSecondFactorForVerificationAction()
46
    {
47
        $context = $this->getResponseContext();
48
        $originalRequestId = $context->getInResponseTo();
49
50
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
51
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
52
        $logger->notice('Determining which second factor to use...');
53
54
        try {
55
            $requiredLoa = $this
56
                ->getStepupService()
57
                ->resolveHighestRequiredLoa(
58
                    $context->getRequiredLoa(),
59
                    $context->getIdentityNameId(),
60
                    $context->getSchacHomeOrganization(),
61
                    $context->getServiceProvider()
0 ignored issues
show
Bug introduced by
It seems like $context->getServiceProvider() can be null; however, resolveHighestRequiredLoa() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

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