Passed
Push — feature/add-azure-mfa-registra... ( 3767e0...d31ca4 )
by
unknown
02:11
created

verifySmsSecondFactorChallenge()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 69
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 45
nc 6
nop 1
dl 0
loc 69
rs 8.2666
c 0
b 0
f 0

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 Surfnet\StepupBundle\Command\VerifyPossessionOfPhoneCommand;
23
use Surfnet\StepupBundle\Value\PhoneNumber\InternationalPhoneNumber;
24
use Surfnet\StepupBundle\Value\SecondFactorType;
25
use Surfnet\StepupGateway\GatewayBundle\Command\ChooseSecondFactorCommand;
26
use Surfnet\StepupGateway\GatewayBundle\Command\SendSmsChallengeCommand;
27
use Surfnet\StepupGateway\GatewayBundle\Command\VerifyYubikeyOtpCommand;
28
use Surfnet\StepupGateway\GatewayBundle\Container\ContainerController;
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\ChooseSecondFactorType;
35
use Surfnet\StepupGateway\GatewayBundle\Form\Type\SendSmsChallengeType;
36
use Surfnet\StepupGateway\GatewayBundle\Form\Type\VerifySmsChallengeType;
37
use Surfnet\StepupGateway\GatewayBundle\Form\Type\VerifyYubikeyOtpType;
38
use Surfnet\StepupGateway\GatewayBundle\Saml\ResponseContext;
39
use Surfnet\StepupGateway\GatewayBundle\Service\SecondFactor\SecondFactorInterface;
40
use Surfnet\StepupGateway\GatewayBundle\Service\SecondFactorService;
41
use Surfnet\StepupGateway\GatewayBundle\Sso2fa\CookieService;
42
use Surfnet\StepupGateway\SamlStepupProviderBundle\Controller\SamlProxyController;
43
use Surfnet\StepupGateway\SecondFactorOnlyBundle\Service\Gateway\SecondfactorGsspFallback;
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\HttpKernel\Exception\BadRequestHttpException;
50
use Symfony\Component\Routing\Attribute\Route;
51
use function is_null;
52
use const FILTER_DEFAULT;
53
use const FILTER_FORCE_ARRAY;
54
55
/**
56
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
57
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
58
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
59
 */
