Passed
Push — develop ( c85f88...7c450d )
by Peter
14:03
created

selfAssertedTokenRegistrationRecoveryTokenAction()   C

Complexity

Conditions 11
Paths 10

Size

Total Lines 73
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
eloc 52
nc 10
nop 3
dl 0
loc 73
rs 6.9006
c 0
b 0
f 0

How to fix   Long Method    Complexity   

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 2022 SURFnet B.V.
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\StepupSelfService\SelfServiceBundle\Controller;
20
21
use Psr\Log\LoggerInterface;
22
use Surfnet\StepupBundle\Service\LoaResolutionService;
23
use Surfnet\StepupBundle\Value\PhoneNumber\InternationalPhoneNumber;
24
use Surfnet\StepupSelfService\SelfServiceBundle\Command\PromiseSafeStorePossessionCommand;
25
use Surfnet\StepupSelfService\SelfServiceBundle\Command\SafeStoreAuthenticationCommand;
26
use Surfnet\StepupSelfService\SelfServiceBundle\Command\SelfAssertedTokenRegistrationCommand;
27
use Surfnet\StepupSelfService\SelfServiceBundle\Command\SendRecoveryTokenSmsAuthenticationChallengeCommand;
28
use Surfnet\StepupSelfService\SelfServiceBundle\Command\VerifySmsRecoveryTokenChallengeCommand;
29
use Surfnet\StepupSelfService\SelfServiceBundle\Form\Type\AuthenticateSafeStoreType;
30
use Surfnet\StepupSelfService\SelfServiceBundle\Form\Type\PromiseSafeStorePossessionType;
31
use Surfnet\StepupSelfService\SelfServiceBundle\Form\Type\VerifySmsChallengeType;
32
use Surfnet\StepupSelfService\SelfServiceBundle\Service\SecondFactorService;
33
use Surfnet\StepupSelfService\SelfServiceBundle\Service\SelfAssertedTokens\AuthenticationRequestFactory;
34
use Surfnet\StepupSelfService\SelfServiceBundle\Service\SelfAssertedTokens\RecoveryTokenService;
35
use Surfnet\StepupSelfService\SelfServiceBundle\Service\SelfAssertedTokens\SafeStoreService;
36
use Surfnet\StepupSelfService\SelfServiceBundle\Service\SmsRecoveryTokenService;
37
use Symfony\Component\HttpFoundation\Request;
38
use Symfony\Component\HttpFoundation\Response;
39
40
/**
41
 *
42
 * @SuppressWarnings(PHPMD.CyclomaticComplexity)
43
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
44
 */
