Completed
Pull Request — develop (#125)
by
unknown
14:04 queued 12:06
created

SecondFactorController   D

Complexity

Total Complexity 42

Size/Duplication

Total Lines 551
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 25

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 42
c 2
b 0
f 0
lcom 1
cbo 25
dl 0
loc 551
rs 4.1818

15 Methods

Rating   Name   Duplication   Size   Complexity  
B selectSecondFactorForVerificationAction() 0 66 5
B chooseSecondFactorAction() 0 52 4
B verifyGssfAction() 0 37 2
B gssfVerifiedAction() 0 32 2
B verifyYubiKeySecondFactorAction() 0 52 5
B verifySmsSecondFactorAction() 0 44 4
B verifySmsSecondFactorChallengeAction() 0 52 6
A initiateU2fAuthenticationAction() 0 48 2
B verifyU2fAuthenticationAction() 0 62 4
A cancelU2fAuthenticationAction() 0 4 1
A getStepupService() 0 4 1
A getResponseContext() 0 4 1
A getAuthenticationLogger() 0 4 1
A getSelectedSecondFactor() 0 12 2
A selectAndRedirectTo() 0 18 2

How to fix   Complexity   

Complex Class

Complex classes like SecondFactorController often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SecondFactorController, and based on these observations, apply Extract Interface, too.

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\RuntimeException;
31
use Surfnet\StepupGateway\GatewayBundle\Saml\ResponseContext;
32
use Surfnet\StepupGateway\U2fVerificationBundle\Value\KeyHandle;
33
use Surfnet\StepupU2fBundle\Dto\SignResponse;
34
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
35
use Symfony\Component\Form\FormError;
36
use Symfony\Component\HttpFoundation\RedirectResponse;
37
use Symfony\Component\HttpFoundation\Request;
38
use Symfony\Component\HttpFoundation\Response;
39
use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBagInterface;
40
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
41
42
/**
43
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
44
 */
45
class SecondFactorController extends Controller
46
{
47
    public function selectSecondFactorForVerificationAction()
48
    {
49
        $context = $this->getResponseContext();
50
        $originalRequestId = $context->getInResponseTo();
51
52
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
53
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
54
        $logger->notice('Determining which second factor to use...');
55
56
        $requiredLoa = $this
57
            ->getStepupService()
58
            ->resolveHighestRequiredLoa(
59
                $context->getRequiredLoa(),
60
                $context->getIdentityNameId(),
61
                $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...
62
            );
63
64
        if ($requiredLoa === null) {
65
            $logger->notice(
66
                'No valid required Loa can be determined, no authentication is possible, Loa cannot be given'
67
            );
68
69
            return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendLoaCannotBeGiven');
70
        } else {
71
            $logger->notice(sprintf('Determined that the required Loa is "%s"', $requiredLoa));
72
        }
73
74
        if ($this->getStepupService()->isIntrinsicLoa($requiredLoa)) {
75
            $this->get('gateway.authentication_logger')->logIntrinsicLoaAuthentication($originalRequestId);
76
77
            return $this->forward($context->getResponseAction());
78
        }
79
80
        $secondFactorCollection = $this
81
            ->getStepupService()
82
            ->determineViableSecondFactors(
83
                $context->getIdentityNameId(),
84
                $requiredLoa,
85
                $this->get('gateway.service.whitelist')
86
            );
87
88
        switch (count($secondFactorCollection)) {
89
            case 0:
90
                $logger->notice('No second factors can give the determined Loa');
91
                return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendLoaCannotBeGiven');
92
                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...
93
94
            case 1:
95
                $secondFactor = $secondFactorCollection->first();
96
                $logger->notice(sprintf(
97
                    'Found "%d" second factors, using second factor of type "%s"',
98
                    count($secondFactorCollection),
99
                    $secondFactor->secondFactorType
100
                ));
101
102
                return $this->selectAndRedirectTo($secondFactor, $context);
103
                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...
104
105
            default:
106
                return $this->forward(
107
                    'SurfnetStepupGatewayGatewayBundle:SecondFactor:chooseSecondFactor',
108
                    ['secondFactors' => $secondFactorCollection]
109
                );
110
                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...
111
        }
112
    }
113
114
    /**
115
     * @Template
116
     * @param Request $request
117
     * @return array|RedirectResponse|Response
118
     */
119
    public function chooseSecondFactorAction(Request $request)
120
    {
121
        $context = $this->getResponseContext();
122
        $originalRequestId = $context->getInResponseTo();
123
124
        $requiredLoa = $this
125
            ->getStepupService()
126
            ->resolveHighestRequiredLoa(
127
                $context->getRequiredLoa(),
128
                $context->getIdentityNameId(),
129
                $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...
130
            );
131
132
        $secondFactors = $this
133
            ->getStepupService()
134
            ->determineViableSecondFactors(
135
                $context->getIdentityNameId(),
136
                $requiredLoa,
0 ignored issues
show
Bug introduced by
It seems like $requiredLoa defined by $this->getStepupService(...->getServiceProvider()) on line 124 can be null; however, Surfnet\StepupGateway\Ga...neViableSecondFactors() 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...
137
                $this->get('gateway.service.whitelist')
138
            );
139
140
        $command = new ChooseSecondFactorCommand();
141
        $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...
142
143
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
144
145
        $form = $this
146
            ->createForm(
147
                'gateway_choose_second_factor',
148
                $command,
149
                ['action' => $this->generateUrl('gateway_verify_second_factor_choose_second_factor')]
150
            )
151
            ->handleRequest($request);
152
153
        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...
154
            return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendAuthenticationCancelledByUser');
155
        }
156
157
        if ($form->isSubmitted() && $form->isValid()) {
158
            $secondFactorIndex = $command->selectedSecondFactor;
159
            $secondFactor = $secondFactors->get($secondFactorIndex);
0 ignored issues
show
Documentation introduced by
$secondFactorIndex is of type object<Surfnet\StepupGat...le\Entity\SecondFactor>, but the function expects a string|integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

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

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

class MyClass { }

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

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

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
585
586
        $route = 'gateway_verify_second_factor_';
587
        if ($secondFactorTypeService->isGssf($secondFactorType)) {
588
            $route .= 'gssf';
589
        } else {
590
            $route .= strtolower($secondFactor->secondFactorType);
591
        }
592
593
        return $this->redirect($this->generateUrl($route));
594
    }
595
}
596