60
class SecondFactorController extends ContainerController
61
{
62
    public const MODE_SFO = 'sfo';
63
    public const MODE_SSO = 'sso';
64
65
    public function selectSecondFactorForVerificationSso(
66
        Request $request,
67
    ): Response {
68
        return $this->selectSecondFactorForVerification(self::MODE_SSO, $request);
69
    }
70
71
    public function selectSecondFactorForVerificationSfo(
72
        Request $request,
73
    ): Response {
74
        return $this->selectSecondFactorForVerification(self::MODE_SFO, $request);
75
    }
76
77
    /**
78
     * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
79
     */
80
    public function selectSecondFactorForVerification(
81
        string $authenticationMode,
82
        Request $request,
83
    ): Response|RedirectResponse {
84
        $this->supportsAuthenticationMode($authenticationMode);
85
        $context = $this->getResponseContext($authenticationMode);
86
        $originalRequestId = $context->getInResponseTo();
87
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
88
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
89
        $logger->notice('Determining which second factor to use...');
90
        try {
91
            // Retrieve all requirements to determine the required LoA
92
            $requestedLoa = $context->getRequiredLoa();
93
            $spConfiguredLoas = $context->getServiceProvider()->get('configuredLoas');
94
            $identityNameId = $context->getIdentityNameId();
95
            $normalizedIdpSho = $context->getNormalizedSchacHomeOrganization();
96
            $normalizedUserSho = $this->getStepupService()->getNormalizedUserShoByIdentityNameId($identityNameId);
97
            $requiredLoa = $this
98
                ->getStepupService()
99
                ->resolveHighestRequiredLoa(
100
                    $requestedLoa,
101
                    $spConfiguredLoas,
0 ignored issues
show
Bug introduced by
It seems like $spConfiguredLoas can also be of type null; however, parameter $spConfiguredLoas of Surfnet\StepupGateway\Ga...lveHighestRequiredLoa() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

101
                    /** @scrutinizer ignore-type */ $spConfiguredLoas,
Loading history...
102
                    $normalizedIdpSho,
103
                    $normalizedUserSho,
104
                );
105
        } catch (LoaCannotBeGivenException $e) {
106
            // Log the message of the domain exception, this contains a meaningful message.
107
            $logger->notice($e->getMessage());
108
109
            return $this->forward(
110
                'Surfnet\StepupGateway\GatewayBundle\Controller\GatewayController::sendLoaCannotBeGiven',
111
                ['authenticationMode' => $authenticationMode],
112
            );
113
        }
114
115
        $logger->notice(sprintf('Determined that the required Loa is "%s"', $requiredLoa));
116
        if ($this->getStepupService()->isIntrinsicLoa($requiredLoa)) {
117
            $this->get('gateway.authentication_logger')->logIntrinsicLoaAuthentication($originalRequestId);
118
119
            return $this->forward($context->getResponseAction());
0 ignored issues
show
Bug introduced by
It seems like $context->getResponseAction() can also be of type null; however, parameter $controller of Symfony\Bundle\Framework...ctController::forward() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

119
            return $this->forward(/** @scrutinizer ignore-type */ $context->getResponseAction());
Loading history...
120
        }
121
122
        // The preconditions must be met in order to give SSO on 2FA
123
        // 1: AuthNRequest is not force authn. 2: The SP allows SSO on 2FA.
124
        if ($this->getCookieService()->preconditionsAreMet($context)) {
125
            // Now read the SSO cookie
126
            $ssoCookie = $this->getCookieService()->read($request);
127
            // Test if the SSO cookie can satisfy the second factor authentication requirements
128
            if ($this->getCookieService()->maySkipAuthentication($requiredLoa->getLevel(), $identityNameId, $ssoCookie, $context)) {
129
                $logger->notice(
130
                    'Skipping second factor authentication. Required LoA was met by the LoA recorded in the cookie',
131
                    [
132
                        'required-loa' => $requiredLoa->getLevel(),
133
                        'cookie-loa' => $ssoCookie->getLoa(),
134
                    ],
135
                );
136
                // We use the SF from the cookie as the SF that was used for authenticating the second factor authentication
137
                $secondFactor = $this->getSecondFactorService()->findByUuid($ssoCookie->secondFactorId(), $context);
138
                $this->getResponseContext($authenticationMode)->saveSelectedSecondFactor($secondFactor);
139
                $this->getResponseContext($authenticationMode)->markSecondFactorVerified();
140
                $this->getResponseContext($authenticationMode)->markVerifiedBySsoOn2faCookie(
141
                    $this->getCookieService()->getCookieFingerprint($request),
142
                );
143
                $this->getAuthenticationLogger()->logSecondFactorAuthentication($originalRequestId, $authenticationMode);
0 ignored issues
show
Bug introduced by
It seems like $originalRequestId can also be of type null; however, parameter $requestId of Surfnet\StepupGateway\Ga...dFactorAuthentication() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

143
                $this->getAuthenticationLogger()->logSecondFactorAuthentication(/** @scrutinizer ignore-type */ $originalRequestId, $authenticationMode);
Loading history...
144
145
                return $this->forward($context->getResponseAction());
146
            }
147
        }
148
149
        $secondFactorCollection = $this
150
            ->getStepupService()
151
            ->determineViableSecondFactors(
152
                $context->getIdentityNameId(),
153
                $requiredLoa,
154
                $this->get('gateway.service.whitelist'),
155
            );
156
        switch (count($secondFactorCollection)) {
157
            case 0:
158
                $logger->notice('No second factors can give the determined Loa');
159
160
                // todo: handle sso registration bypass.
161
                if ($authenticationMode === self::MODE_SFO) {
162
                    // - the user does not have an active token
163
164
                    // - a LoA1.5 (i.e. self asserted) authentication is requested
165
                    // - a fallback GSSP is configured
166
                    // - this "fallback" option is enabled for the institution that the user belongs to.
167
                    // - the configured user attribute is present in the AuthnRequest
168
                    // - handle authentication by forwarding it to a designated GSSP using the GSSP protocol instead of returning an error.
169
170
//                    var_dump($requiredLoa);
171
//                    die();
172
173
                    $secondFactor = SecondfactorGsspFallback::create('azuremfa', $request->getLocale());
174
                    return $this->selectAndRedirectTo($secondFactor, $context, $authenticationMode);
175
                }
176
177
                return $this->forward(
178
                    'Surfnet\StepupGateway\GatewayBundle\Controller\GatewayController::sendLoaCannotBeGiven',
179
                    ['authenticationMode' => $authenticationMode],
180
                );
181
            case 1:
182
                $secondFactor = $secondFactorCollection->first();
183
                $logger->notice(sprintf(
184
                    'Found "%d" second factors, using second factor of type "%s"',
185
                    count($secondFactorCollection),
186
                    $secondFactor->secondFactorType,
187
                ));
188
189
                return $this->selectAndRedirectTo($secondFactor, $context, $authenticationMode);
190
            default:
191
                return $this->forward(
192
                    'Surfnet\StepupGateway\GatewayBundle\Controller\SecondFactorController::chooseSecondFactor',
193
                    ['authenticationMode' => $authenticationMode, 'secondFactors' => $secondFactorCollection],
194
                );
195
        }
196
    }
197
198
    /**
199
     * The main WAYG screen
200
     * - Shows the token selection screen if you own > 1 token
201
     * - Directly goes to SF auth when identity owns 1 token.
202
     *
203
     * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
204
     */
205
    #[Route(
206
        path: '/choose-second-factor/{authenticationMode}',
207
        name: 'gateway_verify_second_factor_choose_second_factor',
208
        requirements: ['authenticationMode' => 'sso|sfo'],
209
        methods: ['GET', 'POST']
210
    )]
