Completed
Pull Request — develop (#143)
by
unknown
06:31 queued 03:35
created

SecondFactorController::chooseSecondFactorAction()   C

Complexity

Conditions 8
Paths 10

Size

Total Lines 98
Code Lines 60

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 98
rs 5.332
cc 8
eloc 60
nc 10
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\StepupBundle\Value\SecondFactorType;
26
use Surfnet\StepupGateway\GatewayBundle\Command\ChooseSecondFactorCommand;
27
use Surfnet\StepupGateway\GatewayBundle\Command\SendSmsChallengeCommand;
28
use Surfnet\StepupGateway\GatewayBundle\Command\VerifyYubikeyOtpCommand;
29
use Surfnet\StepupGateway\GatewayBundle\Entity\SecondFactor;
30
use Surfnet\StepupGateway\GatewayBundle\Exception\InvalidArgumentException;
31
use Surfnet\StepupGateway\GatewayBundle\Exception\LoaCannotBeGivenException;
32
use Surfnet\StepupGateway\GatewayBundle\Exception\RuntimeException;
33
use Surfnet\StepupGateway\GatewayBundle\Saml\ResponseContext;
34
use Surfnet\StepupGateway\U2fVerificationBundle\Value\KeyHandle;
35
use Surfnet\StepupU2fBundle\Dto\SignResponse;
36
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
37
use Symfony\Component\Form\FormError;
38
use Symfony\Component\HttpFoundation\RedirectResponse;
39
use Symfony\Component\HttpFoundation\Request;
40
use Symfony\Component\HttpFoundation\Response;
41
use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBagInterface;
42
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
43
44
/**
45
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
46
 */
47
class SecondFactorController extends Controller
48
{
49
    public function selectSecondFactorForVerificationAction()
50
    {
51
        $context = $this->getResponseContext();
52
        $originalRequestId = $context->getInResponseTo();
53
54
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
55
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
56
        $logger->notice('Determining which second factor to use...');
57
58
        try {
59
            // Retrieve all requirements to determine the required LoA
60
            $requestedLoa = $context->getRequiredLoa();
61
            $spConfiguredLoas = $context->getServiceProvider()->get('configuredLoas');
62
63
            $normalizedIdpSho = $context->getNormalizedSchacHomeOrganization();
64
            $normalizedUserSho = $this->getStepupService()->getNormalizedUserShoByIdentityNameId($context->getIdentityNameId());
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 128 characters

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Let’s take a look at an example:

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

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

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

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

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

Available Fixes

  1. Change the type-hint for the parameter:

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

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

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
318
            return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendAuthenticationCancelledByUser');
319
        }
320
321
        if (!$form->isValid()) {
322
            // OTP field is rendered empty in the template.
323
            return ['form' => $form->createView()];
324
        }
325
326
        $result = $this->getStepupService()->verifyYubikeyOtp($command);
327
328
        if ($result->didOtpVerificationFail()) {
329
            $form->addError(new FormError('gateway.form.verify_yubikey.otp_verification_failed'));
330
331
            // OTP field is rendered empty in the template.
332
            return ['form' => $form->createView()];
333
        } elseif (!$result->didPublicIdMatch()) {
334
            $form->addError(new FormError('gateway.form.verify_yubikey.public_id_mismatch'));
335
336
            // OTP field is rendered empty in the template.
337
            return ['form' => $form->createView()];
338
        }
339
340
        $this->getResponseContext()->markSecondFactorVerified();
341
        $this->getAuthenticationLogger()->logSecondFactorAuthentication($originalRequestId);
342
343
        $logger->info(
344
            sprintf(
345
                'Marked Yubikey Second Factor "%s" as verified, forwarding to Saml Proxy to respond',
346
                $selectedSecondFactor
347
            )
348
        );
349
350
        return $this->forward($context->getResponseAction());
351
    }
352
353
    /**
354
     * @Template
355
     * @param Request $request
356
     * @return array|Response
357
     */
358
    public function verifySmsSecondFactorAction(Request $request)
359
    {
360
        $context = $this->getResponseContext();
361
        $originalRequestId = $context->getInResponseTo();
362
363
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
364
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
365
366
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
367
368
        $logger->notice('Verifying possession of SMS second factor, preparing to send');
369
370
        $command = new SendSmsChallengeCommand();
371
        $command->secondFactorId = $selectedSecondFactor;
372
373
        $form = $this->createForm('gateway_send_sms_challenge', $command)->handleRequest($request);
374
375
        $stepupService = $this->getStepupService();
376
        $phoneNumber = InternationalPhoneNumber::fromStringFormat(
377
            $stepupService->getSecondFactorIdentifier($selectedSecondFactor)
378
        );
379
380
        $otpRequestsRemaining = $stepupService->getSmsOtpRequestsRemainingCount();
381
        $maximumOtpRequests = $stepupService->getSmsMaximumOtpRequestsCount();
382
        $viewVariables = ['otpRequestsRemaining' => $otpRequestsRemaining, 'maximumOtpRequests' => $maximumOtpRequests];
383
384
        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...
385
            return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendAuthenticationCancelledByUser');
386
        }
387
388
        if (!$form->isValid()) {
389
            return array_merge($viewVariables, ['phoneNumber' => $phoneNumber, 'form' => $form->createView()]);
390
        }
391
392
        $logger->notice('Verifying possession of SMS second factor, sending challenge per SMS');
393
394
        if (!$stepupService->sendSmsChallenge($command)) {
395
            $form->addError(new FormError('gateway.form.send_sms_challenge.sms_sending_failed'));
396
397
            return array_merge($viewVariables, ['phoneNumber' => $phoneNumber, 'form' => $form->createView()]);
398
        }
399
400
        return $this->redirect($this->generateUrl('gateway_verify_second_factor_sms_verify_challenge'));
401
    }
402
403
    /**
404
     * @Template
405
     * @param Request $request
406
     * @return array|Response
407
     */
408
    public function verifySmsSecondFactorChallengeAction(Request $request)
409
    {
410
        $context = $this->getResponseContext();
411
        $originalRequestId = $context->getInResponseTo();
412
413
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
414
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
415
416
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
417
418
        $command = new VerifyPossessionOfPhoneCommand();
419
        $form = $this->createForm('gateway_verify_sms_challenge', $command)->handleRequest($request);
420
421
        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...
422
            return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendAuthenticationCancelledByUser');
423
        }
424
425
        if (!$form->isValid()) {
426
            return ['form' => $form->createView()];
427
        }
428
429
        $logger->notice('Verifying input SMS challenge matches');
430
431
        $verification = $this->getStepupService()->verifySmsChallenge($command);
432
433
        if ($verification->wasSuccessful()) {
434
            $this->getStepupService()->clearSmsVerificationState();
435
436
            $this->getResponseContext()->markSecondFactorVerified();
437
            $this->getAuthenticationLogger()->logSecondFactorAuthentication($originalRequestId);
438
439
            $logger->info(
440
                sprintf(
441
                    'Marked Sms Second Factor "%s" as verified, forwarding to Saml Proxy to respond',
442
                    $selectedSecondFactor
443
                )
444
            );
445
446
            return $this->forward($context->getResponseAction());
447
        } elseif ($verification->didOtpExpire()) {
448
            $logger->notice('SMS challenge expired');
449
            $form->addError(new FormError('gateway.form.send_sms_challenge.challenge_expired'));
450
        } elseif ($verification->wasAttemptedTooManyTimes()) {
451
            $logger->notice('SMS challenge verification was attempted too many times');
452
            $form->addError(new FormError('gateway.form.send_sms_challenge.too_many_attempts'));
453
        } else {
454
            $logger->notice('SMS challenge did not match');
455
            $form->addError(new FormError('gateway.form.send_sms_challenge.sms_challenge_incorrect'));
456
        }
457
458
        return ['form' => $form->createView()];
459
    }
