Completed
Push — test-gssp-jodi ( e7ebf3 )
by Joost van
02:57
created

SecondFactorController   D

Complexity

Total Complexity 39

Size/Duplication

Total Lines 506
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 24

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 39
c 2
b 0
f 0
lcom 1
cbo 24
dl 0
loc 506
rs 4.4408

14 Methods

Rating   Name   Duplication   Size   Complexity  
B selectSecondFactorForVerificationAction() 0 79 6
B verifyGssfAction() 0 37 2
B gssfVerifiedAction() 0 32 2
B verifyYubiKeySecondFactorAction() 0 52 5
B verifySmsSecondFactorAction() 0 44 4
B verifySmsSecondFactorChallengeAction() 0 52 6
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
A selectAndRedirectTo() 0 18 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\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
        $requiredLoa = $this
55
            ->getStepupService()
56
            ->resolveHighestRequiredLoa(
57
                $context->getRequiredLoa(),
58
                $context->getIdentityNameId(),
59
                $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...
60
            );
61
62
        if ($requiredLoa === null) {
63
            $logger->notice(
64
                'No valid required Loa can be determined, no authentication is possible, Loa cannot be given'
65
            );
66
67
            return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendLoaCannotBeGiven');
68
        } else {
69
            $logger->notice(sprintf('Determined that the required Loa is "%s"', $requiredLoa));
70
        }
71
72
        if ($this->getStepupService()->isIntrinsicLoa($requiredLoa)) {
73
            $this->get('gateway.authentication_logger')->logIntrinsicLoaAuthentication($originalRequestId);
74
75
            return $this->forward($context->getResponseAction());
76
        }
77
78
        $secondFactorCollection = $this
79
            ->getStepupService()
80
            ->determineViableSecondFactors($context->getIdentityNameId(), $requiredLoa);
81
82
        if (count($secondFactorCollection) === 0) {
83
            $logger->notice('No second factors can give the determined Loa');
84
85
            return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendLoaCannotBeGiven');
86
        }
87
88
        // will be replaced by a second factor selection screen once we support multiple
89
        /** @var \Surfnet\StepupGateway\GatewayBundle\Entity\SecondFactor $secondFactor */
90
        $secondFactor = $secondFactorCollection->first();
91
        // when multiple second factors are supported this should be moved into the
92
        // StepUpAuthenticationService::determineViableSecondFactors and handled in a performant way
93
        // currently keeping this here for visibility
94
        if (!$this->get('gateway.service.whitelist')->contains($secondFactor->institution)) {
95
            $logger->notice(sprintf(
96
                'Second factor "%s" is listed for institution "%s" which is not on the whitelist, sending Loa '
97
                . 'cannot be given response',
98
                $secondFactor->secondFactorId,
99
                $secondFactor->institution
100
            ));
101
102
            return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendLoaCannotBeGiven');
103
        }
104
105
        $logger->notice(sprintf(
106
            'Found "%d" second factors, using second factor of type "%s"',
107
            count($secondFactorCollection),
108
            $secondFactor->secondFactorType
109
        ));
110
111
        $context->saveSelectedSecondFactor($secondFactor);
112
113
        $this->getStepupService()->clearSmsVerificationState();
114
115
        $route = 'gateway_verify_second_factor_';
116
        if ($secondFactor->isGssf()) {
0 ignored issues
show
Bug introduced by
The method isGssf() does not seem to exist on object<Surfnet\StepupGat...le\Entity\SecondFactor>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

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

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
538
539
        $route = 'gateway_verify_second_factor_';
540
        if ($secondFactorTypeService->isGssf($secondFactorType)) {
541
            $route .= 'gssf';
542
        } else {
543
            $route .= strtolower($secondFactor->secondFactorType);
544
        }
545
546
        return $this->redirect($this->generateUrl($route));
547
    }
548
}
549