211
    public function chooseSecondFactor(
212
        Request $request,
213
        string $authenticationMode,
214
    ): Response|RedirectResponse|array {
215
        $this->supportsAuthenticationMode($authenticationMode);
216
        $context = $this->getResponseContext($authenticationMode);
217
        $originalRequestId = $context->getInResponseTo();
218
219
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
220
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
221
        $logger->notice('Ask the user which one of his suitable second factor tokens to use...');
222
223
        try {
224
            // Retrieve all requirements to determine the required LoA
225
            $requestedLoa = $context->getRequiredLoa();
226
            $spConfiguredLoas = $context->getServiceProvider()->get('configuredLoas');
227
228
            $normalizedIdpSho = $context->getNormalizedSchacHomeOrganization();
229
            $normalizedUserSho = $this->getStepupService()->getNormalizedUserShoByIdentityNameId($context->getIdentityNameId());
230
231
            $requiredLoa = $this
232
                ->getStepupService()
233
                ->resolveHighestRequiredLoa(
234
                    $requestedLoa,
235
                    $spConfiguredLoas,
0 ignored issues
show
Bug introduced by
It seems like $spConfiguredLoas can also be of type null; however, parameter $spConfiguredLoas of Surfnet\StepupGateway\Ga...lveHighestRequiredLoa() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

235
                    /** @scrutinizer ignore-type */ $spConfiguredLoas,
Loading history...
236
                    $normalizedIdpSho,
237
                    $normalizedUserSho,
238
                );
239
        } catch (LoaCannotBeGivenException $e) {
240
            // Log the message of the domain exception, this contains a meaningful message.
241
            $logger->notice($e->getMessage());
242
243
            return $this->forward('Surfnet\StepupGateway\GatewayBundle\Controller\GatewayController::sendLoaCannotBeGiven');
244
        }
245
246
        $logger->notice(sprintf('Determined that the required Loa is "%s"', $requiredLoa));
247
248
        $secondFactors = $this
249
            ->getStepupService()
250
            ->determineViableSecondFactors(
251
                $context->getIdentityNameId(),
252
                $requiredLoa,
253
                $this->get('gateway.service.whitelist'),
254
            );
255
256
        $command = new ChooseSecondFactorCommand();
257
        $command->secondFactors = $secondFactors;
0 ignored issues
show
Documentation Bug introduced by
It seems like $secondFactors of type Doctrine\Common\Collections\Collection is incompatible with the declared type Surfnet\StepupGateway\Ga...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...
258
259
        $form = $this
260
            ->createForm(
261
                ChooseSecondFactorType::class,
262
                $command,
263
                [
264
                    'action' => $this->generateUrl(
265
                        'gateway_verify_second_factor_choose_second_factor',
266
                        ['authenticationMode' => $authenticationMode]
267
                    )
268
                ],
269
            )
270
            ->handleRequest($request);
271
        $cancelForm = $this->buildCancelAuthenticationForm($authenticationMode)->handleRequest($request);
272
273
        if ($form->isSubmitted() && $form->isValid()) {
274
            $buttonName = $form->getClickedButton()->getName();
0 ignored issues
show
Bug introduced by
The method getClickedButton() does not exist on Symfony\Component\Form\FormInterface. It seems like you code against a sub-type of Symfony\Component\Form\FormInterface such as Symfony\Component\Form\Form. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

274
            $buttonName = $form->/** @scrutinizer ignore-call */ getClickedButton()->getName();
Loading history...
275
            $formResults = $request->request->filter(
276
                'gateway_choose_second_factor',
277
                false,
278
                FILTER_DEFAULT,
279
                ['flags' => FILTER_FORCE_ARRAY]
280
            );
281
            if (!isset($formResults[$buttonName])) {
282
                throw new InvalidArgumentException(
283
                    sprintf(
284
                        'Second factor type "%s" could not be found in the posted form results.',
285
                        $buttonName
286
                    )
287
                );
288
            }
289
290
            $secondFactorType = $formResults[$buttonName];
291
292
            // Filter the selected second factor from the array collection
293
            $secondFactorFiltered = $secondFactors->filter(
294
                fn ($secondFactor): bool => $secondFactorType === $secondFactor->secondFactorType,
295
            );
296
297
            if ($secondFactorFiltered->isEmpty()) {
298
                throw new InvalidArgumentException(
299
                    sprintf(
300
                        'Second factor type "%s" could not be found in the collection of available second factors.',
301
                        $secondFactorType
302
                    )
303
                );
304
            }
305
306
            $secondFactor = $secondFactorFiltered->first();
307
308
            $logger->notice(sprintf('User chose "%s" to use as second factor', $secondFactorType));
309
310
            // Forward to action to verify possession of second factor
311
            return $this->selectAndRedirectTo($secondFactor, $context, $authenticationMode);
312
        } elseif ($form->isSubmitted() && !$form->isValid()) {
313
            $form->addError(
314
                new FormError(
315
                    $this->get('translator')
316
                      ->trans('gateway.form.gateway_choose_second_factor.unknown_second_factor_type'),
317
                ),
318
            );
319
        }
320
321
        return $this->render(
322
            '@default/second_factor/choose_second_factor.html.twig',
323
            [
324
                'form' => $form->createView(),
325
                'cancelForm' => $cancelForm->createView(),
326
                'secondFactors' => $secondFactors,
327
            ]
328
        );
329
    }
