Completed
Push — develop ( 91a0ef...68f4fc )
by Michiel
03:36 queued 10s
created

verifyYubiKeySecondFactorAction()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 58

Duplication

Lines 4
Ratio 6.9 %

Importance

Changes 0
Metric Value
dl 4
loc 58
rs 8.2941
c 0
b 0
f 0
cc 6
nc 5
nop 1

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * Copyright 2014 SURFnet bv
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 *     http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18
19
namespace Surfnet\StepupGateway\GatewayBundle\Controller;
20
21
use Psr\Log\LoggerInterface;
22
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
23
use Surfnet\StepupBundle\Command\VerifyPossessionOfPhoneCommand;
24
use Surfnet\StepupBundle\Value\PhoneNumber\InternationalPhoneNumber;
25
use Surfnet\StepupBundle\Value\SecondFactorType;
26
use Surfnet\StepupGateway\GatewayBundle\Command\ChooseSecondFactorCommand;
27
use Surfnet\StepupGateway\GatewayBundle\Command\SendSmsChallengeCommand;
28
use Surfnet\StepupGateway\GatewayBundle\Command\VerifyYubikeyOtpCommand;
29
use Surfnet\StepupGateway\GatewayBundle\Entity\SecondFactor;
30
use Surfnet\StepupGateway\GatewayBundle\Exception\InvalidArgumentException;
31
use Surfnet\StepupGateway\GatewayBundle\Exception\LoaCannotBeGivenException;
32
use Surfnet\StepupGateway\GatewayBundle\Exception\RuntimeException;
33
use Surfnet\StepupGateway\GatewayBundle\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
0 ignored issues
show
Deprecated Code introduced by
The class Symfony\Bundle\Framework...e\Controller\Controller has been deprecated with message: since Symfony 4.2, use "Symfony\Bundle\FrameworkBundle\Controller\AbstractController" instead.

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
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(
255
                new FormError(
256
                    $this->get('translator')
257
                      ->trans('gateway.form.gateway_choose_second_factor.unknown_second_factor_type')
258
                )
259
            );
260
        }
261
262
        return [
263
            'form' => $form->createView(),
264
            'cancelForm' => $cancelForm->createView(),
265
            'secondFactors' => $secondFactors,
266
        ];
267
    }
268
269
    public function verifyGssfAction(Request $request)
270
    {
271
        if (!$request->get('authenticationMode', false)) {
272
            throw new RuntimeException('Unable to determine the authentication mode in the GSSP verification action');
273
        }
274
        $authenticationMode = $request->get('authenticationMode');
275
        $this->supportsAuthenticationMode($authenticationMode);
276
        $context = $this->getResponseContext($authenticationMode);
277
278
        $originalRequestId = $context->getInResponseTo();
279
280
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
281
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
282
        $logger->info('Received request to verify GSSF');
283
284
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
0 ignored issues
show
Bug introduced by
It seems like $context defined by $this->getResponseContext($authenticationMode) on line 276 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...
285
286
        $logger->info(sprintf(
287
            'Selected GSSF "%s" for verfication, forwarding to Saml handling',
288
            $selectedSecondFactor
289
        ));
290
291
        /** @var \Surfnet\StepupGateway\GatewayBundle\Service\SecondFactorService $secondFactorService */
292
        $secondFactorService = $this->get('gateway.service.second_factor_service');
293
        /** @var \Surfnet\StepupGateway\GatewayBundle\Entity\SecondFactor $secondFactor */
294
        $secondFactor = $secondFactorService->findByUuid($selectedSecondFactor);
295
        if (!$secondFactor) {
296
            throw new RuntimeException(sprintf(
297
                'Requested verification of GSSF "%s", however that Second Factor no longer exists',
298
                $selectedSecondFactor
299
            ));
300
        }
301
302
        // Also send the response context service id, as later we need to know if this is regular SSO or SFO authn.
303
        $responseContextServiceId = $context->getResponseContextServiceId();
304
305
        return $this->forward(
306
            'SurfnetStepupGatewaySamlStepupProviderBundle:SamlProxy:sendSecondFactorVerificationAuthnRequest',
307
            [
308
                'provider' => $secondFactor->secondFactorType,
309
                'subjectNameId' => $secondFactor->secondFactorIdentifier,
310
                'responseContextServiceId' => $responseContextServiceId,
311
            ]
312
        );
313
    }
