Completed
Push — bugfix/remove-usage-of-mopa-bo... ( dec083...82d2d0 )
by
unknown
01:57
created

SecondFactorController   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 714
Duplicated Lines 1.54 %

Coupling/Cohesion

Components 1
Dependencies 28

Importance

Changes 0
Metric Value
wmc 55
lcom 1
cbo 28
dl 11
loc 714
rs 5.886
c 0
b 0
f 0

19 Methods

Rating   Name   Duplication   Size   Complexity  
A selectSecondFactorForVerificationSsoAction() 0 4 1
A selectSecondFactorForVerificationSfoAction() 0 4 1
B selectSecondFactorForVerificationAction() 0 82 5
C chooseSecondFactorAction() 0 101 8
A verifyGssfAction() 0 45 3
A gssfVerifiedAction() 0 35 2
B verifyYubiKeySecondFactorAction() 0 54 5
B verifySmsSecondFactorAction() 0 57 4
B verifySmsSecondFactorChallengeAction() 0 54 6
B initiateU2fAuthenticationAction() 0 56 2
B verifyU2fAuthenticationAction() 0 72 4
A cancelAuthenticationAction() 0 4 1
A getStepupService() 0 4 1
A getResponseContext() 11 11 3
A getAuthenticationLogger() 0 4 1
A getSelectedSecondFactor() 0 12 2
A selectAndRedirectTo() 0 18 2
A buildCancelAuthenticationForm() 0 13 1
A supportsAuthenticationMode() 0 6 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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\InvalidArgumentException;
31
use Surfnet\StepupGateway\GatewayBundle\Exception\LoaCannotBeGivenException;
32
use Surfnet\StepupGateway\GatewayBundle\Exception\RuntimeException;
33
use Surfnet\StepupGateway\GatewayBundle\Form\Type\CancelAuthenticationType;
34
use Surfnet\StepupGateway\GatewayBundle\Form\Type\CancelSecondFactorVerificationType;
35
use Surfnet\StepupGateway\GatewayBundle\Form\Type\ChooseSecondFactorType;
36
use Surfnet\StepupGateway\GatewayBundle\Form\Type\SendSmsChallengeType;
37
use Surfnet\StepupGateway\GatewayBundle\Form\Type\VerifySmsChallengeType;
38
use Surfnet\StepupGateway\GatewayBundle\Form\Type\VerifyYubikeyOtpType;
39
use Surfnet\StepupGateway\GatewayBundle\Saml\ResponseContext;
40
use Surfnet\StepupGateway\U2fVerificationBundle\Value\KeyHandle;
41
use Surfnet\StepupU2fBundle\Dto\SignResponse;
42
use Surfnet\StepupU2fBundle\Form\Type\VerifyDeviceAuthenticationType;
43
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
44
use Symfony\Component\Form\FormError;
45
use Symfony\Component\Form\FormInterface;
46
use Symfony\Component\HttpFoundation\RedirectResponse;
47
use Symfony\Component\HttpFoundation\Request;
48
use Symfony\Component\HttpFoundation\Response;
49
use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBagInterface;
50
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
51
52
/**
53
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
54
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
55
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
56
 */