330
331
    #[Route(
332
        path: '/verify-second-factor/gssf',
333
        name: 'gateway_verify_second_factor_gssf',
334
        methods: ['GET']
335
    )]
336
    public function verifyGssf(Request $request): Response
337
    {
338
        if (!$request->get('authenticationMode', false)) {
339
            throw new RuntimeException('Unable to determine the authentication mode in the GSSP verification action');
340
        }
341
        $authenticationMode = $request->get('authenticationMode');
342
        $this->supportsAuthenticationMode($authenticationMode);
343
        $context = $this->getResponseContext($authenticationMode);
344
345
        $originalRequestId = $context->getInResponseTo();
346
347
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
348
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
349
        $logger->info('Received request to verify GSSF');
350
351
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
352
353
        $logger->info(sprintf(
354
            'Selected GSSF "%s" for verfication, forwarding to Saml handling',
355
            $selectedSecondFactor,
356
        ));
357
358
        /** @var SecondFactorService $secondFactorService */
359
        $secondFactorService = $this->get('gateway.service.second_factor_service');
360
        $secondFactor = $secondFactorService->findByUuid($selectedSecondFactor, $context);
361
        if (!$secondFactor) {
362
            throw new RuntimeException(
363
                sprintf(
364
                    'Requested verification of GSSF "%s", however that Second Factor no longer exists',
365
                    $selectedSecondFactor
366
                )
367
            );
368
        }
369
370
        // Also send the response context service id, as later we need to know if this is regular SSO or SFO authn.
371
        $responseContextServiceId = $context->getResponseContextServiceId();
372
373
        return $this->forward(
374
            SamlProxyController::class . '::sendSecondFactorVerificationAuthnRequest',
375
            [
376
                'provider' => $secondFactor->getSecondFactorType(),
377
                'subjectNameId' => $secondFactor->getSecondFactorIdentifier(),
378
                'responseContextServiceId' => $responseContextServiceId,
379
                'relayState' => $context->getRelayState(),
380
            ],
381
        );
382
    }