45
class SelfAssertedTokensController extends Controller
46
{
47
    use RecoveryTokenControllerTrait;
0 ignored issues
show
Bug introduced by
The trait Surfnet\StepupSelfServic...eryTokenControllerTrait requires the property $id which is not provided by Surfnet\StepupSelfServic...ssertedTokensController.
Loading history...
48
    /**
49
     * @var RecoveryTokenService
50
     */
51
    private $recoveryTokenService;
52
53
    /**
54
     * @var SecondFactorService
55
     */
56
    private $secondFactorService;
57
58
    /**
59
     * @var LoggerInterface
60
     */
61
    private $logger;
62
63
    /**
64
     * @var SafeStoreService
65
     */
66
    private $safeStoreService;
67
68
    /**
69
     * @var SmsRecoveryTokenService
70
     */
71
    private $smsService;
72
73
    /**
74
     * @var LoaResolutionService
75
     */
76
    private $loaResolutionService;
77
78
    /**
79
     * @var AuthenticationRequestFactory
80
     */
81
    private $authnRequestFactory;
82
83
    public function __construct(
84
        RecoveryTokenService $recoveryTokenService,
85
        SafeStoreService $safeStoreService,
86
        SecondFactorService $secondFactorService,
87
        SmsRecoveryTokenService $smsService,
88
        LoaResolutionService $loaResolutionService,
89
        AuthenticationRequestFactory $authenticationRequestFactory,
90
        LoggerInterface $logger
91
    ) {
92
        $this->recoveryTokenService = $recoveryTokenService;
93
        $this->safeStoreService = $safeStoreService;
94
        $this->secondFactorService = $secondFactorService;
95
        $this->smsService = $smsService;
96
        $this->logger = $logger;
97
        $this->loaResolutionService = $loaResolutionService;
98
        $this->authnRequestFactory = $authenticationRequestFactory;
99
    }
100
101
    /**
102
     * Self-asserted token registration: Registration entrypoint
103
     *
104
     * Select(s) the recovery token to perform the self-asserted second factor
105
     * token registration with.
106
     *
107
     * Possible outcomes:
108
     * 1. Shows a recovery token selection screen when more than one token are available
109
     * 2. Selects the one and only available recovery token and redirects to the recovery token authentication route
110
     * 3. Starts registration of a recovery token if non are in possession
111
     */
112
    public function selfAssertedTokenRegistrationAction($secondFactorId): Response
113
    {
114
        $this->logger->info('Checking if Identity has a recovery token');
115
        $identity = $this->getIdentity();
116
        $this->assertSecondFactorInPossession($secondFactorId, $identity);
117
        $secondFactor = $this->secondFactorService->findOneVerified($secondFactorId);
118
        if ($this->recoveryTokenService->hasRecoveryToken($identity)) {
119
            $tokens = $this->recoveryTokenService->getAvailableTokens($identity, $secondFactor);
0 ignored issues
show
Bug introduced by
It seems like $secondFactor can also be of type null; however, parameter $secondFactor of Surfnet\StepupSelfServic...e::getAvailableTokens() does only seem to accept Surfnet\StepupMiddleware...to\VerifiedSecondFactor, 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
            $tokens = $this->recoveryTokenService->getAvailableTokens($identity, /** @scrutinizer ignore-type */ $secondFactor);
Loading history...
120
            if (count($tokens) === 0) {
121
                // User is in possession of a recovery token, but it is not safe to use here (for example sms recovery
122
                // token is not available while activating a SMS second factor)
123
                $this->addFlash('error', 'ss.self_asserted_tokens.second_factor.no_available_recovery_token.alert.failed');
124
                return $this->redirectToRoute('ss_second_factor_list');
125
            }
126
            if (count($tokens) > 1) {
127
                $this->logger->info('Show recovery token selection screen');
128
                return $this->render(
129
                    '@SurfnetStepupSelfServiceSelfService/registration/self_asserted_tokens/select_available_recovery_token.html.twig',
130
                    [
131
                        'secondFactorId' => $secondFactorId,
132
                        'showAvailable' => true,
133
                        'availableRecoveryTokens' => $tokens,
134
                    ]
135
                );
136
            }
137
            $this->logger->info('Continue to recovery token authentication screen for the one available recovery token');
138
            $token = reset($tokens);
139
            return $this->redirect($this->generateUrl(
140
                'ss_second_factor_self_asserted_tokens_recovery_token',
141
                [
142
                    'secondFactorId' => $secondFactorId,
143
                    'recoveryTokenId' => $token->recoveryTokenId
144
                ]
145
            ));
146
        }
147
        $this->logger->info('Start registration of a recovery token (none are available yet)');
148
        return $this->redirect(
149
            $this->generateUrl('ss_second_factor_new_recovery_token', ['secondFactorId' => $secondFactorId])
150
        );
151
    }
152
153
    /**
154
     * Self-asserted token registration: Authenticate recovery token
155
     *
156
     * Identity must authenticate the recovery token in order to perform a
157
     * self-asserted token registration. But only when this action is
158
     * performed while recovering a Second Factor token. During initial
159
     * registration of a recovery token in the self-asserted token registration
160
     * flow, authentication is not required.
161
     */
162
    public function selfAssertedTokenRegistrationRecoveryTokenAction(
163
        Request $request,
164
        string $secondFactorId,
165
        string $recoveryTokenId
166
    ): Response {
167
        $this->logger->info('Start authentication of recovery token to perform self-asserted token registration');
168
        $identity = $this->getIdentity();
169
        $this->assertSecondFactorInPossession($secondFactorId, $identity);
170
        $this->assertRecoveryTokenInPossession($recoveryTokenId, $identity);
171
        $token = $this->recoveryTokenService->getRecoveryToken($recoveryTokenId);
172
173
        switch ($token->type) {
174
            case "sms":
175
                if ($this->smsService->wasTokenCreatedDuringSecondFactorRegistration()) {
176
                    // Forget we created the recovery token during token registration. Next time Identity
177
                    // must fill its password.
178
                    $this->smsService->forgetTokenCreatedDuringSecondFactorRegistration();
179
                    $secondFactor = $this->secondFactorService->findOneVerified($secondFactorId);
180
181
                    $command = new SelfAssertedTokenRegistrationCommand();
182
                    $command->identity = $this->getIdentity();
183
                    $command->secondFactor = $secondFactor;
184
                    $command->recoveryTokenId = $recoveryTokenId;
185
186
                    if ($this->secondFactorService->registerSelfAssertedToken($command)) {
187
                        $this->addFlash('success', 'ss.self_asserted_tokens.second_factor.alert.successful');
188
                    } else {
189
                        $this->addFlash('error', 'ss.self_asserted_tokens.second_factor.alert.failed');
190
                    }
191
                    return $this->redirectToRoute('ss_second_factor_list');
192
                }
193
                $number = InternationalPhoneNumber::fromStringFormat($token->identifier);
194
                $command = new SendRecoveryTokenSmsAuthenticationChallengeCommand();
195
                $command->identifier = $number;
196
                $command->institution = $identity->institution;
197
                $command->recoveryTokenId = $recoveryTokenId;
198
                $command->identity = $identity->id;
199
                $this->smsService->authenticate($command);
200
                return $this->redirectToRoute(
201
                    'ss_second_factor_self_asserted_tokens_recovery_token_sms',
202
                    ['secondFactorId' => $secondFactorId, 'recoveryTokenId' => $recoveryTokenId]
203
                );
204
            case "safe-store":
205
                // No authentication of the safe store token is required if created during SF token registration
206
                if ($this->safeStoreService->wasSafeStoreTokenCreatedDuringSecondFactorRegistration()) {
207
                    // Forget we created the safe-store recovery token during token registration. Next time Identity
208
                    // must fill its password.
209
                    $this->safeStoreService->forgetSafeStoreTokenCreatedDuringSecondFactorRegistration();
210
                    if ($this->invokeSelfAssertedTokenRegistrationCommand($secondFactorId, $recoveryTokenId)) {
211
                        $this->addFlash('success', 'ss.self_asserted_tokens.second_factor.vetting.alert.successful');
212
                    } else {
213
                        $this->addFlash('error', 'ss.self_asserted_tokens.second_factor.vetting.alert.failed');
214
                    }
215
                    return $this->redirectToRoute('ss_second_factor_list');
216
                }
217
                $command = new SafeStoreAuthenticationCommand();
218
                $command->recoveryToken = $token;
219
                $command->identity = $identity;
220
                $form = $this->createForm(AuthenticateSafeStoreType::class, $command)->handleRequest($request);
221
                if ($form->isSubmitted() && $form->isValid()) {
222
                    if ($this->recoveryTokenService->authenticateSafeStore($command)) {
223
                        if ($this->invokeSelfAssertedTokenRegistrationCommand($secondFactorId, $recoveryTokenId)) {
224
                            $this->addFlash('success', 'ss.self_asserted_tokens.second_factor.vetting.alert.successful');
225
                            return $this->redirectToRoute('ss_second_factor_list');
226
                        }
227
                        $this->addFlash('error', 'ss.self_asserted_tokens.second_factor.vetting.alert.failed');
228
                    } else {
229
                        $this->addFlash('error', 'ss.self_asserted_tokens.safe_store.authentication.alert.failed');
230
                    }
231
                }
232
                return $this->render(
233
                    '@SurfnetStepupSelfServiceSelfService/registration/self_asserted_tokens/authenticate_safe_store.html.twig',
234
                    ['form' => $form->createView()]
235
                );
236
        }
237
    }
238
239
    /**
240
     * Self-asserted token registration: choose recovery token type
241
     *
242
     * The user can select which recovery token to add. Some limitations may
243
     * apply. For example, using a SMS Recovery Token for registration of an
244
     * SMS Second Factor is only allowed when different phone numbers are used.
245
     */
246
    public function newRecoveryTokenAction($secondFactorId)
247
    {
248
        $this->logger->info('Determining which recovery token are available');
249
        $identity = $this->getIdentity();
250
        $this->assertSecondFactorInPossession($secondFactorId, $identity);
251
        $this->assertNoRecoveryTokens($identity);
252
253
        $secondFactor = $this->secondFactorService->findOneVerified($secondFactorId);
254
        $availableRecoveryTokens = $this->recoveryTokenService->getRemainingTokenTypes($identity);
255
        if ($secondFactor && $secondFactor->type === 'sms') {
256
            $this->logger->notice('SMS recovery token type is not allowed as we are vetting a SMS second factor');
257
            unset($availableRecoveryTokens['sms']);
258
        }
259
260
        return $this->render(
261
            '@SurfnetStepupSelfServiceSelfService/registration/self_asserted_tokens/new_recovery_token.html.twig',
262
            [
263
                'secondFactorId' => $secondFactorId,
264
                'availableRecoveryTokens' => $availableRecoveryTokens
265
            ]
266
        );
267
    }
268
269
    /**
270
     * Self-asserted token registration: Authenticate a SMS recovery token
271
     *
272
     * The previous action (selfAssertedTokenRegistrationRecoveryTokenAction)
273
     * sent the Identity a OTP via SMS. The Identity must reproduce that OTP
274
     * in this action. Proving possession of the recovery token.
275
     */
276
    public function selfAssertedTokenRecoveryTokenSmsAuthenticationAction(
277
        Request $request,
278
        string $secondFactorId,
279
        string $recoveryTokenId
280
    ): Response {
281
        $identity = $this->getIdentity();
282
        $this->assertSecondFactorInPossession($secondFactorId, $identity);
283
        $this->assertRecoveryTokenInPossession($recoveryTokenId, $identity);
284
285
        // Then render the authentication (proof of possession screen
286
        if (!$this->smsService->hasSmsVerificationState($recoveryTokenId)) {
287
            $this->get('session')->getFlashBag()->add('notice', 'ss.registration.sms.alert.no_verification_state');
288
            return $this->redirectToRoute(
289
                'ss_second_factor_self_asserted_tokens',
290
                ['secondFactorId' => $secondFactorId]
291
            );
292
        }
293
294
        $secondFactor = $this->secondFactorService->findOneVerified($secondFactorId);
295
296
        $command = new VerifySmsRecoveryTokenChallengeCommand();
297
        $command->identity = $identity->id;
298
        $command->resendRouteParameters = ['secondFactorId' => $secondFactorId, 'recoveryTokenId' => $recoveryTokenId];
299
300
        $form = $this->createForm(VerifySmsChallengeType::class, $command)->handleRequest($request);
301
302
        if ($form->isSubmitted() && $form->isValid()) {
303
            $command->recoveryTokenId = $recoveryTokenId;
304
            $result = $this->smsService->verifyAuthentication($command);
305
            if ($result->authenticated()) {
306
                $this->smsService->clearSmsVerificationState($recoveryTokenId);
307
308
                $command = new SelfAssertedTokenRegistrationCommand();
309
                $command->identity = $this->getIdentity();
310
                $command->secondFactor = $secondFactor;
311
                $command->recoveryTokenId = $recoveryTokenId;
312
313
                if ($this->secondFactorService->registerSelfAssertedToken($command)) {
314
                    $this->addFlash('success', 'ss.self_asserted_tokens.second_factor.alert.successful');
315
                } else {
316
                    $this->addFlash('error', 'ss.self_asserted_tokens.second_factor.alert.failed');
317
                }
318
319
                return $this->redirectToRoute('ss_second_factor_list');
320
            } elseif ($result->wasIncorrectChallengeResponseGiven()) {
321
                $this->addFlash('error', 'ss.prove_phone_possession.incorrect_challenge_response');
322
            } elseif ($result->hasChallengeExpired()) {
323
                $this->addFlash('error', 'ss.prove_phone_possession.challenge_expired');
324
            } elseif ($result->wereTooManyAttemptsMade()) {
325
                $this->addFlash('error', 'ss.prove_phone_possession.too_many_attempts');
326
            } else {
327
                $this->addFlash('error', 'ss.prove_phone_possession.proof_of_possession_failed');
328
            }
329
        }
330
        return $this->render(
331
            '@SurfnetStepupSelfServiceSelfService/registration/self_asserted_tokens/registration_sms_prove_possession.html.twig',
332
            [
333
                'form' => $form->createView(),
334
            ]
335
        );
336
    }
337
338
    /**
339
     * Self-asserted token registration: Create Recovery Token (safe-store)
340
     *
341
     * Shows the one-time secret and asks the Identity to store the
342
     * password in a safe location.
343
     */
344
    public function registerCreateRecoveryTokenSafeStoreAction(Request $request, $secondFactorId)
345
    {
346
        $identity = $this->getIdentity();
347
        $this->assertSecondFactorInPossession($secondFactorId, $identity);
348
        $this->assertNoRecoveryTokenOfType('safe-store', $identity);
349
350
        $secret = $this->safeStoreService->produceSecret();
351
        $command = new PromiseSafeStorePossessionCommand();
352
353
        $form = $this->createForm(PromiseSafeStorePossessionType::class, $command)->handleRequest($request);
354
355
        if ($form->isSubmitted() && $form->isValid()) {
356
            $command->secret = $secret;
357
            $command->identity = $this->getIdentity();
358
359
            $executionResult = $this->safeStoreService->promisePossession($command);
360
            if (!$executionResult->getErrors()) {
361
                return $this->redirect(
362
                    $this->generateUrl('ss_second_factor_self_asserted_tokens', ['secondFactorId' => $secondFactorId])
363
                );
364
            }
365
            $this->addFlash('error', 'ss.form.recovery_token.error.error_message');
366
        }
367
368
        return $this->render(
369
            '@SurfnetStepupSelfServiceSelfService/registration/self_asserted_tokens/recovery_token_safe_store.html.twig',
370
            [
371
                'form' => $form->createView(),
372
                'secondFactorId' => $secondFactorId,
373
                'secret' => $secret,
374
            ]
375
        );
376
    }
377
378
    /**
379
     * Self-asserted token registration: Create the SMS recovery token
380
     * Step 1: Send an OTP to phone of Identity
381
     *
382
     * Note: Shares logic with the recovery token SMS send challenge action
383
     */
384
    public function registerRecoveryTokenSmsAction(Request $request, string $secondFactorId)
385
    {
386
        return $this->handleSmsChallenge(
387
            $request,
388
            '@SurfnetStepupSelfServiceSelfService/registration/self_asserted_tokens/recovery_token_sms.html.twig',
389
            'ss_registration_recovery_token_sms_proof_of_possession',
390
            $secondFactorId
391
        );
392
    }
393
394
    /**
395
     * Self-asserted token registration: Create the SMS recovery token
396
     * Step 2: Process proof of phone possession of Identity
397
     *
398
     * Note: Shares logic with the recovery token SMS send challenge action
399
     */
400
    public function registerRecoveryTokenSmsProofOfPossessionAction(Request $request, string $secondFactorId)
401
    {
402
        return $this->handleSmsProofOfPossession(
403
            $request,
404
            '@SurfnetStepupSelfServiceSelfService/registration/self_asserted_tokens/registration_sms_prove_possession.html.twig',
405
            'ss_second_factor_self_asserted_tokens',
406
            $secondFactorId
407
        );
408
    }
409
410
    private function invokeSelfAssertedTokenRegistrationCommand(string $secondFactorId, string $recoveryTokenId): bool
411
    {
412
        $secondFactor = $this->secondFactorService->findOneVerified($secondFactorId);
413
        $command = new SelfAssertedTokenRegistrationCommand();
414
        $command->identity = $this->getIdentity();
415
        $command->secondFactor = $secondFactor;
416
        $command->recoveryTokenId = $recoveryTokenId;
417
        return $this->secondFactorService->registerSelfAssertedToken($command);
418
    }
419
}
420