57
class SecondFactorController extends Controller
58
{
59
    const MODE_SFO = 'sfo';
60
    const MODE_SSO = 'sso';
61
62
    public function selectSecondFactorForVerificationSsoAction()
63
    {
64
        return $this->selectSecondFactorForVerificationAction(self::MODE_SSO);
65
    }
66
67
    public function selectSecondFactorForVerificationSfoAction()
68
    {
69
        return $this->selectSecondFactorForVerificationAction(self::MODE_SFO);
70
    }
71
72
    public function selectSecondFactorForVerificationAction($authenticationMode)
73
    {
74
        $this->supportsAuthenticationMode($authenticationMode);
75
        $context = $this->getResponseContext($authenticationMode);
76
77
        $originalRequestId = $context->getInResponseTo();
78
79
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
80
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
81
        $logger->notice('Determining which second factor to use...');
82
83
        try {
84
            // Retrieve all requirements to determine the required LoA
85
            $requestedLoa = $context->getRequiredLoa();
86
            $spConfiguredLoas = $context->getServiceProvider()->get('configuredLoas');
87
88
            $normalizedIdpSho = $context->getNormalizedSchacHomeOrganization();
89
            $normalizedUserSho = $this->getStepupService()->getNormalizedUserShoByIdentityNameId($context->getIdentityNameId());
90
91
            $requiredLoa = $this
92
                ->getStepupService()
93
                ->resolveHighestRequiredLoa(
94
                    $requestedLoa,
95
                    $spConfiguredLoas,
96
                    $normalizedIdpSho,
97
                    $normalizedUserSho
98
                );
99
        } catch (LoaCannotBeGivenException $e) {
100
            // Log the message of the domain exception, this contains a meaningful message.
101
            $logger->notice($e->getMessage());
102
103
            return $this->forward(
104
                'SurfnetStepupGatewayGatewayBundle:Gateway:sendLoaCannotBeGiven',
105
                ['authenticationMode' => $authenticationMode]
106
            );
107
        }
108
109
        $logger->notice(sprintf('Determined that the required Loa is "%s"', $requiredLoa));
110
111
        if ($this->getStepupService()->isIntrinsicLoa($requiredLoa)) {
112
            $this->get('gateway.authentication_logger')->logIntrinsicLoaAuthentication($originalRequestId);
113
114
            return $this->forward($context->getResponseAction());
115
        }
116
117
        $secondFactorCollection = $this
118
            ->getStepupService()
119
            ->determineViableSecondFactors(
120
                $context->getIdentityNameId(),
121
                $requiredLoa,
122
                $this->get('gateway.service.whitelist')
123
            );
124
125
        switch (count($secondFactorCollection)) {
126
            case 0:
127
                $logger->notice('No second factors can give the determined Loa');
128
129
                return $this->forward(
130
                    'SurfnetStepupGatewayGatewayBundle:Gateway:sendLoaCannotBeGiven',
131
                    ['authenticationMode' => $authenticationMode]
132
                );
133
                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...
134
135
            case 1:
136
                $secondFactor = $secondFactorCollection->first();
137
                $logger->notice(sprintf(
138
                    'Found "%d" second factors, using second factor of type "%s"',
139
                    count($secondFactorCollection),
140
                    $secondFactor->secondFactorType
141
                ));
142
143
                return $this->selectAndRedirectTo($secondFactor, $context, $authenticationMode);
0 ignored issues
show
Bug introduced by
It seems like $context defined by $this->getResponseContext($authenticationMode) on line 75 can be null; however, Surfnet\StepupGateway\Ga...::selectAndRedirectTo() 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...
144
                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...
145
146
            default:
147
                return $this->forward(
148
                    'SurfnetStepupGatewayGatewayBundle:SecondFactor:chooseSecondFactor',
149
                    ['authenticationMode' => $authenticationMode, 'secondFactors' => $secondFactorCollection]
150
                );
151
                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...
152
        }
153
    }
154
155
    /**
156
     * @Template
157
     * @param Request $request
158
     * @param string $authenticationMode
159
     * @return array|RedirectResponse|Response
160
     * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
161
     */
162
    public function chooseSecondFactorAction(Request $request, $authenticationMode)
163
    {
164
        $this->supportsAuthenticationMode($authenticationMode);
165
        $context = $this->getResponseContext($authenticationMode);
166
        $originalRequestId = $context->getInResponseTo();
167
168
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
169
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
170
        $logger->notice('Ask the user which one of his suitable second factor tokens to use...');
171
172
        try {
173
            // Retrieve all requirements to determine the required LoA
174
            $requestedLoa = $context->getRequiredLoa();
175
            $spConfiguredLoas = $context->getServiceProvider()->get('configuredLoas');
176
177
            $normalizedIdpSho = $context->getNormalizedSchacHomeOrganization();
178
            $normalizedUserSho = $this->getStepupService()->getNormalizedUserShoByIdentityNameId($context->getIdentityNameId());
179
180
            $requiredLoa = $this
181
                ->getStepupService()
182
                ->resolveHighestRequiredLoa(
183
                    $requestedLoa,
184
                    $spConfiguredLoas,
185
                    $normalizedIdpSho,
186
                    $normalizedUserSho
187
                );
188
        } catch (LoaCannotBeGivenException $e) {
189
            // Log the message of the domain exception, this contains a meaningful message.
190
            $logger->notice($e->getMessage());
191
            return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendLoaCannotBeGiven');
192
        }
193
194
        $logger->notice(sprintf('Determined that the required Loa is "%s"', $requiredLoa));
195
196
        $secondFactors = $this
197
            ->getStepupService()
198
            ->determineViableSecondFactors(
199
                $context->getIdentityNameId(),
200
                $requiredLoa,
201
                $this->get('gateway.service.whitelist')
202
            );
203
204
        $command = new ChooseSecondFactorCommand();
205
        $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...
206
207
        $form = $this
208
            ->createForm(
209
                ChooseSecondFactorType::class,
210
                $command,
211
                ['action' => $this->generateUrl('gateway_verify_second_factor_choose_second_factor', ['authenticationMode' => $authenticationMode])]
212
            )
213
            ->handleRequest($request);
214
        $cancelForm = $this->buildCancelAuthenticationForm($authenticationMode)->handleRequest($request);
215
216
        if ($form->isSubmitted() && $form->isValid()) {
217
            $buttonName = $form->getClickedButton()->getName();
218
            $formResults = $request->request->get('gateway_choose_second_factor', false);
219
220
            if (!isset($formResults[$buttonName])) {
221
                throw new InvalidArgumentException(
222
                    sprintf(
223
                        'Second factor type "%s" could not be found in the posted form results.',
224
                        $buttonName
225
                    )
226
                );
227
            }
228
229
            $secondFactorType = $formResults[$buttonName];
230
231
            // Filter the selected second factor from the array collection
232
            $secondFactorFiltered = $secondFactors->filter(
233
                function ($secondFactor) use ($secondFactorType) {
234
                    return $secondFactorType === $secondFactor->secondFactorType;
235
                }
236
            );
237
238
            if ($secondFactorFiltered->isEmpty()) {
239
                throw new InvalidArgumentException(
240
                    sprintf(
241
                        'Second factor type "%s" could not be found in the collection of available second factors.',
242
                        $secondFactorType
243
                    )
244
                );
245
            }
246
247
            $secondFactor = $secondFactorFiltered->first();
248
249
            $logger->notice(sprintf('User chose "%s" to use as second factor', $secondFactorType));
250
251
            // Forward to action to verify possession of second factor
252
            return $this->selectAndRedirectTo($secondFactor, $context, $authenticationMode);
0 ignored issues
show
Bug introduced by
It seems like $context defined by $this->getResponseContext($authenticationMode) on line 165 can be null; however, Surfnet\StepupGateway\Ga...::selectAndRedirectTo() 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...
253
        } else if ($form->isSubmitted() && !$form->isValid()) {
254
            $form->addError(new FormError('gateway.form.gateway_choose_second_factor.unknown_second_factor_type'));
255
        }
256
257
        return [
258
            'form' => $form->createView(),
259
            'cancelForm' => $cancelForm->createView(),
260
            'secondFactors' => $secondFactors,
261
        ];
262
    }
263
264
    public function verifyGssfAction(Request $request)
265
    {
266
        if (!$request->get('authenticationMode', false)) {
267
            throw new RuntimeException('Unable to determine the authentication mode in the GSSP verification action');
268
        }
269
        $authenticationMode = $request->get('authenticationMode');
270
        $this->supportsAuthenticationMode($authenticationMode);
271
        $context = $this->getResponseContext($authenticationMode);
272
273
        $originalRequestId = $context->getInResponseTo();
274
275
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
276
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
277
        $logger->info('Received request to verify GSSF');
278
279
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
0 ignored issues
show
Bug introduced by
It seems like $context defined by $this->getResponseContext($authenticationMode) on line 271 can be null; however, Surfnet\StepupGateway\Ga...tSelectedSecondFactor() 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...
280
281
        $logger->info(sprintf(
282
            'Selected GSSF "%s" for verfication, forwarding to Saml handling',
283
            $selectedSecondFactor
284
        ));
285
286
        /** @var \Surfnet\StepupGateway\GatewayBundle\Service\SecondFactorService $secondFactorService */
287
        $secondFactorService = $this->get('gateway.service.second_factor_service');
288
        /** @var \Surfnet\StepupGateway\GatewayBundle\Entity\SecondFactor $secondFactor */
289
        $secondFactor = $secondFactorService->findByUuid($selectedSecondFactor);
290
        if (!$secondFactor) {
291
            throw new RuntimeException(sprintf(
292
                'Requested verification of GSSF "%s", however that Second Factor no longer exists',
293
                $selectedSecondFactor
294
            ));
295
        }
296
297
        // Also send the response context service id, as later we need to know if this is regular SSO or SFO authn.
298
        $responseContextServiceId = $context->getResponseContextServiceId();
299
300
        return $this->forward(
301
            'SurfnetStepupGatewaySamlStepupProviderBundle:SamlProxy:sendSecondFactorVerificationAuthnRequest',
302
            [
303
                'provider' => $secondFactor->secondFactorType,
304
                'subjectNameId' => $secondFactor->secondFactorIdentifier,
305
                'responseContextServiceId' => $responseContextServiceId,
306
            ]
307
        );
308
    }
309
310
    public function gssfVerifiedAction(Request $request)
311
    {
312
        $authenticationMode = $request->get('authenticationMode');
313
        $this->supportsAuthenticationMode($authenticationMode);
314
        $context = $this->getResponseContext($authenticationMode);
315
316
        $originalRequestId = $context->getInResponseTo();
317
318
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
319
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
320
        $logger->info('Attempting to mark GSSF as verified');
321
322
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
0 ignored issues
show
Bug introduced by
It seems like $context defined by $this->getResponseContext($authenticationMode) on line 314 can be null; however, Surfnet\StepupGateway\Ga...tSelectedSecondFactor() 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...
323
324
        /** @var \Surfnet\StepupGateway\GatewayBundle\Entity\SecondFactor $secondFactor */
325
        $secondFactor = $this->get('gateway.service.second_factor_service')->findByUuid($selectedSecondFactor);
326
        if (!$secondFactor) {
327
            throw new RuntimeException(
328
                sprintf(
329
                    'Verification of GSSF "%s" succeeded, however that Second Factor no longer exists',
330
                    $selectedSecondFactor
331
                )
332
            );
333
        }
334
335
        $context->markSecondFactorVerified();
336
        $this->getAuthenticationLogger()->logSecondFactorAuthentication($originalRequestId, $authenticationMode);
337
338
        $logger->info(sprintf(
339
            'Marked GSSF "%s" as verified, forwarding to Gateway controller to respond',
340
            $selectedSecondFactor
341
        ));
342
343
        return $this->forward($context->getResponseAction());
344
    }
345
346
    /**
347
     * @Template
348
     * @param Request $request
349
     * @return array|Response
350
     */
351
    public function verifyYubiKeySecondFactorAction(Request $request)
352
    {
353
        if (!$request->get('authenticationMode', false)) {
354
            throw new RuntimeException('Unable to determine the authentication mode in Yubikey verification action');
355
        }
356
        $authenticationMode = $request->get('authenticationMode');
357
        $this->supportsAuthenticationMode($authenticationMode);
358
        $context = $this->getResponseContext($authenticationMode);
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);
0 ignored issues
show
Bug introduced by
It seems like $context defined by $this->getResponseContext($authenticationMode) on line 358 can be null; however, Surfnet\StepupGateway\Ga...tSelectedSecondFactor() 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...
365
366
        $logger->notice('Verifying possession of Yubikey second factor');
367
368
        $command = new VerifyYubikeyOtpCommand();
369
        $command->secondFactorId = $selectedSecondFactor;
370
371
        $form = $this->createForm(VerifyYubikeyOtpType::class, $command)->handleRequest($request);
372
        $cancelForm = $this->buildCancelAuthenticationForm($authenticationMode)->handleRequest($request);
373
374
        if (!$form->isValid()) {
375
            // OTP field is rendered empty in the template.
376
            return ['form' => $form->createView(), 'cancelForm' => $cancelForm->createView()];
377
        }
378
379
        $result = $this->getStepupService()->verifyYubikeyOtp($command);
380
381
        if ($result->didOtpVerificationFail()) {
382
            $form->addError(new FormError('gateway.form.verify_yubikey.otp_verification_failed'));
383
384
            // OTP field is rendered empty in the template.
385
            return ['form' => $form->createView(), 'cancelForm' => $cancelForm->createView()];
386
        } elseif (!$result->didPublicIdMatch()) {
387
            $form->addError(new FormError('gateway.form.verify_yubikey.public_id_mismatch'));
388
389
            // OTP field is rendered empty in the template.
390
            return ['form' => $form->createView(), 'cancelForm' => $cancelForm->createView()];
391
        }
392
393
        $this->getResponseContext($authenticationMode)->markSecondFactorVerified();
394
        $this->getAuthenticationLogger()->logSecondFactorAuthentication($originalRequestId, $authenticationMode);
395
396
        $logger->info(
397
            sprintf(
398
                'Marked Yubikey Second Factor "%s" as verified, forwarding to Saml Proxy to respond',
399
                $selectedSecondFactor
400
            )
401
        );
402
403
        return $this->forward($context->getResponseAction());
404
    }