383
384
    public function gssfVerified(Request $request): Response
385
    {
386
        $authenticationMode = $request->get('authenticationMode');
387
        $this->supportsAuthenticationMode($authenticationMode);
388
        $context = $this->getResponseContext($authenticationMode);
389
390
        $originalRequestId = $context->getInResponseTo();
391
392
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
393
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
394
        $logger->info('Attempting to mark GSSF as verified');
395
396
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
397
398
        if (!$context->isSecondFactorFallback()) {
399
            /** @var SecondFactor $secondFactor */
400
            $secondFactor = $this->get('gateway.service.second_factor_service')->findByUuid($selectedSecondFactor);
401
        } else {
402
            $secondFactor = SecondfactorGsspFallback::create('azuremfa', $context->getSelectedLocale());
403
        }
404
405
        if (!$secondFactor) {
406
            throw new RuntimeException(
407
                sprintf(
408
                    'Verification of GSSF "%s" succeeded, however that Second Factor no longer exists',
409
                    $selectedSecondFactor
410
                )
411
            );
412
        }
413
414
        $this->getAuthenticationLogger()->logSecondFactorAuthentication($originalRequestId, $authenticationMode);
0 ignored issues
show
Bug introduced by
It seems like $originalRequestId can also be of type null; however, parameter $requestId of Surfnet\StepupGateway\Ga...dFactorAuthentication() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

414
        $this->getAuthenticationLogger()->logSecondFactorAuthentication(/** @scrutinizer ignore-type */ $originalRequestId, $authenticationMode);
Loading history...
415
        $context->markSecondFactorVerified();
416
417
        $logger->info(sprintf(
418
            'Marked GSSF "%s" as verified, forwarding to Gateway controller to respond',
419
            $selectedSecondFactor,
420
        ));
421
422
        return $this->forward($context->getResponseAction());
0 ignored issues
show
Bug introduced by
It seems like $context->getResponseAction() can also be of type null; however, parameter $controller of Symfony\Bundle\Framework...ctController::forward() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

422
        return $this->forward(/** @scrutinizer ignore-type */ $context->getResponseAction());
Loading history...
423
    }
424
425
    #[Route(
426
        path: '/verify-second-factor/{authenticationMode}/yubikey',
427
        name: 'gateway_verify_second_factor_yubikey',
428
        requirements: ['authenticationMode' => 'sso|sfo'],
429
        methods: ['GET', 'POST']
430
    )]
431
    public function verifyYubiKeySecondFactor(Request $request): Response
432
    {
433
        if (!$request->get('authenticationMode', false)) {
434
            throw new RuntimeException('Unable to determine the authentication mode in Yubikey verification action');
435
        }
436
        $authenticationMode = $request->get('authenticationMode');
437
        $this->supportsAuthenticationMode($authenticationMode);
438
        $context = $this->getResponseContext($authenticationMode);
439
        $originalRequestId = $context->getInResponseTo();
440
441
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
442
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
443
444
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
445
446
        $logger->notice('Verifying possession of Yubikey second factor');
447
448
        $command = new VerifyYubikeyOtpCommand();
449
        $command->secondFactorId = $selectedSecondFactor;
450
451
        $form = $this->createForm(VerifyYubikeyOtpType::class, $command)->handleRequest($request);
452
        $cancelForm = $this->buildCancelAuthenticationForm($authenticationMode)->handleRequest($request);
453
454
        if ($form->isSubmitted() && $form->isValid()) {
455
            $result = $this->getStepupService()->verifyYubikeyOtp($command);
456
            if ($result->didOtpVerificationFail()) {
457
                $form->addError(
458
                    new FormError($this->get('translator')->trans('gateway.form.verify_yubikey.otp_verification_failed')),
459
                );
460
461
                // OTP field is rendered empty in the template.
462
                return $this->render(
463
                    '@default/second_factor/verify_yubikey_second_factor.html.twig',
464
                    ['form' => $form->createView(), 'cancelForm' => $cancelForm->createView()],
465
                );
466
            } elseif (!$result->didPublicIdMatch()) {
467
                $form->addError(
468
                    new FormError($this->get('translator')->trans('gateway.form.verify_yubikey.public_id_mismatch')),
469
                );
470
471
                // OTP field is rendered empty in the template.
472
                return $this->render(
473
                    '@default/second_factor/verify_yubikey_second_factor.html.twig',
474
                    ['form' => $form->createView(), 'cancelForm' => $cancelForm->createView()],
475
                );
476
            }
477
478
            $this->getResponseContext($authenticationMode)->markSecondFactorVerified();
479
            $this->getAuthenticationLogger()->logSecondFactorAuthentication($originalRequestId, $authenticationMode);
0 ignored issues
show
Bug introduced by
It seems like $originalRequestId can also be of type null; however, parameter $requestId of Surfnet\StepupGateway\Ga...dFactorAuthentication() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

479
            $this->getAuthenticationLogger()->logSecondFactorAuthentication(/** @scrutinizer ignore-type */ $originalRequestId, $authenticationMode);
Loading history...
480
481
            $logger->info(
482
                sprintf(
483
                    'Marked Yubikey Second Factor "%s" as verified, forwarding to Saml Proxy to respond',
484
                    $selectedSecondFactor,
485
                ),
486
            );
487
488
            return $this->forward($context->getResponseAction());
0 ignored issues
show
Bug introduced by
It seems like $context->getResponseAction() can also be of type null; however, parameter $controller of Symfony\Bundle\Framework...ctController::forward() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

488
            return $this->forward(/** @scrutinizer ignore-type */ $context->getResponseAction());
Loading history...
489
        }
490
491
        // OTP field is rendered empty in the template.
492
        return $this->render(
493
            '@default/second_factor/verify_yubikey_second_factor.html.twig',
494
            ['form' => $form->createView(), 'cancelForm' => $cancelForm->createView()],
495
        );
496
    }