314
315
    public function gssfVerifiedAction(Request $request)
316
    {
317
        $authenticationMode = $request->get('authenticationMode');
318
        $this->supportsAuthenticationMode($authenticationMode);
319
        $context = $this->getResponseContext($authenticationMode);
320
321
        $originalRequestId = $context->getInResponseTo();
322
323
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
324
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
325
        $logger->info('Attempting to mark GSSF as verified');
326
327
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
0 ignored issues
show
Bug introduced by
It seems like $context defined by $this->getResponseContext($authenticationMode) on line 319 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...
328
329
        /** @var \Surfnet\StepupGateway\GatewayBundle\Entity\SecondFactor $secondFactor */
330
        $secondFactor = $this->get('gateway.service.second_factor_service')->findByUuid($selectedSecondFactor);
331
        if (!$secondFactor) {
332
            throw new RuntimeException(
333
                sprintf(
334
                    'Verification of GSSF "%s" succeeded, however that Second Factor no longer exists',
335
                    $selectedSecondFactor
336
                )
337
            );
338
        }
339
340
        $context->markSecondFactorVerified();
341
        $this->getAuthenticationLogger()->logSecondFactorAuthentication($originalRequestId, $authenticationMode);
342
343
        $logger->info(sprintf(
344
            'Marked GSSF "%s" as verified, forwarding to Gateway controller to respond',
345
            $selectedSecondFactor
346
        ));
347
348
        return $this->forward($context->getResponseAction());
349
    }
350
351
    /**
352
     * @Template
353
     * @param Request $request
354
     * @return array|Response
355
     */
356
    public function verifyYubiKeySecondFactorAction(Request $request)
357
    {
358
        if (!$request->get('authenticationMode', false)) {
359
            throw new RuntimeException('Unable to determine the authentication mode in Yubikey verification action');
360
        }
361
        $authenticationMode = $request->get('authenticationMode');
362
        $this->supportsAuthenticationMode($authenticationMode);
363
        $context = $this->getResponseContext($authenticationMode);
364
        $originalRequestId = $context->getInResponseTo();
365
366
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
367
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
368
369
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
0 ignored issues
show
Bug introduced by
It seems like $context defined by $this->getResponseContext($authenticationMode) on line 363 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...
370
371
        $logger->notice('Verifying possession of Yubikey second factor');
372
373
        $command = new VerifyYubikeyOtpCommand();
374
        $command->secondFactorId = $selectedSecondFactor;
375
376
        $form = $this->createForm(VerifyYubikeyOtpType::class, $command)->handleRequest($request);
377
        $cancelForm = $this->buildCancelAuthenticationForm($authenticationMode)->handleRequest($request);
378
379 View Code Duplication
        if ($form->isSubmitted() && !$form->isValid()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
380
            // OTP field is rendered empty in the template.
381
            return ['form' => $form->createView(), 'cancelForm' => $cancelForm->createView()];
382
        }
383
384
        $result = $this->getStepupService()->verifyYubikeyOtp($command);
385
386
        if ($result->didOtpVerificationFail()) {
387
            $form->addError(
388
                new FormError($this->get('translator')->trans('gateway.form.verify_yubikey.otp_verification_failed'))
389
            );
390
391
            // OTP field is rendered empty in the template.
392
            return ['form' => $form->createView(), 'cancelForm' => $cancelForm->createView()];
393
        } elseif (!$result->didPublicIdMatch()) {
394
            $form->addError(
395
                new FormError($this->get('translator')->trans('gateway.form.verify_yubikey.public_id_mismatch'))
396
            );
397
398
            // OTP field is rendered empty in the template.
399
            return ['form' => $form->createView(), 'cancelForm' => $cancelForm->createView()];
400
        }
401
402
        $this->getResponseContext($authenticationMode)->markSecondFactorVerified();
403
        $this->getAuthenticationLogger()->logSecondFactorAuthentication($originalRequestId, $authenticationMode);
404
405
        $logger->info(
406
            sprintf(
407
                'Marked Yubikey Second Factor "%s" as verified, forwarding to Saml Proxy to respond',
408
                $selectedSecondFactor
409
            )
410
        );
411
412
        return $this->forward($context->getResponseAction());
413
    }