405
406
    /**
407
     * @Template
408
     * @param Request $request
409
     * @param string $authenticationMode
0 ignored issues
show
Bug introduced by
There is no parameter named $authenticationMode. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
410
     * @return array|Response
411
     */
412
    public function verifySmsSecondFactorAction(Request $request)
413
    {
414
        if (!$request->get('authenticationMode', false)) {
415
            throw new RuntimeException('Unable to determine the authentication mode in the SMS verification action');
416
        }
417
        $authenticationMode = $request->get('authenticationMode');
418
        $this->supportsAuthenticationMode($authenticationMode);
419
        $context = $this->getResponseContext($authenticationMode);
420
        $originalRequestId = $context->getInResponseTo();
421
422
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
423
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
424
425
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
0 ignored issues
show
Bug introduced by
It seems like $context defined by $this->getResponseContext($authenticationMode) on line 419 can be null; however, Surfnet\StepupGateway\Ga...tSelectedSecondFactor() 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...
426
427
        $logger->notice('Verifying possession of SMS second factor, preparing to send');
428
429
        $command = new SendSmsChallengeCommand();
430
        $command->secondFactorId = $selectedSecondFactor;
431
432
        $form = $this->createForm(SendSmsChallengeType::class, $command)->handleRequest($request);
433
        $cancelForm = $this->buildCancelAuthenticationForm($authenticationMode)->handleRequest($request);
434
435
        $stepupService = $this->getStepupService();
436
        $phoneNumber = InternationalPhoneNumber::fromStringFormat(
437
            $stepupService->getSecondFactorIdentifier($selectedSecondFactor)
438
        );
439
440
        $otpRequestsRemaining = $stepupService->getSmsOtpRequestsRemainingCount();
441
        $maximumOtpRequests = $stepupService->getSmsMaximumOtpRequestsCount();
442
        $viewVariables = ['otpRequestsRemaining' => $otpRequestsRemaining, 'maximumOtpRequests' => $maximumOtpRequests];
443
444
        if (!$form->isValid()) {
445
            return array_merge(
446
                $viewVariables,
447
                ['phoneNumber' => $phoneNumber, 'form' => $form->createView(), 'cancelForm' => $cancelForm->createView()]
448
            );
449
        }
450
451
        $logger->notice('Verifying possession of SMS second factor, sending challenge per SMS');
452
453
        if (!$stepupService->sendSmsChallenge($command)) {
454
            $form->addError(new FormError('gateway.form.send_sms_challenge.sms_sending_failed'));
455
456
            return array_merge(
457
                $viewVariables,
458
                ['phoneNumber' => $phoneNumber, 'form' => $form->createView(), 'cancelForm' => $cancelForm->createView()]
459
            );
460
        }
461
462
        return $this->redirect(
463
            $this->generateUrl(
464
                'gateway_verify_second_factor_sms_verify_challenge',
465
                ['authenticationMode' => $authenticationMode]
466
            )
467
        );
468
    }