497
498
    #[Route(
499
        path: '/verify-second-factor/sms/send-challenge',
500
        name: 'gateway_verify_second_factor_sms',
501
        methods: ['GET', 'POST']
502
    )]
503
    public function verifySmsSecondFactor(
504
        Request $request,
505
    ): Response|RedirectResponse {
506
        if (!$request->get('authenticationMode', false)) {
507
            throw new RuntimeException('Unable to determine the authentication mode in the SMS verification action');
508
        }
509
        $authenticationMode = $request->get('authenticationMode');
510
        $this->supportsAuthenticationMode($authenticationMode);
511
        $context = $this->getResponseContext($authenticationMode);
512
        $originalRequestId = $context->getInResponseTo();
513
514
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
515
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
516
517
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
518
519
        $logger->notice('Verifying possession of SMS second factor, preparing to send');
520
521
        $command = new SendSmsChallengeCommand();
522
        $command->secondFactorId = $selectedSecondFactor;
523
524
        $form = $this->createForm(SendSmsChallengeType::class, $command)->handleRequest($request);
525
        $cancelForm = $this->buildCancelAuthenticationForm($authenticationMode)->handleRequest($request);
526
527
        $stepupService = $this->getStepupService();
528
        $phoneNumber = InternationalPhoneNumber::fromStringFormat(
529
            $stepupService->getSecondFactorIdentifier($selectedSecondFactor),
530
        );
531
532
        $otpRequestsRemaining = $stepupService->getSmsOtpRequestsRemainingCount($selectedSecondFactor);
533
        $maximumOtpRequests = $stepupService->getSmsMaximumOtpRequestsCount();
534
        $viewVariables = ['otpRequestsRemaining' => $otpRequestsRemaining, 'maximumOtpRequests' => $maximumOtpRequests];
535
536
        if ($form->isSubmitted() && !$form->isValid()) {
537
            return $this->render(
538
                '@default/second_factor/verify_sms_second_factor.html.twig',
539
                array_merge(
540
                    $viewVariables,
541
                    [
542
                        'phoneNumber' => $phoneNumber,
543
                        'form' => $form->createView(),
544
                        'cancelForm' => $cancelForm->createView(),
545
                    ],
546
                )
547
            );
548
        }
549
550
        $logger->notice('Verifying possession of SMS second factor, sending challenge per SMS');
551
552
        if (!$stepupService->sendSmsChallenge($command)) {
553
            $form->addError(
554
                new FormError($this->get('translator')->trans('gateway.form.send_sms_challenge.sms_sending_failed')),
555
            );
556
557
            return $this->render(
558
                '@default/second_factor/verify_sms_second_factor.html.twig',
559
                array_merge(
560
                    $viewVariables,
561
                    [
562
                        'phoneNumber' => $phoneNumber,
563
                        'form' => $form->createView(),
564
                        'cancelForm' => $cancelForm->createView(),
565
                    ],
566
                )
567
            );
568
        }
569
570
        return $this->redirect(
571
            $this->generateUrl(
572
                'gateway_verify_second_factor_sms_verify_challenge',
573
                ['authenticationMode' => $authenticationMode],
574
            ),
575
        );
576
    }