414
415
    /**
416
     * @Template
417
     * @param Request $request
418
     * @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...
419
     * @return array|Response
420
     */
421
    public function verifySmsSecondFactorAction(Request $request)
422
    {
423
        if (!$request->get('authenticationMode', false)) {
424
            throw new RuntimeException('Unable to determine the authentication mode in the SMS verification action');
425
        }
426
        $authenticationMode = $request->get('authenticationMode');
427
        $this->supportsAuthenticationMode($authenticationMode);
428
        $context = $this->getResponseContext($authenticationMode);
429
        $originalRequestId = $context->getInResponseTo();
430
431
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
432
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
433
434
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
0 ignored issues
show
Bug introduced by
It seems like $context defined by $this->getResponseContext($authenticationMode) on line 428 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...
435
436
        $logger->notice('Verifying possession of SMS second factor, preparing to send');
437
438
        $command = new SendSmsChallengeCommand();
439
        $command->secondFactorId = $selectedSecondFactor;
440
441
        $form = $this->createForm(SendSmsChallengeType::class, $command)->handleRequest($request);
442
        $cancelForm = $this->buildCancelAuthenticationForm($authenticationMode)->handleRequest($request);
443
444
        $stepupService = $this->getStepupService();
445
        $phoneNumber = InternationalPhoneNumber::fromStringFormat(
446
            $stepupService->getSecondFactorIdentifier($selectedSecondFactor)
447
        );
448
449
        $otpRequestsRemaining = $stepupService->getSmsOtpRequestsRemainingCount();
450
        $maximumOtpRequests = $stepupService->getSmsMaximumOtpRequestsCount();
451
        $viewVariables = ['otpRequestsRemaining' => $otpRequestsRemaining, 'maximumOtpRequests' => $maximumOtpRequests];
452
453
        if ($form->isSubmitted() && !$form->isValid()) {
454
            return array_merge(
455
                $viewVariables,
456
                ['phoneNumber' => $phoneNumber, 'form' => $form->createView(), 'cancelForm' => $cancelForm->createView()]
457
            );
458
        }
459
460
        $logger->notice('Verifying possession of SMS second factor, sending challenge per SMS');
461
462
        if (!$stepupService->sendSmsChallenge($command)) {
463
            $form->addError(
464
                new FormError($this->get('translator')->trans('gateway.form.send_sms_challenge.sms_sending_failed'))
465
            );
466
467
            return array_merge(
468
                $viewVariables,
469
                ['phoneNumber' => $phoneNumber, 'form' => $form->createView(), 'cancelForm' => $cancelForm->createView()]
470
            );
471
        }
472
473
        return $this->redirect(
474
            $this->generateUrl(
475
                'gateway_verify_second_factor_sms_verify_challenge',
476
                ['authenticationMode' => $authenticationMode]
477
            )
478
        );
479
    }
480
481
    /**
482
     * @Template
483
     * @param Request $request
484
     * @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...
485
     * @return array|Response
486
     */
487
    public function verifySmsSecondFactorChallengeAction(Request $request)
