Completed
Pull Request — develop (#144)
by
unknown
07:14 queued 02:28
created

SecondFactorController::getSelectedSecondFactor()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 6
nc 2
nop 2
1
<?php
2
3
/**
4
 * Copyright 2014 SURFnet bv
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 *     http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18
19
namespace Surfnet\StepupGateway\GatewayBundle\Controller;
20
21
use Psr\Log\LoggerInterface;
22
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
23
use Surfnet\StepupBundle\Command\VerifyPossessionOfPhoneCommand;
24
use Surfnet\StepupBundle\Value\PhoneNumber\InternationalPhoneNumber;
25
use Surfnet\StepupBundle\Value\SecondFactorType;
26
use Surfnet\StepupGateway\GatewayBundle\Command\ChooseSecondFactorCommand;
27
use Surfnet\StepupGateway\GatewayBundle\Command\SendSmsChallengeCommand;
28
use Surfnet\StepupGateway\GatewayBundle\Command\VerifyYubikeyOtpCommand;
29
use Surfnet\StepupGateway\GatewayBundle\Entity\SecondFactor;
30
use Surfnet\StepupGateway\GatewayBundle\Exception\InvalidArgumentException;
31
use Surfnet\StepupGateway\GatewayBundle\Exception\LoaCannotBeGivenException;
32
use Surfnet\StepupGateway\GatewayBundle\Exception\RuntimeException;
33
use Surfnet\StepupGateway\GatewayBundle\Saml\ResponseContext;
34
use Surfnet\StepupGateway\U2fVerificationBundle\Value\KeyHandle;
35
use Surfnet\StepupU2fBundle\Dto\SignResponse;
36
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
37
use Symfony\Component\Form\FormError;
38
use Symfony\Component\HttpFoundation\RedirectResponse;
39
use Symfony\Component\HttpFoundation\Request;
40
use Symfony\Component\HttpFoundation\Response;
41
use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBagInterface;
42
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
43
44
/**
45
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
46
 */
47
class SecondFactorController extends Controller
48
{
49
    public function selectSecondFactorForVerificationAction()
50
    {
51
        $context = $this->getResponseContext();
52
        $originalRequestId = $context->getInResponseTo();
53
54
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
55
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
56
        $logger->notice('Determining which second factor to use...');
57
58
        try {
59
            // Retrieve all requirements to determine the required LoA
60
            $requestedLoa = $context->getRequiredLoa();
61
            $spConfiguredLoas = $context->getServiceProvider()->get('configuredLoas');
62
63
            $normalizedIdpSho = $context->getNormalizedSchacHomeOrganization();
64
            $normalizedUserSho = $this->getStepupService()->getNormalizedUserShoByIdentityNameId($context->getIdentityNameId());
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 128 characters

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Let’s take a look at an example:

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

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

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

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

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

Available Fixes

  1. Change the type-hint for the parameter:

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

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

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

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

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

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

Loading history...
406
            );
407
        }
408
409
        return $this->redirect($this->generateUrl('gateway_verify_second_factor_sms_verify_challenge'));
410
    }
411
412
    /**
413
     * @Template
414
     * @param Request $request
415
     * @return array|Response
416
     */
417
    public function verifySmsSecondFactorChallengeAction(Request $request)
418
    {
419
        $context = $this->getResponseContext();
420
        $originalRequestId = $context->getInResponseTo();
421
422
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
423
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
424
425
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
426
427
        $command = new VerifyPossessionOfPhoneCommand();
428
        $form = $this->createForm('gateway_verify_sms_challenge', $command)->handleRequest($request);
429
430
        $cancelFormAction = $this->generateUrl('gateway_cancel_authentication');
431
        $cancelForm = $this->createForm(
432
            'gateway_cancel_authentication',
433
            null,
434
            ['action' => $cancelFormAction]
435
        )->handleRequest($request);
436
437
        if (!$form->isValid()) {
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('gateway_cancel_second_factor_verification', 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
            'surfnet_stepup_u2f_verify_device_authentication',
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
                'surfnet_stepup_u2f_verify_device_authentication',
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('gateway_cancel_second_factor_verification', null, ['action' => $cancelFormAction]);
560
561
        if (!$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