460
461
    /**
462
     * @Template
463
     */
464
    public function initiateU2fAuthenticationAction()
465
    {
466
        $context = $this->getResponseContext();
467
        $originalRequestId = $context->getInResponseTo();
468
469
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
470
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
471
472
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
473
        $stepupService = $this->getStepupService();
474
475
        $cancelFormAction = $this->generateUrl('gateway_verify_second_factor_u2f_cancel_authentication');
476
        $cancelForm =
477
            $this->createForm('gateway_cancel_second_factor_verification', null, ['action' => $cancelFormAction]);
478
479
        $logger->notice('Verifying possession of U2F second factor, looking for registration matching key handle');
480
481
        $service = $this->get('surfnet_stepup_u2f_verification.service.u2f_verification');
482
        $keyHandle = new KeyHandle($stepupService->getSecondFactorIdentifier($selectedSecondFactor));
483
        $registration = $service->findRegistrationByKeyHandle($keyHandle);
484
485
        if ($registration === null) {
486
            $logger->critical(
487
                sprintf('No known registration for key handle of second factor "%s"', $selectedSecondFactor)
488
            );
489
            $this->addFlash('error', 'gateway.u2f.alert.unknown_registration');
490
491
            return ['authenticationFailed' => true, 'cancelForm' => $cancelForm->createView()];
492
        }
493
494
        $logger->notice('Creating sign request');
495
496
        $signRequest = $service->createSignRequest($registration);
497
        $signResponse = new SignResponse();
498
499
        /** @var AttributeBagInterface $session */
500
        $session = $this->get('gateway.session.u2f');
501
        $session->set('request', $signRequest);
502
503
        $formAction = $this->generateUrl('gateway_verify_second_factor_u2f_verify_authentication');
504
        $form = $this->createForm(
505
            'surfnet_stepup_u2f_verify_device_authentication',
506
            $signResponse,
507
            ['sign_request' => $signRequest, 'action' => $formAction]
508
        );
509
510
        return ['form' => $form->createView(), 'cancelForm' => $cancelForm->createView()];
511
    }