469
470
    /**
471
     * @Template
472
     * @param Request $request
473
     * @param string $authenticationMode
0 ignored issues
show
Bug introduced by
There is no parameter named $authenticationMode. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
474
     * @return array|Response
475
     */
476
    public function verifySmsSecondFactorChallengeAction(Request $request)
477
    {
478
        if (!$request->get('authenticationMode', false)) {
479
            throw new RuntimeException('Unable to determine the authentication mode in the SMS challenge action');
480
        }
481
        $authenticationMode = $request->get('authenticationMode');
482
        $this->supportsAuthenticationMode($authenticationMode);
483
        $context = $this->getResponseContext($authenticationMode);
484
        $originalRequestId = $context->getInResponseTo();
485
486
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
487
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
488
489
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
0 ignored issues
show
Bug introduced by
It seems like $context defined by $this->getResponseContext($authenticationMode) on line 483 can be null; however, Surfnet\StepupGateway\Ga...tSelectedSecondFactor() 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...
490
491
        $command = new VerifyPossessionOfPhoneCommand();
492
        $form = $this->createForm(VerifySmsChallengeType::class, $command)->handleRequest($request);
493
        $cancelForm = $this->buildCancelAuthenticationForm($authenticationMode)->handleRequest($request);
494
495
        if (!$form->isValid()) {
496
            return ['form' => $form->createView(), 'cancelForm' => $cancelForm->createView()];
497
        }
498
499
        $logger->notice('Verifying input SMS challenge matches');
500
501
        $verification = $this->getStepupService()->verifySmsChallenge($command);
502
503
        if ($verification->wasSuccessful()) {
504
            $this->getStepupService()->clearSmsVerificationState();
505
506
            $this->getResponseContext($authenticationMode)->markSecondFactorVerified();
507
            $this->getAuthenticationLogger()->logSecondFactorAuthentication($originalRequestId, $authenticationMode);
508
509
            $logger->info(
510
                sprintf(
511
                    'Marked Sms Second Factor "%s" as verified, forwarding to Saml Proxy to respond',
512
                    $selectedSecondFactor
513
                )
514
            );
515
516
            return $this->forward($context->getResponseAction());
517
        } elseif ($verification->didOtpExpire()) {
518
            $logger->notice('SMS challenge expired');
519
            $form->addError(new FormError('gateway.form.send_sms_challenge.challenge_expired'));
520
        } elseif ($verification->wasAttemptedTooManyTimes()) {
521
            $logger->notice('SMS challenge verification was attempted too many times');
522
            $form->addError(new FormError('gateway.form.send_sms_challenge.too_many_attempts'));
523
        } else {
524
            $logger->notice('SMS challenge did not match');
525
            $form->addError(new FormError('gateway.form.send_sms_challenge.sms_challenge_incorrect'));
526
        }
527
528
        return ['form' => $form->createView(), 'cancelForm' => $cancelForm->createView()];
529
    }