488
    {
489
        if (!$request->get('authenticationMode', false)) {
490
            throw new RuntimeException('Unable to determine the authentication mode in the SMS challenge action');
491
        }
492
        $authenticationMode = $request->get('authenticationMode');
493
        $this->supportsAuthenticationMode($authenticationMode);
494
        $context = $this->getResponseContext($authenticationMode);
495
        $originalRequestId = $context->getInResponseTo();
496
497
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
498
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
499
500
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
0 ignored issues
show
Bug introduced by
It seems like $context defined by $this->getResponseContext($authenticationMode) on line 494 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...
501
502
        $command = new VerifyPossessionOfPhoneCommand();
503
        $form = $this->createForm(VerifySmsChallengeType::class, $command)->handleRequest($request);
504
        $cancelForm = $this->buildCancelAuthenticationForm($authenticationMode)->handleRequest($request);
505
506 View Code Duplication
        if ($form->isSubmitted() && !$form->isValid()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
507
            return ['form' => $form->createView(), 'cancelForm' => $cancelForm->createView()];
508
        }
509
510
        $logger->notice('Verifying input SMS challenge matches');
511
512
        $verification = $this->getStepupService()->verifySmsChallenge($command);
513
514
        if ($verification->wasSuccessful()) {
515
            $this->getStepupService()->clearSmsVerificationState();
516
517
            $this->getResponseContext($authenticationMode)->markSecondFactorVerified();
518
            $this->getAuthenticationLogger()->logSecondFactorAuthentication($originalRequestId, $authenticationMode);
519
520
            $logger->info(
521
                sprintf(
522
                    'Marked Sms Second Factor "%s" as verified, forwarding to Saml Proxy to respond',
523
                    $selectedSecondFactor
524
                )
525
            );
526
527
            return $this->forward($context->getResponseAction());
528
        } elseif ($verification->didOtpExpire()) {
529
            $logger->notice('SMS challenge expired');
530
            $form->addError(
531
                new FormError($this->get('translator')->trans('gateway.form.send_sms_challenge.challenge_expired'))
532
            );
533
        } elseif ($verification->wasAttemptedTooManyTimes()) {
534
            $logger->notice('SMS challenge verification was attempted too many times');
535
            $form->addError(
536
                new FormError($this->get('translator')->trans('gateway.form.send_sms_challenge.too_many_attempts'))
537
            );
538
        } else {
539
            $logger->notice('SMS challenge did not match');
540
            $form->addError(
541
                new FormError(
542
                    $this->get('translator')->trans('gateway.form.send_sms_challenge.sms_challenge_incorrect')
543
                )
544
            );
545
        }
546
547
        return ['form' => $form->createView(), 'cancelForm' => $cancelForm->createView()];
548
    }
549
550
    /**
551
     * @Template
552
     * @param string $authenticationMode
553
     * @return array
554
     */
555
    public function initiateU2fAuthenticationAction($authenticationMode)
556
    {
557
        $this->supportsAuthenticationMode($authenticationMode);
558
        $context = $this->getResponseContext($authenticationMode);
559
560
        $originalRequestId = $context->getInResponseTo();
561
562
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
563
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
564
565
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
0 ignored issues
show
Bug introduced by
It seems like $context defined by $this->getResponseContext($authenticationMode) on line 558 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...
566
        $stepupService = $this->getStepupService();
567
568
        $cancelFormAction = $this->generateUrl(
569
            'gateway_verify_second_factor_u2f_cancel_authentication',
570
            ['authenticationMode' => $authenticationMode]
571
        );
572
        $cancelForm =
573
            $this->createForm(CancelSecondFactorVerificationType::class, null, ['action' => $cancelFormAction]);
574
575
        $logger->notice('Verifying possession of U2F second factor, looking for registration matching key handle');
576
577
        $service = $this->get('surfnet_stepup_u2f_verification.service.u2f_verification');
578
        $keyHandle = new KeyHandle($stepupService->getSecondFactorIdentifier($selectedSecondFactor));
579
        $registration = $service->findRegistrationByKeyHandle($keyHandle);
580
581
        if ($registration === null) {
582
            $logger->critical(
583
                sprintf('No known registration for key handle of second factor "%s"', $selectedSecondFactor)
584
            );
585
            $this->addFlash('error', 'gateway.u2f.alert.unknown_registration');
586
587
            return ['authenticationFailed' => true, 'cancelForm' => $cancelForm->createView()];
588
        }
589
590
        $logger->notice('Creating sign request');
591
592
        $signRequest = $service->createSignRequest($registration);
593
        $signResponse = new SignResponse();
594
595
        /** @var AttributeBagInterface $session */
596
        $session = $this->get('gateway.session.u2f');
597
        $session->set('request', $signRequest);
598
599
        $formAction = $this->generateUrl(
600
            'gateway_verify_second_factor_u2f_verify_authentication',
601
            ['authenticationMode' => $authenticationMode]
602
        );
603
        $form = $this->createForm(
604
            VerifyDeviceAuthenticationType::class,
605
            $signResponse,
606
            ['sign_request' => $signRequest, 'action' => $formAction]
607
        );
608
609
        return ['form' => $form->createView(), 'cancelForm' => $cancelForm->createView()];
610
    }