577
578
    #[Route(
579
        path: '/verify-second-factor/sms/verify-challenge',
580
        name: 'gateway_verify_second_factor_sms_verify_challenge',
581
        methods: ['GET', 'POST']
582
    )]
583
    public function verifySmsSecondFactorChallenge(
584
        Request $request,
585
    ): Response|array {
586
        if (!$request->get('authenticationMode', false)) {
587
            throw new RuntimeException('Unable to determine the authentication mode in the SMS challenge action');
588
        }
589
        $authenticationMode = $request->get('authenticationMode');
590
        $this->supportsAuthenticationMode($authenticationMode);
591
        $context = $this->getResponseContext($authenticationMode);
592
        $originalRequestId = $context->getInResponseTo();
593
594
        /** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */
595
        $logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId);
596
597
        $selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);
598
599
        $command = new VerifyPossessionOfPhoneCommand();
600
        $form = $this->createForm(VerifySmsChallengeType::class, $command)->handleRequest($request);
601
        $cancelForm = $this->buildCancelAuthenticationForm($authenticationMode)->handleRequest($request);
602
603
        if ($form->isSubmitted() && $form->isValid()) {
604
            $logger->notice('Verifying input SMS challenge matches');
605
            $command->secondFactorId = $selectedSecondFactor;
606
            $verification = $this->getStepupService()->verifySmsChallenge($command);
607
608
            if ($verification->wasSuccessful()) {
609
                $this->getStepupService()->clearSmsVerificationState($selectedSecondFactor);
610
611
                $this->getResponseContext($authenticationMode)->markSecondFactorVerified();
612
                $this->getAuthenticationLogger()->logSecondFactorAuthentication($originalRequestId, $authenticationMode);
0 ignored issues
show
Bug introduced by
It seems like $originalRequestId can also be of type null; however, parameter $requestId of Surfnet\StepupGateway\Ga...dFactorAuthentication() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

612
                $this->getAuthenticationLogger()->logSecondFactorAuthentication(/** @scrutinizer ignore-type */ $originalRequestId, $authenticationMode);
Loading history...
613
614
                $logger->info(
615
                    sprintf(
616
                        'Marked Sms Second Factor "%s" as verified, forwarding to Saml Proxy to respond',
617
                        $selectedSecondFactor,
618
                    ),
619
                );
620
621
                return $this->forward($context->getResponseAction());
0 ignored issues
show
Bug introduced by
It seems like $context->getResponseAction() can also be of type null; however, parameter $controller of Symfony\Bundle\Framework...ctController::forward() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

621
                return $this->forward(/** @scrutinizer ignore-type */ $context->getResponseAction());
Loading history...
622
            } elseif ($verification->didOtpExpire()) {
623
                $logger->notice('SMS challenge expired');
624
                $form->addError(
625
                    new FormError($this->get('translator')->trans('gateway.form.send_sms_challenge.challenge_expired')),
626
                );
627
            } elseif ($verification->wasAttemptedTooManyTimes()) {
628
                $logger->notice('SMS challenge verification was attempted too many times');
629
                $form->addError(
630
                    new FormError($this->get('translator')->trans('gateway.form.send_sms_challenge.too_many_attempts')),
631
                );
632
            } else {
633
                $logger->notice('SMS challenge did not match');
634
                $form->addError(
635
                    new FormError(
636
                        $this->get('translator')->trans('gateway.form.send_sms_challenge.sms_challenge_incorrect'),
637
                    ),
638
                );
639
            }
640
        }
641
642
        return $this->render(
643
            '@default/second_factor/verify_sms_second_factor_challenge.html.twig',
644
            [
645
                'form' => $form->createView(),
646
                'cancelForm' => $cancelForm->createView(),
647
            ],
648
        );
649
    }
650
651
    #[Route(
652
        path: '/authentication/cancel',
653
        name: 'gateway_cancel_authentication',