530
531
    /**
532
     * @Template
533
     * @param string $authenticationMode
534
     * @return array
535
     */
536
    public function initiateU2fAuthenticationAction($authenticationMode)
537
    {
538
        $this->supportsAuthenticationMode($authenticationMode);
539
        $context = $this->getResponseContext($authenticationMode);
540
541
        $originalRequestId = $context->getInResponseTo();
542
543
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
544
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
545
546
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
0 ignored issues
show
Bug introduced by
It seems like $context defined by $this->getResponseContext($authenticationMode) on line 539 can be null; however, Surfnet\StepupGateway\Ga...tSelectedSecondFactor() 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...
547
        $stepupService = $this->getStepupService();
548
549
        $cancelFormAction = $this->generateUrl(
550
            'gateway_verify_second_factor_u2f_cancel_authentication',
551
            ['authenticationMode' => $authenticationMode]
552
        );
553
        $cancelForm =
554
            $this->createForm(CancelSecondFactorVerificationType::class, null, ['action' => $cancelFormAction]);
555
556
        $logger->notice('Verifying possession of U2F second factor, looking for registration matching key handle');
557
558
        $service = $this->get('surfnet_stepup_u2f_verification.service.u2f_verification');
559
        $keyHandle = new KeyHandle($stepupService->getSecondFactorIdentifier($selectedSecondFactor));
560
        $registration = $service->findRegistrationByKeyHandle($keyHandle);
561
562
        if ($registration === null) {
563
            $logger->critical(
564
                sprintf('No known registration for key handle of second factor "%s"', $selectedSecondFactor)
565
            );
566
            $this->addFlash('error', 'gateway.u2f.alert.unknown_registration');
567
568
            return ['authenticationFailed' => true, 'cancelForm' => $cancelForm->createView()];
569
        }
570
571
        $logger->notice('Creating sign request');
572
573
        $signRequest = $service->createSignRequest($registration);
574
        $signResponse = new SignResponse();
575
576
        /** @var AttributeBagInterface $session */
577
        $session = $this->get('gateway.session.u2f');
578
        $session->set('request', $signRequest);
579
580
        $formAction = $this->generateUrl(
581
            'gateway_verify_second_factor_u2f_verify_authentication',
582
            ['authenticationMode' => $authenticationMode]
583
        );
584
        $form = $this->createForm(
585
            VerifyDeviceAuthenticationType::class,
586
            $signResponse,
587
            ['sign_request' => $signRequest, 'action' => $formAction]
588
        );
589
590
        return ['form' => $form->createView(), 'cancelForm' => $cancelForm->createView()];
591
    }