611
612
    /**
613
     * @Template("SurfnetStepupGatewayGatewayBundle:second_factor:initiate_u2f_authentication.html.twig")
614
     *
615
     * @param Request $request
616
     * @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...
617
     * @return array|Response
618
     */
619
    public function verifyU2fAuthenticationAction(Request $request)
620
    {
621
        // u2f is not supported, hardcoded to use SSO auth mode in verification mode. Will break SFO.
622
        $authenticationMode = self::MODE_SSO;
623
        $this->supportsAuthenticationMode($authenticationMode);
624
        $context = $this->getResponseContext($authenticationMode);
625
626
        $originalRequestId = $context->getInResponseTo();
627
628
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
629
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
630
631
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
0 ignored issues
show
Bug introduced by
It seems like $context defined by $this->getResponseContext($authenticationMode) on line 624 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...
632
633
        $logger->notice('Received sign response from device');
634
635
        /** @var AttributeBagInterface $session */
636
        $session = $this->get('gateway.session.u2f');
637
        $signRequest = $session->get('request');
638
        $signResponse = new SignResponse();
639
640
        $formAction = $this->generateUrl(
641
            'gateway_verify_second_factor_u2f_verify_authentication',
642
            ['authenticationMode' => $authenticationMode]
643
        );
644
        $form = $this
645
            ->createForm(
646
                VerifyDeviceAuthenticationType::class,
647
                $signResponse,
648
                ['sign_request' => $signRequest, 'action' => $formAction]
649
            )
650
            ->handleRequest($request);
651
652
        $cancelFormAction = $this->generateUrl(
653
            'gateway_verify_second_factor_u2f_cancel_authentication',
654
            ['authenticationMode' => $authenticationMode]
655
        );
656
        $cancelForm =
657
            $this->createForm(CancelSecondFactorVerificationType::class, null, ['action' => $cancelFormAction]);
658
659
        if ($form->isSubmitted() && !$form->isValid()) {
660
            $logger->error('U2F authentication verification could not be started because device send illegal data');
661
            $this->addFlash('error', 'gateway.u2f.alert.error');
662
663
            return ['authenticationFailed' => true, 'cancelForm' => $cancelForm->createView()];
664
        }
665
666
        $service = $this->get('surfnet_stepup_u2f_verification.service.u2f_verification');
667
        $result = $service->verifyAuthentication($signRequest, $signResponse);
668
669
        if ($result->wasSuccessful()) {
670
            $context->markSecondFactorVerified();
671
            $this->getAuthenticationLogger()->logSecondFactorAuthentication($originalRequestId, $authenticationMode);
672
673
            $logger->info(
674
                sprintf(
675
                    'Marked U2F second factor "%s" as verified, forwarding to Saml Proxy to respond',
676
                    $selectedSecondFactor
677
                )
678
            );
679
680
            return $this->forward($context->getResponseAction());
681
        } elseif ($result->didDeviceReportError()) {
682
            $logger->error('U2F device reported error during authentication');
683
            $this->addFlash('error', 'gateway.u2f.alert.device_reported_an_error');
684
        } else {
685
            $logger->error('U2F authentication verification failed');
686
            $this->addFlash('error', 'gateway.u2f.alert.error');
687
        }
688
689
        return ['authenticationFailed' => true, 'cancelForm' => $cancelForm->createView()];
690
    }
