Completed
Push — feature/pdp-cbac ( e99222 )
by
unknown
13:10
created

selectSecondFactorForVerificationAction()   C

Complexity

Conditions 7
Paths 11

Size

Total Lines 90
Code Lines 54

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 54
nc 11
nop 1

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