592
593
    /**
594
     * @Template("SurfnetStepupGatewayGatewayBundle:SecondFactor:initiateU2fAuthentication.html.twig")
595
     *
596
     * @param Request $request
597
     * @param string $authenticationMode
0 ignored issues
show
Bug introduced by
There is no parameter named $authenticationMode. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
598
     * @return array|Response
599
     */
600
    public function verifyU2fAuthenticationAction(Request $request)
601
    {
602
        // u2f is not supported, hardcoded to use SSO auth mode in verification mode. Will break SFO.
603
        $authenticationMode = self::MODE_SSO;
604
        $this->supportsAuthenticationMode($authenticationMode);
605
        $context = $this->getResponseContext($authenticationMode);
606
607
        $originalRequestId = $context->getInResponseTo();
608
609
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
610
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
611
612
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
0 ignored issues
show
Bug introduced by
It seems like $context defined by $this->getResponseContext($authenticationMode) on line 605 can be null; however, Surfnet\StepupGateway\Ga...tSelectedSecondFactor() 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...
613
614
        $logger->notice('Received sign response from device');
615
616
        /** @var AttributeBagInterface $session */
617
        $session = $this->get('gateway.session.u2f');
618
        $signRequest = $session->get('request');
619
        $signResponse = new SignResponse();
620
621
        $formAction = $this->generateUrl(
622
            'gateway_verify_second_factor_u2f_verify_authentication',
623
            ['authenticationMode' => $authenticationMode]
624
        );
625
        $form = $this
626
            ->createForm(
627
                VerifyDeviceAuthenticationType::class,
628
                $signResponse,
629
                ['sign_request' => $signRequest, 'action' => $formAction]
630
            )
631
            ->handleRequest($request);
632
633
        $cancelFormAction = $this->generateUrl(
634
            'gateway_verify_second_factor_u2f_cancel_authentication',
635
            ['authenticationMode' => $authenticationMode]
636
        );
637
        $cancelForm =
638
            $this->createForm(CancelSecondFactorVerificationType::class, null, ['action' => $cancelFormAction]);
639
640
        if (!$form->isValid()) {
641
            $logger->error('U2F authentication verification could not be started because device send illegal data');
642
            $this->addFlash('error', 'gateway.u2f.alert.error');
643
644
            return ['authenticationFailed' => true, 'cancelForm' => $cancelForm->createView()];
645
        }
646
647
        $service = $this->get('surfnet_stepup_u2f_verification.service.u2f_verification');
648
        $result = $service->verifyAuthentication($signRequest, $signResponse);
649
650
        if ($result->wasSuccessful()) {
651
            $context->markSecondFactorVerified();
652
            $this->getAuthenticationLogger()->logSecondFactorAuthentication($originalRequestId, $authenticationMode);
653
654
            $logger->info(
655
                sprintf(
656
                    'Marked U2F second factor "%s" as verified, forwarding to Saml Proxy to respond',
657
                    $selectedSecondFactor
658
                )
659
            );
660
661
            return $this->forward($context->getResponseAction());
662
        } elseif ($result->didDeviceReportError()) {
663
            $logger->error('U2F device reported error during authentication');
664
            $this->addFlash('error', 'gateway.u2f.alert.device_reported_an_error');
665
        } else {
666
            $logger->error('U2F authentication verification failed');
667
            $this->addFlash('error', 'gateway.u2f.alert.error');
668
        }
669
670
        return ['authenticationFailed' => true, 'cancelForm' => $cancelForm->createView()];
671
    }
