Completed
Pull Request — develop (#119)
by
unknown
04:45 queued 02:10
created

SecondFactorController   D

Complexity

Total Complexity 39

Size/Duplication

Total Lines 507
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 26

Importance

Changes 4
Bugs 0 Features 0
Metric Value
wmc 39
c 4
b 0
f 0
lcom 1
cbo 26
dl 0
loc 507
rs 4.2439

14 Methods

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