Completed
Push — feature/restyle-of-wayg ( 8847c7 )
by
unknown
13:04
created

verifySmsSecondFactorChallengeAction()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 52
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

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