672
673
    public function cancelAuthenticationAction()
674
    {
675
        return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendAuthenticationCancelledByUser');
676
    }
677
678
    /**
679
     * @return \Surfnet\StepupGateway\GatewayBundle\Service\StepupAuthenticationService
680
     */
681
    private function getStepupService()
682
    {
683
        return $this->get('gateway.service.stepup_authentication');
684
    }
685
686
    /**
687
     * @return ResponseContext
0 ignored issues
show
Documentation introduced by
Should the return type not be ResponseContext|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
688
     */
689 View Code Duplication
    private function getResponseContext($authenticationMode)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
690
    {
691
        switch ($authenticationMode) {
692
            case self::MODE_SFO:
693
                return $this->get($this->get('gateway.proxy.sfo.state_handler')->getResponseContextServiceId());
694
                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...
695
            case self::MODE_SSO:
696
                return $this->get($this->get('gateway.proxy.sso.state_handler')->getResponseContextServiceId());
697
                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...
698
        }
699
    }
700
701
    /**
702
     * @return \Surfnet\StepupGateway\GatewayBundle\Monolog\Logger\AuthenticationLogger
703
     */
704
    private function getAuthenticationLogger()
705
    {
706
        return $this->get('gateway.authentication_logger');
707
    }
708
709
    /**
710
     * @param ResponseContext $context
711
     * @param LoggerInterface $logger
712
     * @return string
713
     */