654
        methods: ['POST']
655
    )]
656
    public function cancelAuthentication(): Response
657
    {
658
        return $this->forward(GatewayController::class . '::sendAuthenticationCancelledByUser');
659
    }
660
661
    /**
662
     * @return \Surfnet\StepupGateway\GatewayBundle\Service\StepupAuthenticationService
663
     */
664
    private function getStepupService()
665
    {
666
        return $this->get('gateway.service.stepup_authentication');
667
    }
668
669
    /**
670
     * @return ResponseContext
671
     */
672
    private function getResponseContext($authenticationMode)
673
    {
674
        // Select the state handler that matches the current authentication mode
675
        $stateHandlerServiceId = match ($authenticationMode) {
676
            self::MODE_SFO => 'gateway.proxy.sfo.state_handler',
677
            self::MODE_SSO => 'gateway.proxy.sso.state_handler',
678
            default => throw new InvalidArgumentException('Invalid authentication mode requested'),
679
        };
680
681
        // We then load the correct state handler service. And retrieve the ResponseContext service id that was set on it
682
        $responseContextServiceId = $this->get($stateHandlerServiceId)->getResponseContextServiceId();
683
        if (is_null($responseContextServiceId)) {
684
            throw new RuntimeException('The RequestContext service id is not set on the state handler %s');
685
        }
686
        // Finally return the ResponseContext
687
        return $this->get($responseContextServiceId);
688
    }
689
690
    /**
691
     * @return \Surfnet\StepupGateway\GatewayBundle\Monolog\Logger\AuthenticationLogger
692
     */
693
    private function getAuthenticationLogger()
694
    {
695
        return $this->get('gateway.authentication_logger');
696
    }
697
698
    private function getCookieService(): CookieService
699
    {
700
        return $this->get('gateway.service.sso_2fa_cookie');
701
    }
702
703
    private function getSecondFactorService(): SecondFactorService
704
    {
705
        return $this->get('gateway.service.second_factor_service');
706
    }
707
708
    private function getSelectedSecondFactor(ResponseContext $context, LoggerInterface $logger): string
709
    {
710
        $selectedSecondFactor = $context->getSelectedSecondFactor();
711
712
        if (!$selectedSecondFactor) {
713
            $logger->error('Cannot verify possession of an unknown second factor');
714
715
            throw new BadRequestHttpException('Cannot verify possession of an unknown second factor.');
716
        }
717
718
        return $selectedSecondFactor;
719
    }
720
721
    private function selectAndRedirectTo(
722
        SecondFactorInterface $secondFactor,
723
        ResponseContext $context,
724
        $authenticationMode,
725
    ): RedirectResponse {
726
        $context->saveSelectedSecondFactor($secondFactor);
727
728
        $this->getStepupService()->clearSmsVerificationState($secondFactor->getSecondFactorId());
729
730
        $secondFactorTypeService = $this->get('surfnet_stepup.service.second_factor_type');
731
        $secondFactorType = new SecondFactorType($secondFactor->getSecondFactorType());
732
733
        $route = 'gateway_verify_second_factor_';
734
        if ($secondFactorTypeService->isGssf($secondFactorType)) {
735
            $route .= 'gssf';
736
        } else {
737
            $route .= strtolower($secondFactor->secondFactorType);
0 ignored issues
show
Bug introduced by
Accessing secondFactorType on the interface Surfnet\StepupGateway\Ga...r\SecondFactorInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
738
        }
739
740
        return $this->redirect($this->generateUrl($route, ['authenticationMode' => $authenticationMode]));
741
    }
742
743
    /**
744
     * @param string $authenticationMode
745
     */
746
    private function buildCancelAuthenticationForm($authenticationMode): FormInterface
747
    {
748
        $cancelFormAction = $this->generateUrl(
749
            'gateway_cancel_authentication',
750
            ['authenticationMode' => $authenticationMode],
751
        );
752
753
        return $this->createForm(
754
            CancelAuthenticationType::class,
755
            null,
756
            ['action' => $cancelFormAction],
757
        );
758
    }
759
760
    private function supportsAuthenticationMode($authenticationMode): void
761
    {
762
        if (self::MODE_SSO !== $authenticationMode && self::MODE_SFO !== $authenticationMode) {
763
            throw new InvalidArgumentException('Invalid authentication mode requested');
764
        }
765
    }
766
}
767