512
513
    /**
514
     * @Template("SurfnetStepupGatewayGatewayBundle:SecondFactor:initiateU2fAuthentication.html.twig")
515
     *
516
     * @param Request $request
517
     * @return array|Response
518
     */
519
    public function verifyU2fAuthenticationAction(Request $request)
520
    {
521
        $context = $this->getResponseContext();
522
        $originalRequestId = $context->getInResponseTo();
523
524
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
525
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
526
527
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
528
529
        $logger->notice('Received sign response from device');
530
531
        /** @var AttributeBagInterface $session */
532
        $session = $this->get('gateway.session.u2f');
533
        $signRequest = $session->get('request');
534
        $signResponse = new SignResponse();
535
536
        $formAction = $this->generateUrl('gateway_verify_second_factor_u2f_verify_authentication');
537
        $form = $this
538
            ->createForm(
539
                'surfnet_stepup_u2f_verify_device_authentication',
540
                $signResponse,
541
                ['sign_request' => $signRequest, 'action' => $formAction]
542
            )
543
            ->handleRequest($request);
544
545
        $cancelFormAction = $this->generateUrl('gateway_verify_second_factor_u2f_cancel_authentication');
546
        $cancelForm =
547
            $this->createForm('gateway_cancel_second_factor_verification', null, ['action' => $cancelFormAction]);
548
549
        if (!$form->isValid()) {
550
            $logger->error('U2F authentication verification could not be started because device send illegal data');
551
            $this->addFlash('error', 'gateway.u2f.alert.error');
552
553
            return ['authenticationFailed' => true, 'cancelForm' => $cancelForm->createView()];
554
        }
555
556
        $service = $this->get('surfnet_stepup_u2f_verification.service.u2f_verification');
557
        $result = $service->verifyAuthentication($signRequest, $signResponse);
558
559
        if ($result->wasSuccessful()) {
560
            $context->markSecondFactorVerified();
561
            $this->getAuthenticationLogger()->logSecondFactorAuthentication($originalRequestId);
562
563
            $logger->info(
564
                sprintf(
565
                    'Marked U2F second factor "%s" as verified, forwarding to Saml Proxy to respond',
566
                    $selectedSecondFactor
567
                )
568
            );
569
570
            return $this->forward($context->getResponseAction());
571
        } elseif ($result->didDeviceReportError()) {
572
            $logger->error('U2F device reported error during authentication');
573
            $this->addFlash('error', 'gateway.u2f.alert.device_reported_an_error');
574
        } else {
575
            $logger->error('U2F authentication verification failed');
576
            $this->addFlash('error', 'gateway.u2f.alert.error');
577
        }
578
579
        return ['authenticationFailed' => true, 'cancelForm' => $cancelForm->createView()];
580
    }
581
582
    public function cancelU2fAuthenticationAction()
583
    {
584
        return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendAuthenticationCancelledByUser');
585
    }
586
587
    /**
588
     * @return \Surfnet\StepupGateway\GatewayBundle\Service\StepupAuthenticationService
589
     */
590
    private function getStepupService()
591
    {
592
        return $this->get('gateway.service.stepup_authentication');
593
    }
594
595
    /**
596
     * @return ResponseContext
597
     */
598
    private function getResponseContext()
599
    {
600
        return $this->get($this->get('gateway.proxy.state_handler')->getResponseContextServiceId());
601
    }
602
603
    /**
604
     * @return \Surfnet\StepupGateway\GatewayBundle\Monolog\Logger\AuthenticationLogger
605
     */
606
    private function getAuthenticationLogger()
607
    {
608
        return $this->get('gateway.authentication_logger');
609
    }
610
611
    /**
612
     * @param ResponseContext $context
613
     * @param LoggerInterface $logger
614
     * @return string
615
     */
616
    private function getSelectedSecondFactor(ResponseContext $context, LoggerInterface $logger)
617
    {
618
        $selectedSecondFactor = $context->getSelectedSecondFactor();
619
620
        if (!$selectedSecondFactor) {
621
            $logger->error('Cannot verify possession of an unknown second factor');
622
623
            throw new BadRequestHttpException('Cannot verify possession of an unknown second factor.');
624
        }
625
626
        return $selectedSecondFactor;
627
    }
628
629
    private function selectAndRedirectTo(SecondFactor $secondFactor, ResponseContext $context)
630
    {
631
        $context->saveSelectedSecondFactor($secondFactor);
632
633
        $this->getStepupService()->clearSmsVerificationState();
634
635
        $secondFactorTypeService = $this->get('surfnet_stepup.service.second_factor_type');
636
        $secondFactorType = new SecondFactorType($secondFactor->secondFactorType);
637
638
        $route = 'gateway_verify_second_factor_';
639
        if ($secondFactorTypeService->isGssf($secondFactorType)) {
640
            $route .= 'gssf';
641
        } else {
642
            $route .= strtolower($secondFactor->secondFactorType);
643
        }
644
645
        return $this->redirect($this->generateUrl($route));
646
    }
647
}
648