714
    private function getSelectedSecondFactor(ResponseContext $context, LoggerInterface $logger)
715
    {
716
        $selectedSecondFactor = $context->getSelectedSecondFactor();
717
718
        if (!$selectedSecondFactor) {
719
            $logger->error('Cannot verify possession of an unknown second factor');
720
721
            throw new BadRequestHttpException('Cannot verify possession of an unknown second factor.');
722
        }
723
724
        return $selectedSecondFactor;
725
    }
726
727
    private function selectAndRedirectTo(SecondFactor $secondFactor, ResponseContext $context, $authenticationMode)
728
    {
729
        $context->saveSelectedSecondFactor($secondFactor);
730
731
        $this->getStepupService()->clearSmsVerificationState();
732
733
        $secondFactorTypeService = $this->get('surfnet_stepup.service.second_factor_type');
734
        $secondFactorType = new SecondFactorType($secondFactor->secondFactorType);
735
736
        $route = 'gateway_verify_second_factor_';
737
        if ($secondFactorTypeService->isGssf($secondFactorType)) {
738
            $route .= 'gssf';
739
        } else {
740
            $route .= strtolower($secondFactor->secondFactorType);
741
        }
742
743
        return $this->redirect($this->generateUrl($route, ['authenticationMode' => $authenticationMode]));
744
    }
745
746
    /**
747
     * @param string $authenticationMode
748
     * @return FormInterface
749
     */
750
    private function buildCancelAuthenticationForm($authenticationMode)
751
    {
752
        $cancelFormAction = $this->generateUrl(
753
            'gateway_cancel_authentication',
754
            ['authenticationMode' => $authenticationMode]
755
        );
756
757
        return $this->createForm(
758
            CancelAuthenticationType::class,
759
            null,
760
            ['action' => $cancelFormAction]
761
        );
762
    }
763
764
    private function supportsAuthenticationMode($authenticationMode)
765
    {
766
        if (!($authenticationMode === self::MODE_SSO || $authenticationMode === self::MODE_SFO)) {
767
            throw new InvalidArgumentException('Invalid authentication mode requested');
768
        }
769
    }
770
}
771