691
692
    public function cancelAuthenticationAction()
693
    {
694
        return $this->forward('SurfnetStepupGatewayGatewayBundle:Gateway:sendAuthenticationCancelledByUser');
695
    }
696
697
    /**
698
     * @return \Surfnet\StepupGateway\GatewayBundle\Service\StepupAuthenticationService
699
     */
700
    private function getStepupService()
701
    {
702
        return $this->get('gateway.service.stepup_authentication');
703
    }
704
705
    /**
706
     * @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...
707
     */
708 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...
709
    {
710
        switch ($authenticationMode) {
711
            case self::MODE_SFO:
712
                return $this->get($this->get('gateway.proxy.sfo.state_handler')->getResponseContextServiceId());
713
                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...
714
            case self::MODE_SSO:
715
                return $this->get($this->get('gateway.proxy.sso.state_handler')->getResponseContextServiceId());
716
                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...
717
        }
718
    }
719
720
    /**
721
     * @return \Surfnet\StepupGateway\GatewayBundle\Monolog\Logger\AuthenticationLogger
722
     */
723
    private function getAuthenticationLogger()
724
    {
725
        return $this->get('gateway.authentication_logger');
726
    }
727
728
    /**
729
     * @param ResponseContext $context
730
     * @param LoggerInterface $logger
731
     * @return string
732
     */
733
    private function getSelectedSecondFactor(ResponseContext $context, LoggerInterface $logger)
734
    {
735
        $selectedSecondFactor = $context->getSelectedSecondFactor();
736
737
        if (!$selectedSecondFactor) {
738
            $logger->error('Cannot verify possession of an unknown second factor');
739
740
            throw new BadRequestHttpException('Cannot verify possession of an unknown second factor.');
741
        }
742
743
        return $selectedSecondFactor;
744
    }
745
746
    private function selectAndRedirectTo(SecondFactor $secondFactor, ResponseContext $context, $authenticationMode)
747
    {
748
        $context->saveSelectedSecondFactor($secondFactor);
749
750
        $this->getStepupService()->clearSmsVerificationState();
751
752
        $secondFactorTypeService = $this->get('surfnet_stepup.service.second_factor_type');
753
        $secondFactorType = new SecondFactorType($secondFactor->secondFactorType);
754
755
        $route = 'gateway_verify_second_factor_';
756
        if ($secondFactorTypeService->isGssf($secondFactorType)) {
757
            $route .= 'gssf';
758
        } else {
759
            $route .= strtolower($secondFactor->secondFactorType);
760
        }
761
762
        return $this->redirect($this->generateUrl($route, ['authenticationMode' => $authenticationMode]));
763
    }
764
765
    /**
766
     * @param string $authenticationMode
767
     * @return FormInterface
768
     */
769
    private function buildCancelAuthenticationForm($authenticationMode)
770
    {
771
        $cancelFormAction = $this->generateUrl(
772
            'gateway_cancel_authentication',
773
            ['authenticationMode' => $authenticationMode]
774
        );
775
776
        return $this->createForm(
777
            CancelAuthenticationType::class,
778
            null,
779
            ['action' => $cancelFormAction]
780
        );
781
    }
782
783
    private function supportsAuthenticationMode($authenticationMode)
784
    {
785
        if (!($authenticationMode === self::MODE_SSO || $authenticationMode === self::MODE_SFO)) {
786
            throw new InvalidArgumentException('Invalid authentication mode requested');
787
        }
788
    }
789
}
790